Node.js文件写入漏洞提升为RCE的技术分析
漏洞概述
本文详细分析如何将Node.js应用程序中的文件写入漏洞提升为远程代码执行(RCE),即使在文件系统以只读方式挂载的加固环境中也能实现。这种技术通过利用暴露的管道文件描述符来绕过系统限制,最终获得代码执行能力。
文件写入漏洞的危害
任意文件写入漏洞是一种高危漏洞类型,攻击者通常可以将其转化为代码执行,完全控制应用服务器。常见的利用方式包括:
- 在网站根目录写入PHP、JSP、ASPX等可执行文件
- 覆盖服务端模板引擎处理的模板文件
- 写入配置文件(如uWSGI的.ini或Jetty的.xml文件)
- 添加Python的站点特定配置钩子
- 使用通用手法:写入SSH密钥、添加定时任务或覆盖.bashrc文件
加固环境中的漏洞利用挑战
在加固环境中,文件系统通常以只读方式挂载,用户权限受限。例如以下Node.js漏洞代码:
app.post('/upload', (req, res) => {
const { filename, content } = req.body;
fs.writeFile(filename, content, res.json({ message: 'File uploaded!' }));
});
虽然存在任意文件写入漏洞,但由于文件系统只读和权限限制,传统利用方法失效。
利用procfs和管道文件描述符
procfs简介
Linux的procfs虚拟文件系统挂载在/proc目录,提供对内核内部运作的访问,包括:
- 运行中进程信息
- 系统内存状态
- 硬件配置
- 进程打开的文件描述符
管道文件描述符
通过/proc/<pid>/fd/可以查看进程打开的文件描述符,包括:
- 传统文件
- 设备文件
- 套接字
- 管道(包括匿名管道)
匿名管道通常无法直接访问,但通过procfs可以引用:
echo hello > /proc/`pidof node`/fd/5
关键点:即使procfs以只读方式挂载,写入管道仍然可能,因为管道由内核的pipefs处理。
Node.js事件循环与管道
Node.js使用libuv库实现异步非阻塞事件循环,libuv使用匿名管道进行事件通信。这些管道通过procfs暴露,可以被攻击者利用。
libuv信号处理机制
libuv中的uv__signal_event处理器从管道读取uv__signal_msg_t类型的消息:
static void uv__signal_event(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
uv__signal_msg_t* msg;
// [...]
do {
r = read(loop->signal_pipefd[0], buf + bytes, sizeof(buf) - bytes);
for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) {
msg = (uv__signal_msg_t*) (buf + i);
uv__signal_msg_t结构包含:
typedef struct {
uv_signal_t* handle; // 指向uv_signal_s结构的指针
int signum; // 信号编号
} uv__signal_msg_t;
uv_signal_t结构包含关键的函数指针:
struct uv_signal_s {
UV_HANDLE_FIELDS
uv_signal_cb signal_cb; // 回调函数指针
int signum;
// [...]
};
当signum匹配时,回调函数会被调用:
handle = msg->handle;
if (msg->signum == handle->signum) {
handle->signal_cb(handle, handle->signum);
}
漏洞利用技术
利用步骤
- 向管道写入伪造的
uv_signal_s数据结构 - 将
signal_cb函数指针设置为目标地址 - 向管道写入伪造的
uv__signal_msg_t数据结构 - 设置
handle指针指向伪造的uv_signal_s - 确保两个数据结构的
signum值匹配
地址空间挑战
由于ASLR随机化栈、堆和库地址,攻击者面临指针引用问题。但Node.js二进制通常不启用PIE:
$ checksec /opt/node-v22.9.0-linux-x64/bin/node
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
这使得攻击者可以引用Node.js段中的固定地址数据。
数据结构伪造技术
攻击者需要:
- 在Node.js二进制中搜索可用的数据结构片段
- 这些片段应包含:
- 可执行命令字符串(如"touch /tmp/pwned")
- 指向有用指令序列的指针
搜索算法示例:
for addr, len in nodejs_segments:
for offset in range(len - 7):
ptr = read_mem(addr + offset, 8)
if is_mapped(ptr) and is_executable(ptr):
instr = read_mem(ptr, n)
if is_useful_gadget(instr):
print('gadget at %08x' % addr + offset)
UTF-8编码限制与绕过
Node.js的fs.writeFile通常限制为有效UTF-8数据,这带来两个挑战:
- 伪造数据结构的基地址必须是有效UTF-8
signum值可能包含非UTF-8字节
解决方案:
- 对于
signum中的非UTF-8字节(如0xf0),添加3个继续字节构造有效UTF-8序列 - 利用补齐字节空间添加这些继续字节
完整利用链
- 识别目标Node.js进程的管道文件描述符
- 在Node.js二进制中搜索合适的ROP gadget和数据结构
- 构造伪造的
uv_signal_s和uv__signal_msg_t结构- 确保
signum匹配 - 处理UTF-8编码限制
- 确保
- 通过文件写入漏洞将构造的数据写入管道
- 触发libuv事件处理器执行恶意回调
防御建议
-
代码层面:
- 严格验证用户输入的文件路径和内容
- 避免使用用户可控参数直接进行文件操作
-
系统加固:
- 启用PIE(位置无关可执行文件)
- 限制procfs访问权限
- 使用命名空间和容器隔离
-
运行时保护:
- 启用栈保护(Stack Canary)
- 使用内存保护机制(如ASLR)
-
监控与审计:
- 监控异常的文件描述符访问
- 定期审计依赖库(如libuv)的使用
结论
这个案例展示了基础设施加固不能完全替代代码安全。攻击者可以通过创新的技术绕过看似安全的限制,强调了对源代码进行彻底安全审计的重要性。Unix的"一切皆文件"哲学在带来便利的同时,也创造了不常见的攻击面,安全设计需要全面考虑这些因素。