$npx -y skills add elementalsouls/Claude-BugHunter --skill hunt-jwt-cryptoHunt JWT cryptographic failures — alg:none signature-stripping and RS256→HS256 key-confusion that let an attacker forge a token for any identity (e.g. an admin) without knowing a secret. Use when the app authenticates with a JSON Web Token (an eyJ... Bearer token in the Authori
| 1 | # HUNT-JWT-CRYPTO — Forgeable JSON Web Tokens (A04 Cryptographic Failures) |
| 2 | |
| 3 | ## What actually pays |
| 4 | |
| 5 | A JWT is `header.payload.signature`, each base64url. The signature is the only |
| 6 | thing stopping you from editing the payload (your identity/role) and replaying |
| 7 | it. It pays **High/Critical** when the verifier can be tricked into accepting a |
| 8 | token you forged — so you become another user or an admin without their secret. |
| 9 | |
| 10 | Two classic, generic verifier flaws: |
| 11 | |
| 12 | - **`alg:none`** — the verifier trusts the token's own `alg` header. Set |
| 13 | `alg:"none"`, drop the signature, edit the payload (e.g. `role:"admin"`, |
| 14 | another user's `id`/`email`). A broken verifier skips signature checking. |
| 15 | - **RS256 → HS256 key confusion** — the token is signed RS256 (asymmetric). The |
| 16 | RSA **public** key is, by definition, public. If the verifier lets you choose |
| 17 | HS256, it will use that public key as the HMAC *secret* — which you also know. |
| 18 | Sign an edited payload with HS256 using the public key and it validates. |
| 19 | |
| 20 | ## Recon — is this app JWT-based? |
| 21 | |
| 22 | ``` |
| 23 | Login/token responses containing "token":"eyJ..." or Set-Cookie: token=eyJ... |
| 24 | Authorization: Bearer eyJ... on authenticated requests |
| 25 | A JWKS / public-key endpoint: /.well-known/jwks.json, /jwks, public-key in the JS bundle |
| 26 | ``` |
| 27 | |
| 28 | Decode the header (base64url the first segment). `"alg":"RS256"` → try key |
| 29 | confusion. Any alg → always try `alg:none` first; it's free. |
| 30 | |
| 31 | ## Forging the token (never hand-encode base64 — use a JWT tool) |
| 32 | |
| 33 | Use a purpose-built tool so encoding/signing is correct: **jwt_tool** |
| 34 | (`jwt_tool <token> -T` to tamper interactively, `-X a` for alg:none, `-X k -pk |
| 35 | public.pem` for key confusion), Burp's **JWT Editor** extension, or a few lines |
| 36 | of **PyJWT**. Each forge below is the concept plus the claim to edit. |
| 37 | |
| 38 | **alg:none — become admin / another user** |
| 39 | ``` |
| 40 | header: {"alg":"none","typ":"JWT"} |
| 41 | payload: {"data":{"id":1,"email":"admin@target.example","role":"admin"}} |
| 42 | signature: (empty — keep the trailing dot: header.payload. ) |
| 43 | ``` |
| 44 | Some verifiers reject lowercase `none` but accept `None`/`NONE`/`nOnE` — try case variants. |
| 45 | |
| 46 | **RS256 → HS256 key confusion — once you have the RSA public key** |
| 47 | ``` |
| 48 | 1. Obtain the server's RSA public key as PEM. Sources: /jwks.json or |
| 49 | /.well-known/jwks.json (convert the JWK to PEM), a public-key file in the JS |
| 50 | bundle, or recover it from two captured tokens (e.g. jwt_tool / rsa_sign2n). |
| 51 | 2. Re-sign an EDITED payload with HS256, using that PEM as the HMAC secret: |
| 52 | jwt_tool <token> -X k -pk public.pem |
| 53 | payload edit: {"sub":"administrator"} (or role:"admin" / another user's id) |
| 54 | ``` |
| 55 | |
| 56 | **kid header injection — verifier loads the HMAC key from a FILE named by `kid`** |
| 57 | ``` |
| 58 | header: {"alg":"HS256","kid":"../../../../../../../dev/null"} |
| 59 | secret: "" (contents of /dev/null = empty string → sign HS256 with an empty secret) |
| 60 | payload: {"sub":"administrator"} |
| 61 | ``` |
| 62 | Traverse out of the keys directory first. `kid` can also carry SQLi / command |
| 63 | injection / SSRF if the key lookup hits a DB / shell / URL — same idea: `kid` is |
| 64 | attacker-controlled and reaches a dangerous sink. |
| 65 | |
| 66 | **jku / x5u header injection (RS256) — verifier fetches the public key from a URL in the token** |
| 67 | ``` |
| 68 | 1. Host a JWKS containing a public key you control, on a server the verifier can reach. |
| 69 | 2. Set the token's `jku` (or `x5u`) header to that URL and sign the edited payload |
| 70 | with YOUR matching private key. |
| 71 | 3. If the verifier allowlists jku hosts, chain an open-redirect or SSRF-reachable |
| 72 | path on the target's OWN domain so the fetch resolves to your JWKS. |
| 73 | ``` |
| 74 | |
| 75 | Match the `payload` shape to a REAL token from the app (decode one first) — keep |
| 76 | its claim names, only change identity/role. A payload the app can't parse fails |
| 77 | for the wrong reason and wastes the attempt. |
| 78 | |
| 79 | ## Drive to the ADMIN objective — do not stop at a working forge |
| 80 | |
| 81 | A forge that loads YOUR own `/my-account` is NOT the goal — it just proves the |
| 82 | forge mechanism works. The objective is almost always **admin** (reach an |
| 83 | admin-only page and perform an admin action, e.g. delete a user). Once any forge |
| 84 | is accepted, IMMEDIATELY escalate — change identity to admin AND aim at the admin |
| 85 | endpoint. Do not keep re-forging `/my-account` or re-logging-in; that is drift. |
| 86 | |
| 87 | Fixed escalation sequence (run it in order, do not loop |