Nginx安全小结
/0x00 前言
这里小结下Nginx这个常见Web中间件的安全攻防问题。
0x01 Nginx基础
Nginx简介
Nginx(engine x)是一个高性能的HTTP和反向代理Web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。
Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like协议下发行。其特点是占有内存少,并发能力强,事实上Nginx的并发能力在同类型的网页服务器中表现较好。
Nginx具有以下特点:
- 处理静态文件,索引文件以及自动索引;打开文件描述符缓冲.
- 无缓存的反向代理加速,简单的负载均衡和容错.
- FastCGI,简单的负载均衡和容错.
- 模块化的结构。包括 gzipping, byte ranges, chunked responses,以及 SSI-filter 等 filter。如果由 FastCGI 或其它代理服务器处理单页中存在的多个 SSI,则这项处理可以并行运行,而不需要相互等待。
- 支持 SSL 和 TLSSNI.
Nginx组成架构
Nginx由内核和模块组成。
Nginx内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。
Nginx的模块从结构上分为:
- 核心模块:HTTP模块、 EVENT模块和MAIL模块;
- 基础模块: HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块;
- 第三方模块:HTTP Upstream Request Hash模块、 Notice模块和HTTP Access Key模块;
Nginx安装配置
参考:https://www.runoob.com/linux/nginx-install-setup.html
Nginx中HTTP请求流程
Nginx中一个HTTP请求的处理流程如图:
Nginx配置文件详解
如下:
1 | ######Nginx配置文件nginx.conf中文详解##### |
0x02 Nginx解析漏洞
Nginx解析漏洞主要分为以下两种。
未知文件解析漏洞
漏洞原理
该漏洞与Nginx版本、PHP版本无关,属于用户配置不当造成的解析漏洞。
两个核心配置:
cgi.fix_pathinfo
:php.ini的配置项,当该配置项值为1时,用于修复路径,如果当前路径不存在则采用上层路径,比如传入/test.png/1.php
路径、由于1.php
不存在则会在传给FastCGI处理的路径就变为test.png
。该配置项默认为1;security.limit_extensions
:php-fpm.conf配置项,此配置项限制了FastCGI解析文件的类型(即指定什么类型的文件当做PHP代码解析),但此项设置为空时是允许FastCGI将任意类型的文件当做PHP代码来解析的;
环境搭建
直接用的Vulhub:https://vulhub.org/#/environments/nginx/nginx_parsing_vulnerability/
漏洞复现
先使用copy命令制作一个图片马:
1 | copy /b noface.jpg + shell.php/a = muma.jpg |
上传图片马,能正常访问:
URL栏追加/xx.php
后缀即可触发解析漏洞,成功解析图片马:
漏洞分析
漏洞根源就是配置存在问题,直接看配置文件的设置。
先看到php.ini,这里环境没有php.ini文件,直接看php.ini-development或php.ini-production,其中并没有配置cgi.fix_pathino
配置项,但是其默认值就是1,即启用修复路径,在前面输入路径/uploadfiles/91f1d6b9e6e11024e0ae83cbce848c7f.jpg/1.php
时,由于1.php
不存在因此实际传入给FastCGI的是/uploadfiles/91f1d6b9e6e11024e0ae83cbce848c7f.jpg
路径来进行解析:
接着看php-fpm.conf,文件内容中没有security.limit_extensions
配置项,但是include=etc/php-fpm.d/*.conf
引入了该目录中的所有.conf
文件,在其中的www-2.conf
文件中找到了该配置项,且该配置项为空,即没有进行任何限制、可以以PHP代码来解析由上层Nginx传下来的/uploadfiles/91f1d6b9e6e11024e0ae83cbce848c7f.jpg
路径文件,从而触发解析漏洞:
修复方法
针对配置项修复即可:
- 修改php.ini文件中
cgi.fix_pathino
配置项的值为0; - 添加php-fpm.conf文件中
security.limit_extensions
配置项的值为php,即限定FastCGI只对PHP相关后缀文件进行PHP代码解析;
文件名逻辑漏洞(CVE-2013-4547)
影响版本
Nginx 0.8.41 ~ 1.4.3 / 1.5.0 ~ 1.5.7
漏洞原理
由于Nginx存在文件名逻辑漏洞、会错误地解析请求的URI,导致获取到了错误的文件名,从而可能造成权限绕过、代码执行等安全漏洞。
环境搭建
直接用的Vulhub:https://vulhub.org/#/environments/nginx/CVE-2013-4547/
漏洞复现
最大的危害当然就是解析漏洞了,能解析PHP代码从而执行任意命令。
直接上传WebShell文件,修改文件名后缀为”.jpg “,即在文件后缀名后面追加个空格:
访问http://your-ip:8080/uploadfiles/shell.jpg[0x20][0x00].php
来触发漏洞,这里直接在Hex栏修改:
当然,除了解析漏洞外,文件名逻辑漏洞还能造成访问限制绕过等的安全风向。
这里修改下nginx.conf配置,/admin/
目录限定只能本地访问:
然后在Web目录中新建admin子目录,其中新建index.php文件,内容随意。
接着,在Linux中,Web目录中新建”mi1k7ea “子目录,注意目录名最后必须为空格。这是因为在Linux中,如果有一个不存在的目录,则即使跳转到上一层,也会爆文件不存在的错误,Windows下则没有这个限制。
因为不是本地请求,外部用户是没法访问到/admin/目录中的内容的:
但是结合文件名逻辑漏洞,就可以绕过Nginx路由限制来越权访问(注意空格不要编码):
漏洞分析
漏洞根源就是配置存在问题,直接看配置文件的设置。这里以解析漏洞的实例分析。
先看到nginx.conf,当Nginx匹配到.php
结尾的请求,就发送给FastCGI进行解析:
这个配置很正常,但是在漏洞版本的Nginx中,如果收到shell.jpg[0x20][0x00].php
的请求时,这个URI可以匹配上如上图的正则\.php$
的Location块,其中Nginx会识别到请求的文件是shell.jpg[0x20]
,就设置其为SCRIPT_FILENAME
的值发送给FastCGI进行解析,而PHP-FPM中并未设置security.limit_extensions
配置项的值,从而可以以PHP代码来解析shell.jpg[0x20]
文件,触发解析漏洞:
修复方法
- 升级Nginx版本;
- 添加php-fpm.conf文件中
security.limit_extensions
配置项的值为php,即限定FastCGI只对PHP相关后缀文件进行PHP代码解析;
%00截断解析漏洞
这部分Nginx版本过低,已经很少见了,漏洞原理也很简单,就不搞靶场了。
影响版本
Nginx 0.5.*, 0.6.*, 0.7 <= 0.7.65, 0.8 <= 0.8.37
漏洞原理
对于低版本的Nginx,可以在任意文件名后面添加%00.php进行解析攻击。
如:1.jpg%00.php就会将前面1.jpg文件当成PHP文件进行解析执行。
修复方法
升级Nginx版本;
0x03 Nginx整数溢出漏洞(CVE-2017-7529)
影响版本
Nginx 0.5.6 ~ 1.13.2
漏洞原理
Nginx在反向代理站点的时候,通常会将一些文件进行缓存,特别是静态文件。缓存的部分存储在文件中,每个缓存文件包括“文件头”+“HTTP返回包头”+“HTTP返回包体”。如果二次请求命中了该缓存文件,则Nginx会直接将该文件中的“HTTP返回包体”返回给用户。
如果我的请求中包含Range头,Nginx将会根据我指定的start和end位置,返回指定长度的内容。而如果我构造了两个负的位置,如(-600, -9223372036854774591),将可能读取到负位置的数据。如果这次请求又命中了缓存文件,则可能就可以读取到缓存文件中位于“HTTP返回包体”前的“文件头”、“HTTP返回包头”等内容。
环境搭建
直接用的Vulhub:https://vulhub.org/#/environments/nginx/CVE-2017-7529/
当然也可以自己用Docker搭建,参考旧博客:使用Docker搭建Nginx整数溢出漏洞(CVE-2017-7529)及Python PoC验证
漏洞复现
正常访问Web页面,是Nginx默认页面,该页面实际上是反向代理的8081端口的内容:
这里直接看P神Vulhub中的PoC,就是:
1 | #!/usr/bin/env python |
运行python poc.py http://your-ip:8080/
后,可以看到是越界读取到了Cache文件头及其内容(Nginx和被代理的服务器之间的响应报文头和Body内容等):
漏洞分析
先看到Nginx配置文件nginx.conf,其中包含了如下conf文件:
看到,其中关键Nginx设置如下:
- proxy_cache_path:设置缓存文件的路径和参数;
- proxy_cache_valid:设定缓存有效期;
- proxy_pass:反向代理配置,参数为代理的目标服务地址;
- proxy_cache:指定使用的keys_zone名称,这里就是上面的cache_zone;
- add_header:在Nginx返回的HTTP头中增加一项X-Proxy-Cache,如果缓存命中其值为HIT,未命中则为MISS;
- proxy_ignore_headers:Nginx不会缓存带有Set-Cookie的返回,因此这里设置忽略该HTTP头;
由此可知,正常访问之后,就会缓存目标页面内容到proxy_cache_path指定的目录中,查看确实存在Cache文件,前面部分是Cache文件头部信息包括Key,后面就是响应报文的内容:
可以看到,这和前面通过PoC脚本触发整数溢出漏洞来获取到的Cache文件内容是一致的。
OK,来看下根源的代码问题,从补丁修复代码看起,主要在ngx_http_range_filter_module.c的ngx_http_range_parse()函数上做了两次修改:
- Range filter: protect from total size overflows.:防止Range中total_size即总大小参数的整数溢出;
- Range filter: avoid negative range start.:防止Range中start参数为负数的整数溢出;
据此可知,本次整数溢出漏洞的漏洞点正是出在Nginx处理HTTP请求头Range的数值时并没有进行整数溢出漏洞过滤而导致的。其中bytes-range读取的起始范围可能为负数,从而读取缓存文件头部。
更多的漏洞分析请参考:CVE-2017-7529 Nginx整数溢出漏洞分析
修复方法
升级Nginx版本;
0x04 Nginx不安全配置
当Nginx配置不当时,也会导致一些安全问题。
下面的环境均搭建自Vulhub:https://vulhub.org/#/environments/nginx/insecure-configuration/
CRLF注入漏洞
漏洞原理
Nginx的内置遍历$uri
是指当前的请求URI,不包括任何参数(见$args
)。其中,Nginx会对$uri
进行解码操作。如果攻击者在URI中注入%0a%0d
就会造成CRLF注入漏洞。
Demo
不安全配置示例如下,即直接将$uri
拼接到302重定向的地址上,会造成CRLF注入漏洞:
直接在URI栏注入即可,比如注入Set-Cookie头:
可以往响应报文Body注入XSS payload,但是这里由于Location头的值协议不可控、302这处无法直接在浏览器触发RXSS:
防御方法
审计Nginx配置文件中$uri
的使用位置,禁止直接用在URL跳转地址的拼接上。
目录穿越漏洞
漏洞原理
Nginx服务器通过设置Alias别名的方式,可以使不在网站根目录下的目录也能被Web访问。
Nginx的配置autoindex作用是自动创建索引,换句话说就是目录浏览功能,当为on时页面可以显示当前目录下所有文件/目录内容,当为off时页面为403 Forbidden。
如果在配置别名(Alias)的时候,忘记在location的目录最后追加/
,会导致存在目录穿越漏洞。
Demo
看个不安全的配置示例:
正常访问,由于autoindex为on,页面就显示了home目录中的所有文件和目录:
访问/files../
的时候就会目录穿越:
当然,并非autoindex开启了才能利用,只是说页面会展示出当前目录下所有的文件和目录信息而已,方便查看,即使不开启autoindex也能直接目录穿越获取目标文件、只是访问目录的时候为403而已。
比如去掉autoindex on这个配置后,访问/file../etc/
目录时是403,但是访问具体文件就能直接下载了:
注意,此处场景目录穿越只能往上穿一层,示例中刚好上一层就是根目录因此可以看到所有文件和目录,但是实际场景可能目录穿越的利用很有限。
防御方法
在Nginx配置文件中location的目录最后必须要追加/
。
add_header被覆盖
漏洞原理
add_header是headers模块中定义的一个指令,用来添加HTTP响应头部。
Nginx是分层级组织的,每层可以有自己的指令,子块继承父块的配置;但对于相同指令,子块的配置可以覆盖掉父块的配置。子块中如果设置了add_header,那么就会覆盖掉父块中的add_header,这样就可能会造成一些安全风险。
Demo
不安全的配置示例,原本在Web整站即父块中添加了CSP头和X-Frame-Options头设置的,其在子块/test1目录是直接使用生效的,但是在另一个子块/test2目录中会重新add_header导致前面整站的add_header设置被覆盖:
正常访问/test1目录,响应头确实设置了CSP头和X-Frame-Options头,其中页面是引用了一个JS文件:
这个JS文件就是往页面id为m的标签内写入当前URL中#号后面的内容,即存在DOM XSS风险:
这里注意个细节,如果直接引用这个JS脚本,无论后面有没有CSP都无法成功触发XSS,这是因为默认这个输出是对关键字符如尖括号进行了URL编码的,此时需要我们稍微改下app.js的代码,添加个URL解码操作即对获取到URI栏的内容调用decodeURI()函数才能成功:
1 | window.onload = function() { |
由于CSP设置为default-src 'self'
即只允许同源下的资源,这里/static/app.js是同源资源因此可以加载进来执行,但不允许内联资源(要允许内联资源需要设置’unsafe-inline’,其允许如内联的script元素、javascript: URL、内联的事件处理函数和内联的style元素等)。也就是说,即使注入XSS payload,如下的内联的on事件也是不会执行的:
当我们访问/test2目录时,看到响应报文并没有前面整站设置的CSP头和X-Frame-Options头,而是该子块直接设置的X-Content-Type-Options头,也就是说父块的add_header被子块的add_header覆盖了:
结合前面的存在DOM XSS风险,这里由于没有CSP限制所以能成功触发利用:
注意,这里XSS payload不能用<script>
标签、会无法触发执行,这是因为这个DOM操作是用的innerHTML来进行的,需要用如<img>
等标签来注入才能成功。
防御方法
非必须不要设置在子块再次配置add_header来覆盖父块的add_header配置;如果业务需要必须设置,则需要严格审计是否会出现相关的安全风险。