JWT Attacks: The Complete Practical Guide
JWT Algorithm Confusion (RS256 to HS256)
How to forge a JWT by tricking an RS256 verifier into treating the RSA public key as an HMAC secret, how to get the public key, and the exact-bytes gotcha.
Published
Algorithm confusion is the elegant key attack: you forge a token without cracking anything, by abusing the fact that an RSA public key is, by design, public. If a server verifies RS256 tokens with its public key but can be talked into verifying HS256, you sign an HS256 token using that public key as the HMAC secret, and the server's own verification matches it. This page walks it, including the byte-for-byte detail that trips most people up. Practice on a free live target.
RS256 versus HS256
Two algorithm families matter here:
HS256is symmetric HMAC. One secret both signs and verifies. The secret must stay private.RS256is asymmetric RSA. A private key signs, a public key verifies. The public key is meant to be shared freely, for example so clients can validate tokens.
The vulnerability lives in the seam between them. A server that verifies RS256 holds the public key in a variable it treats as "the key." If that same server will also honour an HS256 token, it HMACs the token using "the key," which is the public key. And the public key is something you can get too.
Why it works
Verification should be pinned: a server that issues RS256 should only ever verify RS256. The bug is a server that reads the token's alg header and switches behaviour. Send it HS256 and it computes an HMAC of the token using the public key bytes as the secret, then compares that to your signature. If you produced your signature by HMAC-ing with the same public key bytes, the two match, and the token is accepted as valid. You never needed the private key, only the public one everyone is allowed to have.
Step 1: get the public key
Collect the server's RSA public key. In order of ease:
- A JWKS endpoint, often at a well-known path:
curl https://target.tld/.well-known/jwks.json- Other common locations:
/jwks,/keys, an OpenID Connect discovery document, or a certificate the site already serves. - If none are exposed, the public key can be recovered from two captured tokens signed with the same key, because an RSA signature leaks enough to reconstruct the modulus. Silent Signal's
rsa_sign2ndoes this, most easily through PortSwigger's Docker wrapper:
docker run --rm -it portswigger/sig2n <token1> <token2>This prints one or more candidate public keys in PEM form, each with a matching forged token, because several modulus values are possible. Test each candidate against the server.
Whichever route you take, you end up with a PEM public key file (public.pem). A JWKS gives you the n and e values, which tooling turns into PEM.
Step 2: sign HS256 with the public key as the secret
Now build an HS256 token and HMAC it using the contents of the public key file as the secret. Conceptually:
signature = HMAC-SHA256( base64url(header_alg_HS256) + "." + base64url(payload), contents_of_public.pem )jwt_tool does the whole thing, taking your PEM and producing the forged token:
python3 jwt_tool.py <TOKEN> -X k -pk public.pem-X k is the key-confusion exploit and -pk supplies the public key file. The HMAC key is the raw PEM bytes in both tools. Doing this by hand in Burp's JWT editor, you paste the public key into a symmetric-key JWK whose k field is base64url, so Burp stores the base64 of the PEM and decodes it back to the same raw bytes before signing. jwt_tool takes the PEM file directly.
The exact-bytes gotcha
This is where the attack usually fails, so get it right. The HMAC secret must be the public key in the exact byte form the server loads it as. In almost all cases that is the full PEM text, including the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- armor lines and the trailing newline, exactly as stored on disk. If the server reads its key from a .pem file, your HMAC secret has to be those identical bytes, armor and newline included.
Common ways it goes wrong:
- Dropping the header and footer lines, or the trailing newline.
- Using the wrong PEM flavour (a PKCS#1
RSA PUBLIC KEYversus an X.509PUBLIC KEY). Match what the server uses. - Re-deriving the key into a slightly different encoding than the server's stored copy.
When the forged token is rejected, the first thing to check is whether your secret bytes match the server's key file byte for byte. A single differing newline breaks the HMAC.
Step 3: send it and confirm
Replay the forged HS256 token. A vulnerable server verifies it with its public key, the HMAC matches, and your tampered claims are trusted. Confirmation is the same behavioural signal as any forge: you are now the role or user you set in the payload.
Fixing it
- Pin the algorithm. A server that issues
RS256must verifyRS256only and rejectHS256outright. This single control kills algorithm confusion. - Use separate verification keys per algorithm so a key can never be reused across HMAC and RSA.
- Never let the token's
algchoose the verification path.
Full detail in preventing JWT attacks. Next, nominate a key the server never intended with JWK, JKU, and KID injection, or try confusion on a free target.