本来想将p神的JavaScript污染链文章并在我写的基础浅入里,但是感觉还是得分开学习记录,p神干货还得和我水的文章分开,浅入就讲概念理解学习吧,深入就涉及深入认识污染了
prototype
和__proto__
分别是什么
这里引用p神的文章,加上部分自己的理解
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
理解
在JavaScript中,我们如果要定义一个类,需要以定义“构造函数”
的方式定义:
1 | function Foo() { |
其中Foo函数
就是Foo类
的构造函数,其中的this.bar
是Foo类
中的一个属性。
也就是说
利用function定义类
的同时,构造函数也被定义好了
在ES6中引入了
class
的概念,两者是有区别,但是不是很大
class
本质还是基于原型prototype
的实现方式进一步的封装,class
本质还是函数function
但是使用
class
定义类比function
使得代码更加直观,也更加灵活
,因为class定义类时,可以直接使用constructor()
方法来定义构造函数
,并且可以使用关键字extends
来继承父类
1 | class Foo { |
可以看到用class定义类的时候,构造函数也是被定义好了
并且function和class两者的构造函数的名字也是一样的
在一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数的内部
1 | function Foo() { |
代码中我们可以看到,show
的定义利用了function
,但是并没有被定义为show类
,也就是说这个show方法并不是和上面一样是所谓show类
的构造函数,而是Foo类
中Foo
这个构造函数的一个方法
如下图结果可以理解,在新建实例化Foo对象,执行构造函数时,show
方法也会被执行可知,show
只是构造函数中的一个方法,是绑定在对象中的吗,而不是绑定在类
但由此出现了一个新问题,那如果每次新建一个实例化对象就执行一次show方法
,有时在实际生产中并不需要,可能大多数时候只想在创建类后执行一次
即可
那肯定就不能把show
方法再写到function Foo{}
中去了,
这里就需要使用原型
prototype
1 | function Foo() { |
这段代码其实也好理解,我们将show
方法单独提出来当作一个函数,并加到Foo
的原型当作一个单独的函数,prototype
可以当作Foo
类自带的一个属性,所有的创建的Foo
对象都将拥有Foo
类中所有内容,包括变量和方法。
可以看到和在构造函数中定义show
不同的是,show
作为和构造函数一样的显示在原型内容中,属于是新创建的Foo
类对象可以利用的一部分。
这里将Foo
类实例化后给foo
,foo
也完全可以直接调用show
函数的内容,foo
自身创建开始就具有Foo
中的所有变量方法可以调用。
关键点
我们可以通过Foo.prototype
来访问Foo
类的原型,但是Foo
类实例化出来的对象,如上的foo
,是不能直接调用prototype
进行访问原型的
所以这里就需要提到__proto__
,类实例化后的对象可以通过__proto__
来直接访问类的原型。
也就是
1 | foo.__proto__ == Foo.prototype |
进一步思考,也就是实例化后的对象【[比如foo
]可以像上面例子一样,通过__proto__
访问原型定义一个新的函数,如下例子,利用foo.__proto__
定义了一个新函数hacker
并且能成功调用执行,打印’i get shell
‘
1 | foo.__proto__.hacker = function hacker() { |
总结
prototype
是一个类的属性
,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
JavaScript原型链继承
继承在java中很常见,在JavaScript中作用也差不多,只不过概念被替换成原型了
所有类对象在实例化的时候将会拥有
prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如,
1 | function Father() { |
这里的,
1 | Son.prototype = new Father() |
可以理解为Son的原型为实例化的Father类,Son类就会继承Father类得得last_name
属性,但是继承不会覆盖自己原有的属性,所以first_name
还是Son类自己的
然后再将Son
实例化给son
,那么son
就具有Son类
和Father类
的所有属性
1 | let son = new Son() |
可以在这里的层次就可以看出,son
的first_name
是以Son类为主
的,而由于自身没有last_name
,就会从自己原型Father类
继承last_name
然后获取值
于是当打印时,最后输出的是Name: Melania Trump
。
1 | console.log(`Name: ${son.first_name} ${son.last_name}`) |
总结
以上述的Son类和Father类为例,我们可以看到son的层次中,是有两个Prototype
,这就是原型链的最简单的格式。最后一个Object
就是null
,他在原型Father类的后面,这也是原型链默认的最后一个原型。
我们可以考虑一个问题,当我在打印${son.last_name}
,如果在Father类
中也没有这个属性会怎么样呢,那son就会不断的顺着原型链一直找下去,直到null
就会执行,
1 | son.__proto__.__proto__.__proto__.__proto__ |
JavaScript
这种查找机制,被用在面向对象的继承中,被称作prototype
继承链。
以上是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记下面几点即可:
- 每个构造函数都有一个原型对象
- 对象的
__proto__
属性,指向类的原型对象prototype
- JavaScript使用prototype链实现继承
原型链污染是什么
在最开始写过foo.__proto__
指向Foo类的prototype
,我们也试过通过foo.__proto__
新增一个新函数,也可以加到原型中去,并且可以成功调用与构造函数同一层次,那么如果修改foo.__proto__
中的一些值,同理也可以修改Foo类中的一些东西
我们简单试一下,
1 | // foo是一个简单的JavaScript对象 |
首先我们先构造一个简单对象foo,
1 | let foo = {bar: 1} |
由于foo并不是某个具体类的实例,但是所有对象都有一个实例存在,那就Object
类
然后我们先看看,
1 | console.log(foo.bar) |
同样可以看到foo
确实存在一个原型,并且为Object
。
那么如果我们通过foo.__proto__
修改原型中的某些值呢?
1 | foo.__proto__.bar = 2 |
这里我们通过foo.__proto__
将bar
值修改为2,
然后打印看看,
突然发现,打印的结果是1,而不是我们刚才修改的结果2
这是为什么呢?
我们再次看看foo就知道了
我们可以看到,
bar: 1
是foo自身的属性,而原型Object中的bar: 2
,层次要低于foo自身的属性,所以相同的属性,还是以自身优先,不存在或者不完善,才从原型中继承,这和上面讲的Son类和Father类是一样的。
所以这里打印结果还是1,就是这个原因。
但是,如果我们再新建一个对象,让它和foo一样,原型也是只有Object
,但是没有bar属性,再次打印会怎么样呢
1 | let zoo = {} |
可以看到打印结果为2,因为这里zoo对象没有bar属性,所以这里zoo就直接继承Object中bar属性,而Object本身并没有bar属性,是foo通过foo.__proto__
来新增的一个属性,却能达到影响zoo对象的作用。
那么,这种在一个应用中,攻击者控制并修改了一个对象的原型,那么将可能影响所有和这个对象来自同一个类。父祖类的对象。这种攻击方式就是原型链攻击
哪些情况下原型链会被污染呢?
根据上述内容,我们发现关键在于能够顺利调用__proto__
并设置其值即可,那么如何成功呢?
关键在于找到能够控制数字【对象】的“键名”的操作即可:
- 对象
merge
- 对象
clone
(其实内核就是将待操作的对象merge到一个空对象中)
以对象merge为例,我们想象一个简单的merge函数:
1 | function merge(target, source) { |
这个函数是一个递归函数,它的目的是将
source
对象中的属性合并到target
对象中。它通过遍历source
对象中的每一个键值对,如果target
对象中也存在这个键,那么就递归调用merge
函数将两个对象中对应键的值进行合并;否则,就直接将source
对象中的键值对复制到target
对象中。最终,target
对象将包含原来的属性以及source
对象中的所有属性。这个函数通常用于合并两个或多个对象,以便于在一个对象中访问所有属性。
我们看到,在合并时存在复制操作target[key] = source[key]
,那么如果这个key是__proto__
,是否就可以成功造成污染呢?
试一下,
1 | let o1 = {} |
这里可以看到o1和o2确实是合并成功了,但是o2.__proto__
的b却没有合并进去,
但是我们的__proto__
不见了,取而代之发现多了个Object
原型,也就是说我们的__proto__
被当作o2的原型,也就是其自身的Object
,但是并没有影响最后一个每个对象共有Object
,
这里的 __proto__
实际上不是一个普通的属性,而是将 o2
的原型设置为 {b: 2}
。所以 o2
的结构如下:
o2.a
是自身的属性,值为1
。o2
的原型有一个属性b
,值为2
。
而merge合并只合并自身属性,所以这种污染是无效的。
此时遍历o2的所有键名,拿到的只有[a,b],__proto__
并不是一个key,自然也不无法修改Object的原型。
所以当我们想用o3
测试是否污染时,发现是没有的
那么如何将__proto__
被当作是一个键名呢,只要加一个解析就行
1 | let o1 = {} |
再看看o2的数据,
发现这次__proto__
,没有被当作o2的原型了,而是一个键值
此时再次打印o3,发现成功污染Object
JSON解析的情况下,
__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
可以是为什么能实现将属性b以及其值污染到Object中呢?
其实很简单,
我们实际修改原型时用的语法是
1 | o2.__proto__.b = 2 |
而读取数组中键值也是如此语法,所以当键值为__proto__
时,调用__proto__
其后的数据因为语法,会被当作修改原型的值,从而实现原型链污染的作用
ps.
merge
操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
最后可以做一下p神的
Code-Breaking 2018 Thejs
https://github.com/phith0n/code-breaking/blob/master/2018/thejs/web/server.js