本来想将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
%E6%B7%B1%E5%85%A5/image-20230901135415750.png)
%E6%B7%B1%E5%85%A5/image-20230901154849363.png)
%E6%B7%B1%E5%85%A5/image-20230901155300768.png)
%E6%B7%B1%E5%85%A5/image-20230901160722382.png)
%E6%B7%B1%E5%85%A5/image-20230901150808861.png)
%E6%B7%B1%E5%85%A5/image-20230901170534691.png)
%E6%B7%B1%E5%85%A5/image-20230901170427183.png)
%E6%B7%B1%E5%85%A5/image-20230901171115585.png)
%E6%B7%B1%E5%85%A5/image-20230901172350796.png)
%E6%B7%B1%E5%85%A5/image-20230901172719167.png)
%E6%B7%B1%E5%85%A5/image-20230901182724283.png)
%E6%B7%B1%E5%85%A5/image-20230901182756972.png)
%E6%B7%B1%E5%85%A5/image-20230905094854794.png)
%E6%B7%B1%E5%85%A5/image-20230905095519407.png)
%E6%B7%B1%E5%85%A5/image-20230905095812582.png)
%E6%B7%B1%E5%85%A5/image-20230905100719829.png)
%E6%B7%B1%E5%85%A5/image-20230905101905435.png)
%E6%B7%B1%E5%85%A5/image-20230905102214488.png)
%E6%B7%B1%E5%85%A5/image-20230905102456014.png)
%E6%B7%B1%E5%85%A5/image-20230905102953845.png)
%E6%B7%B1%E5%85%A5/image-20240531103907131.png)
%E6%B7%B1%E5%85%A5/image-20230905110442713.png)
%E6%B7%B1%E5%85%A5/image-20230905111044306.png)
%E6%B7%B1%E5%85%A5/image-20230905111219362.png)
%E6%B7%B1%E5%85%A5/image-20230905111651534.png)
%E6%B7%B1%E5%85%A5/image-20230905111841511.png)