0x00 漏洞概述
S2-009是S2-003与S2-005的补丁绕过,当时的补丁是增加了正则以及相关的限制(这些限制可以通过执行OGNL表达式进行修改),主要的防御还是正则。
这次的问题还是出现在ParameterInterceptor这个拦截器上,其漏洞原理类似于二次注入,先将Payload注入到上下文中,取出来时通过某个特定语法就可以执行之前设置过的Payload。
影响版本:2.0.0 – 2.3.1.1
官方issue地址:https://cwiki.apache.org/confluence/display/WW/S2-009
0x01 环境搭建
首先编写一个最简单的Action类,其中只需要存在一个属性即可:
public class TestAction {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String execute() throws Exception {
return "success";
}
}
接着编写struts.xml用于定义路由:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="st2-demo" extends="struts-default">
<action name="test" class="TestAction">
<result name="success">index.jsp</result>
</action>
</package>
</struts>
最后老规矩,定义一个Filter:
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
0x02 漏洞分析
ParameterInterceptor的作用就是将当前请求中的参数与Bean中的属性绑定在一起,所以http://127.0.0.1:8080/test.action?message=xxx
会将xxx这个值赋到当前请求对象TestAction的message属性中,在setValue调用完毕后可以通过getValue取出来:
接下来会继续对下一个参数进行解析,通过S2-003与S2-005的分析中我们得知,如果能够通过ParameterInterceptor的相关验证逻辑,那么是会对参数名进行一次OGNL表达式解析的,S2-003与S2-005的漏洞也出于此,后续的修复方案是增加了静态方法相关的禁用以及一个用于验证参数名的正则。
但是在S2-009中我们可以通过top['message']
的方式获取到刚刚赋到message属性上的值,并通过()
执行OGNL表达式解析,并且top['message']
是符合正则条件的:
所以完整的Payload如下(实际情况利用时需要进行URL编码):
http://localhost:8082/test.action?message=#context['xwork.MethodAccessor.denyMethodExecution']=false,#_memberAccess["allowStaticMethodAccess"]=new java.lang.Boolean(true),@java.lang.Runtime@getRuntime().exec('open -a /System/Applications/Calculator.app'),#request&top['message'](0)
0x03 修复方案
这次漏洞修复体现在多处代码,首先就是ParametersInterceptor中,其将原先调用的setValue修改为setParameter,将两者作为两个模块区分开了。
区别是什么呢?重点就是传给setValue的第四个参数:
public void setParameter(String expr, Object value) {
this.setValue(expr, value, this.devMode, false);
}
public void setValue(String expr, Object value, boolean throwExceptionOnFailure) {
this.setValue(expr, value, throwExceptionOnFailure, true);
}
可以发现,setParameter的第四个参数为false,而setValue的第四个参数为true,这影响到了后续的调用流程,让我们接着跟到最后的调用流程中:
protected void setValue(String name, Map<String, Object> context, Object root, Object value, boolean evalName) throws OgnlException {
Object tree = this.compile(name);
if (!evalName && this.isEvalExpression(tree, context)) {
throw new OgnlException("Eval expression cannot be used as parameter name");
} else {
Ognl.setValue(tree, context, root, value);
}
}
这里会判断当前的name是否为evalName,此处为setParameter,因此evalName为false,所以这里为true,接着会通过isEvalExpression来判断当前的name是否符合要求。
private boolean isEvalExpression(Object tree, Map<String, Object> context) throws OgnlException {
if (tree instanceof SimpleNode) {
SimpleNode node = (SimpleNode)tree;
return node.isEvalChain((OgnlContext)context);
} else {
return false;
}
}
isEvalExpression中会通过isEvalChain来判断当前的node是否为链式调用(先取值再执行就是链式调用),Debug一下会发现之前的Payload在此处已经返回true了,被标为危险的name,因此这里会直接抛出异常而不会进行接下来的OGNL表达式解析:
由于我们的Node会被解析为ASTEvalNode,其isEvalChain相关逻辑如下:
public boolean isEvalChain(OgnlContext context) throws OgnlException {
return true;
}
可以发现是直接返回true的,因此所有EvalNode都不能在这个漏洞点中使用了,如果想要继续挖掘只能换一个Node看看是否能进行二次解析或是能够达到与EvalNode相同作用(通过继承逻辑)。
上面是一个修复点,还有另外一个修复点在2.3.1.2中似乎没有启用,就是xx中的正则被修改为了:
\w+((\\.\\w+)|(\\[\\d+\\])|(\\(\\d+\\))|(\\['\\w+'\\])|(\\('\\w+'\\)))*
这个正则的作用是匹配name中的字母与数字,匹配不了特殊符号,我认为Struts2官方应该是想在这里取出name中不包含特殊符号的部分,接着通过setValue进行一个赋值,如下:
但不知道为什么在这个版本没有启用,我认为这算是一个比较好的修复方案,不会太影响后面的业务逻辑,直接从漏洞点出发而不是直接在底层封死了,可能Struts2有它们自己的考究吧。