缓存实现缺陷的利用
Last updated
Last updated
在之前的实验中,你学会了如何通过操控典型的非键控输入,如HTTP标头和Cookie,来利用Web缓存投毒漏洞。虽然这种方法有效,但这只是Web缓存投毒的问题的冰山一角。
在本节中,我们将展示如何通过利用特定缓存系统实现的特点,来获取更大的Web缓存投毒攻击面。
特别是,我们将了解为什么缓存键生成方式的缺陷,有时会通过一些传统上被认为不可利用的漏洞,使网站易受到缓存投毒攻击。
我们还将展示如何将经典的技术进一步发挥,毒化应用程序级缓存,这往往带来毁灭性的结果。
这些技术首次由我们的研究总监James Kettle在2020年BlackHat USA上的演讲《Web缓存纠缠:投毒的新途径》中记录。如果你有兴趣了解他是如何在现实中发现和利用这些漏洞的,你可以从我们的研究页面访问该演讲的录像和配套的白皮书。
Research
通常来说,网站从URL路径和查询字符串获取大部分输入。因此,对于各种黑客技术来说,这是一个常见的攻击面。然而,由于请求行通常是缓存键的一部分,这些输入在传统上并未被认为适合用于缓存投毒。任何通过键控输入注入的payload都将作为缓存克星,这意味着你的恶意缓存条目几乎不会提供给其他任何用户。
然而,仔细观察,各个缓存系统的行为并不总是符合预期。实际上,许多网站和CDN在将键控组件保存到缓存键时,会对它们进行各种转换。这可能包括:
排除查询字符串
过滤掉特定的查询参数
规范化键控组件中的输入
这些转换可能会引入一些意想不到的问题。这些问题主要是围绕着写入缓存键的数据和传递给应用程序代码的数据之间的差异,尽管这些数据都是来自于相同的输入。这些缓存键的缺陷可以通过最初看似不可用的输入来毒害缓存。
在完全集成的应用程序级缓存中,这些问题可能更加极端。事实上,内部缓存可能非常不可预测,以至于在进行测试时,有时难以避免在不经意间毒化在线用户的缓存。
探测缓存实现缺陷的方法与典型的Web缓存投毒方法略有不同。这些较新的技术依赖于缓存的具体实现和配置方面的缺陷,可能因站点而异。这意味着你需要更深入地了解目标缓存及其行为。
在这一节中,我们将概述探测缓存以了解其行为并识别潜在缺陷的高级方法。然后,我们将提供一些更具体的例子来说明常见的缓存键缺陷,以及如何利用它们。
方法包括以下步骤:
确定一个合适的缓存预言机
探测键处理
确定一个可利用的gadget
第一步是确定一个适合“缓存预言机”,可以用来进行测试。缓存预言机只是一个提供有关缓存行为反馈的页面或端点。它需要是可缓存的,并且必须以某种方式表明你收到的是缓存响应还是直接来自服务器的响应。这种反馈可以有多种形式,例如:
明确告诉你是否获得缓存命中的HTTP标头
动态内容的可观察变化
不同的响应时间
理想情况下,缓存预言机还会在响应中反映整个URL和至少一个查询参数。这将使得在缓存和应用程序之间寻找解析差异更容易,对于后面构造不同的利用时会很有用。
如果你能确定一个正在使用特定的第三方缓存,你也可以查阅相应的文档。里面可能包含有关默认缓存键是如何构造的信息。你甚至可能会偶然发现一些实用的提示和技巧,例如允许你直接查看缓存键的功能。比如,基于Akamai的网站可能支持Pragma: akamai-x-get-cache-key
标头,你可以使用它在响应标头中显示缓存键:
下一步是研究在生成缓存键时,缓存是否对你的输入进行了任何额外的处理。你要找的是隐藏在看似键控的组件中的额外攻击面。
你应该特别注意正在发生的任何转换。在将键控组件添加到缓存键时,是否排除了什么东西?常见的例子是会排除特定的查询参数,甚至整个查询字符串,以及从Host
标头中删除端口。
如果你足够幸运能直接访问缓存键,你只需在注入不同输入后比较键。否则,你可以利用对缓存预言机的了解来推断是否收到了正确的缓存响应。对于每一种你想测试的情况,都发送两个类似的请求并比较响应。
比如说,我们假设缓存预言机是目标网站的主页。它会自动将用户重定向到一个特定区域的页面。它使用Host
标头动态生成响应中的Location
标头:
为了测试端口是否被排除在缓存键之外,我们首先需要请求一个任意端口,并确保从服务器接收到一个反映此输入的新响应:
接下来,我们将发送另一个请求,但这次我们不指定端口:
如你所见,即使请求中的Host
标头没有指定端口,但我们仍然收到了缓存的响应。这证明了端口被排除在缓存键之外。重要的是,完整的标头仍然被传递到应用程序代码中并反映在响应中。
简而言之,尽管Host
标头是键控的,但缓存对其进行转换的方式允许我们将payload传递到应用程序中,同时仍保留一个“正常”缓存键,并将其映射到其他用户的请求中。这种行为是我们将在本节中讨论的所有利用背后的关键概念。
你可以使用类似的方法来调查缓存对你的输入进行的任何其他处理。你的输入是否以某种方式被规范化?你的输入如何存储?你是否注意到任何异常?稍后我们将使用具体示例来回答这些问题。
到现在为止,你应该对目标网站的缓存行为有了相对扎实的了解,并可能在缓存键的构造方式中发现了一些有趣的缺陷。最后一步是确定一个适当的gadget,你可以将其与缓存键缺陷进行链接。这是一项重要技能,因为任何Web缓存投毒攻击的严重程度,在很大程度上取决于你能够利用的gadget。
这些gadget通常是经典的客户端漏洞,如反射型XSS和打开重定向。通过将这些漏洞与Web缓存投毒结合起来,可以大大提升这些攻击的严重程度,将一个反射型漏洞转变为一个存储型漏洞。你不再需要诱使受害者访问特制的URL,你的payload将自动为访问普通、完全合法的URL的任何人提供。
或许更有趣的是,这些技术使你能够利用许多未分类的漏洞,这些漏洞通常被视为“不可利用的”而没有得到修复。这包括在资源文件中使用动态内容,以及需要浏览器绝不会发送的畸形请求的漏洞。
现在你已经熟悉了高级方法,让我们来看看一些典型的缓存键缺陷以及如何利用它们。我们将介绍:
未键控的端口
未键控的查询字符串
未键控的查询参数
缓存参数隐藏
规范化缓存键
缓存键注入
内部缓存投毒
Host
标头通常是缓存键的一部分,因此,一开始看起来不太可能注入任何类型的payload。然而,一些缓存系统会解析该标头,并将端口从缓存键中排除。
在这种情况下,你可以潜在地使用此标头进行Web缓存投毒。例如,考虑我们之前看到的一个例子,其中重定向URL是基于Host
标头动态生成的。这可能使你能够通过简单地向请求中添加一个任意端口来构造一个拒绝服务攻击。所有浏览主页的用户都会被重定向到一个无效的端口,直到缓存过期,主页将无法正常访问。
如果网站允许你指定一个非数字端口,那么这种攻击可以进一步升级。例如,你可以使用这种方法来注入一个XSS payload。
与Host
标头一样,请求行通常是键控的。然而,排除整个查询字符串是最常见的缓存键转换之一。
如果响应明确地告诉你是否命中了缓存,那么这种转换就相对容易发现,但如果没有呢?这会使动态页面看起来像是完全静态的,因为很难知道你是在与缓存还是服务器进行通信。
要识别一个动态页面,通常可以观察更改参数值对响应的影响。但如果查询字符串是未键控的,那么大多数情况下你仍然会得到一个缓存命中,因此无论你添加任何参数,响应都不会改变。显然,这也使得传统的cache-buster查询参数无效。
幸运的是,还有其他添加cache buster的方法,例如将其添加到一个不干扰应用程序行为的键控标头中。一些典型的例子包括:
如果你使用Param Miner,你还可以选择选项“Add static/dynamic cache buster”和“Include cachebusters in headers”。然后,在你使用Burp手动测试工具发送的任何请求中,它会自动将cache buster添加到常用键控标头中。
另一种方法是观察缓存和后端在规范化请求路径时是否存在差异。由于路径几乎可以保证是键控的,有时可以利用这一点发出具有不同键控但仍然到达相同端点的请求。例如,以下条目可能都被单独缓存,但在后端被视为等同于GET /
:
Apache:GET //
Nginx:GET /%2F
PHP:GET /index.php/xyz
.NET:GET /(A(xyz)/
这种转换有时会掩盖本来非常明显的反射型XSS漏洞。如果渗透测试人员或自动扫描器只接收缓存响应而没有意识到这一点,就会显得页面上似乎没有反射型XSS。
从缓存键中排除查询字符串实际上会使这些反射型XSS漏洞变得更严重。
通常,这种攻击依赖于诱使受害者访问一个恶意制作的URL。然而,通过未键控的查询字符串对缓存进行投毒,将使payload提供给访问本来完全正常的URL的用户。这有可能影响到更多的受害者,而不需要攻击者进一步互动。
LAB
到目前为止,我们已经看到,在某些网站上,整个查询字符串都被排除在缓存键之外。但是,有些网站只排除了与后端应用程序无关的特定查询参数,例如用于分析或提供定向广告的参数。在测试期间,像utm_content
这样的UTM参数是需要检查的好候选者。
从缓存键中排除的参数不太可能对响应产生重大影响。很可能没有任何有用的gadget接受这些参数的输入。话虽如此,有些页面以易受攻击的方式处理整个URL,这使得可以利用任意参数。
LAB
如果缓存从缓存键中排除了一个无害的参数,并且你无法根据完整的URL找到任何可利用的gadget,你可能会认为已经走到了死胡同。然而,事实上,这正是事情变得有趣的地方。
如果你能弄清楚缓存如何解析URL以识别和删除不需要的参数,你可能会发现一些有趣的特性。特别值得关注的是缓存和应用程序之间的任何解析差异。这可能允许你通过将任意参数“cloaking”在排除参数中,从而将其悄悄传递到应用程序逻辑中。
例如,实际标准是,如果参数是查询字符串中的第一个参数,那么它前面会有一个问号(?
),否则以和号(&
)开头。一些编写不佳的解析算法会将任何?
视为一个新参数的开始,而不管它是否是第一个参数。
假设从缓存键中排除参数的算法以这种方式工作,但服务器的算法只接受第一个?
作为分隔符。考虑以下请求:
在这种情况下,缓存会识别两个参数,并将第二个参数排除在缓存键之外。然而,服务器不接受第二个?
作为分隔符,而是只看到一个参数example
,其值是整个查询字符串的剩余部分,包括我们的payload。如果example
的值传递给一个有用的gadget,我们就成功地在不影响缓存键的情况下注入了我们的payload。
类似的参数隐藏问题可能出现在相反的情况下,即后端识别出不同的参数,而缓存没有。例如,Ruby on Rails框架将和号(&
)和分号(;
)都视为分隔符。当与不允许这样做的缓存一起使用时,你可以潜在地利用另一个特性覆盖应用程序逻辑中一个键控参数的值。
考虑以下请求:
顾名思义,keyed_param
包含在缓存键中,但excluded_param
不包含。许多缓存只会将其理解为两个参数,由和号分隔:
keyed_param=abc
excluded_param=123;keyed_param=bad-stuff-here
解析算法移除excluded_param
之后,缓存键将只包含keyed_param=abc
。然而,在后端,Ruby on Rails看到了分号,并将查询字符串拆分成三个独立的参数:
keyed_param=abc
excluded_param=123
keyed_param=bad-stuff-here
但现在有一个重复的keyed_param
。这就是第二个特性发挥作用的地方。如果有重复的参数,每个参数具有不同的值,Ruby on Rails会优先考虑最后出现的那个。最终结果是缓存键包含一个无辜的、预期的参数值,使得缓存的响应可以正常地提供给其他用户。然而,在后端,相同的参数具有完全不同的值,这就是我们注入的payload。正是这第二个值将传递给gadget并反映在毒化的响应中。
如果这个漏洞让你控制一个将要执行的函数,那么它会特别强大。例如,如果一个网站使用JSONP进行跨域请求,通常会包含一个callback
参数,以在返回的数据上执行一个给定的函数:
在这种情况下,你可以使用这些技术覆盖预期的回调函数,并执行任意JavaScript。
LAB
在某些情况下,HTTP方法可能不会被键控。这可能允许你使用包含恶意payload的POST
请求体来投毒缓存。然后,你的payload甚至会作为在响应用户GET
请求时被提供。尽管这种情况非常罕见,但你有时可以通过简单地在GET
请求中添加一个请求体来创建一个“fat“ GET
请求来实现类似的效果:
在这种情况下,缓存键将基于请求行,但服务器端的参数值将从请求体中获取。
LAB
只有当一个网站接受带有请求体的GET
请求时,这才有可能实现,但可能有一些变通方法。例如,你可以通过覆盖HTTP方法来鼓励fat GET
处理:
只要X-HTTP-Method-Override
标头未键控,你可以在保留从请求行派生的GET
缓存键的同时提交一个伪POST
请求。
导入的资源文件通常是静态的,但有些会反映来自查询字符串的输入。这主要被认为是无害的,因为浏览器在直接查看这些文件时很少执行它们,攻击者也无法控制用于加载页面子资源的URL。然而,通过将其与Web缓存投毒结合,你偶尔可以将内容注入资源文件中。
例如,考虑一个页面,它将当前查询字符串反映在导入语句中:
你可以利用这种行为注入恶意CSS,从导入/style.css
的任何页面窃取敏感信息。
如果导入CSS文件的页面没有指定doctype
,甚至可以利用静态CSS文件。在正确的配置下,浏览器将简单地搜索文档寻找CSS,然后执行它。这意味着,你偶尔可以通过触发服务器错误(反映被排除的查询参数)来投毒静态CSS文件:
应用于缓存键的任何规范化也可能引入可利用的行为。事实上,它偶尔会使一些本来几乎不可能的利用成为可能。
例如,当你在一个参数中发现反射型XSS时,通常在实践中是无法利用的。这是因为现代浏览器在发送请求时通常会对必要的字符进行URL编码,而服务器不会对它们进行解码。预期的受害者收到的响应只会包含一个无害的URL编码字符串。
某些缓存实现在将键控输入添加到缓存键时对其进行规范化处理。在这种情况下,以下两个请求将具有相同的键:
这种行为可以让你利用这些本来被认为是”不可利用“的XSS漏洞。如果你使用Burp Repeater发送一个恶意请求,你可以使用一个未编码的XSS payload来投毒缓存。当受害者访问恶意URL时,payload仍然会被他们的浏览器URL编码;然而,一旦URL被缓存规范化,它将具有与包含你未编码payload的响应相同的缓存键。
因此,缓存将提供毒化的响应,payload将在客户端执行。你只需要确保在受害者访问URL时缓存被投毒。
LAB
你有时会在一个键控的标头中发现客户端漏洞。这也是一个经典的”不可利用“问题,有时可以通过缓存投毒来利用。
键控的组件通常被捆绑在一个字符串中,以创建缓存键。
如果缓存没有对组件之间的分隔符进行适当的转义,你就有可能利用这种行为来制作两个具有相同缓存键的不同请求。
以下示例使用双下划线分隔缓存键中的不同组件,但没有对它们进行转义。你可以利用这一点,首先用一个包含payload的请求在相应的键控标头中投毒缓存:
如果随后让受害用户访问以下URL,他们将收到毒化的响应:
LAB
迄今为止,我们已经了解了如何利用外部Web缓存实现方式中的缺陷,来暴露看似键控组件内部隐藏的扩展攻击面。然而,有些网站除了使用独特的外部组件外,还将缓存行为直接集成到应用程序中。这样做有几个优点,比如避免我们之前研究的解析差异。
由于这些集成缓存是为特定应用程序量身定制的,这也给了开发者更大的自由度来调整它们的行为。因此,这些缓存有时会表现出不寻常的行为,这是你在更标准化的、需要与多个应用程序兼容的外部缓存中中通常看不到的。有时,这些奇怪的行为也可能为一些高危缓存投毒攻击提供机会。
有些缓存并不缓存整个响应,而是将响应拆分成可重用的片段,分别进行缓存。例如,导入广泛使用的资源的片段可能被存储为独立的缓存条目。然后,用户可能会收到一个由来自服务器的混合内容以及来自缓存的几个单独片段组成的响应。
由于这些缓存片段旨在于跨多个不同的响应中重用,所以缓存键的概念实际上并不适用。包含给定片段的每个响应都将重用相同的缓存片段,即使响应的其余部分完全不同。在这种情况下,投毒缓存可能会产生广泛的影响,特别是如果你毒化了每个页面都使用的片段。由于没有缓存键,你只需一次请求就可以污染每个页面,对每个用户造成影响。
这通常只需要使用基本的Web缓存投毒技术,例如操纵Host
头。
集成的应用程序级缓存所带来的挑战之一是,它们很难识别和调查,因为通常没有面向用户的反馈。要识别这些缓存,可以寻找一些提示性的迹象。
例如,如果响应同时反映了你发送的最后一个请求的输入和前一个请求的输入,这是一个关键指示,表明缓存存储的是片段而非整个响应。如果你的输入反映在多个不同页面的响应中,特别是在你从未尝试过注入你的输入的页面上,这也适用。
有时,缓存的行为可能会非常不寻常,以至于最合乎逻辑的结论是它一定是一个独特且专门的内部缓存。
当一个网站实现多层缓存时,它可能使得理解幕后发生的事情和了解网站的缓存系统行为变得困难。
LAB
在测试普通的Web缓存时,我们建议使用一个cache buster来防止你的投毒响应被提供给其他用户。然而,如果一个集成缓存没有缓存键的概念,那么传统的cache buster就毫无用处了。这意味着很容易意外地为真实用户投毒缓存。
因此,在测试这类漏洞时,重要的是要尽力减轻潜在的破坏。在发送每个请求之前,请仔细考虑你注入payload的影响。特别是,你应该确保只使用你控制的域来毒化缓存,而不是使用某个任意的evil-user.net
。这样,如果出现问题,你也可以控制接下来的事情。