JWT attacks: alg none, weak secrets, algorithm confusion and kid injection
How to read a JSON Web Token, crack its secret with hashcat, abuse alg none and RS256 to HS256 confusion, and inject into the kid and jku headers to sign your own tokens.
A JWT is the cookie or Authorization: Bearer eyJ... header many applications use to know who you are. It carries your data (user, role) signed, not encrypted: anyone can read it, but changing it requires being able to sign. JWT attacks are about getting the server to accept a signature you made yourself. There is an in-browser decoder in JWT.
What a JWT looks like
Three base64url parts separated by dots: header.payload.signature.
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
# {"alg":"HS256","typ":"JWT"}
echo 'eyJ1c2VyIjoiYW5hIiwicm9sZSI6InVzZXIifQ' | base64 -d
# {"user":"ana","role":"user"}
What matters in the header:
| Field | What it is | Why you care |
|---|---|---|
alg |
Signing algorithm (HS256, RS256, none...) |
If the server trusts it, you choose how it is validated |
kid |
Key identifier | If it is used to read a file or run a query, it can be injected |
jku / x5u |
URL to fetch the public key from | If unrestricted, it fetches it from your server |
jwk |
The public key, inside the token itself | If accepted, you sign with your own key |
jwt_tool tries almost everything in this guide automatically:
python3 jwt_tool.py 'eyJ...' -M at -t 'http://target.htb/api/me' -rh 'Authorization: Bearer eyJ...'
1. The server does not check the signature
First things first: change the role to admin without touching the signature and send it. More often than you would think, the application decodes the token but never verifies it.
2. alg: none
Some old libraries accept unsigned tokens if the header says none:
python3 jwt_tool.py 'eyJ...' -X a # builds none, None, NONE, nOnE variants
By hand: header {"alg":"none","typ":"JWT"}, your payload and an empty signature (the token ends with a dot).
3. Weak secret (HS256)
With HS256 the signature is an HMAC with a secret. If it is short or a dictionary word, it cracks offline:
hashcat -m 16500 token.txt /usr/share/wordlists/rockyou.txt
hashcat -m 16500 token.txt jwt-secrets.txt # there are lists of known JWT secrets
john token.txt --wordlist=rockyou.txt --format=HMAC-SHA256
With the secret, sign whatever you like:
python3 jwt_tool.py 'eyJ...' -T -S hs256 -p 'secret123'
import jwt # pip install pyjwt
print(jwt.encode({"user": "admin", "role": "admin"}, "secret123", algorithm="HS256"))
Tip
The secret may also be in the code. If you find an exposed repository or an .env file (through an LFI or an exposed .git, say), look for JWT_SECRET, SECRET_KEY or TOKEN_SECRET. That is what happens on Secret.
4. Algorithm confusion (RS256 → HS256)
With RS256 the server signs with a private key and verifies with the public one. If the library lets the token choose the algorithm, you can send HS256 and sign with the public key as if it were the secret: the server computes the HMAC with the same key and it matches.
- Get the public key:
/.well-known/jwks.json,/jwks.json,/api/keys, the source code, or derive it from two signed tokens (rsa_sign2n). - Sign with it in exactly the format the server uses (PEM, with its trailing newline):
python3 jwt_tool.py 'eyJ...' -X k -pk public.pem
5. kid injection
If the server uses kid to look up the secret in a file or a database:
{"alg":"HS256","typ":"JWT","kid":"../../../../dev/null"}
With /dev/null, the secret is an empty string: sign with "". If it goes into a query, try an SQL injection that returns the secret of your choice (' UNION SELECT 'abc' -- -). If it is passed to a command, a command injection. On TheNotebook, the kid is a URL to the private key that you end up pointing at your own server.
6. jku, x5u and jwk
If the server fetches the key from the URL the token gives, generate your own key pair, publish the JWKS on your server and point jku at it:
python3 jwt_tool.py 'eyJ...' -X s -ju http://10.10.14.5/jwks.json
Many applications check that the URL starts with their own domain: look for an open redirect there and use it. That is the idea behind Unicode.
Common errors
- The server rejects your token even though the secret is right: it is nearly always the base64url (no trailing
=, and-and_instead of+and/). Use a library or jwt_tool instead of building it by hand. - The token expires straight away: also change
exp(andiat/nbf) when signing. - Algorithm confusion does not work: the public key must be exactly the same string the server uses, with or without a trailing newline. Try both.
What next
With an admin token, check what the application now lets you do: upload panels (file upload), diagnostic features (command injection) or internal APIs. Also look for access control flaws in IDOR and APIs.