去年,国外安全研究人员对 Adobe ColdFusion 中的多个漏洞进行了深入分析,获得了很多启发,其中之一涉及 CFM 和 CFC 处理、解析和执行。于是他们想知道是否还有其他 CFML 服务器。在这之前安全人员在使用 Lucee 作为底层服务器的多个 Apple 服务器上展示了预身份验证导致的远程代码执行 (RCE),具体阅读下方文章:
在早期研究中检查了 Lucee 的管理面板后,安全人员发现在未经身份验证的情况下,你只能访问四个 CFM 文件,因此没有太多空间可以发现其中的漏洞,因此需要深入研究 Lucee 是如何处理请求的。
检查完 web.xml 文件后,安全人员通过 IntelliJ 设置了 JVM 调试器并添加了 Lucee 的源代码,安全人员计划通过在 Request::exe()
处放置断点来审计代码,这样就可以一步一步地浏览代码,看看 Lucee 是如何处理请求的。
public static void exe(PageContext pc, short type, ...) {
try {
...
if (type == TYPE_CFML) pc.executeCFML(pc.getHttpServletRequest().getServletPath(), throwExcpetion, true);
else if (type == TYPE_LUCEE) pc.execute(pc.getHttpServletRequest().getServletPath(), throwExcpetion, true);
else pc.executeRest(pc.getHttpServletRequest().getServletPath(), throwExcpetion);
}
finally {
...
}
}
core/src/main/java/lucee/runtime/engine/Request.java#L29
Lucee 中处理请求和响应的另一个有趣的类是 core/src/main/java/lucee/runtime/net/http/ReqRspUtil.java
。在该类中,有一些函数可以处理请求的各个方面,例如设置/获取某些标头、查询参数和请求正文等。
在研究该类时,安全人员注意到对 JavaConverter.deserialize()
的调用。显而易见,它是 readObject()
的封装,用于处理 Java 反序列化。
public static Object getRequestBody(PageContext pc, boolean deserialized, ...) {
HttpServletRequest req = pc.getHttpServletRequest();
MimeType contentType = getContentType(pc);
...
if(deserialized) {
int format = MimeType.toFormat(contentType, -1);
obj = toObject(pc, data, format, cs, obj);
}
...
return defaultValue;
}
public static Object toObject(PageContext pc, byte[] data, int format, ...) {
switch (format) {
...
case UDF.RETURN_FORMAT_JAVA: //5
try {
return JavaConverter.deserialize(new ByteArrayInputStream(data));
}
catch (Exception pe) {
}
break;
}
看起来,当请求的内容/类型标头设置为 application/java
时,理论上会到这里结束,对吧?那么如果使用 URLDNS
小工具会是什么结果呢?什么也没发生。难道是反序列化条件没有通过?为了进行调查,在 getRequestbody()
上添加了一个断点,发现根本没有到达该断点。
这是为什么呢?继续跟踪函数调用,发现必须进行某些配置才能进入 if/else 语句,才能到达接收器。鉴于堆栈的复杂性,来对要点做一下简单总结。
Request:exe() - Determines the type of request and handles it appropriately.
↓
PageContextImpl:executeRest() - Looks for Rest mappings and executes the RestRequestListener.
↓
RestRequestListener() -- Sets the "client" attribute with the value "lucee-rest-1-0" on the request object.
↓
ComponentPageImpl:callRest() - Examines the "client" attribute; if it's "lucee-rest-1-0", proceeds to execute callRest() followed by _callRest().
↓
ComponentPageImpl:_callRest() - If the rest mapping involves an argument, invokes ReqRspUtil.getRequestBody with the argument deserialized: true.
↓
ReqRspUtil.getRequestBody() - If the deserialized argument is true, triggers the toObject() function, which deserializes the request body based on the provided content type.
↓
toObject() - Java Deserialization on the request body if the content type is "application/java".
↓
JavaConverter.deserialize() - The final step where the Java Deserialization process occurs.
要重现该 RCE,必须配置带有至少接受一个参数的REST函数映射:
component restpath="/java" rest="true" {
remote String function getA(String a) httpmethod="GET" restpath="deser" {
return a;
}
}
通过 Lucee 上的 REST 映射进行 Java 反序列化
令人吃惊的是,Lucee 的关键更新服务器使用 REST 端点 – https://update.lucee.org/rest/update/provider/echoGet。该服务器在管理各种Lucee安装的所有更新请求方面起着关键作用。
这可能使攻击者能够破坏更新服务器,从而为供应链攻击打开大门。
Lucee 的维护人员认识到情况的严重性,立即实施了修补程序来保护其更新服务器的安全,随后发布了包含必要修复的 Lucee 更新版本 – CVE-2023-38693。
在对代码库有了更深入的了解后,安全人员开始有选择地检查类,其中引起注意的是 CFMLExpressionInterpreter
。这个类的有趣性促使安全人员深入研究它的细节。查看该类后,很明显,当构造函数的布尔参数 limit 设置为 False
(默认为 True
)时,方法 CFMLExpressionInterpreter.interpret(…)
就可以使用执行 CFML 表达式。
像 CFMLExpressionInterpreter(false).interpret("function(arg)")
这样的东西应该能够让我们执行 Lucee 的任何函数。
通过以上观察,安全人员在代码库中进行了彻底的搜索,以识别初始化 CFMLExpressionInterpreter(false)
的实例,并发现了多个实例,其中一个特别令人感兴趣的是 StorageScopeCookie
,其名称似乎与 Cookie 有关。
public abstract class StorageScopeCookie extends StorageScopeImpl {
protected static CFMLExpressionInterpreter evaluator = new CFMLExpressionInterpreter(false);
protected static Struct _loadData(PageContext pc, String cookieName, int type, String strType, Log log) {
String data = (String) pc.cookieScope().get(cookieName, null);
if (data != null) {
try {
Struct sct = (Struct) evaluator.interpret(pc, data);
...
}
...
}
...
}
}
看来 StorageScopeCookie._loadData()
函数接受 Cookie 名称作为其参数之一,从 PageContext
检索其值,然后将其传递给interpret()
。
在跟踪多个代码流后,以下三个代码流程脱颖而出,似乎可以由 Lucee 应用程序调用。
sessionInvalidate() -> invalidateUserScope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
sessionRotate() -> invalidateUserScope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
PageContext.scope() -> getClientScope() -> ClientCookie.getInstance() -> StorageScopeCookie._loadData(…)
public final class ClientCookie extends StorageScopeCookie implements Client {
private static final String TYPE = "CLIENT";
public static Client getInstance(String name, PageContext pc, Log log) {
if (!StringUtil.isEmpty(name)) name = StringUtil.toUpperCase(StringUtil.toVariableName(name));
String cookieName = "CF_" + TYPE + "_" + name;
return new ClientCookie(pc, cookieName, _loadData(pc, cookieName, SCOPE_CLIENT, "client", log));
}
}
调用 sessionInvalidate()
或 sessionRotate()
后,安全人员成功访问了 ClientCookie.getInstance()
,并将 Cookie 名称构造为 CF_CLIENT_LUCEE
。
这意味着任何使用 sessionInvalidate()
或 sessionRotate()
的应用程序都可能通过 CF_CLIENT_LUCEE
Cookie 暴露远程代码执行 (RCE) 漏洞!
其中,“Lucee”代表应用程序上下文名称,该名称可能会根据部署应用程序的不同而有所不同。
在 Lucee 代码库中对这些函数在任何未经身份验证的 CFM 文件或组件 (CFC) 中的使用情况进行了初步搜索,但没有得到任何结果。于是安全人员将搜索范围扩大到 Mura/Masa CMS(Apple 也将其部署在其 Lucee 服务器上),成功发现了两处调用。
public function logout() output=false {
...
if ( getBean('configBean').getValue(property='rotateSessions',defaultValue='false') ) {
...
sessionInvalidate();
...
core/mura/login/loginManager.cfc#L505
不幸的是,能否成功利用此漏洞取决于 Mura/Masa 中启用的 rotateSessions
设置,该设置默认设置为 false。因此无法在 Apple 的部署中触发该漏洞。
带着一丝失望,安全人员将注意力转向 PageContext.scope()
流程。经过调试会话后,很明显,此场景中的 Cookie 名称将是 CF_CLIENT_
。更重要的是,要利用此代码执行,需要从 Lucee 管理员启用客户端管理设置,该设置默认情况下是禁用的,因此再次发现无法在 Apple 的配置上触发此漏洞。
但是无论如何,依然有一个相同的 PoC:
经过多次失败的尝试后,安全人员想到了一个替代方案,如果能够识别更多可能接受用户输入作为字符串并可能导致代码执行的函数,会怎么样?
安全人员的注意力被 VariableInterpreter.parse(,,limited)
所吸引,它初始化 CFMLExpressionInterpreter(limited)
,如果有对 VariableInterpreter.parse(,,false)
的调用,可能会有一种方法来执行代码。
考虑到这一点,安全人员在 VariableInterpreter
类中发现了一些易受攻击的接收器,如果以下任何函数将用户输入传递给 parse(),就可以达到目的:
- getVariable → VariableInterpreter.parse(,,false)
- getVariableEL → VariableInterpreter.parse(,,false)
- getVariableAsCollection → VariableInterpreter.parse(,,false)
- getVariableReference → VariableInterpreter.parse(,,false)
- removeVariable → VariableInterpreter.parse(,,false)
- isDefined → VariableInterpreter.parse(,,false)
为了缩小搜索范围,安全人员调查了导入 VariableInterpreter
类的类并确定了以下可疑对象:
- core/src/main/java/lucee/runtime/PageContextImpl.java
- core/src/main/java/lucee/runtime/functions/decision/IsDefined.java#L41
- core/src/main/java/lucee/runtime/functions/struct/StructGet.java#L37
- core/src/main/java/lucee/runtime/functions/struct/StructSort.java#L74
- core/src/main/java/lucee/runtime/functions/system/Empty.java#L34
- core/src/main/java/lucee/runtime/tag/SaveContent.java#L87
- core/src/main/java/lucee/runtime/tag/Trace.java#L170
考虑到 PageContextImpl
的复杂性,安全人员选择首先关注其他类,从函数类开始,他们测试了 StructGet("abc")
并成功命中断点 VariableInterpreter.parse()
,但是,尝试之前用于 CFMLExpressionInterpreter.interpret()
调用的Payloads并未执行 imageRead()
。
查看 parse()
后,他们意识到,由于从[]分割字符串后调用了 CFMLExpressionInterpreter.interpretPart()
函数,因此需要将Payloads修改为 x[imageRead('')]
,然后就可以通过 StrucGet("")
调用任意函数。
以下函数会允许CFML Evaluation,当它们包含用户输入时允许远程代码执行 (RCE):
- StructGet(“…”)
- isDefined(“…”)
- Empty(“…”)
通过在 Masa/Mura CMS 的代码库中进行了快速搜索,尽管没有找到对 StructGet()
和 Empty()
的调用,但却发现了大量对 isDefined()
的调用。
如此多调用的原因是 isDefined(String var)
用于检查给定字符串是否被定义为变量,也就是说,isDefined("url.search")
并不意味着查询参数 search 的值会被传递到这里,因此需要调用 isDefined("#url.search#")
才可以。
在 grep 完 isDefined\(.#\)
后,安全人员遇到了一些调用,其中core/mura/client/api/feed/v1/apiUtility.cfc#L122 的 FEED API 中的调用以及 JSON API 中的调用,都可以被预认证触发。
function processRequest(){
try {
var responseObject=getpagecontext().getresponse();
var params={};
var result="";
getBean('utility').suppressDebugging();
structAppend(params,url);
structAppend(params,form);
structAppend(form,params);
...
if (isDefined('params.method') && isDefined('#params.method#')){
...
}
}
}
param
结构由 url
和 form
结构填充,它们分别存储了 GET 和 POST 参数,因此, param
结构包含用户输入,当 Mura/Masa CMS 部署在 Lucee 服务器上时,执行 isDefined("#param.method#")
会带来远程代码执行 (RCE) 的风险。
最终安全人员在 Apple 上成功执行了代码!
这些发现第一时间报告给 Apple 和 Lucee 团队, Apple 公司在 48 小时内便做出了修复。并向安全人员奖励了 20,000 美元的奖金,而 Lucee 也迅速实施了漏洞修复。
以下nuclei模板可用于识别 Lucee 实例是否易受到可能导致远程代码执行的 Cookie 解析问题的影响:
id: lucee-rce
info:
name: Lucee < 6.0.1.59 - Remote Code Execution
author: rootxharsh,iamnoooob,pdresearch
severity: critical
metadata:
max-request: 1
shodan-query: http.title:"Lucee"
verified: true
tags: lucee,rce,oast
http:
- raw:
- |
GET / HTTP/1.1
Host: {{Hostname}}
Cookie: CF_CLIENT_=render('<cfscript>writeoutput(ToBinary("{{base64('{{randstr}}')}}"))</cfscript>');
matchers:
- type: dsl
dsl:
- contains(body, "{{randstr}}")
- contains(header, "cfid")
- contains(header, "cftoken")
condition: and