CVE-2019-0230: S2-059 远程代码执行漏洞分析
2020-09-01 17:48

报告编号:B6-2020-090103

报告来源:360CERT

报告作者:Hu3sky

更新日期:2020-09-01

0x01 漏洞简述

2020年08月13日, 360CERT监测发现Apache官方发布了Struts2远程代码执行漏洞的风险通告,该漏洞编号为CVE-2019-0230,漏洞等级:高危。漏洞评分:8.5

攻击者可以通过构造恶意的OGNL表达式,并将其设置到可被外部输入进行修改,且会执行OGNL表达式的Struts2标签的属性值,引发OGNL表达式解析,最终造成远程代码执行的影响。

对此,360CERT建议广大用户及时将Apache Struts2进行升级完成漏洞修复。与此同时,请做好资产自查以及预防工作,以免遭受黑客攻击。

0x02 风险等级

360CERT对该漏洞的评定结果如下

评定方式 等级
威胁等级 高危
影响面 广泛
360CERT评分 8.5分

0x03 影响版本

  • Apache Struts2:2.0.0-2.5.20

0x04 漏洞详情

根据官方发布的信息来看,漏洞产生的主要原因是因为Apache Struts框架在强制执行时,会对分配给某些标签属性(如id)的属性值执行二次ognl解析,对于精心设计的请求,这可能导致远程代码执行(RCE)。

官方给出的利用场景如下:

<s:url var="url" namespace="/employee" action="list"/><s:a id="%{skillName}" href="%{url}">List available Employees</s:a>

这里的id属性里的值使用了ognl表达式进行包裹,同时该值如果可控,那么就会因为两次的解析而造成ognl表达式执行。

该分析在较低版本中进行测试,仅对漏洞产生原理进行分析,如在高版本中进行执行命令,需绕过沙箱。

拦截器处理请求值

首先,我们需要明确jsp是如何进行取值的。 在com.opensymphony.xwork2.interceptor.ParametersInterceptor这个拦截器里,会获取我们传入的值。 ActionContext存储着当前上下文的请求信息。 ParametersInterceptor里,会把请求的值存入ValueStack类型的栈里,这里是OgnlValueStack,具体在setParameters里。 会调用stacksetValue方法进行赋值。 接着就会进行一系列复杂的处理,获取action指向的jsp。然后开始处理对应的jsp标签。

id标签解析

org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag方法中。 valueStack里获取相关的请求值,然后调用populateParams方法,该方法对标签进行处理,其中包括id标签。

于是跟进org.apache.struts2.components.Component#setId方法。 id属性不为null,于是继续跟进findString方法。 然后跟入findValue,由于altSyntax默认为true(这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了),所以最终进入translateVariables方法(高版本对应的是evaluate方法)。 这里会对id,也就是%{skillName}进行ognl表达式执行,执行完剩余部分就是skillNametranslateVariables代码如下:

public static Object translateVariables(char[] openChars, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
        Object result = expression;
        char[] arr$ = openChars;
int len$ = openChars.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            char open = arr$[i$];
            int loopCount = 1;
            int pos = 0;
String lookupChars = open + "{";

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

                //防止递归解析
                if (loopCount > maxLoopCount) {
                    break;
}

                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) {
                    break;
}

                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);
                } else {
                    expression = left.concat(right);
                    result = expression;
}

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

        XWorkConverter conv = (XWorkConverter)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(XWorkConverter.class);
        return conv.convertValue(stack.getContext(), result, asType);
}

然后调用stack.findValue方法从stackValuecontext上下文中获取之前传入的该参数的具体值。 接着将expression值进行重新赋值,赋值为request请求传入的skillName参数的值,比如%{1+1},这里由于有如下判断:

        if (loopCount > maxLoopCount) {
            break;
}

此段代码用来防止递归解析ognl,所以最终将%{1+1}赋值给result后,会跳出循环,不再继续解析传入的值,接着往下执行:

        XWorkConverter conv = (XWorkConverter)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(XWorkConverter.class);
return conv.convertValue(stack.getContext(), result, asType);

会获取上下文中的ContainerImpl,并实例化XWorkConverter: 然后调用convertValue,该方法里也没有做ognl解析,判断value不为null并且类型和一开始预定的类型一致,就返回value

id值二次解析

回退到setId,执行完findString后,将Ancohr.id重新赋值。 继续returndoStartTag,执行完populateParams方法。 跟入Anchor.start 继续跟进evaluateParams,这里会跟据标签做进一步操作。 在判断完一系列标签的值为null之后,会调用populateComponentHtmlId方法。 该方法再次获取id值,而这里的id值已经是之前经过populateParams方法处理过后的值。 接着调用findStringIfAltSyntax,传入id值。 根据altSyntax值,判断是否执行findString方法,进入findString之后的步骤就和前面对标签的ognl表达式执行一样,在translateVariables方法执行最终的ognl表达式。

总结

该漏洞主要是id标签的二次ognl解析产生的,第一次是在解析id标签属性的时候,这时候id的值已经被替换为用户输入,而第二次再次获取id值,此时的id值已经是用户可控的值,这时候就会解析用户输入的ognl表达式。

该漏洞限制条件较多:

  1. Struts2标签的属性值可执行OGNL表达式(比如id)。
  2. Struts2标签的属性值可被外部输入修改。
  3. Struts2标签的属性值未经安全验证。
  4. 高版本需绕过沙箱执行命令。
  5. useAltSyntaxtrue

0x05 时间线

2020-08-13 Apache Struts2官方发布安全通告

2020-08-13 360CERT发布通告

2020-09-01 360-CERT 发布分析

0x06 参考链接

  1. CVE-2019-0230:Apache Struts2远程代码执行漏洞通告
  2. Apache Struts2官方安全通告