Smi1e@Pentes7eam
漏洞信息:https://cwiki.apache.org/confluence/display/WW/S2-007
当启用动态方法调用时,由于特殊参数名前缀
method:
的处理方法中未对参数进行过滤,而在执行Action
时会对其进行OGNL表达式解析,从而导致OGNL表达式执行。
漏洞复现
影响范围
Struts 2.3.20 – 2.3.28(2.3.20.3和2.3.24.3除外)
漏洞分析
首先该漏洞需要在 struts.xml
中将DynamicMethodInvocation
设置为true才能利用成功,2.3.15.1之前默认为true,2.3.15.2开始默认为false。
"struts.enable.DynamicMethodInvocation"value="true"/>
该选项是指是否开启 Dynamic Method Invocation
动态方法调用。动态方法调用是指:表单元素的action
不直接等于某个Action
的名字,而是以感叹号后加方法名来指定对应的动作名,如login!test.action
。当指定调用某一方法来处理请求时,就不会走默认执行处理请求的execute
方法。
我们知道官方为了修复 S2-016 特殊参数名前缀导致的OGNL表达式执行,删除了 DefaultActionMapper
中对特殊参数名前缀redirect:
和redirectAction
的处理,只留下了action:
和method:
。
在 DefaultActionMapper
处理method:
前缀的地方下断点。因为我们开启了DynamicMethodInvocation
,所以这里会进入if条件中把我们的参数名method:
及其后面的字符串赋值给ActionMapping
对象的method
属性。
ActionMapping
对象存储有Action
的配置信息,它接着会做为findActionMapping
方法的结果返回给StrutsPrepareAndExecuteFilte
。
接着会去调用 this.execute.executeAction(request, response, mapping);
,根据ActionMapping
对象的信息调用对应的Action
。
public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
this.dispatcher.serviceAction(request, response, mapping);
}
它调用了Dispatcher
的serviceAction
方法,首先使用request
、response
、mapping
对象封装了一个ContextMap
对象,并把OgnlValueStack
put进去。然后从mapping
中获取 action 对应的命名空间namespace
、请求的action名name
、请求方法method
。接着根据前面获取的信息创建用户自定义Action的ActionProxy
对象。
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
Map extraContext = this.createContextMap(request, response, mapping);
ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");
boolean Stack = stack == ;
if (Stack) {
ActionContext ctx = ActionContext.getContext;
if (ctx != ) {
stack = ctx.getValueStack;
}
}
if (stack != ) {
extraContext.put("com.opensymphony.xwork2.util.ValueStack.ValueStack", this.valueStackFactory.createValueStack(stack));
}
String timerKey = "Handling request from Dispatcher";
try {
UtilTimerStack.push(timerKey);
String namespace = mapping.getNamespace;
String name = mapping.getName;
String method = mapping.getMethod;
ActionProxy proxy = ((ActionProxyFactory)this.getContainer.getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, method, extraContext, true, false);
request.setAttribute("struts.valueStack", proxy.getInvocation.getStack);
if (mapping.getResult != ) {
Result result = mapping.getResult;
result.execute(proxy.getInvocation);
} else {
proxy.execute;
}
......
}
跟进DefaultActionProxyFactory
的createActionProxy
方法,它会实例化一个DefaultActionInvocation
对象,后面会通过它的invoke
来正式执行一系列的拦截器以及Action。最后又调用this.createActionProxy
,继续跟进。
然后会实例化 StrutsActionProxy
对象并返回,继续跟进其构造方法。
public ActionProxy createActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
StrutsActionProxy proxy = new StrutsActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
this.container.inject(proxy);
proxy.prepare;
return proxy;
}
这里会调用父类 DefaultActionProxy
的构造方法,继续跟进
public StrutsActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
super(inv, namespace, actionName, methodName, executeResult, cleanupContext);
}
DefaultActionProxy
的构造方法会把传入的参数赋值给其对应的属性,在赋值methodName
时会调用StringEscapeUtils.escapeHtml4
和StringEscapeUtils.escapeEcmaScript
进行过滤。
escapeEcmaScript(String input) / unescapeEcmaScript(String input)
:转义/反转义js脚本
escapeHtml4(String input) / unescapeHtml4(String input)
:转义/反转义html脚本
因此我们的payload中不能出现 < > " ' &
等字符串,因此对于字符串参数的传入需要使用#parameters.参数名[0]
从OgnlValueStack
中获取。
最终 StrutsActionProxy
对象返回并执行其execute
方法。然后执行this.invocation.invoke;
,this.invocation
在上面已经知道是DefaultActionInvocation
对象,跟进。
public String execute throws Exception {
ActionContext previous = ActionContext.getContext;
ActionContext.setContext(this.invocation.getInvocationContext);
String var2;
try {
var2 = this.invocation.invoke;
} finally {
if (this.cleanupContext) {
ActionContext.setContext(previous);
}
}
return var2;
}
该 invoke
方法会执行一系列的拦截器以及Action,直接跳过所有拦截器的执行步入执行Action的地方,跟进。
继续跟进
public String invokeActionOnly throws Exception {
return this.invokeAction(this.getAction, this.proxy.getConfig);
}
可以看到这里有一个对 methodName
进行getValue
的操作,而我们的payload后会拼接一个,因此我们的payload后面要加一个
1?#xx:#request.toString
来防止报错。
最终执行我们的payload
为什么低版本不行呢?低版本的 com.opensymphony.xwork2.DefaultActionInvocation.invokeAction
方法中不会对methodName
值做 OGNL 表达式计解析。
另外Struts 2.3.20 的配置文件中新增加了黑名单配置 struts.excludedClasses
、excludedPackageNamePatterns
,用来严格验证排除一些不安全的对象类型。
因此以前的payload中通过反射修改 SecurityMemberAccess
对象的allowStaticMethodAccess
属性和直接调用构造函数已经无法使用了。不过_memberAccess
仍然可以访问,而且还有一个静态对象DefaultMemberAccess
也可以通过ognl.OgnlContext
的静态属性DEFAULT_MEMBER_ACCESS
访问,而DefaultMemberAccess
是SecurityMemberAccess
的父类。所以这里可以使用@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
覆盖掉#_memberAccess
。
为什么覆盖掉 #_memberAccess
就能绕过沙盒呢?
这是因为 OgnlContext
中的_memberAccess
与OgnlValueStack
中的securityMemberAccess
其实是同一个SecurityMemberAccess
类的实例。
首先在初始化 OgnlValueStack
对象的时候会调用setRoot
,而其中会创建一个SecurityMemberAccess
对象。
然后调用 createDefaultContext
时会把该SecurityMemberAccess
对象赋值给OgnlContext
的_memberAccess
属性。
所以覆盖掉 #_memberAccess
就相当于覆盖掉了OgnlValueStack
中的securityMemberAccess
。
漏洞修复
特殊参数名前缀 method:
的处理方法中,在对ActionMapping
对象的method
属性赋值前使用正则[a-zA-Z0-9._!/-]*
进行过滤。
https://www.easyaq.com