一只網安小白對Struts2遠程命令執行的嘗試梳理
一.Struts2基礎背景
Apache Struts2是一個用于開發JavaEE網絡應用程序的開放源代碼網頁應用程序框架。單詞Struts在編程語言中譯為結構體,而在高級編程語言中結構體正是面向對象的類(Class)的前身。
Struts2是一個基于Client/Server模式、HTTP協議的Web應用程序的MVC模式框架,本質上相當于一個Servlet(容器)。MVC設計模式即模型(Model)、視圖(View)和控制器(Control)。Struts2作為控制器(Controller)來建立模型與視圖之間的數據交互,是Struts的下一代產品,以Struts1和WebWork的技術基礎進行合并升級的Java Web框架。
從Struts1到Struts2變化非常大,但Struts2相對于WebWork的變化很小。簡單理解:以WebWork的核心技術框架為血肉披上Struts的知名度表皮經過包裝整合成為Struts2。
1.1 Struts2背景和現狀
自2000年5月發展至今,Struts1市場占有率超過20%,開發人群豐富,穩定性和可靠性都得到廣泛的證明,已然成為一個高度成熟的框架,隱有事實行業標準的趨勢。隨著時間推移技術不斷進步,視圖效果要求越來越高,Struts1的局限性也越來越多地暴露出來,并且制約Struts1繼續發展。
由于Struts1框架與JSP/Servlet關聯非常緊密導致一些嚴重問題:
—Struts1支持的視圖技術單一。由于Struts1出現年代較早,那時尚無Free Marker、Velocity等技術,因此它不能與視圖層的模版技術進行整合;
—Struts1與Servlet API嚴重耦合,應用難于測試;
—Struts1代碼嚴重依賴于Struts1 API。
技術層面上,出現許多與Struts1競爭的視圖層框架,比如JSF、Tapestry和spring MVC等。這些框架由于出現年代比較近,并應用最新設計理念,同時也從Struts1中吸取經驗、克服不足。競爭的同時也促進Struts的發展。
Struts1已經分化成為兩個框架:
—第一個是在傳統的Struts1的基礎上,融合另外的一個視圖層技術優勢的Web框架WebWork的Struts2。Struts2雖是在Struts1的基礎上發展而來,但是實質上是以WebWork為核心。Struts2為傳統Struts1注入WebWork先進設計理念,融合統一Struts1和WebWork兩個框架;
—第二個分化框架是Shale。該框架遠遠超出Struts1原有設計思想,關聯很少。Shale更像一個新的框架而不是Struts1的分化升級款。
Struts2的體系結構與Struts1的體系結構差別巨大,但Struts2向后兼容Strust1。Struts2以WebWork設計思想為核心,采用攔截器的機制處理用戶請求,這樣的設計也使得業務邏輯控制器能夠與Servlet API完全脫離,所以Struts2也可以理解為WebWork的更新升級產品。
自2007年官方發布S2-001公告第一個遠程代碼執行(Remote Code Execution,RCE)至梳理本篇文章時,安全公告已經發布至S2-061,其中31個安全公告涉及RCE,且每個安全公告至少涉及一個CVE,當之無愧Java安全領域的“漏洞之王”。
1.2 Struts2工作原理
Web應用程序與傳統網站的不同之處在于Web應用程序可以創建動態響應,傳統網站只提供靜態頁面。Web應用程序可以與數據庫和業務邏輯引擎以自定義響應的方式產生交互。
基于 Java Server Pages(JSP)的Web應用程序有時會混合數據庫代碼、頁面設計代碼和控制流代碼。應用實踐中,除非將這些代碼關注點分開,否則大型應用程序的維護將變得很困難。
Struts2在軟件應用程序中分離關注點的方法就是使用MVC模式,即模型(Model)、視圖(View)和控制器(Control)。Model映射業務或數據庫代碼,View映射頁面設計代碼,Controller映射導航定位的控制流代碼,Struts可以很好地與傳統的 REST 應用程序以及 SOAP 和 AJAX 等技術配合使用。
Struts2框架為Web應用程序開發提供三個關鍵組件:
— 一個由應用程序開發人員提供的映射到標準 URI 的“請求”處理程序。
— 一個控制轉移請求到另一個可完成響應的資源的“響應”處理程序。
— 一個標簽庫,可幫助應用程序開發人員使用服務器頁面創建基于表單的交互式應用程序。
Struts2框架在MVC基本思想模式下的構成:
—M模型部分:一系列Interceptors(攔截器)、Action組件和ActionContext組件;
—V視圖組件:Result組件;
—C控制部分:DispatchFilter(調度過濾器)、StrutsPrepareAndExecuteFilter。
1.2.1 針對框架組件給出名詞解析
—Servlet:
服務程序的容器,如常見tomcat weblogic websphere jboss resin glassfish等;
—FilterDispatcher:
過濾器調度,控制部分的核心,即MVC中C控制層的核心,FilterDispatcher進行初始化并啟用核心doFilter(在早期的 Struts2 開發中使用,它從 Struts 2.1.3 開始被棄用);
—StrutsPrepareAndExecuteFilter:
直譯為Struts的預處理和執行過濾器,StrutsPrepareAndExecuteFilter類似Struts2.1.3以前版本中的FilterDispatcher,是Struts2框架的核心控制器。該控制器作為一個Filter運行在Web應用中,它負責過濾所有的HttpServletRequest請求。當HttpServletRequest請求到達時,該Filter會過濾用戶請求,如果用戶請求以.action結尾,該請求就會被轉入Struts2框架處理;
—ActionMapper:
動作映射器,HttpServletRequest請求被運行在Web應用中的、作為一個Filter的控制器StrutsPrepareAndExecuteFilter過濾處理,如該請求以.action結尾,就會被轉入Struts2框架的ActionMapper記錄處理,獲取*.action請求后,將根據*.action請求的前面部分決定調用那個業務邏輯組件。例如:ActionMapper對于login.action請求,調用名為login的Action來處理該請求;
—ActionProxy:
動作類代理,用戶實現的Action是Struts2的ActionProxy的代理目標。用戶實現的業務控制器Action則包含了對用戶請求的處理,用戶的請求數據被封裝在HttpServletRequest中,而用戶Action Class無需訪問HttpServletRequest對象,過濾器會將請求數據解析出來并傳給業務邏輯組件Action實例;
—ConfigurationManager
配置管理器,讀取框架內用戶的配置文件struts.xml,配置文件內是用戶編寫的Action Class;
—Action Invocation
動作調用;
—Tag Subsystem
標簽子系統,如HTML、Dojo、forms等;
—Interceptor
攔截器;
—Template
模板,指JSP、FreeMarker、Velocity等。
Struts2框架由三部分構成:核心控制器、業務控制器和用戶實現的業務邏輯組件。三部分中,Struts2框架提供核心控制器StrutsPrepareAndExecuteFilter,而用戶需要實現業務控制層和業務邏輯層。Struts2官方給出的框架如圖1.2.1,現如今的Struts2經過一系列的修修補補已經和當時的組織框架有一定的差別。

圖1.2.1 Struts2官方框架圖
1.2.2 Struts2中正常請求的響應處理流程
—用戶請求數據被封裝在一個HttpServletRequest請求中并由客戶端提交,指向一個Servlet容器,Servlet容器初步解析該請求,并確定處理該請求的Web程序;
—Struts2參考該Web程序下的web.xml配置文件(Struts2.5時過濾器移到top文件)并確定過濾器Filter,注意順序是先ActionContextCleanUp,再其他過濾器(SiteMesh、Plugin等),最后是核心控制器FilterDispatcher(Struts2.1.3時核心控制器變更為StrutsPrepareAndExecuteFilter);
—核心控制器FilterDispatcher(StrutsPrepareAndExecuteFilter)調用ActionMapper以確定是否需要調用某個Action來處理該HttpServletRequest,ActionMapper確定需要調用的Action;
—ActionMapper確定需要調用的Action后,核心控制器則把請求的處理交給ActionProxy,ActionProxy通過Configuration Manager詢問Struts2框架的配置文件struts.xml,確定需要調用的Action Class;
—ActionProxy創建一個ActionInvocation實例,同時ActionInvocation通過命名模式調用Action。但在調用之前,ActionInvocation會根據配置加載Action相關的所有Interceptor(攔截器);
—調用執行Action完畢后,ActionInvocation負責根據struts.xml中的配置確定對應的返回結果Result,該過程需要ActionMapper參與;
—Result通常對應的是Template里面需被表示的JSP、Free Marker和Velocity等視圖模板,也可能是另一個Action鏈;
—Result視圖結果信息傳遞給ActionInvocation,此過程會反向加載對應調用Action時加載的所有Interceptor;
—最終處理結果被封裝到HttpServletResponse中,通過核心控制器反饋客戶端并呈現頁面給用戶。
1.3 Struts2安全機制
從嚴格意義來講,Struts2框架并不提供獨立的安全機制,黑名單或白名單方式構建沙箱的基礎也是基于框架核心的攔截器(Interceptor)在一次次攻擊防御重修修補補所實現的。換而言之,就是Struts2的默認表達式語言OGNL存在只允許其范圍內的,阻止訪問或者只允許訪問特定類SecurityMemberAccess和Java包(Jar),其內部安全機制是隨著版本修復更新一步一步添加進去的。
阻止訪問或者只允許訪問特定類具體體現在MemberAccess接口中規定了OGNL的對象方法或屬性的訪問策略。實現MemberAccess接口的有兩類:一個是在OGNL中實現的DefaultMemberAccess,默認禁止訪問private、protected、package protected修飾的屬性方法。另一個是xwork中對對象方法訪問策略進行了擴展的SecurityMemberAccess,指定是否支持靜態訪問方法,默認設置為false。
由于安全機制的問題和Struts2的版本修復有伴生關系,簡單三言兩語也無法直接展現整個形成過程,故而計劃單開一個章節梳理一下漏洞驗證利用催化安全修復所形成的安全機制。
二.OGNL語法介紹
OGNL的全稱是對象圖導航語言( Object-Graph Navigation Language),它是一種用于獲取和設置 Java對象屬性的開源表達式語言,以及其他附加功能,是Struts2的默認表達式語言。使用這種表達式語言,可以利用表達式語法樹規則,存儲Java對象的任意屬性,調用Java對象的方法,同時能夠自動實現期望的類型轉換。OGNL最初是作為一種使用屬性名稱在UI組件和控制器之間建立關聯的方法。如果將表達式看作是一種帶有語義的字符串,那么OGNL就是這個語義字符串與Java對象之間的溝通橋梁。
簡要概括OGNL的功能就是雙向轉換語義字符串與Java對象數據即轉換String和Object(參考序列與反序列化),原因是視圖層表示的控制源于語義字符串,而業務控制邏輯處理過程涉及的數據不僅限于語義字符串,其中就包含Java對象數據。
2.1 OGNL基本語法樹
Java實現的Web實際就是面向對象的編程,OGNL表達式必然也有面向對象的Action。為便于理解,由訪問方式簡單劃分三類對象:
—靜態類靜態對象
即訪問靜態類或其成員的方法為@,語法規則為根據@開始,后面接上完整的類名。如@java.lang.Math@max(10,20)相當于java中直接調用Math.max(10,20)。
—基本對象
即基本數據類型的對象訪問,不加任何前綴,直接使用即可。基本數據類型包括String、Int、Boolean和List等。
—非基本對象
即一個實例對象的訪問方法為#,語法規則為根據#開頭,后面為對象名,該對象需要在OgnlContext類中,且可以根據對象名可以唯一定位。如#demo。
OGNL中有兩個常用的類:
—ognl.Ognl類
主要用來解析和解釋執行OGNL表達式。
—ognl.OgnlContext類
該類為OGNL表達式提供一個執行環境,該類也實現Map接口,允許通過put(key,object)方法向OgnlContext環境中添加各種類型的對象。
OgnlContext中對象分為兩種:
—Root
根對象,可以存入任何對象通過調用OgnlContext.setRoot(obj)設置為根對象,整個OgnlContext中有且最多只能有一個根對象。
— Context
上下文對象,即Map結構,可以往該Map存入任意key-value鍵值對。該類非基本對象個數類型不受限制,但只能通過制定上下文容器的“#對象名.name”的方式去獲取屬性值,且在對象的類型中要提供getName方法。
OGNL表達式目標對象的方法調用:
—非基本對象的方法訪問,#開頭,對象與方法之間用.連接;
\#obj.method( 參數 )
—靜態對象的方法訪問,@開頭,對象與方法之間用@連接;
@xxx@method( 參數 )
—基本對象的方法訪問,和非基本對象方法方式一致。
"name".length()
OGNL表達式目標對象的成員訪問:
—非基本對象的成員訪問,#開頭,對象與成員之間用.連接;
\#obj.field
—靜態對象的成員訪問,@開頭,對象與成員之間用@連接;
@xxx@field
—基本對象的成員訪問,和非基本對象成員方式一致。
"name".hash
2.2 OGNL執行操作
2.2.1 OGNL的三個重要操作符號
OGNL中的三個重要符號:#、%、$。這三者在Struts2的開發使用和RCE漏洞中都作用不可忽視。
#符號的用途一般有三種:
—訪問非根對象屬性,例如#session.msg。由于Struts2中值棧被視為根對象,所以訪問其他非根對象時需要加#前綴。實際上,#相當于調用ActionContext.getContext()。#session.msg表達式相當于
ActionContext.getContext().getSession().getAttribute("msg");
—加在this指針之前表示對this指針的引用;
—以#{}格式構造Map的鍵值對,例如:\#{'foo':'foo value', 'bar':'bar value'}。
%符號的用途是在標志屬性為字符串類型時,計算OGNL表達式的值,類似JS中的函數eval()。例如:\。
$符號主要有兩個方面的用途:
—在國際化資源文件中,引用OGNL表達式,例如國際化資源文件中的代碼:reg.agerange=國際化資源信息:年齡必須在{min}同{max}之間;
—在Struts 2框架的配置文件中引用OGNL表達式。
2.2.2 OGNL的執行操作三要素
包含getValue()與setValue()兩個最基本的操作,OGNL的所有操作實際上都是圍繞著三個參數而進行,這三個參數姑且稱之為OGNL的執行操作三要素。
—表達式(Expression)
表達式是整個OGNL的核心,表達式會確定此次OGNL執行操作到底要干什么,所有OGNL的執行操作都是在針對表達式的解析后進行的。因此,表達式本質上是一個帶有語法含義的字符串,該字符串將規定執行操作的類型和執行操作的內容。
OGNL支持大量的表達式語法,不僅支持“鏈式”描述對象訪問路徑,還支持在表達式中進行簡單的計算,甚至還能夠支持復雜的Lambda表達式等。
—Root對象(Root Object)
OGNL的Root對象可以理解為OGNL的執行操作對象。當OGNL表達式確定“干什么”以后,尚需要指定對誰實施該動作。OGNL的Root對象實際上是一個Java對象,是所有OGNL操作的實際載體。這就意味著,對于一個OGNL的表達式,實際上需要針對Root對象去進行OGNL表達式的評估求值并返回結果。
—上下文環境(Context)
OGNL確定表達式和Root對象的要素后,就可以使用OGNL的基本功能。例如,根據表達式針對OGNL中的Root對象進行“取值”或者“寫值”操作。
事實執行過程中,在OGNL的內部,所有的操作都會在一個特定的數據環境中運行,該數據環境就是OGNL的上下文環境(Context)。該上下文環境確定OGNL的執行操作在哪里實施。OGNL的上下文環境是Map結構,稱之為OgnlContext。之前所提到的Root對象,事實上也會被添加到上下文環境中去,并且將被作為一個特殊的變量進行處理。
三.Struts2 RCE漏洞梳理

自2007年至2020年,官方發布S2-001至S2-061安全公告,61份安全公告中有31份涉及RCE。而這31份中拋開脆弱性細節未直接披露的,仍有相當部分威脅系數高、利用復雜度低的脆弱點。由于Struts2本身不提供安全機制,修修補補地正則匹配也難以根除,Bypass類型RCE也不可忽視。
3.1 Struts2 RCE的概述
—S2-001:表單驗證錯誤OGNL 循環解析導致RCE。
—S2-003:XWork ParameterInterceptors 繞過允許OGNL語句執行。
—S2-005:XWork ParameterInterceptors 繞過導致RCE。
—S2-007:當出現轉換錯誤時,用戶輸入被評估為OGNL表達式。
—S2-008:Struts2 中的多個嚴重漏洞。
—S2-009:ParameterInterceptor 漏洞導致RCE。
—S2-012:Showcase 應用程序漏洞導致RCE。
—S2-013:存在于URL和定位標記的includeParams屬性導致RCE。
—S2-014:通過強制的URL和錨標簽參數包括引入導致RCE,會話訪問和操縱和跨站腳本攻擊。
—S2-015:由通配符匹配機制或OGNL表達的雙重評價引入的漏洞導致RCE。
—S2-016:通過操縱前綴為“action:”/“redirect:”/“redirectAction:”的參數引入的漏洞允許遠程命令執行。
—S2-018:ActionMapper機制支持特殊參數前綴動作的訪問控制漏洞。
—S2-019:動態方法調用的默認開啟和默認禁止。
—S2-020:將Commons FileUpload升級1.3.1版(DoS)并添加“class”以排除ParametersInterceptor 中的參數避免 ClassLoader 操作。
—S2-021:改進了ParametersInterceptor和CookieInterceptor中排除的參數以避免
ClassLoader 操作。
—S2-022:擴展CookieInterceptor中排除的參數以避免操縱Struts的內部結構。
—S2-026:特殊的頂部對象可用于訪問Struts的內部結構。
—S2-029:強制雙重OGNL評估求值,當對標簽屬性中的原始用戶輸入進行評估求值時,可能會導致RCE。
—S2-032:啟用動態方法調用時,可以通過 method: 前綴導致RCE。
—S2-033:啟用動態方法調用時,使用REST插件則運算符!可執行遠程代碼。
—S2-036:強制雙重OGNL評估求值,當對標簽屬性中的原始用戶輸入進行評估求值時,可能會導致RCE。(S2-029的延申)
—S2-037:使用REST插件時可以執行遠程代碼執行。
—S2-045:基于Jakarta Multipart解析器執行文件上傳時可能的RCE。
—S2-046:基于Jakarta Multipart解析器執行文件上傳時可能的RCE。(S2-045的異構)
—S2-048:Struts 2.3.x 系列中Struts 1插件示例中的Struts Showcase應用程序中可能存在的RCE。
—S2-052:使用帶有XStream 處理程序的Struts REST插件來處理XML負載時可能存在的RCE。
—S2-053:在Freemarker標簽中使用無意表達式而不是字符串文字時可能導致RCE。
—S2-055:Jackson JSON庫中的RCE漏洞
—S2-057:alwaysSelectFullNamespace的RCE漏洞觸發條件:定義XML配置時namespace值未設置且上層動作配置(Action Configuration)中未設置或用通配符namespace;url標簽未設置value和action值且上層動作未設置或用通配符namespace。
—S2-059:強制雙重OGNL評估,當對標簽屬性中的原始用戶輸入進行評估時,可能會導致遠程代碼執行。
—S2-061:強制雙重OGNL評估,當對標簽屬性中的原始用戶輸入進行評估時,可能會導致遠程代碼執行。(S2-059的延申)
3.2 Struts2 RCE的位置
初步熟知Struts2的工作原理后,再去理解其RCE漏洞觸發的位置及漏洞成因,有利于建立整體性的脆弱點感知梳理。如圖3.2.1將RCE觸發的隸屬處理環節歸類表示,細節和位置點都已經比較完善,但是仍然不能完美體現在工作流程中。意欲自己梳理并動手制作一個更好的位置成因架構圖,當遇見圖3.2.2時,覺得在Struts2 RCE的位置梳理在整個工作流程生命周期中的體現無人能出其右,但其中S2-008也并非如此單一。

圖3.2.1 Struts2的RCE在框架中的位置和原因

圖3.2.2 Struts2的RCE在生命周期中的位置
3.3 Struts2 RCE的成因
Struts2 RCE的漏洞成因并不能一概而論地歸結到OGNL上,其成因原理涉及角度和利用復雜度均不一致。除全新漏洞觸發點還涉及老漏洞的新打開方式,結合很多在Struts2安全上有貢獻前輩的梳理和整合,粗略梳理Struts2 RCE的漏洞成因和類型。
類型可以概括為四類:用戶可控的不可信數據源解析執行、邏輯處理上的OGNL二次解析、支持使用的第三方組件包含脆弱性和正則防御和沙箱的Bypass。下面詳談的RCE成因都能歸屬到這四類中。
—S2-001
S2-001=CVE-2007-4556=CNVD-2007-5360。
WebWork2.1+和Struts2的altSyntax功能允許將OGNL表達式插入到文本字符串中并進行遞歸處理。用戶提交表單數據并且驗證失敗時,后端會將用戶提交的參數值使用OGNL表達式%{value}進行解析,然后重新填充到對應的表單數據中。例如注冊或登錄頁面,提交失敗時后端會默認返回之前提交的數據,由于后端使用%{value}對提交的數據執行了一次OGNL表達式解析,所以可以直接構造
Payload 進行命令執行。
—S2-003、S2-005
S2-003=CVE-2008-6504;
S2-005=CVE-2010-1870=CNVD-2010-1318。
ParametersInterceptor攔截器對參數名正則過濾不完全導致的OGNL表達式通過#來訪問Struts2的對象,框架正則過濾#字符防止安全問題,但是通過Unicode編碼(u0023)或8進制(43)即可繞過(Bypass)安全限制。漏洞執行前提需要將denyMethodExecution置為false,即acceptableName為true,原因是正則表達式[\\p{Graph}&&[\^,\#:=]]\*檢測特殊字符,防止傳入惡意特殊字符開頭如#等。
官方通過增加安全配置禁止靜態方法調用(allowStaticMethodAcces)和否認方法執行(MethodAccessor.denyMethodExecution)等來修補S2-003。S2-005則是Bypass安全配置,設置allowStaticMethodAccess為true和denyMethodExecution為false(允許靜態方法調用和否認方法執行)再次造成漏洞。
—S2-007
S2-007=CVE-2012-0838=CNVD-2012-8979。
在數據類型轉換錯誤情況下,攔截器會將用戶輸入數據取出并插入到當前值棧(ValueStack)中,之后OGNL強制評估會對標簽進行解析導致表達式執行造成表達式注入,合理構造OGNL即可導致RCE。
—S2-008
S2-008=CVE-2012-0392。
S2-008涉及多個漏洞,為了防止在參數內調用任意方法,默認情況下將標志否認方法執行xwork.MethodAccessor.denyMethodExecution設置為true,并將SecurityMemberAccess字段allowStaticMethodAccess設置為false。此外,為防止訪問上下文變量,自Struts2.2.1.1開始,在ParameterInterceptor中應用針對參數名稱的改進字符白名單:acceptedParamNames= "[a-zA-Z0-9\\.][()_']+"。在某些情況下,可以繞過這些限制以執行惡意Java代碼。
Struts2<= 2.2.3中的遠程命令執行(ExceptionDelegator)(S2-007)
當將參數值應用到屬性時發生異常時,該值被評估為 OGNL表達式。例如,將字符串值設置為整數屬性時會發生這種情況。由于這些值沒有被過濾,攻擊者可以濫用OGNL語言的能力來執行導致遠程命令執行的任意Java代碼。此問題已被報告并在Struts2.2.3.1中得到修復。然而執行任意Java代碼的能力卻被忽視。
Struts2<= 2.3.1中的遠程命令執行(CookieInterceptor)
參數名稱的字符白名單不適用于CookieInterceptor。當Struts2被配置為處理cookie名稱時,攻擊者可以對Java函數的調用靜態方法訪問來執行任意系統命令。即allowStaticMethodAccess標志可以在請求中被設置為true,漏洞觸發和S2-003及S2-005的ParameterInterceptor機制類似。
Struts2<= 2.3.1中的任意文件覆蓋(ParameterInterceptor)
雖然allowStaticMethodAccess從Struts2.2.3.1開始禁止訪問參數中的標志,但攻擊者仍然可以僅使用一個String類型的參數訪問公共構造函數來創建新的Java對象并訪問其setter一個字符串類型的參數。該方法存在濫用創建和覆蓋任意文件,可以使用未初始化的字符串屬性將禁止字符注入文件名。
Struts<= 2.3.17中的遠程命令執行( DebuggingInterceptor)
devMode模式本身不是安全漏洞,但在devMode模式下運行的應用程序攔截器DebuggingInterceptor也容易遠程執行命令。雖然devMode模式在生產環境中幾乎不存在,但該模式下的安全影響仍值得注意。
—S2-009、S2-026
S2-009=CVE-2011-3923=CNVD-2012-0369;
S2-026=CVE-2015-5209=CNVD-2016-01256。
ParametersInterceptor中的正則表達式將top['foo'](0)作為有效的表達式匹配,OGNL將其作為(top['foo'])(0)處理,并將“foo”操作參數的值作為OGNL表達式求值。這使得惡意用戶將任意的OGNL語句放入操作公開的任何String變量中,即可將其評估為OGNL表達式。由于OGNL語句利用點在HTTP參數中,攻擊者可以使用黑名單字符(如#)設置禁用方法執行denyMethodExecution并執行任意方法,繞過ParametersInterceptor和OGNL類的保護。
Struts2版本2.3.24前存在安全漏洞,特定的top對象可用于訪問strtus’
internals,允許攻擊者利用該漏洞繞過安全限制執行未授權操作。ValueStack定義了top代表執行上下文Root的特殊對象。它可用于操作Struts2的內部結構或可用于影響容器的設置。S2-009公告中,使用top['foo'](0)的形式用來對foo進行二次OGNL解析,實際上是也使用了top可以訪問Root中的第一個對象的特性。S2-026公告中,官方禁止了通過參數名使用top訪問上下文中的內容。
—S2-012
S2-012=CVE-2013-1965=CNVD-2013-06101。
OGNL評估已經在S2-003、S2-005和S2-009中得到修復,由于只涉及參數名稱,修復方法是基于將可接受的參數名稱列入白名單并拒絕對參數中包含的表達式進行評估,部分修復只解決參數名稱問題。
包含特制請求參數的請求可用于將任意OGNL代碼注入到屬性中,然后用于重定向地址的請求參數,導致在重定向結果從棧中讀取并使用先前注入的代碼作為重定向參數時發生OGNL評估,在S2-003、S2-005、S2-009進行的參數過濾未對重定向值起到限制過濾作用。
—S2-013、S2-014
S2-013=CVE-2013-1966=CNVD-2013-05924;
S2-014=CVE-2013-2115=CNVD-2013-06279。
Struts2的標簽中和都有一個includeParams屬性,該屬性的主要功能是了解是否包含HTTP請求參數且僅允許三個取值及意義如下:
-none-URL中不包含任何參數(默認);
-get-僅包含URL中的GET參數;
-all-在URL中包含GET和POST參數。
當includeParams=all的時候,標簽和會將本次請求的GET和POST參數都放在URL的GET參數上。包含特制參數的請求可用于將任意OGNL注入堆棧(Stack),然后用作和標簽的請求參數。當或嘗試去解析原始請求參數時,將其參數評估為OGNL表達式,由此啟用方法執行和執行任意方法,繞過Struts和OGNL類的保護。
該問題最初由Struts2.3.14.1和安全公告S2-013修復,S2-014是S2-013的不完全修復的利用。S2-014的修復方法是URL呈現子系統已更改為不向OGNL評估傳遞任何參數名稱或值,且MemberAccess組件的allowStaticMethodAccess屬性現在不可變。
—S2-015
S2-015=CVE-2013-2135=CNVD-2013-06834;
S2-015=CVE-2013-2134=CNVD-2013-06787。
Struts2\*通配符匹配機制可導致OGNL表達式兩次評估求值。Struts2允許定義基于通配符\*的Action映射,如:
"\*" class="example.ExampleSupport"\><result\>/example/{1}.jsp\result\>action\>
如果請求不匹配任何所定義的Action就會匹配通配符\*,且請求的Action名會用于加載基于Action名的JSP文件,即把{1+1}值作為OGNL表達式處理,結果允許在服務端執行任意Java代碼。此漏洞包含兩個問題的組合:
-請求的Action名沒有轉義或針對白名單進行檢查;
-當組合使用$和%開放字符時對TextParseUtil.translateVariables中的OGNL表達式進行兩次求值。
—S2-016、S2-018、S2-019
S2-016=CVE-2013-2251=CNVD-2013-09777;
S2-018=CVE-2013-4310=CNVD-2014-00665;
S2-019=CVE-2013-4316=CNVD-2013-13318。
Struts2 DefaultActionMapper通過在參數前加上action:或redirect:前綴,后跟所需的導航目標表達式來支持短路重定向的方法。在2.3.15.1之前的Struts2中, action:、redirect:或redirectAction:之后的信息沒有被正確清理,且所述信息將作為針對ValueStack的OGNL表達式進行評估求值,導致利用OGNL表達式調用Java靜態方法執行任意系統命令。
S2-016公告中,DefaultActionMapper已更改為正確清理action:前綴信息。S2-017公告中,與redirect:/redirectAction: 前綴參數相關的功能被完全刪除。
S2-018公告中,Struts2.3.15.3中動作映射機制已更改。為避免規避安全約束引入兩個額外常量來引導DefaultActionMapper的行為:
struts.mapper.action.prefix.enabled當設置為false時action:前綴被禁用,默認設置為false;struts.mapper.action.prefix.crossNamespaces當設置為false時,用action:前綴定義的動作必須與當前動作在同一個命名空間中。
S2-019公告中,描述漏洞影響為Dynamic method executions,修復方案是在Struts2版本2.3.15.2之后,將struts2-core包中的default.properties(默認屬性)配置中的struts.enable.DynamicMethodInvocation默認值設置為false,其他版本也可以在struts.xml中使用如下配置:
"struts.enable.DynamicMethodInvocation" value="false"/\> 漏洞和S2-008類似,均是當devMode開啟debug模式下會出現漏洞,參數DynamicMethodInvocation是動態方法調用的flag也就是對應著action:和method:兩個前綴。 有關S2-018和S2-019漏洞復現及其細節的資料網上較少,可能性原因或許是因為對action:和method:兩個前綴挖掘出漏洞利用的方式,還是存在一定的限制性。
—S2-020、S2-021、S2-022
S2-020=CVE-2014-0094=CNVD-2014-01552;
S2-021=CVE-2014-0112=CNVD-2014-02702;
S2-021=CVE-2014-0113=CNVD-2014-02705;
S2-022=CVE-2014-0116=CNVD-2014-02855。
struts2中Root對象是當次訪問的Action對象,而ClassLoader通常由運行環境所提供,例如在Tomcat下,這個ClassLoader應該為當前應用所使用的:org.apache.catalina.loader.WebappClassLoader。由于環境不同,class.classLoader
對應存放的容器結果也不同,因此漏洞利用不具有完全通用性。ClassLoader中存放很多容器運行時上下文(Context)中的所需要的一些屬性值。如果這些值被修改,可能會影響到應用程序的運行方式。能被我們修改的屬性需要有以下幾個條件:
-有set方法,或者是可以使用set方法改變的值;
-修飾符應該是 public;
-屬性的返回值應該是通過用戶的輸入可以被OGNL解析成為相應的對象;
-修改后能夠對應用程序造成影響,導致安全風險。
S2-020公告ApacheStruts2存在安全漏洞,由于ParametersInterceptor允許訪問直接映射到getClass()方法的class參數,允許攻擊者利用漏洞通過請求參數操作ClassLoader。
S2-021公告Apache Struts2.3.16.1版本中S2-020修復引入的用于阻止對getClass()方法的訪問的排除參數模式是不完全修復。可通過特別設計的請求進行繞過。同理,當CookieInterceptor被配置為接受所有Cookie時,即當使用\*來配置CookieName參數時,也容易受到相同類型的攻擊。
S2-022公告Apache Struts2.3.16.2中S2-021修復引入的用于阻止訪問 getClass()方法的排除參數模式也是不完全修復,當使用\*配置CookiesName參數時,可以更改會話、請求等的狀態,通過CookieInterceptor實現對Struts2的內部操作。
—S2-029、S2-036、S2-059、S2-061
S2-029=CVE-2016-0785=CNVD-2016-01714;
S2-036=CVE-2016-4461=CNNVD-201710-559。
該漏洞源于程序對屬性值執行雙重判斷。遠程攻擊者可利用該漏洞執行任意代碼。代碼執行過程大致為先嘗試獲取Value的值,如果Value為空,那么就二次解釋執行了Name,并且在執行前給name加上了%{}。最終造成OGNL二次執行。因此需要的條件極為苛刻:特殊的代碼、Value值為空、可以傳參到Value、控制Name。
主要有兩類標簽屬性存在問題,一類是Id,一類是Name,現知Struts2的i18n,text標簽的Name屬性處理的時候會經過兩次OGNL執行,在最新版本里面要想成功利用該漏洞執行任意代碼,需要繞過Struts2的安全管理器,該漏洞也是標簽語言的OGNL表達式二次執行,歸根于Bypass安全管理器。
S2-036是S2-029的不完全修復,即借助tag屬性中的%{}序列利用該漏洞執行任意代碼。
S2-059公告中,Struts2 會對某些標簽屬性(比如id,其他屬性有待尋找)的屬性值進行二次表達式解析,因此當這些標簽屬性中使用了%{x}且x的值用戶可控時,用戶再傳入一個%{payload} 即可造成OGNL表達式執行。
S2-061是S2-059的不完全修復,S2-059的安全管理修復可以被S2-061完全Bypass。
—S2-032、S2-033、S2-037
S2-032=CVE-2016-3081=CNVD-2016-02506;
S2-033=CVE-2016-3087 =CNVD-2016-03754;
S2-037=CVE-2016-4438=CNVD-2016-04040。
當程序啟用Dynamic Method Invocation時,遠程攻擊者可借助method:前綴利用該漏洞在服務器端執行任意代碼。即當啟用動態方法調用時,可以傳遞可用于在服務器端執行任意代碼的惡意表達式。method:\Action前綴去調用聲明為public的函數。低版本中Strtus2不會對name方法值做OGNL計算,而在高版本中會。
S2-033漏洞依附于S2-032漏洞,Struts官方發布S2-033安全公告只是針對性地對REST插件功能存在的安全問題進行補充,其漏洞技術成因與S2-032相同,利用前提均為配置文件struts.xml的struts.enable.DynamicMethodInvocation設置為true。根據官方漏洞描述,當開啟動態方法調用,并且同時使用了Strut2 REST Plugin插件時,使用“!”操作符調用動態方法可能執行OGNL表達式,導致代碼執行。
S2-037是S2-033不完全修復的利用方式,S2-033需要開啟動態函數調用,且S2-037不需要開啟動態函數調用。
—S2-045、S2-046
S2-045=CVE-2017-5638=CNVD-2017-02474
S2-046=CVE-2017-5638=CNVD-2017-02474
S2-045、S2-046涉及同一個CVE,不同點在于漏洞的利用方式。Struts2上傳默認使用的是org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest類對上傳數據進行解析,只不過最終調用了第三方組件common upload完成上傳操作。
S2-045安全公告中,可以使用惡意Content-Type 值執行RCE攻擊 。如果該Content-Type值無效,則會引發異常,然后用于向用戶顯示錯誤消息。該漏洞源于程序沒有正確處理文件上傳。遠程攻擊者可借助帶有#cmd=字符串的特制Content-TypeHTTP頭利用該漏洞執行任意命令。
S2-046安全公告中,可以使用惡意Content-Disposition 值或不正確的Content-Length標頭嘗試執行RCE攻擊 。如果Content-Disposition 或Content-Length值無效,則會引發異常,然后用于向用戶顯示錯誤消息。這是S2-045中描述的相同漏洞的不同利用方式 。
— S2-048
S2-048=CVE-2017-9791=CNVD-2017-13259
漏洞主要問題出在struts2-struts1-plugin這個插件包上。這個庫的主要作用就是將struts1的Action封裝成struts2的Action以便它能在strut2上運行使用。而由于struts2-struts1-plugin包中的 Struts1Action.java中的execute()函數可以調用getText()函數,這個函數剛好又能執行OGNL表達式,同時這個getText()的參數輸入點,又可以被用戶直接進行控制。如果這個點被惡意攻擊者所控制,就可以構造惡意執行代碼,從而實現一個RCE攻擊。
—S2-052
S2-052=CVE-2017-9805=CNVD-2017-25267。
Struts2 REST插件的XStream組件存在反序列化漏洞,使用XStream組件對XML格式的數據包進行反序列化操作時,未對數據內容進行有效驗證,可直接在數據包中插入惡意代碼,導致服務器被攻陷。
S2-033和S2-037也是涉及REST插件,只不過屬于利用操作符號“!”實現OGNL的執行。而S2-052通過XML實體的反序列化實現OGNL的執行。OGNL本身就是轉換Object和String,兩者有一定的相似原理。
— S2-053
S2-053=CVE-2017-12611=CNVD-2017-25632。
S2-053公告中,在Freemarker標簽中使用錯誤的表達式而不是字符串文字時,導致攻擊者遠程執行代碼攻擊。
當在Freemarker標簽中使用表達式文本或強制表達式時,使用以下請求值可能會導致遠程代碼執行:
<@s.hidden name="redirectUri" value=redirectUri /><@s.hidden name="redirectUri" value="\${redirectUri}" />
這兩種情況下,值屬性都使用可寫屬性,都會受到Freemarker表達式影響。
—S2-055
S2-055涉及CVE-2017-7525=CNVD-2017-27693
FasterXML Jackson是美國FasterXML公司的一款用于Java的數據處理工具,Jackson-databind是其中的一個具有數據綁定功能的組件。FasterXML Jackson-databind中存在Java反序列化遠程代碼執行漏洞。未經過身份驗證的攻擊者通過向ObjectMapper的readValue方法的發送精心制作的序列化數據執行惡意代碼。
從官方公告的描述來看,該漏洞不涉及Struts2本身的框架和代碼,就是插件Jackson的反序列化漏洞。由于Jackson在處理反序列的時候需要支持多態,所以在反序列的時候通過指定特定的類來達到實現多態的目的。這個特性默認關閉,所以在Struts2中影響也是有限。
—S2-057
S2-057=CVE-2018-11776=CNVD-2018-15894。
S2-057公告中,漏洞產生于網站配置XML時如果沒有設置namespace的值,并且上層動作配置中并沒有設置或使用通配符namespace時,可能會導致遠程代碼執行漏洞的發生。同樣也可能因為url標簽沒有設置value和action的值,并且上層動作并沒有設置或使用通配符namespace,從而導致遠程代碼執行漏洞的發生。當訪問動作類型為目標(redirect action,chain,postback)時,會根據URL生成的namespace生成一個跳轉地址location,location會進行OGNL評估求值。可以解析為兩部分:
-alwaysSelectFullNamespace被設置為true,此時namespace的值是從URL中獲取的。URL是可控的,所以namespace也是可控的;
-action元素沒有名稱空間屬性集,或者使用通配符。該名稱空間將由用戶從URL傳遞并解析為OGNL表達式,最終導致遠程代碼執行的脆弱性。
四.RCE攻擊催化的防御安全機制
梳理整合Struts2歷經十多年安全漏洞的修修補補,從每個RCE導致版本的修復方案確實是一條可行的線索。縱觀各路大牛剖析RCE的驗證利用,網文路西法er的觸發點解析安全修復的安全機制產生思路,使人眼前一亮。
4.1 Struts2 安全機制階段劃分
安全修復姑且劃分三個階段。從最初的知攻知防地定點打卡方式去正則過濾用戶可控的不可信數據源的最初階段,到進入Struts2依賴OGNL調用路徑下的數據包com.opensymphony.xwork2.ognl中,自定義一個DefaultMemberAccess的子類SecurityMemberAccess進行安全驗證,阻止訪問或者只允許訪問特定類和Java包的黑名單沙箱機制階段,最后到直接刪除DefaultMemberAccess,并且具體到調用的xwork2.ognl包中,在調用方法之前把黑名單類直接寫死在判斷代碼里,并且屏蔽所有中間件調用包的終極階段。
4.2 安全機制對應的版本和PoC
關于Struts2 RCE系列的漏洞復現和驗證利用方向上,除少數幾個利用難度較為復雜,網上公開細節較少的漏洞,幾乎已經沒有什么新鮮內容了。原計劃該章節對應全部RCE梳理一下其驗證利用PoC在對應官方修復后的獲取OGNL執行操作三要素上的變化,但發現有倆位前輩已經析出了技術總結。考慮單獨針對每一個RCE解釋一下PoC和修復角度的情況又發現了另一位的技術總結。讀下來幾乎全部對應本人對Struts2RCE梳理的構想,本著分享的基本思想取其精華,拋開冗長的源碼分析整理到此處并記錄關鍵的PoC構成。
—Struts 2.0.0-Struts 2.0.8(S2-001)
該版本的Struts2嚴格意義上沒有安全機制且altSyntax默認為true,所以表單提交標簽內容直接被遞歸查詢執行OGNL進一步導致RCE。
官方修復方案是altSyntax默認為false,使得標簽內容不再解析為OGNL并添加判斷使用break限制遞歸查詢。
—Struts 2.0.0-Struts 2.1.8.1(S2-003、S2-005)
Struts 2.0.0-Struts2.0.11.2的ParametersInterceptor攔截器對參數名過濾在XWork2.0.4官方重寫acceptableName方法,才增加敏感字符檢測,S2-003僅僅通過一招敏感字符編碼即可Bypass。
官方修復方案通過增加沙箱機制禁止靜態方法調用(allowStaticMethodAcces)和否認方法執行MethodAccessor.denyMethodExecution等安全配置并且重寫參數攔截器的正則表達式來修補S2-003。至此官方修復之路開始進入第二階段。
第二階段的安全機制在OGNL上的體現在MemberAccess接口中規定OGNL的對象方法或屬性訪問策略。實現MemberAccess接口的有兩類:一個是在OGNL中實現的DefaultMemberAccess,默認禁止訪問private、protected、package protected修飾的屬性方法;另一個是xwork中對對象方法訪問策略進行擴展SecurityMemberAccess,指定是否支持訪問靜態方法,默認設置為false 。
但此時context對象依舊可以訪問,兩個比較關鍵的就有:
–#context-OgnlContext,一種基于 xwork.MethodAccessor.denyMethodExecution屬性值的保護方法執行;
–#_memberAccess-SecurityMemberAccess,其allowStaticAccess阻止靜態方法執行。故而繞過這一版本的防護的 PoC開始具有明顯的結構特點:
(\#_memberAccess['allowStaticMethodAccess']=true). (@java.lang.Runtime@getRuntime().exec('calc'))
驗證利用方法為通過#_memberAccess獲取SecurityMemberAccess實例,通過 setAllowStaticMethodAccess方法設置其值為true,允許執行靜態方法。官方修復方案是在參數攔截器修改為更加嚴格的正則表達式,將忽略簡單屬性導航路徑以外的參數。至此Struts2進入2.2.1版本。
—Struts 2.2.1-Struts 2.3.3.1(S2-007、S2-008、S2-009)
該版本區間沙箱沒有改變,但其安全機制上針對每個RCE都有修復點。
S2-007是標簽內的回顯,是因為數據變量類型報錯導致OGNL表達式的執行。官方針對該漏洞的修復增加對單引號進行轉義處理。
S2-008是參數值的過濾問題,但是需要開啟devmode模式,對于debug需要關注DebuggingInterceptor攔截器。官方針對該漏洞的修復也是修改為更加嚴格acceptedParamNames過濾器的ParameterInterceptor和CookieInterceptor的正則表達式。
S2-009是top ['foo'](0)作為有效的表達式匹配,OGNL將其作為(top
['foo'])(0)處理繞過了ParameterInterceptor的限制。官方針對該漏洞的修復主要有兩點,其一是在值棧(ValueStack)中setParameter方法將不允許參數名稱中包含更多的eval表達式,其二是在struts2.3.1.2中 ParameterInterceptor攔截器的正則表達式變得更為嚴格。
—Struts2.3.14.2及之后版本(S2-012、S2-013、S2-014、S2-015)
Struts2.3.14.2及之后版本沙箱發生改變,修改SecurityMemeberAccess類,刪除setAllowStaticMethodAccess方法,導致無法修改AllowStaticMethodAccess屬性值,同時AllowStaticMethodAccess屬性被final修飾。
S2-012漏洞是在struts.xml對action對象進行重定向配置,重定向配置的參數以OGNL表達式解析造成OGNL二次注入導致任意代碼執行,安全沙箱沒有改進。官方針對該漏洞的修復是OGNLUtil類已更改為默認拒絕eval表達式。
S2-013漏洞跟標簽有關,標簽屬性設置為includeParams=all會造成OGNL表達式執行,在S2-014后URL將不會把參數名或值傳遞給OGNL表達式。沙箱就是在S2-014時發生改變。
S2-015漏洞是通配符問題,此時沙箱機制已經改變。因為setAllowStaticMethodAccess方法被刪掉,但是ognlcontext的上下文屬性還在。所以階段性的PoC特征又出現了:通過反射將allowStaticMethodAccess的值改變
#f=\#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess")`#f.setAccessible(true)#f.set(\#_memberAccess,true)
新建一個 ProcessBuilder 實例,調用start方法來執行命令
(\#p=new java.lang.ProcessBuilder('calc')).(\#p.start())
官方針對性修復方案在新版本引入操作名稱白名單,默認情況下設置為接受與以下正則表達式匹配的操作:[a-z]\*[A-Z]\*[0-9]\*[.\\-_!/]\*
用戶可以通過在 struts.xml 中設置一個新常量來更改定義,如下所示:
刪除對導致執行表達式的雙重評估OgnlTextParser其中 TextParseUtil.translateVariables,在DefaultActionMapper類增加了一個cleanupActionName方法去處理actionname的惡意代碼,并返回安全的actionname。
—Struts2.3.20+(S2-032)
準確來說是S2-029時,Struts2.3.20+版本的沙箱又一次發生變化。SecurityMemberAccess中增加了excludedClasses,excludedPackageNames以及
excludedPackageNamePatterns三個黑名單屬性。這三個屬性在
SecurityMemberAccess#isAccessible方法中遍歷判斷了當前操作類是否在黑名單類中,而在OGNL表達式執行時OgnlRuntime類中callConstructor、getMethodValue、setMethodValue、getFieldValue、isFieldAccessible、isMethodAccessible、invokeMethod調用此方法。也即是在OGNL表達式在執行以上操作時,判斷當前操作類是否在黑名單中,配置文件struts-default.xml定義黑名單屬性。
此時的PoC特征是通過 DefaultMemberAccess 替換 SecurityMemberAccess來完成:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
這樣OGNL計算時的規則就被替換成為DefaultMemberAccess中的規則,也就沒有了黑名單的限制以及靜態方法的限制。此PoC方法獲取類的靜態屬性通過 ognl.OgnlRuntime#getStaticField獲得,而該方法中沒有調用isAccessible方法,故通過@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS可以獲取到DefaultMemberAccess對象,賦值給上下文環境中的_memberAccess,從而Bypass黑名單限制。
—Struts2.3.30+及struts2.5.2+(S2-045)
該版本下的沙箱機制又有改變,進一步增加SecurityMemberAccess
中的黑名單的限制。其中將ognl.DefaultMemberAccess以及ognl.MemberAccess加入黑名單,同時在Struts2.3.30使用的ognl-3.0.19.jar包、struts2.5.2使用的ognl-3.1.10.jar包中的OgnlContext不再支持使用#_memberAccess獲得MemberAccess實例。
此時PoC又展現另外的利用角度。通過ognl.OgnlContext#setMemberAccess方法將DefaultMemberAccess設為OGNL表達式評估求值的規則:
(\#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS). (\#context.setMemberAccess(\#dm))
由于黑名單限制是不再支持通過#_memberAccess的形式獲取實例,利用方式變成直接改變OgnlContext中的_memberAccess屬性,從而實現非期望動作。但是調用setMemberAccess方法會觸發檢查黑名單,ognl.OgnlContext儼然在黑名單中。清空黑名單的方式首次出現,通過容器獲取OgnlUtil實例,由于OgnlUtil是單例模式實現的對象,所以獲取到的實例唯一,接著調用get方法獲取黑名單集合,clear()方法清空集合。即屬性值。
(\#container=\#context[‘com.opensymphony.xwork2.ActionContext.container’]).(\#ognlUtil=\#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(\#ognlUtil.getExcludedPackageNames().clear()).(\#ognlUtil.getExcludedClasses().clear())
OgnlUtil能置空SecurityMemberAccess黑名單的原因是作為過濾器起核心控制功能的StrutsPrepareAndExecuteFilter#doFilter初始化OgnlValueStack中
SecurityMemberAccess的黑名單集合時,是通過ognlUtil中的黑名單集合進行賦值的,即共享同一個黑名單地址,具體的執行命令請參考S2-045的PoC,這里簡述思路就是將DefaultMemberAccess存入OgnlContext上下文環境中,接著使用三目運算符主要為了適配低版本中可以直接取到_memberAccess對象,取不到就按前面繞過的形式將黑名單置空并將DefaultMemberAccess設為默認安全策略。
—Struts2.5.13+(S2-057)
此版本時沙箱的excludedClasses等黑名單集合設為不可變集合(從struts
2.5.12開始就不再可變)通過前面S2-045的PoC思路中的clear()函數來置空黑名單不再可行。同時struts2.5.13使用的ognl-3.1.15.jar包中OgnlContext不再支持使用#context獲取上下文環境。
針對上述獲取上下文環境context的方式是通過上下文環境中其他屬性(比如attr)來獲得context:\#attr['struts.valueStack'].context。
至此置空黑名單的新打開方式又可以通過setExcludedXXX('')方法實現出現:(\#ognlUtil.setExcludedClasses('')).(\#ognlUtil.setExcludedPackageNames(''))
但是執行setExcludedXXX('')的方法中的調用Collections.unmodifiableSet(classes)其實是返回一個新的空集合,并不是之前那個_memberAccess和ognlUtil共享的那個黑名單地址的集合,這里通過再次發送請求即可。原因是提到過OgnlUtil是單例模式實現,應用從始至終都用的同一個OgnlUtil,而_memberAccess的作用域是在一次請求范圍內的,與此同時OgnlUtil中的黑名單集合已經置空,那么重新發一次請求,_memberAccess將重新初始化,通過OgnlUtil中為空的黑名單進行賦值。
直接參考S2-057 PoC的兩次請求包:
/${(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}/login.action
/${(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}/login
—Struts2.5.22+(S2-061)
之前Struts2.5.20中使用的ognl-3.1.21.jar包ognl.OgnlRuntime#getStaticField
中調用了isAccessible方法,同時OgnlUtil中set黑名單集合等修飾符由 public 變成
protected。
在Struts2.5.22+中,ognl.OgnlRuntime#invokeMethod方法調用時屏蔽常用的類,即是就算將黑名單Bypass方法調用時仍會判斷是否是這些常用的類。到這才算正正式進入第三階段的安全機制。同時struts-default.xml中定義的黑名單再次增加。此時的黑名單導致之前的Bypass方式,通過容器創建實例; @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS獲得DefaultMemberAccess實例;使用#attr['struts.valueStack'].context獲得上下文環境等全部失效。
到S2-059的修復完成的第三階段安全機制又被S2-061的Bypass方式突破,其中涉及兩個初次引用類:
org.apache.tomcat.InstanceManager:
使用其默認實現類DefaultInstanceManager的newInstance方法來創建實例。
org.apache.commons.collections.BeanMap:
-通過BeanMap調用setBean方法可以將類實例存入BeanMap中,存入同時進行初始化將其set、get方法存入當前的writeMethod、readMethod集合中;
-通過BeanMap調用get方法可以在當前bean的readMethod集合中找到對應get方法,再反射調用該方法返回一個對象;
-通過BeanMap調用put方法可以在當前bean的writeMethod集合中找到對應set方法,再反射調用該方法。
網上關于S2-061Bypass的復現和剖析幾乎詳盡到無可附加,但還是有必要記錄一下這個最年輕的PoC。
%25{(\#im=\#application['org.apache.tomcat.InstanceManager']).(\#bm=\#im.newInstance('org.apache.commons.collections.BeanMap')).(\#vs=\#request['struts.valueStack']).(\#bm.setBean(\#vs)).(\#context=\#bm.get('context')).(\#bm.setBean(\#context)).(\#access=\#bm.get('memberAccess')).(\#bm.setBean(\#access)).(\#empty=\#im.newInstance('java.util.HashSet')).(\#bm.put('excludedClasses',\#empty)).(\#bm.put('excludedPackageNames',\#empty)).(\#cmdout=\#im.newInstance('freemarker.template.utility.Execute').exec({'whoami'}))}
首先從application中獲得DefaultInstanceManager實例,調用newInstance方法獲得BeanMap實例。接著先將OgnlValueStack存入BeanMap中,通過get方法可以獲得OgnlContext實例,獲得OgnlContext實例就可以通過其獲得MemberAccess實例,接著可以通過put方法調用set方法,將其黑名單置空,黑名單置空后就可以創建一個黑名單中的類實例來執行命令。
—Struts2.5.26
Struts2.5.26版本中再一次增加黑名單,中間件調用包也被屏蔽。至此,Struts2的版本修復催化的安全機制已經梳理完畢,其中針對階段性的PoC也只說明當時的一種可行性方式,并不沒有絕對意味,同時也不意味著最新PoC就能適用低版本。PoC對應的攻擊和官方修復給出的防御一步一步演變為今天的安全機制,將來Struts2還會不會再出RCE,較為客觀評價就是有對應價值驅動,突破當前安全機制的投入產出比可能是個客觀因素。