缓存伪造 缓存投毒 请求伪造
攻击者无需直接接触服务端即可攻击和影响用户行为的安全漏洞 —— Web 缓存污染与请求走私。Web 缓存污染旨在通过攻击者向缓存服务器投递恶意缓存内容,使得用户返回响应结果而触发安全风险。HTTP 请求走私旨在基于前置服务器(CDN、反向代理等)与后置服务器对用户请求体的长度判断标准不一致的特性,构造能够被同一 TCP 连接中其它用户夹带部分恶意内容的攻击请求,从而篡改了受害者的请求与响应行为。两种漏洞均需要通过针对中间件的合理配置与业务接口的合理设计进行排查和防御。
# 缓存污染
简单的来说,就是利用缓存将有害的 HTTP 响应提供给用户
由 CDN 等代理层服务器根据 “缓存键” 缓存用户请求对应的响应,并在某个请求再次到来时直接返回相应的响应包。例如如下场景中,红色字体标识了缓存服务器配置的缓存键内容,A 用户访问服务端返回的结果后,B 用户再次访问仅会取得缓存服务器中的内容,因为缓存服务器认为两者是同一个请求,无需再向业务服务端重新请求一次。

当攻击者的请求中,缓存键和普通用户没有差别,而请求中其它部分存在可体现在响应包中的恶意内容或代码时,该响应被缓存后,其它请求了同一个正常缓存键对应接口的用户会直接得到攻击者提前交给缓存服务器缓存的恶意响应。
所以我们 web 缓存投毒都依赖于非缓存键(因为缓存键是后端判断我们请求是否等效的,所以通常情况下不能改变,也就不能 payload,不然的话跟其它用户就不等效,就无法投毒),缓存投毒的另外一个条件就是,页面存在某个内容是根据非缓存键的值来生成的。
以下是一个简单的例子,业务某个接口存在逻辑:获取用户请求 Host 头的内容,拼接至响应包的 js 链接中作为访问域名。此时攻击者注入恶意域名 http://hack.com,受害者访问缓存资源的时候得到的是和攻击者一样的响应结果。此时攻击者通过 JavaScript 代码几乎劫持了受害者在前端的所有信息和行为,具体的后果则由其中的恶意代码所决定,这与 XSS 的攻击后果是类似的。

Web 缓存能够构造什么样的攻击,取决于在不破坏缓存键的同时,构造能够在响应中体现恶意行为的请求,例如业务逻辑对 Host 头中的值进行校验和请求,但没有校验端口号是否为 443 或 80。此时可以构造请求使得响应跳转至 1337 端口,其它受害者对该接口的访问便不再可用:

例如存在非缓键 X-Forwarded-Host,而页面存在一个 a 标签,它的 href 是根据 X-Forwarded-Host 来生成的
1 | #正常请求 |
当我们利用的时候,找到一个非缓存键并且它的值会影响网页响应包就变得很重要,我们可以使用 Burp 上的插件 Param Miner ,它在 Burp 商店中就可以找到
# 如何确定缓存键的覆盖范
首先需要确认是否存在缓存键:
- HTTP 头直接返回缓存的相关信息
- 观察动态内容的变化
- 返回时间的差异
- 特定的第三方缓存配置头
如何定位缓存键的覆盖范围:
- 对请求 A 改动一处成为请求 B,各自响应有所差异。若请求 B 后得到 A 的缓存结果,则说明 A、B 的缓存键相同,也说明了改动之处并非缓存键。
- 改变请求 A 某处内容发送,响应 cache 头仍然在缓存计时,说明该处内容部分不为缓存键。反之,重新命中,则该处内容包含缓存键。
- 利用特定的头来查询缓存键,例如:Pragma: akamai-x-get-cache-key, akamai-x-get-true-cache-key。
# Web 缓存污染防御手段
# 2.1 禁用缓存配置
对缓存投毒的最强大防御办法就是禁用缓存。
对于一些人来说,这显然是不切实际的建议,但我推测很多网站开始使用 Cloudflare 等服务的目的是进行 DDoS 保护或简化 SSL 的过程,结果就是容易受到缓存投毒的影响,因为默认情况下缓存是启动的。如果对确定哪些内容是 “静态” 的足够确认,那么只对纯静态的响应进行缓存也是有效的。
# 2.2 避免从请求中直接获取输入放在响应中
一旦在应用程序中识别出非缓存键的输入,理想的解决方案就是彻底禁用它们。如果不能实现的话可以在缓存层中剥离该输入,或将它们添加到缓存键。建议使用 Param Miner 等审计应用程序的每个页面以清除非缓存键的输入。
# 请求走私
keepalive
在 http1.1 时代,每个 http 请求都需要打开一个 tcp 连接,keep-alive 可以改善这种状态,提高利用率,即一个长连接,在一次 TCP 连接后不断开连接。 HTTP1.0 的时候没有长连接这个概念,后来引入了长连接并通过 Connection: keep-alive 实现。
但 HTTP1.1 的规则中,所有 HTTP 报文都必须是持久的,除非特意加上 Connection: close ,但实际中很多服务器和浏览器还保留着 Connection: keep-alive
pipline
在 1 个 Tcp 连接中发送多个请求
Content-Length
HTTP 包的一个标头,用来指明发送给接收方的消息的大小
Transfer-Encoding
传输编码
假设我们一个 TCP 连接上,存在多个 HTTP 报文,我怎么知道哪些内容属于第一个报文,哪些是第二个的呢?这个时候 Content-Length 的作用就来了,Content-Length 来告诉对方包的请求体的数据长度。
但是实际情况中, Content-Length 获得起来会存在一些问题,例如一些文件,需要计算其长度就大大增加了内存的消耗,而且当 Content-Length 的数值多或者少的时候都会发生问题。

这个时候 Transfer-Encoding 的优势就来了,它的值为 chunked 时,表示使用分块编码,一个块包含十六进制的长度值和数据,用 0 长度块表示结束块,如下图所示。
在 RFC2616 的第 4.4 节中,规定:如果收到同时存在 Content-Length 和 Transfer-Encoding 这两个请求头的请求包时,在处理的时候必须忽略 Content-Length。但实际处理往往没有遵守该协议。HTTP 请求的开头与结束标志可以通过 Content-Length 来决定,也可以通过声明的 Transfer-Encoding: chunked 对 HTTP 分组来决定。当前置服务器和后置服务器对 HTTP 请求开头和结束标志的判断处理标准不一致时,就可能导致攻击者前一个请求的后半部分在后置被认为是下一个请求的开头,从而出现 HTTP 走私漏洞。
根据前后置服务器的不同请求体长度判断组合模式,有以下攻击场景:
(CL == Content-Length,TE == Transfer-Encoding)前后端在这两种获取长度的方法之间选取一种。

- 前后置服务器对请求体长度的判断标准存在 Content-Length 和 Transfer-Encoding 两种形式
- 前置服务器和后置服务器之间存在 TCP 连接重用,混杂多个用户的请求
# content-length 获取长度
请求体中每个字符为一个字节的长度,换行符包含 \r 和 \n 两个字节长度,Content-Length 标识请求体从开头到最后一个字符的总长度:
1 | POST /search HTTP/1.1 |
请求体中每个字符为一个字节的长度,换行符包含 \r 和 \n 两个字节长度。每段请求内容分别由一行 16 进制长度值和一行内容本身所组成,例如 “q=smuggling” 长度为 11(16 进制:b),“q=smuggle” 长度为 9(16 进制:9)。内容结束后,以 0 和两个换行符结束请求体:
# Transfer-Encoding: chunked 获取长度
1 | POST /search HTTP/1.1 |

# CL-TE
此时,业务前置服务器取用户请求头中 Content-Length 的值为长度判断标准,后置服务器根据 Transfer-Encoding: chunked 解析请求体来判断请求体长度。
如下所示,攻击者构造的请求,前置服务器认为有 6 个字符,包含了 0 和两个换行 以及 G。而后置服务器则根据 Transfer-Encoding: chunked 解析请求体,认为 0 和两个换行符已经是请求的结束标志,字符 G 被滞留在了 TCP 管道中。
此时同一个 TCP 连接中中,一个受害者的请求接踵而至,后置服务器便会将字符 G 拼接至其请求开头,从而使得受害者实际对业务请求了 GGET 方法。


如之前的原理图那样,TCP 传递的这些包不是来自一个人的,比如我这里用火狐当做黑客视角,用星愿浏览器当做普通用户视角。
- 黑客用火狐,抓包,改,发包
- 普通人用星愿去访问这个站,直接拒绝服务
# TE-CL
与 CL - TE 类似,前置服务器先根据 Transfer-Encoding: chunked 放行攻击者的整个请求体,经过后置服务器对 Content-Length 的判断,分割前半部分请求体给业务服务端,后半部分由受害者承接。
可以看到,这里攻击者完全可以劫持受害者的请求,从接口地址到请求头以及请求参数,因此具有较大的危害性。

注意不能这样构造,将 0 也算了进去,因为既然后端是根据 CL 来处理请求的,它不是分块传输,自然就不认识 0 截断块,所以统统当字符串处理。


前端是通过 TE 来处理的,这个 0 还不能扔,那么这种情况就需要我们自己把 GPOST 写出来

因为这里我们将 CL 的长度改成了 4,所以 5,c,/n,/r,那么后边的 GPOST 开头的数据就合并到了后边的数据包中,就将后边数据包的请求方式给覆盖了,还有注意数据块的长度要计算正确,如第一块是从 G 开始到 9 结束。
# TE-TE
这种情况就是前后端都是用 TE 来处理请求,但是我们可以通过混淆 TE 头方式让后端不再根据 TE 处理而是变成了根据 CL 处理。
这里我写了两个 TE 头,不过第二个头后边的 E 是小写,而且值,我瞎写了个 low,这样后端发现了两个,而且值不同,不知道用哪个了,然后看见包里有 CL 那干脆就用 CL 头来处理包。

# HTTP 请求走私攻击的效果
这里举一个例子 —— 捕获其他用户的请求。攻击者发现业务存在 HTTP 请求走私漏洞,同时又找到了评论接口 /post/comment 这种可以回显请求内容的功能点。那么攻击者就可以走私一个 /post/comment 的评论请求,从而让受害者 “被迫” 以这个请求去访问服务端。受害者的请求则被拼接到评论请求中的 comment 参数后,作为请求内容而出现。
攻击者去查看评论区,发现受害者已经将自己先前的请求(连同 Cookie 等信息)一并发送到了评论区。

这里受害者的请求中一旦出现 “&” 字符,就会被当做 POST BODY 的参数分隔符从而中止 comment 评论内容参数的解析。因此评论区仅能看到受害者请求中第一个 “&” 字符之前的内容。
我们这里可以使用 Burp 插件商店里面的 HTTP Request Smuggler
# HTTP 请求走私防御手段
# 4.1 通用防御措施
- 禁用代理服务器与后端服务器之间的 TCP 连接重用。
- 使用 HTTP/2 能够避免请求边界判定标准不一致的问题。
- 前后置服务器使用同样的 web 服务器程序,保证对请求边界的判断标准是一致的。
# 4.2 实际问题
以上的措施有的不能从根本上解决问题,而且有着很多不足,就比如禁用代理服务器和后端服务器之间的 TCP 连接重用,会增大后端服务器的压力。使用 HTTP/2 在现在的网络条件下根本无法推广使用,哪怕支持 HTTP/2 协议的服务器也会兼容 HTTP/1.1。从本质上来说,HTTP 请求走私出现的原因并不是协议设计的问题,而是不同服务器实现的问题。因此要严格保证前后置服务器对请求边界的判断标准是一致的来防护该类型风险的出现。
Web 缓存污染能够使攻击者批量影响共用了同一缓存资源的所有用户,HTTP 请求走私能够使得攻击者随机在长连接中影响其他同时访问业务用户的请求内容,实际造成的影响取决于存在漏洞的接口和业务本身提供了多少能够利用的权限和功能。
因此,如果说有哪种漏洞能够在不直接攻击业务服务器和受害者电脑就能够实施大批量的攻击利用,从而影响到用户请求和收到的响应内容,则 Web 缓存污染和 HTTP 请求走私会是我们重点关注的核心风险问题。
https://zhuanlan.zhihu.com/p/613534137
【由浅入深_打牢基础】WEB 缓存投毒(上) - FreeBuf 网络安全行业门户
秒懂 Http 请求走私 - FreeBuf 网络安全行业门户
缓存投毒 – 学习笔记 - CSDN 博客
HTTP Host 头攻击 – 学习笔记_http 头 hostname 攻击_angry_program 的博客 - CSDN 博客