深入了解JavaScript原型链污染

前言

​ 最近的比赛中遇到过很多次的原型链类型的题目,之前就是简单的了解了一下,但是在做题中还是不能成功解出题目,今天就好好深入的去研究一下。

基础知识

​ 像JavaScript这门语言,其实我刚开始学的时候也就是以为只是js,但是随着后续的深入学习发现这门语言也存在着这样或那样的安全问题,像SQL注入这些常见的,还会出现今天研究的这个原型链污染攻击。JS基于原型实现继承,原型是Javascript的继承的基础。

prototype和__proto__用法

JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:

1
2
3
4
5
function Foo() {
this.bar = 1
}

new Foo()

Foo函数的内容,就是Foo类的构造函数,而this.bar就是Foo类的一个属性。

既然是一个类必定是可以定义方法的,我们可以将方法写在里面

1
2
3
4
5
6
7
8
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}

(new Foo()).show()

但是每当我们去实例化对象的时候就this.show = function...就会执行一次,也就是说这个show方法其实是绑定在实例化的对象上的,并不是绑定在类上。如果希望在创建类的时候只创建一次show方法,就需要使用原型(prototype)了:

1
2
3
4
5
6
7
8
9
10
function Foo() {
this.bar = 1
}

Foo.prototype.show = function show() {
console.log(this.bar)
}

let foo = new Foo()
foo.show()

此时的prototype就是类的一个属性,由于javaScript的继承性,所以所有实例化的对象都会拥有这个属性,而且是这个属性中的所有内容,包括变量和方法。也就是说实例化的对象都会继承得到类中的方法,比如上述例子里的show。我们可以通过prototype来访问类的原型。

但是我们无法访问实例化对象的原型。这个时候就用到了proto,即我们可以使用proto来访问实例化对象那个类的原型

1
foo.__proto__==Foo.prototype

这时候就要去思考一下了,尤其是在JavaScript一切皆对象的前提下

1
foo.__proto__.__proto__==Object.prototype

这返回的原型是哪个呢?是Object

所以可以简单的总结下:

+ prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
+ 一个对象的`__proto__`属性,指向这个对象所在的类的prototype属性

JavaScript 原型链继承

​ 就和我们上面所说的一样所有类对象在实例化的时候将会拥有prototype中的属性和方法,实现JavaScript中的继承机制。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}

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

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

可以看到son.last_name继承了Father的内容,为什么会得到这样一个输出,总结下查询时的操作

  • 首先去我们的son方法中去找last_name
  • 找不到就通过proto去找last_name son._proto_
  • 通过下方的图我们可以看到通过proto去查找,查找到了在Father方法中的last_name 但是如果还是无法找到的时候,可以继续使用son._proto_.__proto__去找,直到找到null值结束
  • Object.prototype__proto__就是null

JavaScript的这个查找的机制,被称作prototype继承链。小总结

1
2
3
每个构造函数(constructor)都有一个原型对象(prototype)
对象的__proto__属性,指向类的原型对象prototype
JavaScript使用prototype链实现继承机制

原型链污染

​ 到了重头戏了,前面说了那么多,现在就可以来进一步的去了解如何去污染原型链。前面说的都是通过proto去访问原型,如果修改了proto的值,是否是修改了原本的类?简单来测试下

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

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

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

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

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)

虽然zoo是一个空对象{},但zoo.bar的结果居然是2:

为什么会变成2呢,其实也很好理解,因为前面我们修改了foo的原型foo._proto_.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。然后我们有新创建了一个zoo继承了所有的属性和方法。也就是说如果我们能够控制一个对象的原型就可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

哪些情况下会导致原型链污染

​ 在实际情况下只要可以找到能够控制的对象的原型就会导致原型链污染或者控制数组(对象)的“键名”的操作即可

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

以对象merge为例,我们想象一个简单的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]
}
}
}

审计这个代码我们可以看到merge(target[key], source[key])这一行代码,如果我们赋值时将key的值赋值为__proto__ 但是要注意的是必须让__proto__被认为是一个键名。

JSON解析的情况下,__proto__会被认为是一个真正的键名,而不代表原型,所以在遍历o2的时候会存在这个键。