Exim漏洞分析及利用方法

阅读量    551860 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

Qualys最近公布了Exim MTA中某个严重漏洞的安全公告:CVE-2019-15846。在安全公告中,Qualys提到他们已经开发出PoC,可以让远程攻击者获得root权限。此外还有一则比较醒目的报道文章:”数百万Exim服务器存在安全漏洞……“。

在2018年时,我们根据Devcore发布的关于Exim另一个漏洞的分析文章,成功开发出漏洞PoC(但并没有对外公布)。因此,我们决定对新披露的这个漏洞采用相同的分析过程。

在本文中,我们从漏洞利用角度来分析Exim内部处理过程,然后介绍了关于两个漏洞利用的一些信息,同时提供了相应的PoC。

Exim中主要有4个处理进程:

1、守护进程(daemon process)监听SMTP入站连接。daemon进程会为每个SMTP连接创建一个新的接收端进程。通常情况下,daemon进程会使用-q参数启动,这样可以设定daemon进程启动队列运行(queue runner)进程所使用的间隔时间(比如,-q30m代表每30分钟就会启动一个queue runner进程)。

2、接收进程(reception process)接收入站消息,然后将其存储在spool目录中(/var/spool/exim/input)。消息由两个文件所组成:-H(消息头)以及-D(消息体)。除非明确指定,否则reception进程会生成新的进程,立即投递。如果启用了queue_only选项,收到的消息会被放入spool目录中,不会自动投递。

3、队列运行(queue runner)进程会遍历spool目录中的消息文件,为每个文件启动投递进程(delivery process)。

4、投递进程(delivery process)会对每则消息执行远程/本地投递操作。delivery进程以root权限运行,因此是值得研究的一个目标。

 

0x01 Exim Pool Allocator

Exim维护着若干个分配池(allocation pool)。POOL_PERM保存着分配资源,只要进程还存在,资源就不会被回收。比如,其中就存储着配置选项以及ACL。POOL_MAIN保存着动态分配资源,这些资源可以被释放。此外还有一个单独的池:POOL_SEARCH,可以用来查找存储数据。

在Exim中,pool为动态分配的storeblock链表(如下文所述)。storeblock的最小大小为0x2000。当Exim请求一定数量的内存时,会判断当前block中是否有足够的空间能够满足该请求。如果当前block中没有足够空间,则会分配新的storeblock

Exim在store_in.c中定义了一组例程,用来管理pool:

1、store_malloc以及store_free:分别为malloc以及free的封装函数;

2、store_get:如果当前block有足够的空间,则返回当前storeblock中的一个指针。否则会分配新的storeblock

3、store_reset:将yield指针指向存储重置点,释放后续的任何storeblock。后面我们会看到,这个函数在漏洞利用过程中非常有用;

4、store_release:该函数作用相当于重分配函数;

5、store_extend:如果待扩展的数据位于storeblock顶部,那么该函数就能发挥作用,可以避免额外的分配/拷贝操作。

 

0x02 Exim堆溢出漏洞利用

假如我们找到了Exim中的一个堆溢出漏洞,我们可以借鉴@mehqq在一篇文章中提到的思路来实现代码执行。

ACL

ACL(访问控制列表)在配置文件中定义,用来控制Exim在收到某些SMTP消息时的处理行为。例如,用户可以设置acl_smtp_mail选项,在收到MAIL FROM命令时执行特定的检查。当Exim处理这些选项时,就会展开这些选项,执行${run{cmd}}定义的命令。

Exim将ACL定义为全局指针,引用载入storeblock(来自POOL_PERM)中的数据。只要重写ACL的内容,就能实现代码执行。

从堆溢出漏洞到UAF

这里的目标是覆盖已分配storeblock的下一个指针,将其链接到包含ACL的storeblock。如果后面这个storeblock链被重置(比如发送新的HELO命令),那么包含ACL的block就会被释放,我们可以发送新的命令取回这个block。

这个利用场景需要如下5个步骤:

1、调整堆布局,以便获得两个连续的storeblock:存在漏洞的storeblock(用于溢出)以及目标storeblock

2、利用刚释放的chunk触发溢出,破坏已分配storeblock的下一个指针,使其指向包含ACL的storeblock。需要注意的是,由于这两个storeblock(被劫持的storeblock以及ACL storeblock)都位于堆中,因此这里我们需要执行一定数量的bruteforce操作;

3、发送新的HELO命令,释放存有ACL的storeblock,这样整个storeblock链都会被释放;

4、发送多个命令取回ACL storeblock(比如AUTH命令),覆盖acl_smtp_mail内容;

5、发送MAIL_FROM命令触发代码执行。

 

0x03 CVE-2018-6789

这个bug位于base64.cb64decode函数中。base64解码函数没有正确计算存储解码数据的缓冲区长度,导致存在堆溢出漏洞。从这里入手,攻击者可以使用传统的技术,通过缩小/扩展chunk大小(即破坏chunk大小字段)来覆盖chunk。

这里我们不会介绍CVE-2018-6789的完整利用步骤,因为@mehqq_已经在之前的文章中详细说明过,我们也会在下文的PoC中 给出细节。

我们将简单介绍Exim中的一些特性,可以根据需要利用这些特性来调整堆的布局。我们的目标是在chunk大小被破坏之前实现下图所示的堆布局。

1、我们可以发送两次“未定义命令+HELO命令”来创建可用的“工作”空间。未识别的命令可以在错误报告过程中触发storeblock分配操作,而HELO命令可以重置之前分配的storeblock链。需要注意的是,Exim会限制未知的命令数量,上限为3个。

2、顶部chunk使用AUTH命令进行分配。

达到上述状态后,我们可以发送AUTH CRAM-MD5命令来扩大工作区中间chunk的大小,触发缓冲区溢出。然后,我们可以发送HELO命令及无效的名称(如HELO a+),在不恢复整个storeblock链的情况下强制中间chunk被释放。这样我们就能提前跳出HELO命令处理代码逻辑,避免调用smtp_reset

最后,我们可以发送比前面被释放chunk更大的新的AUTH命令,这样就能覆盖顶部chunk。

随后,我们可以通过上一节介绍的步骤3~5实现代码执行。

完整利用代码请参考我们的Github仓库。

 

0x04 CVE-2019-15846

理解漏洞

在4.92.1版之前,Exim都存在一个堆溢出漏洞。Zerons在2019年7月21日报告了CVE-2019-15846漏洞,Qualys分析该漏洞后,于2019年9月6日对外公开。

该漏洞位于string_interpret_escape中,当string_unprinting调用该函数时会触发漏洞。漏洞在这个 commit中被修复。

diff --git a/src/src/string.c b/src/src/string.c
index 5e48b445c..c6549bf93 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -224,6 +224,8 @@ interpreted in strings.
 Arguments:
   pp       points a pointer to the initiating "" in the string;
            the pointer gets updated to point to the final character
+           If the backslash is the last character in the string, it
+           is not interpreted.
 Returns:   the value of the character escape
 */

@@ -236,6 +238,7 @@ const uschar *hex_digits= CUS"0123456789abcdef";
 int ch;
 const uschar *p = *pp;
 ch = *(++p);
+if (ch == '\0') return **pp;
 if (isdigit(ch) && ch != '8' && ch != '9')
   {
   ch -= '0';

顾名思义,string_interpret_escape的功能是解析转义序列。比如,\x62会被转换为b

string_unprinting会使用该函数,将输入字符串转换为未转义的输出字符串。输出字符串首先会使用Exim的内存分配器来分配。

len = Ustrlen(s) + 1;
ss = store_get(len);

这里的漏洞在于string_unprinting会读取输入字符串,直到找到NULL字节。当string_interpret_escape被调用时,会前移缓冲区指针,但随后string_unprinting会继续移动该指针。因此,代码会跳过反斜杠后的char,导致程序会跳过输出缓冲区限制,执行拷贝操作。

while (*p)
  {
  if (*p == '\\')
    {
    *q++ = string_interpret_escape((const uschar **)&p);
    p++;
    }
 [...]
 }

堆缓冲区溢出过程如下图所示:

需要注意的是,即使NULL字节没有用于停止处理输入缓冲区,也会被拷贝到输出缓冲区中。

为了利用该漏洞,攻击者必须对齐两个缓冲区(输入及输出缓冲区),确保除了输入缓冲区中的NULL字节之外,这两者之间不存在其他NULL字节。更准确地说,我们需要注意store_get,该函数会将storeblock中的数据按8字节进行对齐。

最后,这两个缓冲区必须属于同一个storeblock,以便溢出多个字节。当string_unprinting被调用时,根据堆布局情况,输入缓冲区可能在大小上非常受限。

不论如何,实际上可以覆盖的数据量并没有限制。read指针会读取刚刚写入的数据。为了避免在拷贝原始NULL字节时停住,我们可以在该字节前多加几个反斜杠。这样我们可以访问到输出缓冲区后的任意地址。

此外,我们可以将NULL字节编码成\x00来覆盖缓冲区。

漏洞利用

为了利用该漏洞,Qualys精心构造了带有反斜杠的SNI。这个SNI最终会由Exim的reception进程写到一个Exim spool文件中,由delivery进程读取。当Exim的delivery进程在spool_read_header中读取这个spool文件时,就会调用存在漏洞的string_unprinting函数。

Exim会给处理的每封邮件分配一个ID,这个ID会在spool文件名、日志等中使用。

Qualys使用这个堆溢出漏洞来覆盖用于构造log文件名的消息ID。在某些情况下,log文件会使用发送方地址来填充。当使用../../../../../../etc/passwd来覆盖消息ID时,就可以在目标系统中添加新用户。

我们准备深入分析存在漏洞的这个函数。前面提到过,spool_read_header用来解析Exim的spool头文件。这个头文件存放于/var/spool/exim4/input/目录中。收到的每封邮件都会生成2个spool文件,每个文件都以消息ID命名。第一个文件名后附加-D字符串,包含消息正文。第二个文件名附加-H字符串,包含各种元数据,其中就包括SNI。

spool_read_header会被多次调用。对deliver_message的调用是我们能找到的唯一可利用的代码路径。我们可以通过两个不同的进程来到达这条路径:exim -Mc以及exim -q。第一个进程对应接直接消息投递过程,第二个路径对应queue runner进程调用的一个后台任务。

为了利用该漏洞,我们以queue runner进程为目标。在这个进程中,消息ID存放于堆中,而如果通过exim -Mc来运行delivery进程,则消息ID存放于栈上。

不幸的是,Exim daemon主进程采用正常的fork+exec方式来运行这两个进程,除了读取spool文件外,这两个进程没有其他交互操作。因此,调整堆布局不会像CVE-2018-6789那么容易。

PoC

为了复现该问题,我们可以在debian 9系统,通过snapshot仓库安装exim4,该版本还没打上补丁。

root@strech:~# cat /etc/apt/sources.list
deb     http://snapshot.debian.org/archive/debian/20190801T025637Z/ stretch main
deb-src http://snapshot.debian.org/archive/debian/20190801T025637Z/ stretch main

需要注意的是,从2017年开始,GNUTLS在SNI值上添加了安全检查机制。因此,如果Exim使用了版本大于3.6.0的GNUTLS,我们就无法利用这个漏洞。

为了测试漏洞可利用性,我们可以简单创建这两个文件,手动运行Exim的queue runner进程。

cp 1i7Jgy-0002dD-Pb-D /var/spool/exim4/1i7Jgy-0002dD-Pb-D
cp 1i7Jgy-0002dD-Pb-H /var/spool/exim4/1i7Jgy-0002dD-Pb-H
/usr/sbin/exim4 -q

然后,我们可以在string_unprinting上设置断点,完成如下操作:

  • 确保可以触发这个缓冲区溢出漏洞
  • 了解溢出时的堆布局
  • 在堆上查找消息ID
gdb --args /usr/sbin/exim4 -q
gef➤  set follow-fork-mode child
gef➤  b string_unprinting
Breakpoint 1 at 0x5600d5924540: file string.c, line 355.
gef➤  r
Thread 2.1 "exim4" hit Breakpoint 1, string_unprinting (s=0x562b1a097790 "abcdef\\") at string.c:355
gef➤  n
[... step until interesting stuff ...]
gef➤  p s
$1 = (uschar *) 0x562b1a097790 "abcdef\\"
gef➤  p len
$2 = 0x8
gef➤  p ss
$4 = (uschar *) 0x562b1a097798 ""
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x562b1a0975e0, size=0x2020, flags=PREV_INUSE)
    [0x0000562b1a0975e0     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x562b1a099600, size=0x1010, flags=PREV_INUSE)
    [0x0000562b1a099600     31 69 37 4a 67 79 2d 30 30 30 32 64 44 2d 50 62    1i7Jgy-0002dD-Pb]
Chunk(addr=0x562b1a09a610, size=0x1fa00, flags=PREV_INUSE)  ←  top chunk

输入和输出缓冲区位于大小为0x2020字节的一个chunk中。在读取头文件时,fgets会分配后面大小为0x1010字节的chunk。这里没有直接可以用来实现代码执行的信息。

接下来搜索消息ID:

gef➤  grep 1i7Jgy-0002dD-Pb
[+] Searching '1i7Jgy-0002dD-Pb' in memory
[+] In (0x562b1a009000-0x562b1a00d000), permission=rw-
  0x562b1a00abb1 - 0x562b1a00abd6  →   "1i7Jgy-0002dD-Pb (queue run pid 2860)" 
  0x562b1a00ae92 - 0x562b1a00aea2  →   "1i7Jgy-0002dD-Pb" 
[+] In '[heap]'(0x562b1a05a000-0x562b1a0ba000), permission=rw-
  0x562b1a097609 - 0x562b1a097619  →   "1i7Jgy-0002dD-Pb" 
  0x562b1a097641 - 0x562b1a097653  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a097663 - 0x562b1a097688  →   "1i7Jgy-0002dD-Pb (queue run pid 2860)" 
  0x562b1a0976a9 - 0x562b1a0976bb  →   "1i7Jgy-0002dD-Pb-D" 
  0x562b1a0976c0 - 0x562b1a0976d2  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a0976f1 - 0x562b1a097703  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a099600 - 0x562b1a099637  →   "1i7Jgy-0002dD-Pb-H\nDebian-exim 103 114\n<redacted[...]" 
  0x562b1a099c16 - 0x562b1a099c4d  →   "1i7Jgy-0002dD-Pb\n\tfor redacted@redacted.com; Mon[...]" 
  0x562b1a099c8d - 0x562b1a099cc4  →   "1i7Jgy-0002dD-Pb@redacted>\n022F From: redacted@re[...]" 
[+] In '[stack]'(0x7fff8da2e000-0x7fff8dab0000), permission=rw-
  0x7fff8da65ae9 - 0x7fff8da65afb  →   "1i7Jgy-0002dD-Pb-H" 
  0x7fff8da65bb0 - 0x7fff8da65bc2  →   "1i7Jgy-0002dD-Pb-H" 
  0x7fff8da65eb9 - 0x7fff8da65ecb  →   "1i7Jgy-0002dD-Pb-H"

这里能用来覆盖文件的唯一一个消息ID位于堆中,但在输出缓冲区溢出中无法访问到这个ID。

最后,我们可以确保溢出操作能成功执行。

gef➤  fin
Run till exit from #0  string_unprinting (s=0x5600d6a60790 "abcdef\\") at string.c:366
gef➤  x/16bx 0x5600d6a60798
0x5600d6a60798: 0x61    0x62    0x63    0x64    0x65    0x66    0x00    0x61
0x5600d6a607a0: 0x62    0x63    0x64    0x65    0x66    0x00    0x00    0x00

可以看到,8字符的输入字符串已经被拷贝两次。

调整堆布局

为了找到可行的利用方式,我们需要仔细分析queue runner进程触发漏洞时的堆状态。

利用该漏洞的主要思路就是调整堆布局,使两个缓冲区(输入及输出缓冲区)可以在先前被释放的chunk中分配。通过这种方法,我们可以在缓冲区溢出期间访问到目标消息ID。为了完成该任务,被释放的chunk的STORE_BLOCK_SIZE至少必须为0x2000,这也是storeblock的最小大小值。

while (  (len = Ustrlen(big_buffer)) == big_buffer_size-1
    && big_buffer[len-1] != 'n'
    )
    {   /* buffer not big enough for line; certs make this possible */
    uschar * buf;
    if (big_buffer_size >= BIG_BUFFER_SIZE*4) goto SPOOL_READ_ERROR;
    buf = store_get_perm(big_buffer_size *= 2);
    memcpy(buf, big_buffer, --len);
    big_buffer = buf;
    if (Ufgets(big_buffer+len, big_buffer_size-len, f) == NULL)
      goto SPOOL_READ_ERROR;
    }

当queue runner进程遍历queue_get_spool_list中的spool文件时,会调用readdir函数。readdir会分配一个内部缓冲区,生成大小为0x8030的一个chunk。当调用closedir时,这个chunk会被释放掉。幸运的是,对于每个文件,queue_get_spool_list也会从当前storeblock中请求0x22个字节。

因此,如果我们可以确保/var/spool/exim4/input/中有足够多的的文件,就有可能强迫应用分配新的storeblock,然后在堆中创建间隙。

current_block[0]的剩余空间可以在yield_length[0]中找到。

gef➤  b opendir
Breakpoint 1 at 0x55b87b85e468
gef➤  c
Continuing.
Breakpoint 1, 0x00007f81aebc49a0 in opendir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  p yield_length[0]
$1 = 0x1ff0

我们至少只需要使用205个spool文件,就能在测试场景中确保成功在堆上创建间隙。

此外,跟在间隙后的数据恰好是用来创建log文件的消息ID。

gef➤  b closedir
Breakpoint 1 at 0x55dcb4d13898
gef➤  c
Continuing.
Breakpoint 1, 0x00007fb8affcc9f0 in closedir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  fin
Run till exit from #0  0x00007fb8affcc9f0 in closedir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x55dcb6d6d5e0, size=0x2020, flags=PREV_INUSE)
    [0x000055dcb6d6d5e0     40 76 d7 b6 dc 55 00 00 00 20 00 00 00 00 00 00    @v...U... ......]
Chunk(addr=0x55dcb6d6f600, size=0x8040, flags=PREV_INUSE)
    [0x000055dcb6d6f600     58 2b 2b b0 b8 7f 00 00 58 2b 2b b0 b8 7f 00 00    X++.....X++.....]
Chunk(addr=0x55dcb6d77640, size=0x2020, flags=)
    [0x000055dcb6d77640     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x55dcb6d79660, size=0x1e9b0, flags=PREV_INUSE)  ←  top chunk

为了最终完成漏洞利用,我们需要确保SNI被分配到间隙中。为了完成该任务,我们需要往current_block[0]填充一些数据,这样当分配SNI时,就会分配一个新的storeblock

string_unprinting中设置断点,计算current_block[0]中剩余空间:

gef➤  b string_unprinting
Breakpoint 1 at 0x55d401799540: file string.c, line 355.
gef➤  c
Continuing.
Thread 2.1 "exim4" hit Breakpoint 3, string_unprinting (s=0x55d4039d6990 'a' <repeats 3194 times>, "\\") at string.c:355
gef➤  p yield_length[0]
$1 = 0x1e68

在SNI之前,程序分配了一些空间,最有趣的是helo_name

int
spool_read_header(uschar *name, BOOL read_headers, BOOL subdir_set)
{
[... skip uninteresting lines ...]
    else if (Ustrncmp(p, "elo_name", 8) == 0)
      sender_helo_name = string_copy(big_buffer + 11);

使用helo_name来填充current_block[0]是非常简单的一个任务。然而,新分配的storeblock必须要有足够的空间来存储输入及输出SNI,或者没有足够的空间来存放任何一个SNI。因此我们可以选择远大于默认storeblock大小的一个helo_name,这是不错的解决方案。

需要注意的是,queue runner进程可能会根据我们选择的ID来分配多个小缓冲区,这些缓冲区会被放置在helo_name之前。

/* Check that the message still exists */

message_subdir[0] = f->dir_uschar;
if (Ustat(spool_fname(US"input", message_subdir, f->text, US""), &statbuf) < 0)
  continue;

我们的目标是实现如下布局:

稍微总结下helo_name的限制条件:

  • 必须大于current_block[0]的剩余空间;
  • 必须小于间隙大小减去两个SNI的大小;
  • 包含helo_namestoreblock必须填满,才能确保两个SNI连续布局;
  • 确保SNI与顶部chunk相邻,这样溢出操作不会覆盖其他数据;
  • 由于big_buffer重分配中存在一个bug,因此大小应小于0x4000

这里注意的是,漏洞利用过程必须适用于位于优先chunk中的消息ID。由于queue_run中的每次循环都会修改堆布局,因此我们需要知道应用什么时候会投递这个ID。如果没有设置queue_run_in_order,那么queue_get_spool_list会采用伪随机顺序方式列出spool文件。随后,处于优先地位的ID将会是第一个或者最后一个被投递的ID。

大家可以参考官方代码中的如下注释:

/ 处理随机列表的创建过程。第一个元素会成为顶部及底部列表元素。后续元素将以随机方式插入顶部或者底部。也就是说,我认为这种方式会比为每个元素分配一个随机编号要快,并且也能节省每个项目所需的编码空间。/

选择正确长度的helo_name后,我们能得到满足如下布局的堆:

gef➤  b closedir
Breakpoint 1 at 0x556862b9b898
gef➤  commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set $ID = ((char *)current_block[0]) + 0x19
>c
>end
gef➤  b queue.c:645 if (int)strcmp(f->text, $ID) == 0
Breakpoint 2 at 0x564f9a43418a: file queue.c, line 647.
gef➤  commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set follow-fork-mode child
>b string_unprinting
>c
>end
gef➤  c
Continuing.
Thread 2.1 "exim4" hit Breakpoint 1, string_unprinting (s=0x558589a70660 "abcdef\\") at string.c:355
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x55ee971d75e0, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971d75e0     40 16 1e 97 ee 55 00 00 00 20 00 00 00 00 00 00    @....U... ......]
Chunk(addr=0x55ee971d9600, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971d9600     30 c6 1d 97 ee 55 00 00 00 20 00 00 00 00 00 00    0....U... ......]
Chunk(addr=0x55ee971db620, size=0x1010, flags=PREV_INUSE)
    [0x000055ee971db620     62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62    bbbbbbbbbbbbbbbb]
Chunk(addr=0x55ee971dc630, size=0x2ff0, flags=PREV_INUSE)
    [0x000055ee971dc630     20 f6 1d 97 ee 55 00 00 d8 2f 00 00 00 00 00 00     ....U.../......]
Chunk(addr=0x55ee971df620, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971df620     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x55ee971e1640, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971e1640     00 96 1d 97 ee 55 00 00 00 20 00 00 00 00 00 00    .....U... ......]
Chunk(addr=0x55ee971e3660, size=0x1e9b0, flags=PREV_INUSE)  ←  top chunk
gef➤ p current_block[0]
$1 = (storeblock *) 0x55ee971df620
gef➤  x/s 0x55ee971e1640 + 0x19
0x55ee971e1659: "16aJgy-baaaad-Pb"

最后我们必须计算SNI,以便填充剩余的空闲chunk,覆盖消息ID。大家可以在exgen.py中了解如何满足这些限制条件以及计算所需结果。

不幸的是,覆盖消息ID同样会覆盖对应的storeblock头,因此会破坏store_reset。为了完美利用这个漏洞,我们首先应该解决掉这个问题。

不论如何,Exim随后会将日志写入以目标ID为文件名的文件。

/* Open the message log file if we are using them. This records details of
deliveries, deferments, and failures for the benefit of the mail administrator.
The log is not used by Exim itself to track the progress of a message; that is
done by rewriting the header spool file. */

if (message_logs)
  {
  uschar * fname = spool_fname(US"msglog", message_subdir, id, US"");
  uschar * error;
  int fd;

  if ((fd = open_msglog_file(fname, SPOOL_MODE, &error)) < 0)
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't %s message log %s: %s", error,
      fname, strerror(errno));
    return continue_closedown();   /* yields DELIVER_NOT_ATTEMPTED */
    }

  /* Make a C stream out of it. */

  if (!(message_log = fdopen(fd, "a")))
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't fdopen message log %s: %s",
      fname, strerror(errno));
    return continue_closedown();   /* yields DELIVER_NOT_ATTEMPTED */
    }
  }

当消息被成功投递后,写入的日志中会包含目的地址。

    if (!addr->parent)
        deliver_msglog("%s %s: %s%s succeededn", now, addr->address,
          driver_name, driver_kind);
  else
    {
    deliver_msglog("%s %s <%s>: %s%s succeededn", now, addr->address,
      addr->parent->address, driver_name, driver_kind);
    child_done(addr, now);
    }

因此,这里我们有可能在passwd中伪造一个有效的密码,获得目标主机的访问权限。

最后,当邮件成功投递给所有收件人后,Exim会unlink或者重命名日志文件。因此,以/etc/passwd为目标似乎并不是最佳选择。

 

0x05 总结

在本文中,我们学习了Exim的内部原理,这些知识可以帮助我们利用堆溢出漏洞,随后我们介绍了如何在两个不同的漏洞中利用这种技术。

此外,适用于CVE-2018-6789的这种利用技术可能同样适用于新公布的基于堆溢出的CVE-2019-16928漏洞,该漏洞可以通过发送较长的HELO命令来触发。

大家可以在我们的Github仓库中找到完整的PoC代码。如果大家找到了利用这些漏洞的其他方法,欢迎给我们发送邮件,一起交流。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多