初识JavaScript原型链污染

0xGame碰到了原型链污染的题,记录一下学习过程


因为第一次接触,所以先了解了Javascript这东西的特性,先看一段简单的构造类的方法:

1
2
3
4
5
6
7
8
9
10
11
function Person(){ //名为person的构造函数,构造函数用于创建拥有特定属性/方法的对象
this.name="rendongjun";//name属性
this.test=function () { //test方法,返回 333
return 333;

}
}
Person.prototype.a=3;//通过原型继承的方式,给Person构造函数的原型对象添加了一个名为a的属性,其值为3
web=new Person();//创建名为web的对象,该对象是通过person构造函数创建的一个实例,构造函数使用new调用
console.log(web.test());//使用console函数进行打印
console.log(web.a)
什么是原型继承?

每个JavaScript对象都有一个原型对象,它是一个普通的对象,包含了一些共享的属性和方法。当我们创建一个新对象时,它会自动继承原型对象的属性和方法。

在JavaScript中,我们可以通过修改原型对象来实现属性和方法的继承。

Person.prototype.a = 3这段代码就是在Person构造函数的原型对象上添加了一个名为a的属性,其值为3。这意味着通过Person构造函数创建的所有对象实例都会继承这个a属性,并且可以通过对象实例访问和使用它。

换句话说,当我们创建一个person对象实例,比如web = new person(),这个实例会继承person构造函数的原型对象上的属性和方法。因此,web对象可以访问和使用a属性,即web.a会返回3。

__proto__属性指向它的构造函数的prototype属性。

什么是原型链?

原型链是JavaScript中一种对象之间的关系模型,它用于实现对象的继承和属性查找。

在JavaScript中,每个对象都有一个原型对象(prototype),原型对象也是一个对象,它包含了一些共享的属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(即Object.prototype)。

原型链的关系可以通过对象的__proto__属性来表示。每个对象都有一个__proto__属性,它指向该对象的原型对象。原型对象也有自己的原型对象,这样就形成了一个链式结构,即原型链。

pol1

constructor

每个实例对象都有一个 constructor 属性指向对应的构造函数,即类。所以以下几种写法其实是相等的,都返回 Foo 类的原型对象。

1
2
3
4
Foo.prototype
foo["__proto__"]
foo.__proto__
foo.constructor.prototype

大段大段的文字总是让人头疼,写个简单代码体会一下:

1
2
3
4
5
6
function Foo(){};
undefined
let foo = new Foo();
undefined
Foo.prototype == foo.__proto__ //foo对象的__proto__属性等于Foo.prototype
true

nodejs1

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Foo() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}

function Son() {
this.first_name = 'Melania'
}

Son.prototype = new Foo()

let son = new Son()
son.__proto__['add_name'] = 'abc'
let son1 = new Son();
console.log(`son Name: ${son.add_name}`)

console.log(`son1 Name: ${son.add_name}`)

//结果: son Name: abc
// son1 Name: abc

我们明明只修改了son对象的__proto__属性,但由于有其它对象的原型属性指向了相同的原型属性(即son1.__proto__与其相同),导致另外一个具有相同原型的对象也受到了影响。

再看一段js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object),foo是一个Object类的实例,实际上是修改了Object类,给其增加了一个属性bar,值为2
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象,他会有一个bar属性
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)
//最后,虽然zoo是一个空对象{},但zoo.bar的结果居然是2:
//参考:https://blog.csdn.net/qq_51586883/article/details/119867720

再通过一段代码加深加深印象 :

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
function Foo() {
this.first_name = 'D'
this.last_name = 'K'
function Son() {
this.first_name = 'A'
}
Son.prototype = new Foo() //Foo { first_name: 'D', last_name: 'K' }
let son = new Son() //Foo { first_name: 'A' }
son.last_name // 'K'
//注意,此时Son.prototype=son.__proto__

son.__proto__ //Foo { first_name: 'Donald', last_name: 'Trump' }
Foo.add_name='viper'
son.add_name //undefined
Son.add_name //undefined,构造函数的属性和方法要通过创建实例才能访问
Foo //[Function: Foo] { add_name: 'viper' },注意这里解释了为啥Son/son.add_name没定义
Foo.prototype['add_name']='notbad'//注意这里
son.add_name//'notbad'
Son.add_name//undefined,同上

JavaScript 的查找机制如下:

在对象son中寻找last_name
如果找不到,则在son.__proto__中寻找last_name
如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
依次寻找,直到找到null结束。比如,Object.prototype 的 __proto__就是 null

那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

总结一下:

1.构造函数.prototype指向的是一个对象(原型)。

2.任何对象都有一个原型对象,这个原型对象由对象的内置属性__proto__指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的。

3.只有构造函数内才有ptorotype属性。

4.每个对象都内含有一个属性:proto,也就是说就算对象里面没有对这个属性进行赋值,那么也是有这个属性的。

5.原型链的核心就是依赖对象__proto__的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype的进行查找,直至查找到Object时,就没有指向了。如果最终查找失败会返回undefined或报错。

通过一段java代码对比两者的异同:

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
package Test;//包名为Test的类

class Father{
public String name;//公共字符串属性name
}
class Son extends Father{//Son类继承自Father类
public Son(){
super.name = "father";//super 关键字用于在子类中访问父类的成员变量、方法和构造函数。
}
void alert() {//alert方法,返回类型为void(不会返回任何值)
System.out.println("i am son");
}
}
public class Test {
public static void main(String args[]) {
Son s1 = new Son();
System.out.println(s1.name);//System.out是一个标准输出流对象,println()是一个方法,用于将指定的参数打印到控制台,并在末尾添加一个换行符。
s1.name = "son";
System.out.println(s1.name);
Son s2 = new Son();
System.out.println(s2.name);
}
}
//结果:
father
son
father

java是基于对象继承,javascript是基于原型继承

污染原理

对于语句:object[a][b] = value 如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。

简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象

利用手段

现在我们大致可以知道:如果可以通过某种方法控制对象.__proto__的值,那我们就可以间接修改继承该原型对象的所有对象。

1.对象merge 对象clone(其实内核就是将待操作的对象merge到一个空对象中) 以对象merge为例,我们想象一个简单的merge函数:

1
2
3
4
5
6
7
8
9
10
//用于将一个源对象的属性合并到目标对象中
function merge(target, source) {
for (let key in source) { //遍历source的所有属性
if (key in source && key in target) {//对于每个属性,若同时存在于源对象和目标对象中
merge(target[key], source[key]) //表示这是一个嵌套对象,需要递归调用merge函数,将源对象的嵌套属性合并到目标对象的对应嵌套属性中。
} else {
target[key] = source[key]//非嵌套属性,直接将源对象的属性值赋给目标对象的对应属性。
}
}
}

这里如果我们让key是__proto__的话那么是不是就可以对原型造成影响,最终影响到实例化出来的类呢?

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.a, o1.b)//1,2
console.log(o1)//{ a: 1, b: 2 },proto没有被当做键名
console.log(o1.__proto__)//[Object: null prototype] {}
o3 = {}
console.log(o3.b)//undefined
console.log(o3)//[Object: null prototype] {}

为什么o1被污染了而o3没有被污染?

打开node看下o2key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Welcome to Node.js v18.18.0.
Type ".help" for more information.
> let o2 = {a:1,"__proto__":{b:2}}
undefined
> for ( let key in o2){console.log(key);}
a
b
undefined
> o2
{ a: 1 }
> o2.__proto__
{ b: 2 }
> o2.__proto__.__proto__
[Object: null prototype] {} //最上层的object,并没有任何属性
> o2.__proto__.__proto__.__proto__
null
>可以看到o2的key只有a,b没有我们需要的__proto__,并注意最后几行!

原因很好理解:在o2 = {a:1,"__proto__":{b:2}}中,"__proto__":{b:2}等价于o2.__proto__={b:2},也就是o2这个实例的__proto__属性。__proto__并没被当成键名解析。

我们需要这样修改(利用JSON.parse):

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 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}}')//JSON.parse的存在可以将JSON字符串解析为JavaScript对象,这样一来就存在o2.__protp__.b=2,并把这东西赋值给之前不存在的o1.__proto__
merge(o1, o2)
//1 2
console.log(o1.a, o1.b)
console.log(o1.__proto__)//[Object: null prototype] { b: 2 }
o3 = {}
//2
console.log(o3.b)
console.log(o3.__proto__)//[Object: null prototype] { b: 2 }

简单分析下运行过程:

首先是o1[a]=02[a]=1,然后进行o1.__proto__o2.__proto__(注意这是共有的,所以进行循环merge过程),即o1.__proto__.b=o2.__proto__.b=2,即Object.prototype.b=2

也就是最顶层的Object.prototype所指向的对象添加了属性,所以我们随便创建一个对象也就有了b这个属性(间接拥有,会向上查一个.proto__)。

参考文章:

1
2
3
https://jlkl.github.io/2020/11/06/Web_16/
https://blog.csdn.net/qq_51586883/article/details/119867720
https://xz.aliyun.com/t/7182

初识JavaScript原型链污染
http://example.com/2023/10/22/2023-10-22-JS原型链污染学习笔记/
作者
notbad3
发布于
2023年10月22日
许可协议