1. 什么是缓存投毒?
缓存投毒,顾名思义攻击点在于服务端的缓存功能,一些服务端会存储/缓存频繁被请求内容,当多个用户请求同一资源文件时,服务端直接返回事先缓存好的资源,从而减少服务端不必要的工作量。但若缓存的内容是被攻击者篡改过的话,那么所有请求该缓存资源的用户都将受到攻击影响,这就是缓存投毒。
完整的说,缓存投毒分为两步:
- 找到如何从后端服务器获取包含某种危险的payload的响应;
- 确保该响应被缓存在服务器,使得受害者请求服务器缓存的资源时,该缓存可成功传递攻击到受害者主机上。
缓存投毒可以是一种破坏性强且多样性的攻击手段,结合XSS、JavaScript注入、开放重定向等。
该技术首次在2018年的研究论文中《Practical Web Cache Poisoning》提出;进而在2020年的研究论文《Web Cache Entanglement: Novel Pathways to Poisoning》提出之后进一步发展。
2. 缓存投毒原理
首先先了解一下缓存投毒漏洞的实现过程。像之前说的,如果每一个服务器在处理单个请求的时候,都处理并返回一个新的响应包,此时服务器无疑是超负荷运作的(在没有负载均衡的情况下),延迟响应就会随之而来。缓存响应就成为了解决该问题的一种途径。
缓存处于服务器和用户之间,服务端以缓存的形式保存特定请求的响应,通常保存缓存的时间是固定的。如果另一个用户发送一个等效的请求(与缓存对应的请求),那么缓存直接被作为响应返回给用户,而不需要再与后端服务器产生任何交互。这通过减少服务器处理的重复请求的数量,大大减轻了服务器上的负载。
- 当用户想web发起请求时,首先进到Cache服务接受处理,Cache首先判断有没有该请求对应的缓存响应直接返回给用户,若有则直接返回缓存的资源;若没有则发送给后端服务器处理请求。其中,Cache通过对比预先定义好的“缓存键”来标识等效的请求是否已缓存资源,通常包含在请求头中,例如下面请求头中的Cache-Control、Pragma或Set-Fetch-*等:
GET /image.png HTTP/1.1
Host: ***.cn
Connection: close
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=21A62****
- 如果传入请求的缓存键与上一个请求的缓存键匹配,则Cache会认为它们是等效的。因此,服务端直接返回缓存资源给请求客户端。这适用于具有匹配缓存键的所有请求,直到缓存过期。
缓存投毒与缓存欺骗的区别:
缓存投毒,攻击者诱导服务器存储一些恶意的缓存内容,且这些恶意内容会作为缓存影响到其他用户。
缓存欺骗,攻击者诱导服务器存储一些其他用户的敏感缓存内容,且攻击者接着从缓存中获取这些敏感内容。
3. 缓存投毒的影响
影响危害很大程度上取决于两个因素:
- 攻击者可以成功缓存哪些内容。与大多数类型的攻击一样,缓存投毒也可以与其他攻击结合使用,以进一步升级潜在影响,例如Host头攻击+缓存投毒可造成类似存储型XSS的攻击效果等。
- 投毒页面的流量大小。若该投毒攻击可能会影响成千上万的用户,即便缓存会在一定时间后失效,但攻击者仍可以通过编写脚本进行无限制的缓存投毒。
4. 缓存投毒攻击过程
总的来说,基本的缓存投毒攻击主要分为三个过程:
4.1 发现缓存键
在缓存投毒收到缓存键的影响,例如在headers请求头中的某个字段A可使服务端对该请求(匹配缓存键)进行缓存,在之后所有匹配缓存键A的请求都将会直接收到服务端的缓存资源,而不用经过服务端的处理。
一般的方法是通过随机修改请求头的某个字段,看是否会对响应包造成影响。如修改的随机值直接出现在响应包中(类似XSS);或响应包发生改变。如下,存在缓存键X-Forwarded-Host:
GET / HTTP/1.1
Host: ***.net
Connection: close
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: */*
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
X-Forwarded-Host: meta.com
响应包成功缓存,且缓存键的值meta.com出现在响应包中:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=*****; Path=/; Secure; HttpOnly
Connection: close
Cache-Control: max-age=30
Age: 3
X-Cache: hit
X-XSS-Protection: 0
Content-Length: 10795
<!DOCTYPE html>
<html>
<head>
<link href=/resources/css/academyLabHeader.css rel=stylesheet>
<link href=/resources/css/labsEcommerce.css rel=stylesheet>
<title>Web cache poisoning with an unkeyed header</title>
</head>
<body>
<script type="text/javascript" src="//meta.com/resources/js/tracking.js"></script>
为了方便自动化测试缓存键参数,推荐使用burp的Param Miner插件:
在手工/使用插件识别缓存键的同时,此时的响应若产生缓存,会发送给其他请求匹配该缓存键资源的真实用户,导致fuzz失败,这时只需要在请求的GET参数中加上我们指定的唯一参数,如:
GET /index.php?meta=qweqwe HTTP/1.1
......
此时,该包含meta参数请求的缓存(若能缓存)只会返回给我们。
4.2 缓存响应
发现缓存键之后,这时候就需要理解如何造成造成服务端的资源缓存。如修改的请求头Host值直接出现在响应包中,且响应包中的某个资源文件是由Host值来控制的,就会造成该资源文件的缓存投毒:
恶意Host值 -> 服务端通过该Host缓存特定的恶意资源文件 -> 服务端分发该恶意资源文件给请求用户
5. 缓存投毒——基础Labs
5.1 缓存投毒&XSS
- 利用一
与普通的XSS无异,通过单纯的反射XSS配合持久化的缓存投毒,可以达到存储XSS的攻击效果。
譬如有一个正常的请求如下:
存在缓存键X-Forwarded-Host:
现在需要指定资源文件将他缓存,且注意需要加上一个唯一GET参数,确保缓存特定请求(便于实验,实战中可以改为常用的请求参数):
在X-Forwarded-Host中打出XSS,并将此资源请求缓存:
当有用户使用 https://acd21f5a1f1b4e2f800c289d00b000a3.web-security-academy.net/?asdfw=2222 请求访问时,即会XSS:
当有用户使用 https://acd21f5a1f1b4e2f800c289d00b000a3.web-security-academy.net/?asdfw=2222 请求访问时,即会XSS:
- 利用二:
当然常用的利用方式还有:通过Host指定服务端缓存攻击者服务器上的恶意资源文件(如js等文件)。
首先攻击服务器上预先放置好同名的资源文件:
然后利用Host重定向资源文件 /resources/js/tracking.js 并缓存:
当用户请求 https://acd21f5a1f1b4e2f800c289d00b000a3.web-security-academy.net/?meta=whatfa 时候,即会被投毒:
5.2 缓存投毒&Cookie缓存键
缓存键可能存在很多地方,如Cookie中。
- 利用一
通过Param Miner插件可快速定位缓存键:
- 利用二
也可以通过手工观察发掘,譬如正常请求后,发现响应包中存在 Set-Cookie: fehost=prod-cache-01字段,且其值出现在响应包体中:
思考: 响应包中出现 Set-Cookie: *** 很大几率是由于请求包的Cookie中没有该字段导致的。(研究过Shiro漏洞原理的都知道),那么尝试在请求头中加上可控Cookie值,看是否在响应包中输出:
果不其然是可控的。
接下来就直接给打缓存:
该请求已被投毒,访问即alert:
5.3 缓存投毒&多个缓存键
多个缓存键存在的情况,用插件扫描时间性价比较高:
单单看此字段,可以对响应包造成影响或产生改变:
但是没有可控参数控制Location重定向,这样的缓存利用起来有点鸡肘,继续寻找到其他缓存键:
X-Forwarded-Host 和 X-Forwarded-Scheme 可造成缓存投毒,继续followdirect过去,发现页面直接重定向到X-Forwarded-Host上了:
理解了缓存过程,接下来可顺利进行投毒了:
- 在攻击者服务器上建立同名资源文件,这里选择 /resources/js/tracking.js
- 将X-Forwarded-Scheme 置为https;X-Forwarded-Host改为攻击者服务器地址。请求资源文件GET /resources/js/tracking.js,并缓存投毒,使其重定向到攻击者服务器:
- 之后所有请求 /resources/js/tracking.js 资源文件的请求都被重定向到攻击者服务器的恶意资源文件,例如访问主页,因为主页中加载了该资源文件,服务端直接返回被投毒的缓存造成XSS攻击:
5.4 缓存投毒&缓存定位
何为缓存定位?我这么定义如下一种现象:一些服务端(对同一请求)并不是针对所有请求都会返回缓存的资源。譬如,服务端缓存了资源A是Web端的html资源;同时又缓存了资源B是Mobile端的html资源,那么在决定是否分发缓存资源的时候,就要通过User-Agent头来判断是否应该分发缓存资源?分发哪一种资源?这样就避免了将非Mobile端的缓存资源分发给Mobile端。
只有了解了服务端的缓存过程,就能够大致达到精准攻击某一部分的群体。
如,通过插件找到缓存键 X-Host:
同样,我们在攻击者服务器上建立同名资源文件,利用X-Host重定向到该资源文件,并缓存该请求:
这时候,携带了User-Agent:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
的用户请求该URL就会收到缓存投毒:
假设需要攻击其他类型的用户,就需要定位到其他某类型用户的User-Agent头的值,如何找到该值呢?
来一个抛砖引玉,这里为了形象配合环境获取目标的信息,可在评论处打一个img标签使受害用户访问我们的服务器,从而获取到用户的User-Agent信息:
给Linux的请求用户投毒:
5.5 缓存投毒&DOM
上述Labs的利用情况都仅仅在于加载JavaScript脚本进行缓存投毒,若服务端使用js来从服务端获取/处理动态数据,如请求Json、Html等文件,如果脚本没有限制和处理好请求源,则会导致CORS引发的DOM漏洞。
如,找到缓存键 X-Forwarded-Host,且其值被利用动态向服务端获取Json资源文件:
在initGeoLocate() 方法中,将Json里的Country字段输出到页面中:
这一系列操作若没有限制好请求源,那么就会造成缓存投毒+CORS引发的DOM XSS漏洞。
- 首先在攻击者服务器上建立同名的恶意资源文件 /resources/json/geolocate.json ,并且指定运行跨域请求:
- 然后进行缓存投毒:
- 被投毒的用户界面结果为DOM XSS:
5.6 缓存投毒&DOM多次投毒
当存在一个页面,需要与用户交互才能完成缓存投毒+DOM型利用的情况:
- 首先请求主页的一系列数据包:
看到请求 / 时,通过插件找到缓存键 X-Forwarded-Host,其值用来动态加载 /resources/json/translations.json 资源文件,并交给initTranslations()处理:
- 继续看到 translations.json 的内容:
处理 translations.json文件的函数initTranslations()在请求/resources/js/translations.js中:
最后处理的值输出到页面中:
- 理解了服务端的处理流程,同样的,在攻击服务器上建立同名恶意文件,并设置运行跨域请求:
意在当主页使用español语言时,触发XSS。
(若不设置运行跨域请求,则投毒后会请求失败)
- 对español语言的页面进行投毒,对应的是 GET /?localized=1 请求:
缓存投毒:
当用户切换español语言时,触发XSS:
- 思考,这样交互式的利用显得有点鸡肘,如何让用户访问主页就触发投毒呢?
观察从English切换到español语言时的数据包:
请求了 /setlang/es?(其中es是español语言),同时设置了Cookie:lang=es,然后跳转。 - 继续寻找到请求/还有缓存键 X-Original-URL:
该缓存键的作用是重定向页面:
- 那么现在思路就明确清晰了,利用过程如下:
(由于请求 /setlang/es?,响应包设置了Cookie:lang=es无法进行缓存,但观察可只用反斜杠进行访问,故可以用反斜杠来缓存资源)
只要用户访问主页,就会被重定向到español语言的页面,并触发XSS:
6. 缓存投毒——进阶Labs
在之前的Labs中,主要是通过典型的可控输入(如HTTP头和cookie)来利用web缓存投毒。但这种利用只触及了缓存投毒的表面。
在实际的站点中,一些缓存系统在保存缓存资源前,会进行一系列的处理和转化,包括:
- 筛选出特定的查询参数
- 规范化的输入
在处理缓存键的过程中,有时也会由于设计缺陷逻辑问题而产生缓存中毒漏洞。如,默认情况下经过筛选和提取有效的GET型参数,对页面:
GET / HTTP/1.1
Host: test.com
进行缓存,当有请求:
GET /?a=1 HTTP/1.1
Host: test.com
经过对参数a的处理,认为该a参数无效不影响系统响应,直接返回了GET / (缓存)。
研究缓存实现过程的缺陷,与常规的缓存投毒利用手法不同,通常步骤如下:
- 判断页面是否可缓存。
一般是通过反馈的响应包来判断响应是缓存系统直接分发的还是服务端处理后分发的。常见的有三种形式:
- HTTP头中存在X-Cache:hit
- 多次放包观察响应包大小是否改变
- 每个响应包的时间差异
- 研究服务端如何处理缓存键。
例如,Web根据Host进行跳转,但服务端默认不缓存Host的内容:
GET / HTTP/1.1
Host: vulnerable-website.com
HTTP/1.1 302 Moved Permanently
Location: https://vulnerable-website.com/login
Cache-Status: miss
但是在Host后面加上端口值之后,服务端对该Host进行了缓存:
GET / HTTP/1.1
Host: vulnerable-website.com:1662
HTTP/1.1 302 Moved Permanently
Location: https://vulnerable-website.com:1662/login
Cache-Status: hit
这个思路之前的实验也用过(用 /setlang\es? 激活缓存键),主要是由于服务端与中间件处理不一致引发的。
与GET / 相同的请求还有:
Apache: GET //
Nginx: GET /%2F
PHP: GET /index.php/xyz
.NET GET /(A(xyz)/
- 组合拳
有时候还可以配合缓存系统的其他缺陷综合利用。如当Host后的端口接受非数字时,可配合XSS进行缓存投毒利用。
6.1 缓存投毒&GET参数缓存键
- 一般的缓存系统
与之前的Lab不同,一般的缓存系统遇到不同的参数或参数值就进行缓存。
参数为meta=1时:
参数为meta=2时,同样可进行缓存:
- 可处理参数的缓存系统
当缓存系统处理参数时,会对参数进行筛选和过滤,无论参数和参数值如何变,都返回缓存的页面。
参数meta=1:
参数meta=2:
换不同参数:
可以看到,无论参数或参数值如何变,都不对当前请求重新缓存。这就是处理参数的缓存系统。
通过插件找到缓存键Origin,然后加入Origin之后,发现改变参数可重新进行缓存:
到这里思路就出来了:
通过Origin缓存键来控制参数缓存在页面中,利用手法就和之前的如出一辙。
利用Origin来控制缓存系统,使其缓存每个不一样的GET参数:
测试完毕之后,删除Origin,将缓存投毒至正常用户的请求包格式:
正常用户访问主页即中毒:
出现这种情况的原因可能是缓存系统仅仅对直接请求的数据包进行参数处理,然后再决定是否进行缓存;但当有Origin时,缓存系统不会但参数进行处理而直接进行缓存。
6.2 缓存投毒&特定参数
上一个lab中,整个GET参数都不可直接缓存。还有一些网站只过滤与后端应用无关的GET参数,接收特定的参数进行缓存并使用。
如下,服务端能缓存任何GET参数:
但是使用该缓存,当用户请求正常请求GET /时,无法缓存中毒:
原因很可能是由于参数meta不影响后端应用响应,也就是无关参数。
通过Param Miner插件的"Guess GET Parameters"找到缓存键参数为utm_content:
检测原理
这个缓存键刚刚是通过插件找出来的,检测原理是什么呢?
当确认是缓存键时,响应包中会有Set-Cookie:
而不是缓存键时则不会:
按照这个原理就可以进行爆破GET型参数的缓存键了。
找到参数缓存键,直接投毒XSS:
缓存了恶意内容之后,服务端接受utm_content参数并有效缓存:
用户正常访问主页GET /即中毒:
6.3 缓存投毒&参数混淆
当缓存系统按照白名单处理参数缓存键时,只允许参数接收特定的值,其他值一律不接收缓存。
如,请求的js通过callback来回调函数setCountryCookie,改变callback的值,其值能够出现在响应包中,且缓存系统能够缓存该请求:
说明callback是一个有用的缓存键,但是该缓存不会被使用,可能是由于白名单处理的缘故:
这种情况下一般无法利用了的,但是当还有其他缓存键控制缓存时就有希望。
- utm_content缓存键
通过插件"Param Minier" > “rails parameter cloaking scan” 找到其他缓存键utm_content:
2. 考虑参数混淆的情况,尝试绕过缓存系统的处理机制
当同时输入两个参数名、不同的参数值时,服务端使用了第二个参数:
3. 其他缓存键utm_content
其他缓存键的作用上一个lab已经演示过了。
4. 组合拳
补充:&和;都算一种分隔符。不同的是&常用于URL参数中;而分号常用于后端服务器中。
- 分号(;)不能区分请求中的参数,只是参数内容中的一部分;但后端可作为分隔符处理。
- and符(&)能够区分请求中的参数,是用来分隔参数的符号。
当缓存系统处理参数的时候,只将&作为分隔符进行处理,而将分号作为参数值的一部分;但在传入Web后端的时候,后端将分号也作为分隔符分割了:
而在传入两个相同参数、参数值时,缓存系统默认使用第二个参数值;配合缓存键utm_content可成功缓存回调函数alert(1):
该缓存能够被作为正常响应分发给用户:
大致利用思路就是:缓存系统认真处理好了非预期参数值,但是后端你给我捣乱,取第二个相同参数的值,并将其作为缓存提供响应。
6.4 缓存投毒&fat请求
fat请求是一种少见的情况,它允许在GET请求中使用POST来传递参数,例如一个简单fat请求:
GET /?meta=normal HTTP/1.1
Host: example.com
......
meta=evil
在这种情况下,缓存键将基于GET请求,但参数的值将取自POST。
可以使用插件"Param Miner" > “fat GET”:
缓存投毒:
请求该js的被投毒:
有时也可以强制覆盖使用请求方法:
GET /?meta=normal HTTP/1.1
Host: example.com
X-HTTP-Method-Override: POST
......
meta=evil
6.5 缓存投毒&404中的XSS
相信读者很多时候会遇到这种情况:(滑稽.jpg)
构造的XSS被后端URL加密了,一般是无法利用的。
但是当Web存在缓存功能时,缓存系统会对URL的路径参数进行规范化处理,包含但不限于:筛选过滤参数、URL解密、规范化字符等。经过其处理后,加密的URL用对应的解密算法解密后缓存在缓存系统中,为下一个请求该URL的用户提供缓存响应。
- 缓存含XSS的URL:
- 将该URL发送给受害者:
https://******.net/123</p><script>alert(1)</script><p>foo
6.6 缓存投毒&内部缓存
内部缓存的含义是,web将缓存行为直接实现到应用程序中,单独的作为一个缓存行为与应用程序结合起来。
这样就省去了缓存系统的处理过程,避免了上诉labs的设计缺陷问题。但这些缓存行为的专门为web定制的,所以缓存行为也可能与之前的不同。
对于特殊的缓存行为,使用插件 “Param Miner” > “Guess headers” > “Add dynamic cachebuster” 来寻找特殊的缓存键:
指定X-Forwarded-Host的值,发现响应包中其值出现了两次,分别是analytics.js 和 /?meta=12&2ne0mu1=1:
- 尝试利用一
观察analytics.js,貌似没有可控参数可利用的:
发现geolocate.js存在可控参数callback:
但是这两个js都无法进行缓存。
- 尝试利用二
观察响应包中的geolocate.js,X-Forwarded-Host的值并没有成功映射过去:
思考geolocate.js是不是存在潜在的缓存行为,尝试多次有间隔地发送请求,果然在50s时成功缓存了:
这就回到最开始的缓存投毒利用方法了:通过Host指定服务端缓存攻击者服务器上的恶意资源文件(geolocate.js),然后利用Host重定向资源文件并缓存。
首先攻击服务器上预先放置好同名的资源文件:
继续用X-Forwarded-Host缓存键间歇性请求,使其缓存恶意服务器的host:
用户访问主页即可中毒:
7. 如何防御缓存投毒?
- 关闭缓存功能,用CDN替代;
- 缓存限制为纯静态响应;
- 禁止fat请求;