Web缓存投毒与缓存欺骗

Web缓存投毒(Web Cache Poisoning)

缓存技术简介

Web缓存位于用户和应用程序服务器之间,用于保存和提供某些响应的副本

在下图中,我们可以看到三个用户一个接一个地获取相同的资源:

FG8EL$L(J_9A(E_F`J%(2X8.png

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

Vary简介

Vary,HTTP协议中的一个响应头,简单来说就是告诉缓存服务器,在哪些HTTP头不同的情况下,缓存需要被重新获取。

如果服务端响应头中包含:Vary: User-Agent。这时候,缓存服务器就会按照请求头里的User-Agent来缓存内容,如果两个浏览器User-Agent相同,他们就会命中同一个缓存。

User-Agent在这也就变成了缓存键

Django开发的网站,如果使用了SessionAuthenticationMiddleware,就会返回Vary: Cookie头。这种情况下,如果前端架设了缓存服务器,Cookie不同的用户的缓存页面就不会相同了。这样就可以避免缓存欺骗的漏洞(下文会详细描述)

漏洞简介

攻击者使缓存服务器存储了有害的页面,此时正常用户如果命中了这个缓存,将会被有害页面攻击

基础例子

作为一个网站开发和运维,如果你自己来部署缓存的这一套东西,你会怎么做。比如,我开发了一个面向全球读者的网站,在用户访问相同Path的情况下,后端根据用户浏览器的语言设置(也就是请求头中的Accept-Language)返回不同语言的页面;另外,为了兼容IE等老浏览器,用户的User-Agent不同,后端返回的页面也可能不同。 所以此时,后端会返回这样的头:Vary: User-Agent, Accept-Language。那么,缓存服务器就根据这几个头的值,加上用户请求的PATH,最终构成比如memcached里的“键名”。

所以,缓存中的键名就是:request.path + request.headers[‘HTTP_ACCEPT_LANGUAGE’] + request.headers[‘HTTP_USER_AGRNT’]。下次遇到这三个值均相同的请求后,直接返回缓存中的值,不再请求后端服务器。

在后端逻辑复杂以后,就有可能存在一种情况:页面里输出了没有被缓存服务器考虑到的HTTP头。这种情况下,将可能导致被缓存投毒攻击。

1
2
3
4
5
6
7
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<title>Sample</title>
<link rel="stylesheet" href="//<?php echo $_SERVER['SERVER_NAME']; ?>/static/css/main.css">
...

在部分情况下中$_SERVER[‘SERVER_NAME’]是可能可以被通过Host头控制的,所以这里存在一处XSS漏洞。但这是一个self xss漏洞,因为攻击者无法控制用户的HTTP头

正常情况下并不会有太大问题,但如果这个页面被架设在上面例子中的缓存服务器后,就可能出现问题。由于Host头没有被缓存考虑到,第一个用户通过如下数据包访问:

1
2
3
GET / HTTP/1.1
Host: example.com"><script>alert(1)</script>
...

于是缓存服务器上存储了如下HTML:

1
2
3
4
5
6
7
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<title>Sample</title>
<link rel="stylesheet" href="//example.com"><script>alert(1)</script>/static/css/main.css">
...

此时,相同浏览器、相同语言的用户访问这个页面,就会触发XSS漏洞。于是,通过缓存投毒,一个self xss被成功转化成存储型XSS了。可以将其扩展到很多存在self xss的场景中,也可能存在于只有自己能够利用的URL跳转漏洞里。

实战案例

https://www.anquanke.com/post/id/156356#h3-10

https://www.anquanke.com/post/id/156551

防御措施

  1. 直接禁用缓存功能(有点不切实际)
  2. 只对纯静态的内容响应进行缓存
  3. 禁用非缓存键的输入

Web缓存欺骗(Web Cache Deception)

漏洞简介

Web缓存是指Web资源以副本的形式介于Web服务器和客户端之间,当下一个相同请求来到的时候,会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。在实际应用中,web缓存十分常见,主要是Web缓存有着如下的优点:产生极小的网络流量,减少对源服务器的请求,降低服务器的压力, 同时能够明显加快页面打开速度。缓存分为以下几种类型:(1)数据库缓存,当web应用的数据库表繁多,为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次从内存缓存直接返回,比如memcached(2)浏览器缓存,浏览器会将一些页面缓存到客户端 ,不同的浏览器有着自己的缓存机制。(3) 服务端缓存:常见的服务端缓存比如:CND、Squid、Nginx反向代理等。

Web Cache Deception的核心是,攻击者去欺骗缓存服务器,让它缓存了本来不应该缓存的页面,导致敏感信息泄露。

基础例子

假设我们要访问的某个网站使用了服务器缓存技术,架构如下:

1
User              Nginx Proxy             Apache+PHP

当注册的用户成功登入了该网站,会跳转到自己的账户页面my.php,该Nginx反向代理服务器会将css、js、jpg等静态资源缓存到nginx设定的目录下。受害者不小心在浏览器中输入了如下的url:http://victim.com/my.php/favicon.ico , 但favicon.ico 并不存在

Nginx反向代理服务器发现url以静态文件的扩展名(.ico)结尾,由于favicon.ico 不存在,它的缓存机制会将 my.php 缓存到缓存目录中,这时攻击者访问了:http://victim.com/my.php/favicon.ico ,之前 缓存的帐户页面便成功返回给了攻击者。

利用条件

1.访问http://victim.com/my.php/favicon.ico 页面时, Web服务器返回了该my.php的内容

  1. 服务器的缓存机制通过url中的扩展名来判断是否进行缓存文件,并且忽略任何缓存头。

3.受害者必须访问过了http://victim.com/my.php/favicon.ico 这种页面,也就是说受害者已经将my.php的内容缓存到了缓存服务器上。

要想满足以上几个条件,需要考虑到不同的web服务器、代理机制以及浏览器着各自的特性。比如:我们在tomcat服务器上访问http://victim.com/my.jsp/1.css,服务器无法返回my.jsp的内容,因此这种攻击无法利用在tomcat+java上面。

深入理解

Nginx + php

缓存服务器,通常用来缓存静态文件,这个时候我需要告诉他用户访问哪些内容的时候应该被缓存,比如我们用Nginx做一个简单的缓存服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
http {
# ...

proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:10m
inactive=24h max_size=1g;
server {
# ...

location \.(css|js|jpeg|jpg|png|gif)$ {
proxy_pass http://10.2.122.1;
proxy_set_header Host $host;
proxy_buffering on;
proxy_cache STATIC;
proxy_cache_valid 200 1d;
}

location / {
proxy_pass http://10.2.122.1;
proxy_set_header Host $host;
}
}
}

解释一下这个配置文件做了什么。Nginx将数据包反向代理到内网的服务器10.2.122.1中,并根据请求的PATH,将请求分成两种类型:当后缀是静态文件(包括css、js和图片)时,Nginx将开启缓存,并设置当响应status是200的时候缓存1天时间;其他情况下,不使用缓存。

这是一个比较常见的逻辑,假设10.2.122.1所在的后端服务器是一个PHP开发的站点,用户访问 http://10.2.122.1/profile.php 即可查看自己的个人信息。

PHP支持一种叫PATH_INFO的CGI变量的,也就是当用户请求一个已存在的PHP脚本时,在后面加上/DATA,从/开始的数据将会保存在$_SERVER['PATH_INFO']中。

当用户请求了 http://10.2.122.1/profile.php/test.css , 用户实际访问的是profile.php页面,但其PATH却是/profile.php/test.css。此时,后端返回的是用户的个人信息页面,然而Nginx会认为这是一个静态文件,并将其内容缓存下来。

也就是说,这个用户的个人信息被保存在缓存服务器中。此时,其他用户再次访问/profile.php/test.css的时候,将可以看到这些信息,导致了信息泄露漏洞。

django

用Django开发的应用,如果配置了/(.*)这样的Router,如:

1
2
3
4
5
from django.urls import re_path

urlpatterns = [
re_path(r'^page/(.*)$', page)
]

这样,用户请求/page/test.css一样可以访问到page这个视图,而缓存服务器会认为这是一个静态文件。

漏洞实例

Paypal的一个实际案例:https://vimeo.com/249130093

作者发现,Paypal会缓存以下后缀的URL:aif, aiff, au, avi, bin, bmp, cab, carb, cct, cdf, class, css, doc, dcr, dtd, gcf, gff, gif, grv, hdml, hqx, ico, ini, jpeg, jpg, js, mov, mp3, nc, pct, ppc, pws, swa, swf, txt, vbs, w32, wav, wbmp, wml, wmlc, wmls, wmlsc, xsd, zip,而其用户页面hxxps://www.paypal.com/myaccount/home,在后面加任何PATH都不影响返回的HTML,如hxxps://www.paypal.com/myaccount/home/attack.css。 这两个特点刚好满足缓存欺骗攻击的条件,于是,攻击者只需让用户访问一次/myaccount/home/attack.css,用户的个人信息就被公之于众了,包括姓名、余额、信用卡信息等等

防御措施

  1. 只有当HTTP Header允许时才允许缓存服务器进行缓存
  2. 根据Content-Type来决定是否缓存,比如.css后缀的URL一定要返回text/css的数据,否则不进行缓存
  3. 关闭PATH_INFO或者通配符的Router,当用户请求不存在的页面时返回404或302

解决缓存欺骗漏洞的根本方法

如果缓存里包含用户信息,那么一定要指定Vary: Cookie头,且缓存服务器要根据这个头来设置缓存键名。这样,即使缓存服务器错误地缓存了不该被缓存的页面,也只有相同Cookie的人才能看到这个缓存。显然攻击者是不知道用户的SESSION ID的

参考资料

https://www.freebuf.com/articles/web/161670.html

p牛的代码审计小密圈

https://www.anquanke.com/post/id/156356

https://www.anquanke.com/post/id/156551