作者:平安科技银河实验室

1、漏洞描述

在 Nginx 的 range filter 中存在整数溢出漏洞,可以通过带有特殊构造的 range 的 HTTP 头的恶意请求引发这个整数溢出漏洞,并导致信息泄露。

2、影响程度

攻击成本:低
危害程度:低
影响范围:Nginx 0.5.6 – 1.13.2

3 、漏洞原理

3.1 HTTP 断点续传:Range

HTTP 的 Range 允许客户端分批次请求资源的一部分,如果服务端资源较大,可以通过 Range 来并发下载;如果访问资源时网络中断,可以断点续传。

Range 设置在 HTTP 请求头中,它是多个 byte-range-spec (或 suffix-byte-range-spec )的集合;

byte-range-set  = ( byte-range-spec | suffix-byte-range-spec )*N  
byte-range-spec = first-byte-pos "-" [last-byte-pos]  
suffix-byte-range-spec = "-" suffix-length  

其中,first-bytes-pos 指定了访问的第一个字节,last-byte-pos 指定了最后一个字节,suffix-length 则表示要访问资源的最后 suffix-length 个字节的内容;例如:

Range:bytes=0-1024 表示访问第 0 到第 1024 字节;

Range:bytes=500-600,601-999,-300 表示分三块访问,分别是 500 到 600 字节,601 到 600 字节,最后的 300 字节;

在 Response 头中设置:

Accept-Ranges:bytes 表示接受部分资源的请求;

Content-Range: bytes START-END/SIZE 表示返回的资源位置;其中 SIZE 等于 Content-Length ;如:Content-Range: bytes 500-600/1000

3.2 Nginx Range Multipart

如果一次请求有多个 range,返回的数据需要 multipart 来组织;格式如下:

HTTP/1.1 206 Partial Content  
Date: Wed, 15 Nov 1995 06:25:24 GMT  
Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT  
Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-type: application/pdf  
Content-range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-type: application/pdf  
Content-range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

Nginx 对 Range 的支持包括 header 处理和 body 处理,分别用来解析客户端发送过来的 Range header 和裁剪返回给客户端的请求数据 Body。其实现分别由 ngx_http_range_header_filter_modulengx_http_range_body_filter_module 两个过滤模块完成。

ngx_http_range_header_filter_module 中调用了 ngx_http_range_header_filter 函数,而该函数进一步调用了 ngx_http_range_parse 函数来解析 header 中的 Range 字段;分别调用 ngx_http_range_singlepart_headerngx_http_range_multipart_header 来生成 single range 和 multi ranges 的 Response Header;

这次的问题就出现在多个 range 时, ngx_http_range_parse 函数对 suffix-length 的处理;

3.3 Nginx Cache

Nginx 可以作为缓存服务器,将 Web 应用服务器返回的内容缓存起来。如果客户端请求的内容已经被缓存,那么就可以直接将缓存内容返回,而无需再次请求应用服务器。由此,可降低应用服务器的负载,并提高服务的响应性能。

下面是使用 Nginx 作为缓存服务器的一个示例。假设 Nginx 监听本地80端口,反向代理百度,那么就有如下配置:

此时,我们访问 http://127.0.0.1 ,即可得到百度的返回:

检查页面资源,存在一个静态图片文件。由于这类静态文件一般不会发生变化,我们可以将其缓存起来。

Nginx 配置缓存主要由以下命令完成:

proxy_cache_key 用于区分 cache 文件。

proxy_cache_path 设置 cache 文件的路径和参数。

  • cache 文件会保存在指定的目录下面,文件名是 cache key 的 MD5 值
  • 通过 level 参数将 cache 文件分多层目录保存,以避免某个目录下存在大量文件造成的性能开销
  • 通过 keys_zone 参数指定 cache key 在内存中的区域及其大小,1M 的区域大概可以保存 8000 条 key 的信息

proxy_cache_valid 对不同返回状态值设定 cache 有效时间

例如,下面这条配置:

指定了以下信息:

使用协议、请求方法、域名、URI 作为 cache key

cache文件保存在目录 /tmp/Nginx/ 下,采取两层目录,keys_zone 名称为 my_zone,大小为 10M

对于返回状态值为 200 的内容,cache 有效时间为 10 分钟

现在,我们配置好了名为 my_zone 的 cache,接下来选择对目录 www.baidu.com/img/ 下的图片做缓存。首先,仍然是设置反向代理:

接下来,我们使用下列命令对 img 目录下的文件进行缓存:

配置命令解释如下:

proxy_cache 指定使用的 keys_zone 名称,就是之前的 my_zone

add_header 在 Nginx 返回的 HTTP 头中,增加一项 X-Proxy-Cache,如果缓存命中其值为 HIT,未命中则为 MISS

proxy_ignore_headers 由于百度对图片的请求也会 Set-Cookie 设置,而 Nginx 不会缓存带有 Set-Cookie 的返回,因此我们这里设置忽略该 HTTP 头

现在,对图片的缓存配置就完成了,完整的配置内容如下

我们使用 curl 命令进行实验,访问 http://127.0.0.1/img/bd_logo1.png 。由于是第一次访问,可以看到返回内容中 X-Proxy-Cache 的值为 MISS:

再次访问时,此时缓存命中,X-Proxy-Cache 的值为 HIT 了

那么现在的 Cache 文件是什么样的呢?我们检查设置的缓存目录 /tmp/Nginx,发现存在以下 Cache 文件:

可见,确实使用了2层目录保存了 Cache 文件。Cache 文件保存了 Nginx 请求得到的返回内容:

可以看到,cache key 的内容保存在了 Cache 文件的头部,此外还有 Nginx 请求后端返回的 HTTP 头,如后端(这里是 www.baidu.com )的服务器为 Apache。正常情况下,这些信息是不会返回给客户端的。而本次的的漏洞,就是由于负数偏移量,导致 Cache 文件的头部信息也被返回,从而造成信息泄漏。

4 、漏洞原理

首先,我们看这次漏洞修复的 commit:

可以看到,在 ngx_http_range_filter_module.cngx_http_range_parse 函数中做了两处修复:

  • 进一步检测了 size 的溢出情况,防止 size 溢出后造成小于 content-length 这条判断的绕过
  • 则直接限定了使用后缀的情况下,start 不能为负的,最小只能是 0,也就是说使用 “-xxx” 的方式对 Cache 文件的读取只能从 0 开始到结束。

根据漏洞修复 commit 的注释,我们知道这次漏洞的主要成因就是 bytes-range 读取的起始范围可能为负数,从而读取缓存文件头部。

首先,如果传入完整的 range 参数,如 start-end,则在 ngx_http_range_parse() 中会检查 start,确保其不会溢出为负值:

因此,如果需要将 start 解析为负数,只能通过 -end 这类后缀型 range 参数实现:

此时的 start 等于 content-length 减去读入的 end 值,所以如果传入的 end 比实际长度还要长,就可以使 start 变为负数,而这就是第二处修复所处理的情形:

同时注意到,在这类情况下,最终 end 的值会被设定为 content-length-1。所以这块 range 的总长度就超过了 content-length。而 Nginx 对 range 总长度会有检查:

一般来说,range 作为原始文件的一部分,其长度应该是小于 content-length 的。所以一旦计算得到的 size 比 content-length 还大,那就直接将原始文件返回了,不再进行 range 处理。为了绕过这一限制,我们就需要利用到第一处修复所处理的情形。

具体而言,检查用到的 size 是将 multipart 的全部 range 长度相加得到的:

因此,一个 range 是不够的,我们至少需要两个 range,其长度之和溢出为负数,就可以绕过总长度的检查了。

要得到一个很大长度的 range,同样可以采用 -end 这种后缀型,将 end 设置为一个非常大的数即可。此处的 start, end, size 均为 64 位有符号整形,所以只需要最终相加得到的 size 为 0x8000000000000000 即可。

5 、漏洞利用

本次复现利用使用 Nginx-1.12.0 作为缓存服务器,缓存配置同上文,访问的目标文件仍然是http://www.baidu.com/img/bd_logo1.png

首先,我们不指定 range,得到该图片文件的长度为 7877:

设置第一段 range 为 -8500,此时的 start 为 7877-8500=-623,即图片在 Cache 文件偏移之前的 623 bytes 也会被返回,而这 623 bytes 中就包含了 Cache 文件头部。

下一步,按照上文所说,第二段 range 的长度需要将总长度溢出。我们的目标总和 size 为 0x8000000000000000,第一段 range 长度为 8500,故第二段 range 长度为 0x8000000000000000-8500=9223372036854767308。

于是,使用 curl 命令,配合 -r 参数指定 bytes range:

可以看到返回内容中,第一段即为 -8500 的 range,而这一段中我们就看到了 Cache 文件头部,例如 cache key 以及后端服务器返回的 HTTP 头。

6、漏洞修复

综合来看,这个漏洞就是整数溢出漏洞的利用,能够从 Cache 文件中获取 Cache 头的信息。在某些配置的情况下 Cache 头中会存在 IP 地址信息,造成信息泄露。

就 Nginx 模块以及常用的第三方模块本身来说,无法通过这个整数溢出来对内存进行操作或者远程执行。

建议升级到 1.13.3 和 1.12.1 版本;如果不能升级,可以在 Nginx 配置文件中添加 max_ranges 1,从而禁用 multipart range。