Struts2 安全機制研究
本文將從struts2漏洞出發,研究struts2安全機制從無到有的過程,研究漏洞發生的原因以及修復的方式。
為了不讓本文過於冗長,本文將適度對一些細節有所刪減,具體詳情請自行查詢相關文件。
S2-001(Struts 2.0.0 - Struts 2.0.8)
這個版本的struts2沒有安全機制,在提交OGNL表示式後,進行了遞迴查詢,導致OGNL表示式的執行。
問題出在TextParseUtil類的translateVariables方法。這個方法是用來做轉換物件操作的。
官方給出的修復方案是將altSyntax預設關閉,使用break打斷遞迴查詢。
那麼這裡主要說一下altSyntax,這個功能是將標籤內的內容當作OGNL表示式解析,關閉了之後標籤內的內容就不會當作OGNL表示式解析了。
S2-003(Struts 2.0.0 - Struts 2.0.11.2)
S2-003是一個引數名注入,所以我們要重點關注一下ParametersInterceptor攔截器,在XWork2.0.4官方重寫了acceptableName方法,增加了敏感字元檢測,如 “,” ”#” “:”,如果acceptableName方法檢測到了這些字元,則返回false。也就是說安全檢測沒有通過,則表示式不會執行。
我們注意到網上的POC通過unicode編碼繞過了敏感字元的檢測。官方針對編碼繞過的修復在新版本的struts2中,增加了更為嚴格的正則表示式檢測字元可能出現的問題。
S2-005(Struts 2.0.0 - Struts 2.1.8.1)
S2-005是一個引數名的注入,官方在這個版本增加了沙箱,並且重寫了引數攔截器的正則表示式,但是它忽略了我們依然可以通過OGNL上下文來訪問靜態方法從而達到任意程式碼執行。
相關上下文如下:
-
#context 一個基於'xwork.MethodAccessor.denyMethodExecution'屬性值的保護方法執行
-
#_memberAccess 禁止靜態方法執行
-
#root
-
#this
-
#_typeResolver
-
#_classResolver
-
#_traceEvaluations
-
#_lastEvaluation
-
#_keepLastEvaluation
我們需要將denyMethodExecution的屬性設定為false,這樣才可以執行方法,通過#_memberAccess上下文設定allowStaticMethodAccess為true
官方的修復是在引數攔截器修改了更為嚴格的正則表示式
S2-008是引數值的注入,但是需要開啟devmode模式,對於debug引起的問題我們需要關注DebuggingInterceptor攔截器,而且沙箱沒有改變,官方對於該漏洞的修復也是修改了acceptedParamNames過濾器的ParameterInterceptor和CookieInterceptor的正則表示式。
S2-007是標籤內的回顯,是因為變數型別報錯導致OGNL表示式的執行,同樣的沙箱也沒有變化。針對該漏洞的修復也是增加了一個方法,對單引號進行轉義處理。
與S2-003和S2-005不同的是S2-009是引數值注入,繞過了ParameterInterceptor的限制,安全沙箱沒有變化,因為之前官方在針對引數過濾器的修復的時候,只是考慮到了引數名,沒有考慮到引數值的安全性,那麼這次官方的修復主要有兩點,其中之一是在值棧中setParameter方法將不允許引數名稱中包含更多的eval表示式,然後又再次重寫了正則表示式,那麼在struts2 2.3.1.2中 ParameterInterceptor攔截器的正則表示式變得更為嚴格,同樣的,也增加了 “排除引數“這樣一個HashMap。
S2-012該漏洞是在struts.xml對action物件做了一個重定向配置,重定向配置的引數以OGNL表示式解析造成OGNL二次注入導致任意程式碼執行,安全沙箱沒有改進。官方的修復拒絕ognlutil類的eval表示式。
S2-013該漏洞跟標籤有關,標籤屬性設定為includeParams=all 會造成OGNL表示式執行,在S2-014後URL將不會把引數名或值傳遞給OGNL表示式。同樣的,沙箱也發生了變化,在Xwork 2.3.14.2 版本,修改了沙箱SecurityMemeberAccess類,刪除了setAllowStaticMethodAccess方法,導致無法修改AllowStaticMethodAccess屬性值,同時AllowStaticMethodAccess屬性被final修飾。
S2-015這個漏洞的POC很有意思,我們失去了setAllowStaticMethodAccess方法導致無法設定AllowStaticMethodAccess屬性值從而無法執行靜態方法,我們來看看S2-015,這個漏洞是萬用字元任意對映導致OGNL表示式執行的問題。
這個點是沒有檢查白名單的,當使用%或$的時候,會在translateVariables方法進行OGNL表示式的二次執行
因為setAllowStaticMethodAccess方法被刪掉,但是ognlcontext的上下文屬性還在,所以可以通過_memberAccess去獲取AllowStaticMethodAccess的值並通過setAccessible方法設定為true,從而達到任意程式碼執行的目的。
官方針對這個漏洞在DefaultActionMapper類增加了一個cleanupActionName方法去處理actionname的惡意程式碼,並返回安全的actionname。
這是攔截成功的
S2-016漏洞的主要原因是DefaultActionMapper類支援 “action:”,“redirect:”或“redirectAction:”三種引數,引數後的的資訊未得到正確清理,導致OGNL表示式執行,官方的修復是修改了DefaultActionMapper類,刪掉了 “redirect:”,“/redirectAction:”保留了method:和action:同時利用cleanupActionName方法進行了過濾。
S2-019漏洞的問題是在Struts 2.0.0 - Struts 2.3.15.1版本的struts.xml開啟了動態方法呼叫
官方的修復是在下一版本將動態方法呼叫預設為false。
S2-029漏洞的問題是OGNL表示式二次執行,對應struts2版本為Struts 2.0.0 - Struts 2.3.24.1(2.3.20.3除外)官方對這個漏洞的簡介是會對分配給某些標記的屬性值進行二次執行,因此可以傳入一個值,該值在rendered tags attribute會再次執行。
我們可以觀察一下struts2是如何處理html的id標籤,在看UIBean類之前,先講一下OGNL二次執行的點,首先是這樣的。
我在example.jsp的程式碼是這樣的,接收引數的值用${value}包含了起來,就像這樣。
那麼我存進去的表示式,就會產生OGNL表示式執行,再看看後端的處理方式。
findString方法執行了一次表示式,隨後findStringIfAltSyntax又解析了一次表示式
只要html標籤的id值可控,就可以任意程式碼執行,name屬性的值同樣。
在struts2 2.3.24版本後官方增加了一些黑名單類列表以及一些包名,在struts-default.xml。
安全管理器也增加了包檢查器,類檢查器和成員類檢查器,關於這些檢查器的實現,都是從定義好的排除類名迭代迴圈匹配
結合POC之所以能夠執行命令,是因為OGNL上下文一個安全檢查方法的邏輯判斷出錯了,導致現有安全機制失效。關注一下ObjectPropertyAccessor類的setPossibleProperty方法,這個方法是對錶達式完成賦值操作的。
我們主要關注其中一個分支判斷
跟進setMethodValue方法
context.getMemberAccess().isAccessible(context, target, m, propertyName)
這個表示式返回true則代表安全檢查通過,返回false則代表安全檢查沒有通過。
這個地方的表示式邏輯寫錯了,取反則意味著當安全檢查沒有通過的時候返回值為true。
為true則意味著result 被賦值為 false,繼續跟進則發現result返回值為false。
當result返回false的時候,我們表示式為true,繞過了沙箱。
當bypass這個沙箱之後,我們只需要使allowStaticMethodAccess為true 允許執行靜態方法。excludedPackageNamePatterns為空集合,允許呼叫相關的包。excludedClasses為空集合,允許呼叫任意類。即可完成任意程式碼執行
S2-032漏洞是一個歷史遺留問題,官方給出的是當啟用動態方法呼叫時候,利用method:引數進行遠端執行程式碼。
要求
受影響的版本Struts 2.3.20 - Struts Struts 2.3.28(2.3.20.3和2.3.24.3除外)。
這是因為S2-016的時候,保留了兩個引數,分別是method:和 action:
又沒有做好過濾導致的。
我們在S2-032漏洞可以看到POC發生了變化,在繞沙箱的語句由一大串變成#[email protected]@DEFAULT_MEMBER_ACCESS,這一句在之前的版本也適用,直接通過DEFAULT_MEMBER_ACCESS覆蓋掉SecurityMemeberAccess類,因為SecurityMemeberAccess類有一堆的安全限制屬性,想要繞過的話,需要利用ognlcontext的上下文,那麼一定會有疑問,為什麼#[email protected]@DEFAULT_MEMBER_ACCESS可以繞過安全沙箱呢?
答案非常簡單,我們可以看到在ognlcontext類 DEFAULT_MEMBER_ACCESS其實是DefaultMemberAccess的一個例項,隨後我們可以注意到一個比較關鍵的點。
_memberAccess上下文是取值於DEFAULT_MEMBER_ACCESS例項,而DefaultMemberAccess又是SecurityMemeberAccess的父類,所以我們在DefaultMemberAccess的isAccessible方法上下斷點
可以看到result返回的值是true,在分支判斷語句的地方可以看出有一個取反的操作,所以不會執行分支下面的判斷邏輯,也就不會取安全屬性的值從而繞過了安全沙箱,DefaultMemeberAccess類的作用是用於設定和獲取非public欄位的訪問程式,以允許訪問private, protected, package protected的類成員。
官方修復禁用動態呼叫方法屬性,以及將method:引數做了過濾
S2-033漏洞的問題是REST外掛啟用了動態方法呼叫導致的遠端程式碼執行漏洞。並且在setMethod的時候沒有做過濾。
“enableEvalexpression的值在showcase找不到,如果有小夥伴找到了可以告訴我,感謝”
同時也與一個關鍵的屬性有關enableEvalexpression,這個屬性為true則允許執行表示式,為false則不允許執行ognl表示式,那麼struts2官方針對這個漏洞進行的修復方式則是將enableEvalexpression屬性設定為false,禁止執行ognl表示式,我們可以根據
這個方法來判斷是否能夠執行ognl表示式,在struts2 2.3.28版本中enableEvalexpression表示式為true。
可以看到這個分支判斷做了一個取反的操作,沒有丟擲這個錯誤,所以ognl表示式順利執行。Btw,在進一步跟進的時候,發現程式在判斷語句是否是evalchain的時候,一直返回null,而在修復後的版本,則準確的判斷出了evalchain,這個條件語句是進行了兩個表示式判斷,需要兩個表示式均為true才會報錯。
最終在invokeAction方法處通過getValue執行表示式。
官方的修復方法則是簡單粗暴,將enableEvalexpression表示式設定為false,來看看修復後的版本的處理方式。那麼在之後的版本修復則是用cleanactionname方法將actionMethod進行了一個過濾。
這個點也是沒搞懂的,反正就丟擲錯誤了。
S2-037這個漏洞也是REST外掛造成的問題,不過不需要開啟動態方法執行,而且繞過了S2-033的安全機制,繞過的方法是使用三元運算子構造。出問題的類依然是RestActionMapper。
關於這個漏洞會有一個問題,為什麼不需要開啟動態方法執行也可以執行遠端程式碼。
可以看到這裡執行完handleDynamicMethodInvocation方法後在下面有一個
mapping.setMethod,同時沒有判斷if (allowDynamicMethodCalls)
官方的修復方式是使用cleanactionname方法過濾了actionmethod
S2-045漏洞產生的原因是Jakarta Multipart解析器執行檔案上傳時造成的遠端程式碼執行,jakarta是struts2的預設解析器。
在包裝請求方法中可以看到只有content-type不為null且值為multipart/form-data的時候才會是一個上傳請求。
漏洞產生的類還是JakartaMultiPartRequest類,當content-type報錯的時候會執行ognl表示式,看看為什麼會這樣。
然後會看到,在資料處理出錯的時候,會走處理錯誤流程,catch流程,我們可以看到在catch流程呼叫了buildErrorMessage方法。看看buildErrorMessage方法的實現。
findtext方法執行ognl表示式,跟入
然後我們進入getDefaultMessage方法看看它的內部實現。
報錯資訊被轉換成物件然後呼叫translateVariables方法的evaluate方法去執行ognl表示式
官方修復的方式是
去掉了e.getmessage方法。
S2-048漏洞問題出現在struts2-struts1-plugin-2.3.32.jar 外掛,這個外掛的作用是可以讓struts2能夠相容struts1的程式碼
getText方法主要是實現struts的國際化的一個方法,比如說郵件,可能發給多個客戶,每個客戶的語種不一樣,不可能針對每一個語種做一個模版,所以就有了getText方法。
入口點在struts1action的execute方法,struts1action的execute方法是呼叫savegangsteraction類的execute方法實現的。
這裡直接取了原始值,導致了漏洞的產生
execute執行完後,到達第一個sink點,getText取值使用者可控
程式碼最終進入getdefaultmessage的translateVariables方法執行ognl表示式
官方的修復是建議使用資源建傳值。
S2-052漏洞是Struts2 REST外掛的XStream元件存在反序列化漏洞,使用XStream元件對XML格式的資料包進行反序列化操作時,未對資料內容進行有效驗證,存在安全隱患,可被遠端攻擊。
檢視struts2-rest-plugin-2.3.33.jar的struts-plugin.xml發現攔截器ContentTypeInterceptor。
很多不同型別的資料都交給不同的Handler進行處理,我們注意到這裡xml的資料是交給XStreamHandler處理的。
contenttypehandler的作用是處理與特定內容型別的物件之間的內容傳輸。
根據這個漏洞,我們主要檢視一下xstreamhandler類,這類主要是是將xml轉換成物件,物件轉換成xml的,也就是比較專業的詞彙叫“marshal“和”“unmarshal”。
我們分別在contenttypeinterceptor攔截器和xstreamhandler處下斷點。
它會先獲取content-type,這也是我們的poc中要修改content-type為application/xml才可以執行程式碼的原因,因為只有設定為application/xml,你的資料流才會到xstreamhandler處執行。
這裡其實是有一個分發的操作,根據你的content-type型別分發給不同的handler進行處理,你設定application/xml就是分給xstreamhandler處理。
這裡是呼叫fromxml 進行一個物件轉換。
關於“marshal“和”“unmarshal” 參考 ofollow,noindex" target="_blank">https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
官方的修復引入了一些介面,介面為每個操作類定義類限制。
-
org.apache.struts2.rest.handler.AllowedClasses
-
org.apache.struts2.rest.handler.AllowedClassNames
-
org.apache.struts2.rest.handler.XStreamPermissionProvider
以及升級struts2,我們會看到增加了一些限制類
針對xml序列化做了限制
S2-057這個漏洞的產生原因是alwaysSelectFullNamespace為true導致的,
沒什麼有意思的地方,唯一有意思的是POC的構造,在利用045的POC測試的時候,會發現這個問題。
你是取不到值棧的,這是為什麼呢?
首先是045的取值棧方法,在045包括之前的時候我們在ognlcontext類有幾個欄位
這就是為什麼之前的漏洞poc都會有_memberAccess這樣的表示標識存在,_memberAccess這個是一個安全沙箱,關於poc的講解我會在文章裡詳細寫明,包括置空三個屬性的值,這樣就可以執行任意程式碼了,呼叫任意包了等等
它可以利用ognlcontext的硬編碼直接訪問上下文,但是在2.3.34版本刪除了這三個屬性
那麼在使用045的poc的時候,會發現值棧返回為空,這是因為無法使用#context,#_memberAccess去獲取訪問物件了
使用requests域來取值棧,可以看到是可以取到值棧的。
request域下有struts.valueStack物件,可以通過這個獲取值棧,達到命令執行的目的。
官方的修復是升級到Apache Struts版本2.3.35或2.5.17。