JS原型链污染

nodejs和javascript语法和标准有些不同,应用场景不一样,但是在原型链污染这方面可以互通。

JS创建对象的三种方法

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
js创建对象的三种方法 :

普通创建

var person={name:'lihuaiqiu','age','19'}

var person={} //创建空对象

构造函数方法创建

function person(){
this.name="liahuqiu";
this.test=function () {
return 1;
}
}
person.prototype.a=3;
web=new person();
console.log(web.test());
console.log(web.a)

通过object创建

var a=new Object();
a.c=3
console.log(a.c)

原型链

原型

javascript里的每一个对象(object),都有一些内置属性(property),原型本身也是一个对象,所以他也有自己的原型,这就形成了一个原型链,当一个原型的原型是null,原型链就会在这里结束。

这个”原型“属性是一个指向对象原型的一个对象,这个属性没有统一的称呼,但是浏览器都支持使用 ”__proto__“来查看原型。访问一个对象原型的标准方法是使用 ”Object.getPrototypeOf()“。

Prototype 原型 | 原型对象

1、Prototype它是【函数的】一个属性

2、Protopype是个对象

3、当我们创建函数的时候会默认添加Prototype这个属性

__proto__ 隐式原型

1、【对象】的属性

2、指向构造函数的Protopype

3、obj._proto_ === test.prototype 等于true

原型链顶层

Object.prototype.__proto__ 等于null

原型链查找

instanceof运算符可以用来判断某个构造函数的prototype属性是否存在另外一个要检测对象的原型链

1
2
3
4
5
6
function My(){}
function You(){}

var myOne=new My();
console.info(myOne instanceof My) //true
console.info(myOne instanceof You) //false

JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test (name) {
this.name=name
this.a=1
} //这只是一个普通函数

const obj = new test('xiaoming') //使用new生成obj对象了,test此时就是构造函数,任何函数都可以是构造函数
console.log(obj.a) //返回 1
test.prototype.b=2 //给函数原型添加b属性,值为2
console.log(obj.b) //返回2
Object.prototype.c=3 //给顶层添加c属性,值为3
console.log(obj.c) //返回3
console.log(obj.__proto__ === test.prototype) //对象的__proto__等于函数的原型
console.log(test.prototype.__proto__ === Object.prototypre) //函数原型对象的__proto__属性就等于Object函数的原型 返回ture
console.log(Object.prototype.__proto__) //Object函数的原型对象的__proto__属性等于NULL,因为Object是顶层
结构剖析
1
2
3
4
5
6
7
8
9
10
obj {
a:1, //a属性,值为1
__proto__:test.prototype ={ //obj.__proto__指向test.prototype
b:2, //函数原型添加的b属性,值为2
__proto__:Object.prototype = { //test.prototype.__proto__指向Object.prototypre
c:3, //顶层添加的c属性,值为3
__proto__:null //Object.prototype.__proto__指向null
}
}
}
链子剖析
1
obj -> __proto__ -> test.prototype -> __proto__ -> Object.prototype -> __proto__ -> null

这就是obj的原型链

原型链污染

因为原型链就是套娃的,一级接着一级,属性是从上面往下叠加的,下面的都会继承上面的属性,如果我们在原型链的上级添加了恶意对象,那么原型链就会被污染,后续创建的对象都可以继承并调用恶意对象。

如果服务端使用某些可以复制、继承变量的函数,函数的参数是用户可控,那么就有可能造成原型链污染攻击。

此时b的上级原型a的属性已经被污染,b继承a后,也可以调用恶意对象。

原型链污染利用

在javascript中,function是一个用于定义函数的关键字,Function是代表所有函数的内置原型对象。

constructor概念

Object.prototype.constructor其中constructor是一个对象的数据属性,创建对象后,访问constructor属性,可以返回构造该对象的来源【就是告诉你它是从哪来的】(不是该对象的原型链上级,两者不同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
demo:
var a=1;
a.__proto__
//Number {0, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, toString: ƒ, …}
a.__proto__.constructor
//Number() { [native code] }
a.constructor
//Number() { [native code] }
a.constructor === Number
//true
var b=Number(1);
b.constructor === a.constructor
//true

constructorprototype 的角色
constructor 的作用
  • 每个 JavaScript 对象都有一个 constructor 属性,默认情况下,它指向创建该对象的构造函数。

  • 例如:

    1
    2
    const obj = {};
    console.log(obj.constructor === Object); // true
prototype 的作用
  • 构造函数的 prototype 属性是实例对象的默认原型(即实例的 __proto__)。

  • 例如:

    1
    2
    3
    function MyClass() {}
    const obj = new MyClass();
    console.log(obj.__proto__ === MyClass.prototype); // true
两者的关系
  • 修改构造函数的 prototype 会影响由该构造函数创建的所有对象的原型。
  • 通过 constructor.prototype 可以间接修改对象的原型链,甚至是全局对象的原型。

所以,当我们的__proto__被过滤了,我们可以直接用constructor.prototype绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function merge(target, source) {
for (let key in source) {
if (key == "__proto__") {
continue;
}
if (key in target && key in source) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

let payload = JSON.parse('{"constructor": {"prototype": {"polluted": "Yes, I am polluted!"}}}');

let user={}
merge(user, payload);

// 验证
console.log({}.polluted); // "Yes, I am polluted!"
Function概念

每一个javascript的function实际上都是Function对象,Function是javascript内置的对象,Function用以实现很多基本的功能,如Number、toString等。

1
2
3
4
(function () {}).constructor
//Function() { [native code] }
(function () {}).constructor === Function
//true
Function() constructor

Function()构造器可以创建一个Function对象,可以直接调用Function()构造器动态的创建函数。但是会存在像eval()的安全隐患和一些性能问题。

eval()和Function区别:

1、eval()可以访问本地的变量、全局变量

2、Funtion()创建函数时只能执行全局变量

image-20241116203304251

image-20241116204011412

JSON.parse

看一个经典的递归合并函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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={a: 1,"__proto__": {b:2}}
merge(o1,o2)
console.log(o1.b)

o3={}
console.log(o3.b)
[Running] node "payload.js"
2
undefined
[Done] exited with code=0 in 0.199 seconds

核心思想是理解 __proto__ 在对象定义中的行为,以及它在合并操作中如何影响目标对象。以下是逐步解释:

1. __proto__ 的行为

在 JavaScript 中,__proto__ 是一个特殊属性,用于设置对象的原型。它不是普通的键,而是与对象的原型链相关。

__proto__ 被用作键:
1
let o2 = {a: 1, "__proto__": {b: 2}};
  • 在这种情况下,__proto__ 表面上看是一个普通的键值对。
  • 实际行为__proto__ 这个键会被特殊处理,将 {b: 2} 作为 o2 的原型,而不是普通的对象属性。

相当于:

1
2
o2.a = 1;
o2.__proto__ = {b: 2}; // o2 继承了 {b: 2} 作为它的原型。

结果:

  • o2.a 是 o2 自己的属性。
  • o2.b 不直接存在于 o2,而是来自于它的原型 {b: 2}

如果直接访问 o2.b,JavaScript 会通过原型链查找,因此 o2.b 的值是 2

2. 合并函数的行为

我们来看 merge 函数:

1
2
3
4
5
6
7
8
9
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];
}
}
}
  • for...in 循环
    遍历 source 的所有可枚举属性,包括继承的属性(从原型链上来的)。
  • key in source 判断: 确保 keysource 中的一个属性(包括自身和原型链)。

3. 为什么只有 a 被合并?

当你执行:

1
2
3
4
let o1 = {};
let o2 = {a: 1, "__proto__": {b: 2}};
merge(o1, o2);
console.log(o1.b); // undefined

关键点:

  • for...in 不会直接枚举 __proto__ 作为普通键。
  • o2 只有一个直接属性 a,因此 merge 函数中只会处理 a
  • __proto__ 被用作原型设置,{b: 2} 成为了 o2 的原型。

因此:

  • o1 合并结果o1.a = 1
  • o1.b 是 undefined:因为 bo2 的原型属性,而 merge 函数不会递归或复制原型链的属性。

4. 为什么 o1.b = 2 这句话不成立?

合并后,o1 并没有 b,因为:

  • b 并不是 o2 自身的属性,而是其原型的属性。
  • merge 函数只处理直接属性,未复制 bo1

总结: __proto__ 在对象字面量中会被解释为设置原型,而不是普通键值对。所以,merge 只会处理 o2 的直接属性(a),不会处理通过原型链继承的 b,因此 o1.b 仍然是 undefined

5.为什么运行结果确是2

运行结果来看,console.log(o1.b) 返回了 2,这表明在某种程度上确实读取到了属性 b,而这是从 __proto__ 原型链继承的属性。我们重新分析以下几个关键点,帮助理解为什么会发生这种现象:

5.1. __proto__ 在对象字面量中的作用

当你定义:

1
let o2 = { a: 1, "__proto__": { b: 2 } };

o2 的结构变成:

1
2
3
4
o2 = {
a: 1,
__proto__: { b: 2 }
};

这意味着 o2 的原型被设置为 { b: 2 },即 o2 通过原型链可以访问 b

5.2. merge 函数如何工作

1
2
3
4
5
6
7
8
9
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];
}
}
}
  • for...in 的行为for...in 会迭代 source可枚举属性,包括继承自原型的属性。

在这个情况下:

  • o2 的直接属性是 a
  • o2 通过原型链继承了属性 b,且 b 是可枚举的。

5.3. 执行过程

merge(o1, o2) 执行时:

  • key = "a":将 o2.a 的值 1 合并到 o1
  • key = "b":由于 b 是从 __proto__ 继承的,它也被 for...in 迭代到,因此 o2.b 的值 2 被赋值到 o1.b

最终,o1 的内容是:

1
2
3
4
o1 = {
a: 1,
b: 2
};

5.4. 为什么 console.log(o1.b) 输出 2

merge 函数运行完后,o1 对象被赋予了一个新属性 b,其值为 2。因此,当你执行:

1
console.log(o1.b); // 输出 2

这正是由于 b 被直接合并到 o1 的原因。

6. 这是否算原型污染?

不算真正的原型污染,原因如下:

  • 污染的定义:原型污染意味着修改了原型对象(如 Object.prototype),导致所有对象都受到影响。
  • 你的操作仅仅是在 o1 上设置了一个普通属性 b,并没有修改全局原型链。因此,其他对象(如 {})并不会受到影响。

7. 验证是否污染了全局原型

你可以验证:

1
console.log({}.b); // 如果是原型污染,会输出 2,但是输出的是undefined

若结果为 undefined,说明没有污染全局原型。

结论

你当前的 merge 操作仅仅是将 b 合并到了 o1 对象上,没有导致原型污染。 要实现原型污染,需要明确操作全局原型链(例如通过修改 Object.prototype

好,那么我们言归正传,如何才能真正污染呢?

主角登场–请看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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}}')
//注意let o2 = {"a": 1, "proto": {"b": 2}}这种写法和let o2 = {a: 1, "__proto__": {b: 2}}写法结果是一样的
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

[Running] node "d:\web\污染\PCB-notadmin\payload.js"
1 2
2

[Done] exited with code=0 in 2.918 seconds

1. JSON.parse__proto__ 的作用

JSON.parse 中,{"__proto__": {"b": 2}} 会被解析为:

1
2
3
4
{
a: 1,
__proto__: { b: 2 }
}

此时,o2__proto__ 实际上是一个普通属性,而不是原型链。

但是,当 merge 函数执行以下语句时:

1
target[key] = source[key];

那么如果 key__proto__,它会触发 原型链修改,这样就相当于给最顶层的Object.prototype所指向的对象添加了属性,所以我们随便创建一个对象也就有了b这个属性 。

2. 原型污染的发生

merge(o1, o2) 中:

  • key = "a"o1.a = 1,无问题。
  • key = "__proto__"o1["__proto__"] = { b: 2 }

当设置 o1["__proto__"] = { b: 2 } 时,实际上修改了 o1 的原型链,使 o1 和所有继承自 Object.prototype 的对象(包括 o3)的原型被污染。

因此:

  • o1.b 等于 2,因为 b 来自于被污染的原型。
  • o3.b 也等于 2,即使 o3 本身没有 b,它仍然从全局的 Object.prototype 继承了污染。

3. 验证污染

可以通过以下方式验证污染:

1
console.log({}.b); // 输出 2,说明 Object.prototype 被污染

这就是JS原型链污染基础了。

js原型链污染(超详细)

服务端原型链污染漏洞