Nodejs原型链污染
对象
在 JavaScript 中,几乎所有的东西都是对象。对象就像是一个收纳盒,可以存放各种东西(属性)
1 2 3 4 5 6 7
| const person = { name: "Alice", age: 25 };
console.log(person.name);
|
原型
每个对象都有一个原型(prototype),可以理解为备用收纳盒
当你在当前对象中找不到某个属性时,JavaScript 会自动去它的原型(备用盒)里找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const obj = {};
console.log(obj.toString);
obj 对象 ↓ (__proto__) Object.prototype(Object.prototype 是 JavaScript 中所有对象的原型链顶端(除了 null)。它是一个特殊的对象,包含了所有对象共享的基础方法) ├── toString() ├── hasOwnProperty() ├── valueOf() └── ... 其他方法 ↓ (__proto__) null (原型链终点)
|
原型链
原型链:对象的__proto__是原型,而原型也是一个对象,也有__proto__属性,原型的__proto_又是原型的原型,就这样可以一直通过__proto__向上找,这便是原型链,当向上找找到 object 的原型的时候,这条原型链便算结束了
1
| obj → 原型对象 → 原型的原型 → ... → null
|
之所以能形成原型链污染最基本的机制就是当在一个 son 对象中寻找一个属性,而子对象没有这个属性,那么就会去 son.__proto__ 中寻找这个属性,如果仍然找不到,则继续在 son.__proto__.__proto__ 中寻找,依次查找直到找到 null 结束。那么当我们需要某个对象的属性为 true 时,如果它没有这个属性,他就会去__proto__上找,这时我可以通过直接污染 object 对象的这个属性值为 true,使得我目标对象该属性为 true。
示例:
1 2 3 4 5 6 7 8 9 10 11 12
| const foo = { bar: 1 }; foo.__proto__.bar = 2 console.log(foo.bar); const zoo = {}; console.log(zoo.bar);
|

可以通过以下方式访问得到某一实例对象的原型对象,
1 2 3
| objectname["__proto__"] objectname.__proto__ objectname.constructor.prototype
|

剩下的 pp2rce 和 ejs 模板命令执行也是差不多的原理,主要是取决于污染什么属性。
原型链污染:
1 2
| 在JavaScript中,每个对象都有一个原型,它是一个指向另一个对象的引用。当我们访问一个对象的属性时,如果该对象没有这个属性,JavaScript引擎会在它的原型对象中查找这个属性。这个过程会一直持续,直到找到该属性或者到达原型链的末尾。 攻击者可以利用这个特性,通过修改一个对象的原型链,来污染程序的行为。例如,攻击者可以在一个对象的原型链上设置一个恶意的属性或方法,当程序在后续的执行中访问该属性或方法时,就会执行攻击者的恶意代码
|
简单的说呢,其实就是我们对原链中的某个属性进行了污染,向其中插入恶意代码,当我们再调用这个链(也就是使用这个对象)时,我们的恶意代码就会被触发,此时就达到了一个执行恶意代码的效果
__proto__和prototype
在JavaScript中,每个对象都有一个名为__proto__的内置属性,它指向该对象的原型。同时,每个函数也都有一个名为 prototype 的属性,它是一个对象,包含构造函数的原型对象应该具有的属性和方法。简单来说,__proto__ 属性是指向该对象的原型,而 prototype属性是用于创建该对象的构造函数的原型
| 概念 |
谁有 |
是什么 |
__proto__ |
所有对象都有 |
指向原型(自己的备用柜子) |
prototype |
只有函数有 |
用来给新对象设置原型 |
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Person(name) { this.name = name; }
Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name}`); };
const person1 = new Person('Alice'); person1.greet(); -------------------------------------------------------------------------------------------------------------------------------- Person.prototype.greet 给 Person.prototype 添加 greet 方法,所有 Person 的实例都会共享这个方法(通过原型链) person.greet() Peron本身没有greet方法,但是通过原型链找到greet方法,并进行调用
|
总结:
1、prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
2、一个对象的__proto__属性,指向这个对象所在的类的prototype属性
具体过程:
示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var a = {number : 111} var b = {number : 222} b.__proto__.number=333 var c= {} c.number
这里c没有number的值,于是在原型(objectprototype)中找到设定的number=333,于是c.nymber就为333 -------------------------------------------------------------------------------------------------------------------------------- 为什么执行过b.__proto__.number=520 后,我们输出b的值,其值仍为1314
这是因为在JavaScript中存在这样一种继承机制: 我们这里调用b.number时,它的具体调用过程是如下所示的
1、在b对象中寻找number属性 2、当在b对象中没有找到时,它会在b.__proto__中寻找number属性 3、如果仍未找到,此时会去b.__proto__.__proto__中寻找number属性
也就是说,它从自身开始寻找,然后一层一层向上递归寻找,直到找到或是递归到null为止,此机制被称为JavaScript继承链,我们这里的污染的属性是在b.__proto__中,而我们的b对象本身就有number,所以其值并未改变
|
示例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function merge(target, source) { for (let key in source) { if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } } let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b)
o3 = {} console.log(o3.b)
|
执行过程:
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
| let o1 = {} let o2 = { "a": 1, "__proto__": {"b": 2} }
merge(o1, o2)
o1.a = 1
Object.prototype.b = 2
console.log(o1.a, o1.b)
console.log(o3.b)
|
拓展(js大小写特性)
toUpperCase()和toLowerCase
这两个是 JavaScript 中用于改变字母大小写的方法,非常简单直观
toUpperCase() - 转大写
将字符串中的所有字母变成大写形式。
1 2 3 4
| "hello".toUpperCase() "Hello World".toUpperCase() "abc123".toUpperCase()
|
toLowerCase() - 转小写
将字符串中的所有字母变成小写形式。
1 2 3 4
| "HELLO".toLowerCase() "Hello World".toLowerCase() "ABC123".toLowerCase()
|
对于toUpperCase()函数
1
| 字符"ı"(拉丁小写点无点i)、"ſ(拉丁长s)" 经过toUpperCase处理后结果为 "I"、"S"
|
对于toLowerCase
1
| 字符"K(开尔文符号)"经过toLowerCase处理后结果为"k"(这个K不是K)
|
[LitCTF 2025]多重宇宙日记
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 65 66
| <script> 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 = {}; 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 }) }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); setTimeout(() => window.location.reload(), 1000); } else { statusEl.textContent = '错误: ' + result.message; } } catch (error) { statusEl.textContent = '请求失败: ' + error.toString(); } });
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); const response = await fetch('/api/profile/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(parsedJson) }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); setTimeout(() => window.location.reload(), 1000); } else { statusEl.textContent = '错误: ' + result.message; } } catch (error) { statusEl.textContent = '请求失败或JSON无效: ' + error.toString(); } } </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const formData = new FormData(event.target); const settingsPayload = {}; // ... 提取 theme 和 language ...
// 发送请求时的关键操作: body: JSON.stringify({ settings: settingsPayload }) // 包装在 "settings"键下,推测后端接收的是settings字段,并且提交的是原始的JSON,并没有任何过滤和限制
普通用户对象在内存中缺失了 isAdmin: false 这个明确的属性,导致后端在访问isAdmin这个属性时找不到,于是从被污染的原型里找到isAdmin:true,绕过验证
利用: 利用settings将原型Object.prototype修改或者添加为 isAdmin : true ------------------------------------------------------------------------------------------------------------
提交JSON: { "settings": { "__proto__": { "isAdmin": true } } }
|