作者:「Tencent Blade Team」leonwxqian
公眾號:騰訊安全應急響應中心

<一> 背景

Tencent Blade Team在代碼審計過程中發現了curl中存在兩個可以通過NTLM遠程觸發的漏洞。這兩個漏洞存在于curl在處理Type-2和Type-3消息的邏輯中。

這兩個漏洞分別為:

(1)遠程內存讀取

(CVE-2018-16890,https://curl.haxx.se/docs/CVE-2018-16890.html

利用此漏洞,攻擊者可以在服務器上遠程獲取客戶端內存至多64KB的原始內存信息。而且因為連接可以多次進行,服務器理論上可以多次重復地獲取客戶端內存。

(2)遠程棧緩沖區溢出

(CVE-2019-3822,https://curl.haxx.se/docs/CVE-2019-3822.html

利用此漏洞,攻擊者可以通過服務器的認證消息對客戶端進行遠程棧緩沖區溢出。通過組合上一個漏洞,理論上攻擊者可以對客戶端進行遠程代碼執行(RCE)。

curl的作者Daniel在博客中提到“我覺得這可能是很長時間以來curl中發現的最嚴重的安全問題”(I think this might be the worst security issue found in curl in a long time.,https://daniel.haxx.se/blog/2019/02/06/curl-7-64-0-like-theres-no-tomorrow/)。

如果編譯curl時,選擇了使用openssl同時禁用md4,則有漏洞的代碼不會被編譯進去。在這些情況下的curl不受此漏洞的影響。

我們先從一些常見的場景和認證模式來介紹一下背景,這樣可以更方便理解curl這些漏洞是如何工作的。

1.1 關于curl

curl雖然比較少作為獨立的軟件出現在大眾面前,但是它可謂是一個不折不扣的幕后大佬。它是許多互聯網程序的必不可少的組件。

curl用于命令行或腳本中傳輸數據。它還用于汽車、電視機、路由器、打印機、音頻設備、移動電話、平板電腦、機頂盒、媒體播放器,是成千上萬每天影響數十億人的軟件應用的互聯網傳輸中樞(https://curl.haxx.se/語)。同時,它也可以作為組件(libcurl)在PHP、Python或者WordPress、Git等等軟件中使用。

要觸發這次提到的兩個漏洞,客戶端除了要使用有問題的版本以外,還必須支持使用libcurl或者curl來進行代理訪問(通過NTLM認證)或者支持通過NTLM驗證獲取請求。

一般來說,curl的binary默認都是支持NTLM的。使用curl --version 查看,如果包含ntlm,即可以通過curl –ntlm -u “用戶名:密碼” 服務器連接遠程服務器。

img

而libcurl則稍稍復雜一點,它需要開發者打開CURLAUTH_NTLM或CURLAUTH_ANY,以表示支持NTLM認證。

img

圖:Git在修改中加入了CURLAUTH_ANY開關,表明支持NTLM認證。

打開開關后要觸發NTLM認證,必須通過命令行或cul_setopt指定用戶名密碼,或者直接在請求的url中指定。例如curl --ntlm http://用戶名:密碼@Server/。

NTLM常用于Windows上的身份認證,所以對有Windows機器的內網而言或者代理服務器而言,NTLM出現的頻次并不很低。雖然說是身份認證,不過需要注意的是,要觸發這次的兩個漏洞,來自客戶端的身份認證信息并不重要,因為服務器端是被黑客控制的,黑客并不在意客戶端發來的是什么,只要按照既定規則發送攻擊載荷即可。因此客戶端哪怕發來的是錯誤的驗證信息都可以繼續觸發漏洞。

黑客唯一需要做的就是,控制一臺服務器。因為這是一組由服務器攻擊客戶端的漏洞。

客戶端一旦使用有漏洞的curl+NTLM連接到黑客的服務器,黑客就可以攻擊客戶端程序。

舉一些例子,為了稱呼方便,我們在這里統稱攻擊者為H(Hacker),而被攻擊的為V(Victim)。在這些場景下,你可能會和黑客“交手”:

  1. 你從網上隨便找了一個公開的代理服務器H,但不幸的是這是一臺黑客控制的服務器。然后把你的博客如WordPress配置了使用curl+NTLM代理的方式訪問服務器H,則你的博客所在的Apache/PHP進程可能都會受到攻擊*

  2. 你使用了git客戶端,配置使用了黑客的代理服務器H,認證過程中就會發生攻擊*

  3. 公司內網中,有一臺服務器H被黑,其他服務器V通過curl+NTLM,向這臺被黑的服務器發起網絡請求時,H可以對這些服務器V進行攻擊*

  4. 你的爬蟲程序V使用了libcurl來連接一個遠程服務器H,并且V打開了支持所有認證模式的開關,這樣H就可以攻擊V了*

諸如此類等等。

前提只有:

  1. 受害者V的客戶端使用了有漏洞版本的curl(7.36.0~7.63.0)且支持NTLM;

  2. 受害者V訪問了黑客H控制的服務器,并使用任意賬號密碼(不正確也無所謂,但是需要提供)發生了NTLM認證流程。

1.2 關于NTLM認證流程

再介紹一下NTLM。在Windows網絡中,名詞NT LAN Manager(NTLM)表示一種微軟的安全協議,該協議可為用戶提供身份驗證。NTLM是Microsoft LAN Manager(LANMAN)中的身份驗證協議的后繼者,這是一種較舊的驗證協議。(https://en.wikipedia.org/wiki/NT_LAN_Manager)。

NTLM的核心認證消息分為三類,三類的消息各不相同,但是名字很直觀。它們分別稱為Type-1、Type-2、Type-3 Message。其中Type-1類似握手的步驟,Type-2和Type-3則用于服務器和客戶端之間的登陸溝通。

使用NTLM認證進行網絡請求的過程如下:

1: C →S GET ... 

2: C←S HTTP 401 Unauthorized  
WWW-Authenticate: NTLM  

3: C →S GET ...  
Authorization: NTLM <經BASE64編碼的Type-1消息>  

4: C←S HTTP 401 Unauthorized  
WWW-Authenticate: NTLM <經BASE64編碼的Type-2消息>  

5: C→S GET ...  
Authorization: NTLM <經BASE64編碼的Type-3消息>  

6: C←S HTTP 200 OK 

即:3~5為實際的認證過程。客戶端(C)會發送Type-1消息和Type-3消息給服務器(S),而服務器會發送Type-2消息給客戶端。 Type1、2、3三類消息的結果都是由之前消息的內容所計算而來的。

具體可以參考微軟的文檔:

https://docs.microsoft.com/zh-cn/windows/desktop/SecAuthN/microsoft-ntlm

curl官方已經發布了詳細的漏洞通告。因為這兩個漏洞的發現和利用仍然有許多有趣而且值得開發人員警醒的地方,所以我決定寫一篇writeup來介紹一下漏洞的發現過程和思考。

<二> curl的客戶端版“心臟滴血”CVE-2018-16890

這個漏洞和“心臟滴血”有那么幾分相似。雖然“心臟滴血”是泄露服務器上的內存,而curl是泄露客戶端上的內存,但是成因、效果上都能看到“心臟滴血”的影子。

這個漏洞位于lib/vauth/ntlm.c: ntlm_decode_type2_target,問題在于處理傳入的NTLM Type-2消息的函數沒有正確驗證傳入數據,最終導致了整數溢出。使用該溢出,惡意的NTLM服務器可以欺騙libcurl接受錯誤的長度+偏移組合,這將導致緩沖區讀取和寫入越界。

細節如下:

當用戶嘗試連接到啟用了NTLM的服務器時,服務器將設置target_info_len (0~0xffff)和target_info_offset (0~0xffffffff)來回復Type-2消息。請注意,在Type-2消息中,長度和offset都是可以被設置的。

而這兩個值恰巧又都是unsigned long,因此此處的驗證并不正確:

img

如果target_info_len + target_info_offset = (unsigned long)0x1 00000000,則結果為零(高位1溢出),0在這里一定會小于“size”(消息長度)。

要觸發整數溢出,target_info_offset的值必須介于0xffff0001~0xffffffff之間,因為它是長整形,這也代表它也一定會大于48。所以這里的兩處安全保護全部會被繞過。

從而觸發這里的越界讀寫

img

2.1 讀取越界→繞過ASLR

我們先說越界讀的問題。可以看到這里target_info_offset雖然定義成了無符號數,但是在方括號的數組索引中,它實際上還是有可能會扮演一個有符號數的角色。

  • 當軟件是32位的時候,方括號中的數字等價于signed long類型。
  • 當軟件是64位的時候,方括號中的數字等價于signed long long類型。

先以32位為例,假如offset是0xffffffff,這里memcpy讀取到的實際上是buffer[0xffffffff]即buffer[-1]的數據,相當于向前讀取了。

而如果是64位程序,則相當于從buffer[0xffffffff]處讀取了數據。

數據存放在target_info中,在下一個NTLM Type-3消息返回給服務器時,curl將把這次讀取到的內容發送回遠程服務器。

根據len + offset的約束,讀取的數據至多可以有64KB大小(0xffff字節),但是可以多次重復觸發泄露。每次泄露的位置根據內存分配算法的不同,從而有所不同。因為消息會被base64編碼,所以后面的堆數據會原樣傳遞給遠程服務器。

通過多次泄露,遠程服務器基本可以知道客戶端的內存布局。而且,一般情況下可以根據獲取到的curl版本以及泄露的堆內容來找到一些可以計算出基址的數據,從而繞過ASLR,為代碼執行埋下鋪墊。

<三> “可能是長期以來curl里最嚴重的安全問題”CVE-2019-3822

Curl的作者在博客中寫道,這可能是長期以來curl里最嚴重的安全問題。這個NTLM Type-3消息中的棧緩沖區溢出非常有趣。它就是一個非常純粹、“old-school”(傳統)的棧溢出。就是memcpy直接拷貝了超過棧變量長度的數據導致了這個溢出。9102年了,為什么會發生這個問題?其中有幾個值得深思的地方。

img

先介紹一下問題。問題出在lib/vauth/ntlm.c:Curl_auth_create_ntlm_type3_message()。創建傳出NTLM Type-3標頭的函數基于先前接收的數據生成請求HTTP標頭內容。如果從惡意的HTTP服務器提供的先前NTLMv2報頭中提取非常大的“nt response”數據,則輸出數據可能比緩沖區大。

“過大的值”需要大約1000字節以上。 復制到目標緩沖區的實際有效負載數據來自NTLMv2 Type 2響應頭。

而且,用于防止本地緩沖區溢出的檢查的實現是錯誤的(使用無符號數學運算),因此它不會阻止溢出發生。

細節如下:

Curl_auth_create_ntlm_type3_message會調用Curl_ntlm_core_mk_ntlmv2_resp來獲取Type-2中得到的消息長度,在Curl_ntlm_core_mk_ntlmv2_resp中有如下定義:

img

其中,NTLMv2_BLOB_LEN定義如下:

\#define NTLMv2_BLOB_LEN (44 -16 + ntlm->target_info_len + 4)

可以看到這其實是一個有target_info_len參與的可變的值,而問題更大的是target_info_len是一個攻擊者可控的值。當這個函數計算完len并把len寫入ntresp_len后,外層Curl_auth_create_ntlm_type3_message中的拷貝邏輯則會使用這個值向ntlmbuf中復制一個很大的內存。

img

而不巧ntlmbuf是一個固定長度的棧上變量。因此這里會發生棧緩沖區溢出。

3.1 有符號/無符號數的錯誤比較→防護失效

但是上面明明有寫size < NTLM_BUFSIZE – ntresplen 呀,為什么沒有生效呢?原因是ntresplen是無符號數,而一旦有符號數的運算中摻有了無符號數,便會發生變量類型的傳播,即隱形轉換以后,整個比較都會以無符號數的方式來進行。

img

這也就代表著,NTLM_BUFSIZE(可以接受的最大值)減去ntresplen(實際值),可能是-2、-3這樣的負數,轉成無符號數則是0xfffffffe、0xfffffffd這么大的值,而size是返回消息的實際長度,一般都很短。所以這里的運算的結果一定會大于size。

img

因此實際上這個size < NTLM_BUFSIZE – ntresplen的判斷并沒有生效,從而導致了堆溢出代碼的執行。

如果統一了符號,則結果就會變得不一樣,程序會走到正確的分支上。這也是patch中所做的事情:

img

3.2 棧緩沖區溢出→任意地址、任意長度的數據讀

你是否注意到這些掛在函數開頭的一長串堆棧變量?仔細看一下這個函數的實現,你會發現一個有意思的事實:有漏洞的這個超大的函數,包含了數百行代碼,數十個棧上變量。這個數字對一個棧漏洞來說非常有吸引力。

當漏洞被觸發時,整個函數僅僅運行了1/3左右。這代表什么呢?分析完流程以后可以知道,如果我們能輕易地控制其他變量,就可以實現任意的遠程內存讀取。

img

(取決于編譯器,對于MSVC,我們可以覆蓋“size”和“result”,即向上覆蓋。而對于GCC,我們可以在上圖中的箭頭方向向下覆蓋變量。)

如此信心十足是因為我們還有足足66%篇幅的邏輯可以控制。

當實現棧溢出以后,我們可以嘗試覆蓋ntresplen為一個負數或很大的值。這樣,當下面代碼執行的時候,size就會被我們控制(自此,函數中僅剩1個無關緊要的變量未被控制)。

img

然后,我們可以控制const char* user。關鍵字const僅僅是提示編譯器,實際編譯成binary然后執行的時候,const指向的內容仍然可以被覆蓋掉。

假如我們覆蓋了user和userlen,比如user覆蓋成0x41414141。在以下代碼執行的時候,我們就可以把0x41414141開始的userlen(可控長度)字節復制到緩沖區中。

img

然后,這個內存會隨著Type-3的消息,發送給攻擊者的服務器。即遠程任意內存泄露。

3.3 遠程代碼執行

現在的問題來了,我們已經有了任意地址讀的攻擊方案,是否有其他什么方法可以讓我們進行代碼執行了?答案是:可以。

如果一個程序,有著這樣的結構:

while(1){ 
   if(cond) 
?      foo(); 
} 

那么只要它不退出,對于foo()來說, stack cookie每次都一樣(當然棧每次也都一樣)。如果foo()中可以觸發這個漏洞,攻擊者就可以得到cookie并向后覆蓋。當然,攻擊者也可以通過自己手動計算,方法很多,這里只是說其中一種最方便快捷的可能性。

可以簡單做一下實驗來證實,對如下代碼:

img

使用-fstack-protector編譯,確保包含callq __stack_chk_fail@plt。

img

編譯后執行結果為:

img

攻擊的步驟就很簡單了:

  1. 利用CVE-2018-16890來獲取程序的Base Addr,并找到堆棧的起始地址,計算出觸發漏洞時的棧地址。

  2. 利用CVE-2019-3822的3.2來獲取執行時棧的內存。

  3. 解出正確的棧內存,并保存。保存的數據包括正確的stack cookie。

  4. 再一次發起請求時,用上一步保存的內容直接進行棧覆蓋,并確保程序返回時,返回到攻擊者可控的地址上(因為已經有幾個寄存器可以控制,因此這步通常是stack pivot 的gadget)。同時,在棧上直接寫入其他ROP gadget,方便后續進行ROP attack。

  5. 代碼執行完成。

如果攻擊者能夠控制客戶端的行為那便是最好了,例如在root某些設備的時候,攻擊者可以控制使用curl的組件重復發送請求。

實際利用時可能需要具體對待,例如,ROP gadget雖然可以基于curl或者PHP去找,但是你并不能確保遠程機器上的curl和PHP都是未修改的。所以可能會有成功率的問題。

3.4 棧緩沖區溢出→堆緩沖區溢出

最后,如果開發人員已經注冊了帶有堆分配的回調,那么它還有可能變成堆緩沖區溢出。而注冊帶堆分配的回調也是常見的操作。

這個奇跡可能發生在下面的代碼中,但是這需要看具體使用者是怎么實現convert_to_network的。我在這里只是提到這種可能,就不細說了。

img

<四> 兩個本可避免的漏洞

漏洞均出于人。人是代碼的創造者,也是災難的創造者。讓我們簡單分析一下這些漏洞是如何產生的,而它們為什么本可以避免在代碼中呆那么久的時間。

img

圖:這兩個漏洞從36版本引入,一直存活到63版本(我報告時的版本)。

4.1被忽視的編譯器警告

不要忽略編譯器的警告。編譯器之所以給出警告,正是代表著代碼已經存在了歧義,雖然開發者可能有A型抽象的理解,但是運行的時候難免會變成機器遵循規則執行機器碼的B型具體的解釋。

其實這個問題單獨抽出來就很容易想明白,有符號數與無符號數相加相減,到底代表什么?為什么描述同一個狀態的緩沖區變量,一個“大小”可以是負數,而另一個“大小”卻只能是正數?與其解釋給自己或者小黃鴨,不如直接在代碼上就規范好所有同類的東西的類型。

4.2過于隱蔽的宏定義

因為是人工審計,我習慣只在*.cc里面搜索,以至于這次差點漏過了這個緩沖區溢出(這個宏定義于.h文件中)。 它的問題出在這個宏給人的感覺就是,它就是一個常量,一個類似于#define PI 3.14的常量。但實際上它不僅值會變,而且還參與了很重要的邏輯的運算。

如果語義上要定義一個動態可變的參數,出于安全考慮,我更建議定義成函數樣式,如:

\#define LENGTH(X) (1 + 2 + (X) - 3) 

或者,只把不變的部分定義成宏,如:

\#define HEADERLEN (1 + 2) 
\#define SUFFIXLEN (3) 
Len = HEADERLEN + x – SUFFIXLEN; 

這樣,當代碼中出現這個宏的時候,基本一眼就能看得出來至少這東西的值可能是會變化的。以免在自己動態調試的時候都可能看花眼略過去。

4.3過長的函數

最后,開發同學們可能都知道,一直會有人強調不要寫一個好幾百行、功能復雜的大函數,而是要把函數分離開。但是深層次原因除了這樣很難閱讀或維護,還有其他的嘛?這里從安全上補充一個建議:為了安全起見,建議不要寫如此龐大的函數。

從安全角度來說有什么影響?就像本文的例子一樣,因為函數的棧幀中有太多的局部變量,一旦某個變量發生緩沖區溢出,或者其他什么變量發生了Out of bounds存取,極有可能會影響到其他局部變量的值。

而如果把函數分成很多小函數,即使發生了棧緩沖區溢出,因為有Stack cookie的保護,攻擊者也不太可能會直接影響到其他函數中的棧幀(因為在調用到那里前就會因為cookie不符合程序直接崩潰)。

當然,關于大函數,這一點可能是利也可能是弊。我們的例子這種,如果攻擊者在函數很靠前的位置就控制了你的函數,那后面這部分代碼很有可能會幫助攻擊者完成更復雜的功能。當然,弊端就是根據實際情況,后面的代碼也有可能會給攻擊者設置障礙。

<五> 結語

對于一些第三方組件,我們在使用的時候也許都會假定他們很安全,可能覺得它沒有那么危險,但如果當它們與PHP或者其他你熟悉的軟件結合起來,那后果可能都是十分嚴重的。

任何的遠程代碼執行、內存泄露,都可能造成另一個特定的攻擊客戶端版本的“心臟滴血”。

感謝Tencent Blade Team和團隊的技術氛圍,研究和討論中我逐漸發現,這些問題的根源很多是來源于開發者的開發習慣上。我也曾經有幾年在做開發,看別人代碼不那么容易,但看自己代碼更難。我也寫過不少有安全問題的代碼,開發不易,測試不易,堅持不易。不過即使不易,我覺得仍要堅守開發的規范,這個既避免自己之后還技術債,也是對產品形象的負責,和對用戶的負責。

最后,也附上CURL官方的修復方案。

(1)受影響的CURL: 低于7.63.0且開啟NTLM認證的CURL
(2)按照優先順序立即采取以下操作之一:

A-將curl升級到版本7.64.0。
B-將修補程序應用到您的軟件上并重新編譯。

PATCH

https://github.com/curl/curl/commit/50c9484278c63b958655a717844f0721263939cc

PATCH

https://github.com/curl/curl/commit/b780b30d1377adb10bbe774835f49e9b237fb9bb

C-關閉NTLM身份驗證

Daniel的修補代碼都十分巧妙,非常簡單有效,因此除了升級,PATCH也是一個比較好的備選方案。當然,如果你不需要NTLM,關閉它是最直接的避免此漏洞的方案。

具體仍請參考curl官網公告:
https://curl.haxx.se/docs/CVE-2018-16890.html

https://curl.haxx.se/docs/CVE-2019-3822.html


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/813/