天天看點

深度剖析Struts2遠端代碼執行漏洞

本文講的是<b>深度剖析Struts2遠端代碼執行漏洞</b>,

深度剖析Struts2遠端代碼執行漏洞

三月初,安全研究人員發現世界上最流行的JavaWeb伺服器架構之一– Apache Struts2存在遠端代碼執行的漏洞,Struts2官方已經确認該漏洞(S2-046,CVE編号為:CVE-2017-5638)風險等級為高危漏洞。

漏洞描述

該漏洞是由于上傳功能的異常處理函數沒有正确處理使用者輸入的錯誤資訊,導緻遠端攻擊者可通過修改HTTP請求頭中的Content-Type值,構造發送惡意的資料包,利用該漏洞進而在受影響伺服器上執行任意系統指令。 

漏洞利用條件和方式

黑客通過Jakarta 檔案上傳插件實作遠端利用該漏洞執行代碼。

漏洞影響範圍

建議大家盡快更新到 Apache Struts 2.3.32 or 2.5.10.1 

近日,我們對這個漏洞的執行代碼進行了詳細的分析,并對野外利用中的有效載荷進行了跟蹤研究。除此之外,我們還提供了CVE-2017-5638運作的有效載荷,這是一個可以繞過隻能檢查請求内容類型的Web應用程式防火牆規則的備用漏洞利用向量。

對于不熟悉SSTI(服務端模闆注入)概念的人員來說,這是一個注入攻擊的典型例子。從模闆引擎的原理可知,哪方實作的模闆引擎,就依賴哪方。在server端實作模闆引擎時,依賴server端。在用戶端實作依賴用戶端。

是以,隻要在用戶端實作一種模闆解析方式(引擎),用來讀取模闆内容,分析并轉為用戶端可執行的程式源碼,并運作,就可以脫離服務端,在浏覽器端渲染頁面,而不依賴服務端,這樣的結果通常是模闆引擎會允許任何形式的代碼執行。對于許多流行的模闆引擎,例如Freemarker,Smarty,Velocity,Jade等,通常可以在引擎之外執行遠端代碼執行(即産生系統shell)。在Struts的案例中,模闆引擎就是使用諸如對象圖導航語言(OGNL)的表達式語言來提供簡單的模闆功能。與OGNL的方法類似,模闆引擎通常也可以在表達式語言之外獲得遠端代碼執行。這些代碼庫提供了幫助緩解遠端執行代碼(如沙盒)的機制,但是預設情況下它們往往被禁用,或者簡單地繞過。

從代碼的角度來看,SSTI存在于應用程式中的最簡單的條件是将使用者輸入傳遞到解析模闆代碼的函數中。将函數句柄值丢失是将各種注入漏洞引入到應用程式中的一種簡單方法。要發現這樣的漏洞,必須仔細追蹤和分析調用堆棧和任何被感染的資料流。

這樣才能充分了解CVE-2017-5638的工作原理,不過要真正了解漏洞的工作原理,我們還需要對庫中的相關代碼進行全面的分析。

我們通過捕獲和記錄CVE-2017-5638在運作時的異常代碼程序,倒推出了次漏洞的工作原理。正如我們在下面的惡意代碼再現中看到的那樣,漏洞可以導緻遠端代碼執行,在Apache公共上傳庫中parseRequest(request)解析出現異常,這是因為請求的内容類型與預期的有效字元串不比對。我們還注意到,這個上傳庫引發的異常消息還包括HTTP請求中提供的無效内容類型頭。這實際上是用使用者輸入來描述異常消息。

使用者輸入要求:

使用者輸入反應:

使用者登入異常:

負責調用生成異常的parseRequest方法的調用者在一個名為JakartaMultiPartRequest的類中,JakartaMultiPartRequest作為圍繞Apache commons fileupload庫的包裝器,定義了一個名為processUpload的方法,如下圖所示,該方法在第91行調用了自己的parseRequest方法。該方法在第151行建立一個新的ServletFileUpload對象,并在第147行調用其parseRequest方法:

檢視 Stacktrace(堆棧軌迹),Stacktrace是一個非常有用的調試工具. 在未捕獲的異常被抛出時(或者手動制造堆棧跟蹤的時候),它會讓我們看到調到的堆(在某一點調用方法的堆)。不僅顯示出出現錯誤的地方,也顯出程式在那個地方是如何結束的。我們可以看到processUpload方法由JakartaMultiPartRequest在第67行的解析方法調用。如下圖所示,調用此方法的任何抛出的異常都在第68行被捕獲,并傳遞給buildErrorMessage。雖然根據引發的異常的類調用processUpload方法可以有多個選擇,但最終都要調用buildErrorMessage方法。在這種情況下,第75行調用buildErrorMessage方法:

由于JakartaMultiPartRequest類沒有定義buildErrorMessage方法,是以我們要檢視它擴充的類:AbstractMultiPartRequest:

它傳回的LocalizedMessage定義了一個簡單的類似容器的對象,其中textKey設定為struts.messages.upload.error.InvalidContentTypeException,defaultMessage被設定為使用者輸入感染的異常消息。

接下來在stacktrace中,我們可以看到JakartaMultiPartRequest的解析方法在第86行的MultiPartRequestWrapper的構造方法中被調用,在第88行調用的addError方法會檢查是否已經看到錯誤,如果不是,它會将它添加到一個包含LocalizedMessage對象集合的執行個體變量中:

在我們對堆棧進行軌迹跟蹤的下一行,我們看到Dispatcher類負責執行個體化一個新的MultiPartRequestWrapper對象并調用上面的構造方法。這裡調用的方法叫做wrapRequest,它負責檢測請求的内容類型是否包含第801行的子串“multipart / form-data”,如果包含,則在第804行建立一個新的MultiPartRequestWrapper并傳回:

在我們分析的這個樣本時,它的HTTP請求已被解析,我們的包裹請求對象(MultiPartRequestWrapper)持有一個錯誤(LocalizedMessage)和我們的預設消息,而一個textKey設定為struts.messages.upload.error.InvalidContentTypeException。

雖然堆棧軌迹的其餘部分不能為我們繼續跟蹤資料流提供任何非常有用的幫助,但是,我們從中發現了一個線索, Struts通過一系列攔截器處理請求。事實證明,名為FileUploadInterceptor的攔截器是Struts配置的預設“堆棧”的一部分。

正如我們在第242行看到的,攔截器會檢查我們的請求對象是否是MultiPartRequestWrapper類的執行個體。我們知道這是因為Dispatcher以前傳回了這個類的一個執行個體,攔截器會繼續檢查MultiPartRequestWrapper對象是否在第261行出現錯誤。然後它在第264行調用LocalizedTextUtil的findText方法,傳遞幾個參數,例如錯誤的textKey和我們的預設的defaultMessage:

LocalizedTextUtil的方法findText的一個版本被調用,它試圖根據以下6個因素找到一個傳回的錯誤資訊:

valueStack設定為ActionContext的valueStack:

因為未找到資源束定義struts.messages.upload.error.InvalidContentTypeException的錯誤消息,是以此過程最終将調用第573行上的getDefaultMessage方法:

同一個類中的getDefaultMessage方法負責最後一次嘗試找到一個給定密鑰和語言環境的錯誤消息。在我們的嘗試過程中,getDefaultMessage方法嘗試失敗,并利用我們的異常消息,在第729行調用TextParseUtil的translateVariables方法:

事實證明,TextParseUtil的translateVariables方法是用于表達式語言評估的資料接收器。它通過評估包含在$ {…}和%{…}的執行個體中的OGNL表達式來提供簡單的模闆功能,定義并調用了幾個版本的translateVariables方法,最後評估第166行的表達式:

有了這個最後一個方法調用,我們就可以跟蹤到使用者輸入的異常消息,一直到OGNL的評估。

大家可能會非常想知道漏洞的有效載荷是如何工作的。首先,我們嘗試提供一個傳回附加标題的簡單OGNL有效載荷。我們需要在開頭包含未使用的變量,以便Dispatcher檢查“multipart / form-data”子串,并将我們的請求解析成檔案上傳。

事實證明,Struts提供了類成員通路的黑名單功能即類方法。預設情況下,使用以下類清單和正規表達式:

為了更好地了解原始的OGNL有效載荷,讓我們嘗試一個實際樣本的載荷過程:

載荷要求:

載荷效果:

我們可以看到,這确實有效。但是如何繞過我們之前看到的黑名單呢?

這個有效載荷是空的排除包名稱和類的清單,進而使黑名單無用。它首先通過擷取與OGNL上下文相關聯的目前容器并将其配置設定給容器變量來實作。大家可能會注意到com.opensymphony.xwork2.ActionContext類包含在上面的黑名單中。那既然這樣,怎麼會躲避黑名單的捕獲呢,因為我們沒有引用類成員,而是通過OGNL值堆棧中已經存在的密鑰(在core/src/main/java/com/opensymphony/xwork2/ActionContext.java:102中定義的)。我們的有效載荷已經利用了這個類的一個執行個體引用。

接下來,有效載荷擷取容器的OgnlUtil執行個體允許我們調用傳回目前排除的類和包名稱的方法。最後一步是簡單地清除每個黑名單并執行我們想要的任何無限制的評估。

一旦黑名單被清空,有效載荷也變空了,直到被代碼覆寫和應用程式重新啟動。我們還發現了一個常見的測試陷阱,當我們試圖重制在野外發現的某些有效載荷或記錄時,有些有效載荷無法工作,因為它們已經假設黑名單已經被清空,這可能是以前在不同有效載荷的測試期間發生的。這充分說明了運作動态測試時重置應用程式狀态的重要性。

大家可能還注意到,使用的原始漏洞利用的有效載荷比我們所列舉的樣本有點複雜。比如,為什麼會執行額外的步驟,例如檢查_memberAccess變量并調用名為setMemberAccess的方法?我們猜想可能是嘗試利用另一種技術來清除黑名單,以防第一種技術不起作用。使用MemberAcess類的預設執行個體調用setMemberAccess方法,該執行個體實際上也會清除黑名單。是以我們可以确認這種技術會在Struts 2.3.31中工作,而不是Struts 2.5.10。不過目前,我仍然不确定三元運算符的目的是檢查和有條件地配置設定_memberAccess。在測試期間,我們沒有觀察到這個變量的評估。

從2.5.10開始,存在針對CVE-2017-5638的其他漏洞利用,這是因為任何不具有關聯錯誤密鑰的使用者輸入的異常消息将都被評估為OGNL。例如,提供帶有空位元組的上傳檔案名将導緻從Apache commons fileupload庫中抛出InvalidFileNameException異常。這也将繞過檢查内容類型标頭的Web應用程式防火牆規則,以下請求中的%00應首先進行URL解碼,結果為使用者輸入的異常消息。

通過檢視stacktrace可以看到,控制流在JakartaMultiPartRequest類的processUpload方法中發生偏差。當在第91行調用parseRequest方法時抛出異常,而不是在調用processFileField方法并擷取105行檔案項的名稱時抛出異常:

總結

我們從這項研究中得到的一個收獲就是,不能總是依賴于檢視CVE描述來了解漏洞的工作原理。比如,CVE-2017-5638存在的可能原因是因為檔案上傳時,攔截器嘗試使用評估OGNL的潛在危險函數來解決錯誤消息。是以,這不是Jakarta請求包裝器的問題,正如CVE描述的那樣,但是檔案上傳時攔截器信任該異常的消息将不會被使用者輸入。

檢測與修複方案

如果您的裝置已經檢測出存在Struts2漏洞,根據您的具體情況有以下三種解決方式:

1.官方解決方案

官方已經釋出版本更新,盡快更新到不受影響的版本(Struts 2.3.32或Struts 2.5.10.1),建議在更新前做好資料備份。

Struts 2.3.32 下載下傳位址,

Struts 2.5.10.1下載下傳位址。

2.臨時修複方案

在使用者不便進行更新的情況下,作為臨時的解決方案,使用者可以進行以下操作來規避風險:

在WEB-INF/classes目錄下的struts.xml 中的struts 标簽下添加

在WEB-INF/classes/ 目錄下添加 global.properties,檔案内容如下:

深度剖析Struts2遠端代碼執行漏洞

配置過濾器過濾Content-Type的内容,在web應用的web.xml中配置過濾器,在過濾器中對Content-Type内容的合法性進行檢測:

深度剖析Struts2遠端代碼執行漏洞

原文釋出時間為:2017年4月6日

本文作者:xiaohui

本文來自雲栖社群合作夥伴嘶吼,了解相關資訊可以關注嘶吼網站。

<a href="http://www.4hou.com/technology/4109.html" target="_blank">原文連結</a>