← Back to Writeups
CUSTOMEasyWeb

JWT Secrets

XESXOR9/2/20263 min read
#web#custom#easy#admin_privilege_escalation#hs256_resign#jwt_forgery#weak_secret_guessing

JWT Secrets

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: admin_privilege_escalation, hs256_resign, jwt_forgery, weak_secret_guessing

Summary

Walkthrough of the CUSTOM Web JWT Secrets. Task: a CryptoHack JWT challenge issues and verifies HS256 tokens, with the flag gated behind admin:true; source comment hints the secret is the PyJWT readme example key. Solution: guess the default H

Description

A CryptoHack "Crypto On The Web" challenge about JWT signing algorithms. Background text explains HS256 (symmetric HMAC-SHA256) vs RS256 (asymmetric RSA), and warns that developers sometimes use default or weak HS256 secret keys. The page provides source code with a create_session(username) endpoint and an authorise(token) endpoint, plus a suspicious comment about the secret key.

Challenge URL: https://web.cryptohack.org/jwt-secrets

The app mints HS256 JWTs for any username (always with admin: False) and has a separate endpoint that decodes a submitted token. If the decoded token contains admin: true, the server returns the flag. The goal is to forge such a token. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

The provided source:

python
import jwt # note this is the PyJWT module, not python-jwt

SECRET_KEY = ? # TODO: PyJWT readme key, change later
FLAG = ?

@chal.route('/jwt-secrets/authorise/<token>/')
def authorise(token):
    try:
        decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    except Exception as e:
        return {"error": str(e)}
    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('/jwt-secrets/create_session/<username>/')
def create_session(username):
    encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256')
    return {"session": encoded}

Key observations:

  • The token is signed and verified with HS256, a symmetric HMAC-SHA256 scheme. The same SECRET_KEY is used to both sign (in create_session) and verify (in authorise). Whoever knows the secret can mint arbitrary valid tokens.
  • create_session hardcodes admin: False, so the legitimate flow can never set admin: true. We must forge a token ourselves.
  • The decisive hint is the comment # TODO: PyJWT readme key, change later. The PyJWT library's README uses the literal example secret secret in its encode/decode snippet. The developer copied that and never changed it. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

So the vulnerability is a weak/default HS256 secret key (secret), enabling full token forgery and privilege escalation to admin.

Solution

  1. Read the source on the challenge page; note the PyJWT-readme comment pointing at the default example secret secret.
  2. Forge an admin token signed with HS256 and key secret.
  3. Submit it to the authorise endpoint.
python
#!/usr/bin/env python3
import jwt
import requests

# The HS256 secret is the PyJWT README example key, never changed.
SECRET = "secret"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Forge a token with admin:true (create_session would force admin:false).
token = jwt.encode({"username": "admin", "admin": True}, SECRET, algorithm="HS256")
print("forged token:", token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
# eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.
# Mozu0MUZmsKUz3aAqi6REcuPETl4CncofpoBDqtDh-c

r = requests.get(f"https://web.cryptohack.org/jwt-secrets/authorise/{token}/")
print(r.json())
# {"response":"Welcome admin, here is your flag: crypto{REDACTED}"}

Or with curl: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

curl -s "https://web.cryptohack.org/jwt-secrets/authorise/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.Mozu0MUZmsKUz3aAqi6REcuPETl4CncofpoBDqtDh-c/"

Response:

{"response":"Welcome admin, here is your flag: crypto{REDACTED}"}

Forged token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.Mozu0MUZmsKUz3aAqi6REcuPETl4CncofpoBDqtDh-c

Lesson / Mitigation

Never use library example/default secrets in production. With symmetric HS256, a leaked or guessable secret allows complete token forgery — including elevating admin to true. Use strong, random secrets, or prefer asymmetric RS256 so the public verifying key can be distributed while the private signing key stays protected. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering admin_privilege_escalation, hs256_resign, jwt_forgery, weak_secret_guessing was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-21 • CUSTOM • JWT Secrets • Difficulty: Easy