Brute-forcing JWT HS256 secrets with hashcat
An HS256 token carries everything an attacker needs to verify a guessed secret offline. How weak HMAC keys fall to hashcat -m 16500, and how to forge tokens after.
A JSON Web Token is three base64url segments joined by dots: header.payload.signature. The header names the algorithm, the payload carries the claims (who you are, what you can do, when it expires), and the signature is what stops you from editing the payload and getting away with it. When the header says alg: HS256, that signature is an HMAC-SHA256 over header.payload using a single shared secret. Server signs with it, server verifies with it. See the JWT format breakdown for how the pieces fit together.
That symmetric design is the whole vulnerability.
Why this is an offline attack
With an asymmetric signature, verification needs the public key and forgery needs the private key, which the attacker does not have. HMAC is symmetric. The key that verifies is the key that signs. And the token hands you everything: the signed message is right there as header.payload, the tag is the third segment. So an attacker who captures one valid token can sit offline and try secrets at GPU speed, recomputing the HMAC for each candidate and comparing against the captured signature. No login endpoint, no lockout, no rate limit, no logs. The token is a self-contained oracle.
This is categorically different from cracking a password hash from a database dump, but the economics rhyme: HMAC-SHA256 is a fast primitive, so throughput is enormous and a weak secret does not survive the first wordlist.
Cracking with hashcat -m 16500
Hashcat mode 16500 is JWT. The hash you feed it is the entire token, unmodified.
# token.txt holds one line: the full header.payload.signature
hashcat -m 16500 token.txt /usr/share/wordlists/rockyou.txt
A concrete example. This token is signed with the secret secret:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Drop that single line into token.txt and run the command above. It cracks instantly, because secret is in rockyou near the top. When the dictionary runs dry, escalate to rules or a mask:
hashcat -m 16500 token.txt rockyou.txt -r /usr/share/hashcat/rules/best64.rule
hashcat -m 16500 -a 3 token.txt ?a?a?a?a?a?a?a?a
Weak secrets are the real-world risk
The interesting failure mode is not exotic. It is that an enormous number of services sign HS256 tokens with a secret somebody typed by hand or pasted from a tutorial. The literal string secret. The framework sample value left in a config. your-256-bit-secret, which is the placeholder in the jwt.io debugger and which people ship to production. changeme, the app name, the company name. These fall in the first few seconds of a run because they are already sitting in wordlists, or one rule away from a word that is.
Once you recover the secret, you do not just read tokens. You mint them. The signature stops being a barrier because you now hold the signing key. Change the payload to "role": "admin", set "sub" to another user, push the exp claim years out, re-sign with the cracked secret, and the server accepts it as genuine. That is straight privilege escalation and account takeover from a single weak string. The crack is the whole attack.
Not the same as alg:none or RS256 to HS256
Two other JWT attacks get mentioned in the same breath, and they are worth keeping separate because they are not cracking and they do not depend on a weak secret.
The alg: none attack abuses servers that honor a header claiming the token is unsigned. You strip the signature, set alg to none, and a broken verifier accepts it. The RS256-to-HS256 confusion attack abuses servers that pick the verification algorithm from the attacker-controlled header: you take a service's RSA public key, which is not secret, and use it as the HMAC secret to sign a forged HS256 token, tricking the server into verifying a symmetric signature with material it published. Both are implementation flaws in verification logic. Brute-forcing the HMAC secret is a key-strength problem. Different root cause, different fix. Do not conflate them.
Defending HS256
If you must use HMAC, the secret has to be long and random: 32 bytes or more from a CSPRNG, stored in a secrets manager, never in source. A secret that size puts you out of reach of wordlists and any mask you could finish this decade. Rotate it, and accept that rotation invalidates live tokens, so plan a key-id (kid) scheme to roll over cleanly.
Better, drop symmetric signing for anything crossing a trust boundary. RS256 or ES256 split signing from verification: the private key signs and never leaves the issuer, services verify with the public key, and a leaked public key forges nothing. That removes the entire class of "we cracked your shared secret" findings. HS256 is fine inside one service that owns both ends. The moment a verifier you do not control needs to check your tokens, go asymmetric.