CVE-2019-0230:Apache Struts OGNL遠程代碼執行漏洞詳解
在Trend Micro Vulnerability Research Service近期公布的一份漏洞報告中,Trend Micro研究團隊成員Kc Udonsi和John Simpson詳細介紹了Apache Struts框架中最近曝出的一個代碼執行漏洞。這個安全漏洞最初是由蘋果信息安全部的Matthias Kaiser發現并報告的。下文節選自他們撰寫的CVE-2019-0230報告,其中做了部分修改。
最近,Apache Struts框架被曝出了一個遠程代碼執行漏洞。該漏洞是由于輸入驗證不足導致在計算原始用戶輸入時強制進行兩次Object Graph Navigation Library(OGNL)計算所致。攻擊者可以通過向目標服務器發送精心制作的請求來利用該漏洞。一旦攻擊得手,他們就能夠以服務器的權限執行任意代碼。
漏洞詳情
Apache Struts是一個用于構建基于Java的web應用程序的模型-視圖-控制器(MVC)框架。該MVC框架用于將信息的表示與用戶與信息的交互隔離開來。其中,模型由負責與數據庫通信等后端工作的業務和應用程序邏輯組成。視圖是提供給用戶的數據的表示形式,而控制器負責協調模型和視圖之間的通信。
Apache Struts將通過Java Servlet API提供一個名為ActionServlet的控制器。而來自客戶端的請求則以XML配置文件中定義的“Action(操作)”形式發送到控制器。這時,控制器會調用模型代碼中相應的Action類,這些類會使用內部邏輯來驗證和執行這些操作,并以新視圖的形式返回結果,而控制器則負責更新這些結果。Apache Struts允許通過Java ServerPages(JSP)對視圖進行編碼,這些視圖將編譯成HTML頁面,以便通過瀏覽器進行查看。
我們可以使用如下所示的URI來訪問相關的操作:
- action name >.action
其中< action name >被替換為操作名。后綴“.action”只是一個慣例。基于Struts的Web應用程序可以使用任何后綴,如“.do”。
HTTP是RFC 7230-7237和其他RFC中描述的請求/響應協議。客戶端將請求發送到服務器,經過相應的處理后,服務器會再將響應發送回客戶端。通常情況下,HTTP請求是由一個請求行、各種標頭、一個空行和一個可選的消息正文組成的,具體如下所示:
其中,CRLF表示回車換行符(CR),后跟換行符(LF),而SP則表示空格字符。參數可以在請求URI或消息正文中作為“名稱-值”對從客戶端傳遞到服務器端,具體取決于所使用的方法和Content-Type標頭。例如,當使用GET方法傳送值為“1”、名為“param”的參數時,相應的HTTP請求將如下所示:
如果使用POST方法的話,則相應HTTP請求將如下所示:
如果有多個參數/值對的話,它們將被編碼為以&為分隔符的“名稱=值”對,具體如下所示:
- var1=value1&var2=value2...
此外,Apache Struts還支持使用對象圖導航語言(Object Graph Navigational Language,OGNL)表達式通過模板.jsp文件動態生成網頁內容。OGNL是一種具有簡潔語法的動態表達式語言(EL),用于獲取和設置Java對象、lambda表達式等屬性。OGNL表達式可以包含組成導航鏈的字符串。這些字符串可以是屬性名、方法調用、數組索引等。OGNL表達式是根據以OGNL上下文形式提供給求值器的初始或根上下文對象進行計算的。
Apache Struts使用名為ActionContext的線程本地容器對象來存儲執行Action所需的各種對象。這些對象通常會包括會話標識符、請求參數、區域設置等。此外,ActionContext還公開了一個ValueStack接口,用于推送和存儲處理動態表達式語言(EL)所需的對象。當EL編譯器需要解析表達式時,它會從推送到堆棧中的最新對象開始向下搜索堆棧。OGNL是Struts使用的主要EL,它具有以下特殊語法:
- --objectName:ValueStack中的對象(OGNL上下文中的默認/根對象),如Action屬性。
- --#objectName:位于ActionContext之內,同時位于ValueStack之外的對象。例如,#parameters.objectName用于請求參數,#session.objectName用于會話作用域屬性,等等。
- --%{ognlexp}:強制對通常為字符串的屬性進行OGNL評估。
- --@full.class.name@Static_Field:用于表示類的靜態字段(變量和方法)。
與此報告相關的功能是強制使用語法%{ognlexp}對通常為字符串的屬性進行OGNL計算。該語法稱為alt語法,是默認啟用的。如果ognlexp是從用戶輸入中獲得的,且沒有進行任何消毒處理,那么很可能會發生雙重計算。如果在標簽屬性中進行強制求值的話,這種情況就非常可能發生。例如(以錨點標簽為例):
如果用戶提供的fileNam,使得原始OGNL表達式未經進一步驗證即進行了傳遞,則當該標簽作為請求的結果渲染時,將會計算走私的OGNL表達式。也就是說,如果filename是OGNL表達式,如%{2+2},則渲染的標簽為:
在渲染響應時,對于每個要渲染的標簽,都會調用在org.apache.struts2.views.jsp.ComponentTagSupport類中定義的doStartTag()方法。然后,這個方法會進一步調用populateParams()方法。然而,這個方法的實際調用版本,則取決于被處理的標簽。對于錨標簽來說,對應的方法被定義為org.apache.struts2.views.jsp.ui.AnchorTag類中的populateParams()方法。實際上,id屬性的第一次計算發生在populateParams()的基類實現中,即調用在org.apache.struts2.component.UIBean類中定義的setId()。
在調用populateParams()方法之后,doStartTag()方法將開始計算標簽并渲染模板。在org.apache.struts2.component.ClosingUIBean類中定義的start()方法中,將會執行函數evaluateParams(),而這個函數會利用populateComponentHtmlId()函數來填充標簽的id屬性。至于這個方法的調用版本,具體取決于被渲染的標簽。對于一個錨標簽來說,將調用org.apache.struts2.component.UIBean類中定義的populateComponentHtmlId()方法。這個實現會在findStringIfAltSyntax()函數中再次將id屬性作為OGNL表達式進行處理。
在Apache Struts 2框架存在一個遠程代碼執行漏洞。該漏洞是由于對OGNL表達式可用的類和包限制不足所致,特別是java.io.、java.nio.、java.net.、sun.misc.包。雖然Apache Struts團隊多次指出重復的OGNL計算是一個潛在的安全風險,并指出開發人員應該只使用強制計算功能來處理受信任的數據,并已經添加了相應緩解控制措施,以最大限度地降低被利用的可能性。這些緩解控制措施包括:
- -- struts.excludedClasses:以逗號分隔的由排除的類組成的列表。
- -- struts.excludedPackageNamePatterns:用于根據Regex來排除包的模式。
- -- struts.excludedPackageNames: 用逗號分隔的由被排除的包組成的列表。
此外,Struts默認限制了對Class構造函數以及靜態方法的訪問。SetOgnlUtil()方法會修改ValueStack的SecurityMemberAccess屬性,以包含上述列表中的排除項。但是,如果遇到不在排除項中的類或包,則仍然允許其執行不安全操作,因為SecurityMemberAccess.isAccessible()方法允許對那些訪問或操作這些包中定義的對象的表達式進行計算。
Apache Struts所使用的FreeMarker Java模板引擎在包freemarker.ext.jsp中提供了一組實用工具類,以方便FreeMarker-JSP的雙向集成。在這些類中,有一個名為TaglibFactory的類。這個類提供了一個與servlet上下文相關聯的哈希模型,可以加載相關的JSP標簽庫。類TaglibFactory能夠通過ObjectWrapper類的實例將Java對象映射到FreeMarker模板語言的類型系統。ObjectWrapper類決定了Java對象的哪些部分可以從模板中訪問,以及如何訪問。此外,類TaglibFactory還公開了一個獲取支持ObjectWrapper實例的getter。同時,這個ObjectWrapper實例還提供了一種創建任意Java對象的方法,以便在需要時分別使用newInstance()和wrap()兩個公共方法對其進行封裝。因此,攻擊者可以通過首先使用wrap()方法對參數進行封裝,然后調用newInstance()指定要實例化的類和封裝的構造函數參數來創建一個具有公共構造函數的任意類實例。這個gadget有效地幫助Struts擺脫了對Class構造函數的默認限制。因此,TaglibFactory的實例可用于OGNL ValueStack中的OGNL表達式。
Guice輕量級依賴注入容器在包com.openymphony.xwork2.inject中提供了一組實用工具類,用于提供構造函數、方法、靜態方法、字段和靜態字段注入。其中,有一個名為ContainerBuilder的類。這個類提供了構建依賴注入容器的工廠方法。更重要的是,它還提供了一個公共方法,可以用來創建Container實例。而Container實例不僅存有依賴關系映射,而且還提供了一個公共方法injection(),該方法可用于創建任意實例并將其注入依賴項映射中。容器實例有效地提供了一種檢索靜態工廠方法保護的實例的方法,否則OGNL表達式將無法使用這些方法。通過調用inject()并指定所需的類,我們就可以獲得沒有公共構造函數,只有返回單例實例的靜態工廠方法的任意類實例。我們可以使用前述的FreeMarker TagLibFactory小工具來獲得ContainerBuilder類的實例,因為它實現了一個公共構造函數。
借助于這些小工具(gadget),我們可以通過獲取受限類sun.misc.Unsafe的實例,從java.net.包中實例化一個類,打開與遠程機器的連接并從遠程機器接收類字節,最后使用sun.misc.Unsafe.defineAnonymousClass()方法以及從遠程服務器接收到的字節來定義一個任意類,從而繞過當前的安全限制。然后,我們就可以使用sun.misc.Unsafe.allocateInstance()方法初始化任意類了。盡管allocateInstance()方法無法調用構造函數,卻可以在創建的實例上調用為該類定義的其他公共方法。或者,我們也可以使用來自java.io或java.nio包的類的實例可以用來創建任意文件(如JSP文件),并將其寫入到任意位置(如Web應用程序根目錄),因為文件路徑引用可用于OGNL ValueStack的OGNL表達式。
遠程攻擊者可以通過向受攻擊的服務器發送包含惡意參數的HTTP請求來利用該漏洞。一旦成功利用該漏洞,他們就能夠執行具有服務器權限的任意代碼。
小結
Apache Struts團隊已經在2020年8月修復了這個安全漏洞。根據他們的說法,相關的補丁是通過確保對傳入的每個值進行正確檢查,并驗證其是否用于標簽的屬性來修復該漏洞的。此外,他們還建議,除非無法避免,否則最好不要使用%{...}或${...}語法對值以外的屬性進行強制求值。最后,需要補充一點,Struts的2.5.22和更高版本不受該漏洞影響。
特別感謝趨勢科技研究團隊的Kc Udonsi和John Simpson對該漏洞提供了如此詳盡的分析。有關趨勢科技研究服務的簡介,請訪問http://go.trendmicro.com/tis/。
本文翻譯自:https://www.thezdi.com/blog/2020/10/7/cve-2019-0230-apache-struts-ognl-remote-code-execution如若轉載,請注明原文地址。