作者:平安科技银河实验室
在 Nginx 的 range filter 中存在整数溢出漏洞,可以通过带有特殊构造的 range 的 HTTP 头的恶意请求引发这个整数溢出漏洞,并导致信息泄露。
攻击成本:低
危害程度:低
影响范围:Nginx 0.5.6 – 1.13.2
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
如果一次请求有多个 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_module
和 ngx_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_header
和 ngx_http_range_multipart_header
来生成 single range 和 multi ranges 的 Response Header;
这次的问题就出现在多个 range 时, ngx_http_range_parse
函数对 suffix-length 的处理;
Nginx 可以作为缓存服务器,将 Web 应用服务器返回的内容缓存起来。如果客户端请求的内容已经被缓存,那么就可以直接将缓存内容返回,而无需再次请求应用服务器。由此,可降低应用服务器的负载,并提高服务的响应性能。
下面是使用 Nginx 作为缓存服务器的一个示例。假设 Nginx 监听本地80端口,反向代理百度,那么就有如下配置:
此时,我们访问 http://127.0.0.1 ,即可得到百度的返回:
检查页面资源,存在一个静态图片文件。由于这类静态文件一般不会发生变化,我们可以将其缓存起来。
Nginx 配置缓存主要由以下命令完成:
proxy_cache_key
用于区分 cache 文件。
proxy_cache_path
设置 cache 文件的路径和参数。
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 文件的头部信息也被返回,从而造成信息泄漏。
首先,我们看这次漏洞修复的 commit:
可以看到,在 ngx_http_range_filter_module.c
的 ngx_http_range_parse
函数中做了两处修复:
根据漏洞修复 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 即可。
本次复现利用使用 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 头。
综合来看,这个漏洞就是整数溢出漏洞的利用,能够从 Cache 文件中获取 Cache 头的信息。在某些配置的情况下 Cache 头中会存在 IP 地址信息,造成信息泄露。
就 Nginx 模块以及常用的第三方模块本身来说,无法通过这个整数溢出来对内存进行操作或者远程执行。
建议升级到 1.13.3 和 1.12.1 版本;如果不能升级,可以在 Nginx 配置文件中添加 max_ranges 1,从而禁用 multipart range。