npm使用和vm沙箱
nmp使用
这里去看官网的详细指令还有民间大佬的精髓总结
NPM (Node Package Manager) 不仅仅是 Node.js 的包管理工具,用于安装、共享和管理项目依赖。CTF中,它更是我们进行依赖环境配置、历史漏洞复现以及供应链安全审计的必备武器。
1. 基础环境与配置
NPM 会随着 Node.js 自动安装。
环境自检: *
node -v检查 Node.js 版本。npm -v检查 NPM 版本。npm doctor检查 NPM 环境的健康状态。
网络加速(国内环境):
由于网络问题,可以配置镜像。
安装并使用淘宝镜像:
npm install -g cnpm --registry=https://registry.npmmirror.com。随后可用cnpm install <package-name>替代 npm 安装。或者通过命令设置代理:
npm config set proxy http://proxy-server:port。
操作配置:
npm config可用于管理 npm 的配置文件。npm get和npm set用于获取或设置配置中的具体值。
2. 项目初始化与依赖管理
开发或打靶机的第一步通常是阅读或生成配置文件(package.json)。
初始化项目
npm init创建一个package.json文件。npm init -y快速生成该文件,跳过所有交互问答。
核心安装命令 (npm install)
常规本地安装:
npm install <package-name>或简写npm i <package-name>。依赖默认保存到dependencies中;若使用--save-dev则添加到devDependencies(开发环境依赖)。全局安装:
npm install -g <package-name>。场景提示: 全局安装通常用于命令行工具。例如在前端开发中常用的脚手架
create-react-app,或者代码监听工具nodemon。指定版本安装(CTF 核心技能):
npm install <package>@<version>。场景提示: 在 CTF 中复现老漏洞时必备。例如题目使用了存在沙箱逃逸漏洞的旧版 lodash,可以指定安装:
npm install lodash@4.17.21。按配置全量安装: 直接输入
npm install会安装package.json中定义的所有依赖。如果需要一个完全干净的、严格匹配锁文件的安装环境(常用于 CI/CD),可以使用npm ci。
依赖维护清理
卸载包:
npm uninstall <package-name>用于本地卸载;npm uninstall -g <package-name>用于全局卸载。更新包:
npm update <package-name>。清理无用依赖:
npm prune可以移除多余的(不在 package.json 中)包。npm dedupe可用于减少依赖树中的重复项。缓存清理: 如果遇到莫名其妙的安装报错,可通过
npm cache clean --force清理缓存(npm cache命令专门用于操作包缓存)。
3. 信息收集与安全审计 (Security & Audit)
安全漏洞扫描:
npm audit用于运行安全审计并检查已知漏洞。npm audit fix尝试自动修复这些漏洞。生成软件物料清单:
npm sbom可以生成项目的 SBOM (Software Bill of Materials)。场景提示 在企业级 Web 安全中,SBOM 是防范供应链攻击(如投毒包)的基础。
查看依赖树: *
npm ls或npm list列出本地已安装的包。npm list -g --depth=0仅查看全局安装的顶层包。npm find-dupes可以在包树中查找重复项。
版本监控:
npm outdated检查项目中是否有过时的包。在线文档与源码: *
npm view查看注册表中的包信息。npm docs在浏览器中打开包的文档。npm repo在浏览器中打开包的代码仓库(如 GitHub)。
4. 脚本执行与工程化
在实战中,我们经常需要快速启动服务或执行测试。
定义脚本: 在
package.json的scripts字段定义,如"start": "node app.js"。运行脚本: *
npm run可以运行在scripts中定义的任意脚本。- 内置快捷命令:
npm start启动包,npm test测试包。
- 内置快捷命令:
免安装执行(神器
npx/npm exec): 运行本地或远程 npm 包中的命令。场景提示: 在 GitHub 上看到一个用于探测 Web 漏洞的 Node.js 脚本,不想污染本地全局环境,可以直接
npx <package-name>临时下载并执行一次。
5. 个人开源与包发布
了解开发者是如何发布包的
账号管理: 去 npm 官网注册账号后,使用
npm login本地登录(npm adduser也可添加注册表账户,npm logout登出)。版本控制: 语义化版本规则为 MAJOR.MINOR.PATCH(如
1.2.3)。可通过npm version patch自动升级补丁版本(如 1.0.0 → 1.0.1)。发布与撤回:
npm publish发布包到公开注册表。npm deprecate废弃包的某个版本(提示用户不再维护)。npm unpublish从注册表中彻底移除包(有严格的时间限制)。
6. 避坑指南
权限错误: 如果在全局安装时报错,Mac/Linux 用户需要加
sudo,Windows 用户需以管理员身份运行 CMD/PowerShell。版本控制符号识别:
package.json中的^1.0.0表示安装 1.x.x 最新版;~1.0.0表示安装 1.0.x 最新版。注意识别这些符号,防止复现环境版本出现偏差。
vm
0x00 前置知识:Node.js 作用域与沙箱的本质
和之前做的python环境沙箱逃逸原理一模一样 但其实vm本来就不是一个安全机制 只是可以用来做一下效果不好的“隔离”。
沙箱机制:沙箱的工作机制主要是依靠重定向,将恶意代码的执行目标重定向到沙箱内部,隔离有害程序。
Node.js 作用域(Context):在 Node.js 中,每一个文件(包)都有一个自己的上下文,包之间的作用域是互相隔离不互通的。所有包都共享一个最高级别的全局对象
global(挂载了process、console等)。为什么不用
eval?eval()容易导致当前作用域的变量污染;而new Function虽然能隔离,但外部变量传参非常麻烦。因此 Node.js 提供了vm模块来开辟独立上下文。
0x01 原生 vm 模块基础 API
vm.runInThisContext(code):在当前 global 下创建作用域,能访问 global,不能访问当前包的局部变量。vm.createContext([sandbox]):将一个普通对象“上下文化”,使其成为后续代码运行的全局对象。vm.runInNewContext(code[, sandbox]):直接传入代码和沙箱对象,编译并在新上下文中运行。
0x02 基础逃逸:利用 this 与原型链
1. this 到底指向谁?
在 vm.runInNewContext(code, context) 中,沙箱内部的 this 默认指向传入的 context(上下文对象)。
JavaScript
1 | const vm = require('vm'); |
2. 经典的 Constructor 逃逸
如果宿主环境传入了一个普通的对象(哪怕是空对象 {}),这个对象也是在宿主环境中创建的。它的底层构造函数(Constructor)依然属于宿主。
我们可以通过 this 顺藤摸瓜:
this-> 宿主传进来的context对象。this.constructor-> 宿主环境的Object。this.constructor.constructor-> 宿主环境的Function。
既然拿到了宿主环境的 Function,我们就可以动态执行代码,并返回宿主的 process 对象:
JavaScript
1 | // 逃逸 Payload |
(注:有时也可以用 this.toString.constructor 达到同样的效果,因为 toString 也是宿主环境的方法。)
避坑提示: 如果传入沙箱的
context是基本类型(如数字、字符串),它们传递的是值而不是引用,沙箱内无法利用。
0x03 危险的上下文(Context)
有时开发者缺乏安全意识,会直接将敏感对象传入沙箱。这种情况下,甚至不需要原型链,直接调用即可。
JavaScript
1 | const vm = require('vm'); |
0x04 进阶防御与挑战:Object.create(null)
为了防御上述的 this.constructor 攻击,聪明的开发者开始使用 Object.create(null) 来创建上下文。
JavaScript
1 | const vm = require('vm'); |
为什么失效了? 因为 Object.create(null) 创建的对象没有原型(__proto__ 为 undefined)。沙箱内的 this 无法调用 .constructor,第一条路被彻底堵死。
0x05 无上下文逃逸:神技 arguments.callee.caller
面对 Object.create(null),原型链断了怎么办?我们可以利用 JavaScript 的调用栈特性。
核心利器:
arguments.callee.caller> 它可以返回当前函数的调用者。如果我们在沙箱里设下埋伏,诱导沙箱外的“宿主”来调用沙箱里的函数,那么caller就会指向宿主环境中的对象!
技巧 A:重写 __toString 诱导宿主
思路:在沙箱中返回一个对象,重写它的 toString 方法。当宿主环境尝试将这个返回值与字符串拼接(例如 console.log('Hello ' + res))时,会隐式触发 toString。 此时,是宿主环境(外)触发了 toString(内),caller 成功捕获宿主。
JavaScript
1 | const vm = require('vm'); |
技巧 B:使用 Proxy 拦截任意属性
思路:返回一个 Proxy(代理)对象。只要宿主环境尝试读取返回结果的任何属性(如 res.abc),就会触发 get 钩子,同样可以捕获 caller。
JavaScript
1 | const vm = require("vm"); |
技巧 C:终极形态——主动抛出异常(Error 逃逸)
思路:以上两种方法都需要宿主配合(拼接字符串或读取属性)。如果宿主仅仅是捕获了异常并打印呢? 我们可以在沙箱中直接 throw(抛出)一个带有 Proxy 的错误对象。当宿主环境 catch(e) 并尝试将错误信息 e 打印出来时(字符串拼接),就会触发逃逸。
JavaScript
1 | const vm = require("vm"); |
然后自己搞了个环境 用最基础的payload跑通
1 | const vm = require('vm'); |
payload如下
1 | // a. 获取沙箱的全局对象 (this 指向 sandbox) |
也是执行完这里就系统调用出了计算机 当然什么命令都是可以执行的
vm2
既然原生的 Context 隔离像筛子一样漏风,vm2 的作者决定在这个“玻璃房”外面,用 JavaScript 的高级特性生生浇筑出一堵“铁墙”。
对比 vm1,vm2 做了以下三次史诗级的升级:
升级一:ES6 Proxy 代理之墙
原生 vm 允许你直接触摸宿主传进来的对象。而 vm2 引入了 ES6 的 Proxy 对象。 在 vm2 中,任何跨越沙箱边界的对象,都会被强制套上一层 Proxy 壳子。
当你试图在沙箱里访问
this.constructor时:原生
vm:直接返回宿主的Function构造器 -> RCE 成功。vm2:触发 Proxy 的get拦截器。vm2的源码会检查:“你想访问constructor?危险!”,然后返回一个沙箱内部的安全Function,或者直接抛出异常 -> 逃逸失败。
升级二:Contextify 与 Decontextify
vm2 在源码中实现了两个极其复杂的对象转换函数,相当于沙箱与宿主之间的“海关”。
Contextify(沙箱化): 当宿主的对象/函数进入沙箱时,必须经过 Contextify。它会把宿主对象的敏感属性全部过滤,并包上 Proxy,变成一个“安全”的伪造对象。
Decontextify(去沙箱化): 当沙箱的对象(比如函数的返回值)要输出给宿主时,必须经过 Decontextify。防止沙箱在返回值里埋地雷(比如我们在 0x04 讲过的,返回一个重写了
toString的恶意对象来反向捕获宿主)。
升级三:重写危险的内置 API (sandbox.js)
在原生 vm 中,如果你不小心把 global 传进去,沙箱就能直接用 setTimeout、Buffer 等内置模块搞事情。 vm2 为了防患于未然,在源码的 sandbox.js 中,直接**重写并 mock(模拟)**了大量的 Node.js 内置对象:
它不给你真实的
Buffer,而是给你一个被阉割过的安全Buffer。它劫持了
setTimeout和setInterval,防止你利用定时器逃逸。彻底屏蔽
process和require
还是有洞 下面复现危害很大的几个CVE
1. CVE-2019-10761:极限爆栈(Call Stack Overflow)逃逸
复现环境: Node.js v12.x 或 v14.x
安装命令:
npm install vm2@3.6.10
这个漏洞的精髓在于**“利用 V8 引擎底层的物理限制”。 我们无法通过正常的 Proxy 访问外部对象,但我们可以逼迫**外部函数在执行时抛出错误。当调用栈(Call Stack)达到 V8 的最大深度(通常是 10000 左右)时,如果恰好轮到宿主的函数(如 Buffer.prototype.write)入栈,引擎会直接抛出属于宿主环境的 RangeError: Maximum call stack size exceeded 对象。我们在沙箱里 catch 住它,就拿到了宿主的 constructor。
1 | "use strict"; |
2. CVE-2021-23449:动态 import() 语法层逃脱
复现环境: Node.js v14.x
安装命令:
npm install vm2@3.9.3
vm2 通过重写 require 和 Proxy 拦截了绝大部分模块导入。但是,作者忽略了 ES6 新增的动态 import()。import() 在 JS 规范中是一个底层的语法关键字,而不是普通的内置函数。vm2 当时没有对 import() 返回的 Promise 对象套 Proxy。沙箱代码一执行 import(),返回的直接就是裸露的宿主 Promise 对象,降维打击瞬间完成。
1 | const { VM } = require('vm2'); |
3. CVE-2023-29017:Error.prepareStackTrace 底层泄露
复现环境: Node.js v18.x (强烈建议 v18.16.0 左右)
安装命令:
npm install vm2@3.9.14
这是 2023 年把 vm2 逼上绝路的关键 CVE。当处理未捕获的异步异常时,V8 会在底层调用 Error.prepareStackTrace 来格式化堆栈。在这个极其偏僻的底层回调中,V8 塞入了一个 frames(调用栈帧)数组。vm2 给几乎所有东西都加了 Proxy,唯独漏了这个由 V8 引擎直接抛给回调的 frames 数组。攻击者只需劫持这个裸露的数组,就能顺着 constructor 爬出去。
1 | const { VM } = require('vm2'); |
4. CVE-2026-22709 —— globalPromise 劫持
复现环境: Node.js v20.x 或以上
安装命令:
npm install vm2@3.10.0(漏洞影响 3.10.1 及更早版本,在 3.10.2 中被修复)
这个是最近的漏洞 影响还是挺大的 该漏洞的根本原因在于:JavaScript 中的 async 函数返回的是底层的 globalPromise 对象,而不是 vm2 内部经过安全净化的 localPromise。 vm2 在 lib/setup-sandbox.js 中严密过滤了 localPromise.prototype.then 的回调函数,却忘了净化 globalPromise.prototype.then 和 catch。
如何利用?既然 catch 回调没被过滤,攻击者可以重写 Function.prototype.call,直接拦截全局 Promise 调用 catch 时的底层执行过程 (globalPromiseCatch.call())。在拦截器中,攻击者绕过了 ensureThis() 检查,直接捕获到保留着宿主 Error 引用的未净化异常对象!
1 | const { VM } = require('vm2'); |
不知道为什么啊 按理说应该可以在比较新的node版本里面复现的 但是这里不行 后面再研究一下
参考 https://wlaq.njupt.edu.cn/2026/0208/c14800a297032/page.htm
https://www.cnblogs.com/zpchcbd/p/16899212.html
https://xz.aliyun.com/news/11305




