JWT Attacks: The Complete Practical Guide

Cracking Weak JWT Secrets and Forging Tokens

How to crack an HS256 JWT secret offline with hashcat or jwt_tool, confirm the key by re-signing, and forge an admin token once you own the signing key.

Published

When a JWT is signed with HMAC (HS256) and the secret is a human-chosen string, the signature is only as strong as that string. Capture one token, crack the secret offline against a wordlist, and you hold the signing key, at which point you can forge any token the server will trust. This page walks the capture, the crack, and the forge. Practice on a free live target.

When this applies

This attack needs two things: an HMAC algorithm (HS256, HS384, or HS512), and a weak secret. You can tell the algorithm by decoding the header. You find out whether the secret is weak by trying to crack it. The reason it is worth trying is that HMAC secrets are frequently chosen by a developer, lifted from a tutorial, or reused from another part of the app, any of which a wordlist will find. A 256-bit random key will not crack, so a failed crack is also useful information.

Step 1: capture a token

You only need one valid token, because the entire attack happens offline against that token's signature. Grab it from your own session: a cookie, an Authorization: Bearer header, local storage, wherever the app keeps it. No need to interact with the server again until you forge.

Step 2: crack the secret offline

Point a cracker at the token with a wordlist. hashcat uses mode 16500 for JWTs:

hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt

Add a rules file to mutate the wordlist and catch secrets like Summer2024!:

hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt -r rules/best64.rule

jwt_tool has a built-in dictionary crack mode as well, which is handy when you are already using it:

python3 jwt_tool.py <TOKEN> -C -d /usr/share/wordlists/rockyou.txt

The -C flag is crack mode and -d supplies the dictionary. Either tool recomputes the HMAC over the token's header and payload for each candidate word and stops when a signature matches. When it finds the secret, it prints it.

Step 3: confirm the key by re-signing

Before forging anything useful, confirm the recovered secret actually re-signs the original token to the same signature. If it reproduces the token byte for byte, the key is correct. jwt_tool tamper mode lets you re-sign interactively with the recovered secret:

python3 jwt_tool.py <TOKEN> -S hs256 -p 'recovered-secret' -T

-S hs256 sets the signing algorithm, -p supplies the secret, and -T opens tamper mode so you can edit claims before it re-signs.

Step 4: forge a token

With the secret in hand, mint whatever token you need. In Python with PyJWT:

import jwt
token = jwt.encode(
    {"sub": "admin", "role": "admin"},
    "recovered-secret",
    algorithm="HS256",
)
print(token)

jwt.encode returns the finished token string in PyJWT 2.x. Send it to the server, which verifies it with the same secret you cracked, and it is accepted as a legitimately signed token. From here you set role, sub, or any claim the app trusts, and extend exp if you want the token to last.

Why a random key kills this

The attack works only because the secret is guessable. A secret with real entropy, generated from a cryptographic random source and at least 256 bits, is not in any wordlist and not reachable by brute force in a meaningful timeframe. The fix is therefore not a filter or a WAF rule, it is the key itself: rotate to a long random secret and the whole offline-cracking path dies. More in preventing JWT attacks.

Where to go next

If the token is RS256 rather than HS256, there is no secret to crack, but there may be algorithm confusion, which abuses the public key instead. If you want to nominate a key the server did not intend to use, see JWK, JKU, and KID injection. Or crack a real one now on a free target.

← Back to JWT Attacks: The Complete Practical Guide