文章源自【字节脉搏社区】-字节脉搏实验室
作者-purplet
因为在CTF中时常也会考察原型链污染的问题,以前也一直让我捉襟见肘,一直没有系统的学习了解过JS原型的这些相关概念,因此写下本文,通过不断总结大佬的文章,写出自己对于此部分内容的理解。同时建议学习本文前要有对面向对象部分知识的一定理解(无论哪种语言)。
JavaScript没有”子类”和”父类”的概念,也没有”类”(class)和”实例(instance)的区分,全靠一种很奇特的”原型链”(prototype chain)模式,来实现继承。 在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype。
一、对象和函数
在学习原型和原型链之前,首先一定要搞清楚对象和函数到底有什么区别和联系:
“对象是由函数创建的,而函数又是一种对象。”这样一句话要深刻记忆。
我们都知道JavaScript可以在浏览器中使用“F12”打开的控制台中输入JavaScript代码进行执行,但也要知道其实在浏览器中已经内置了几个全局函数可供随时调用,如:Number()、String()、Boolean()、Object()等 。
在JavaScript中声明一种数据类型的变量时其实有以下两种方式,而第一种可以更直观的体现对象和函数之间的关系,但第二种在各种语言中都较为常用。
但第二种虽然我们是用赋值形式创建的,但在JavaScript的内部,则仍然是通过调用函数来创建对象的。也就是说他们是一样的。
而对于“函数又是一种对象”这句话,也可以使用 instanceof 关键字来验证:
instanceof 的作用是判断一个对象是不是一个函数的实例。 比如 obj instanceof fn 实际上是判断fn的prototype是不是在obj的原型链上。 比如: obj.__proto__ === fn.prototype obj. __proto__.__proto__=== fn.prototype obj. __proto__ … __proto__ === fn.prototype 以上只要一个成立即可。
以上这个内容如果现在看不懂,不要着急后面会解释什么是原型、原型链和__proto__属性。
首先定义个对象:var str = new String(‘hello’); 输出看看该对象中包含哪些属性:
再创建一个num对象。
可以看到两个不同的对象,但都存在__proto__属性。再看以下例子:
肯定会疑惑valueOf和toString方法是哪里来的呢,其实这两个方法也都是在__proto__属性中带来的,打开__proto__的指向箭头就可以看到
总结:不只是str和num对象,每个对象中都有__proto__属性,JavaScript将这些对象(如:Number(函数也是对象))中的共有属性,拿了出来,全都集中到一个新的对象(num)中。而新对象中,就保存着一个__proto__,指向这个原对象。
而__proto__所指向的这个原对象,也叫做原型对象。后文会继续解释。
而既然存在共有属性,那也一定存在独有属性。string对象有string对象的属性是其他对象没有的;number对象有number对象的属性是其他对象没的;boolean对象有boolean对象的属性是其他对象没有的;以此类推。 那这个“独有对象”又是保存在哪儿的呢?我们来看看什么是prototype。
上面说到,__proto__是每个对象都有的属性,那么要区别记住的是prototype是函数才有的属性。
再继续了解prototype属性前再补充学习几个知识点:
1-什么是构造函数
Person就是一个构造函数,我们使用 new 创建了一个实例对象 person
2-constructor属性
接着按照刚刚的例子查看Person和person的结构输出。
可以看到person的构造函数Person存在的原型包含一个constructor属性。接下来记住一句话:“每个原型(prototype)都有一个 constructor 属性指向关联的构造函数,实例原型指向构造函数 ”。再看person的结果中__proto__属性所指的constructor属性也是与之关联的构造函数,而对于该例中,它的构造函数就是function Person()函数,因此整个结构图如下图所示。
所以以下代码是成立的
其实当认真理解完上面的内容,原型链的概念就基本清楚了,以下总结出几点:
1-从上面的代码中可以看到,创建person对象虽然使用的是由构造函数Person创建,但是对象创建出来之后,这个person对象其实已经与Person构造函数没有任何关系了,person对象的__proto__属性指向的是Person构造函数的原型对象(Person.prototype)。 2-如果使用new Person()创建多个对象person1、person2、person3,则多个对象都会同时指向Person构造函数的原型对象。 3-我们可以手动给这个原型对象添加属性和方法,那么person1、person2、person3这些对象就会共享这些在构造函数的原型对象中添加的属性和方法。 4-如果我们访问person中的一个属性name,如果在person对象中找到,则直接返回。如果person对象中没有找到,则直接去person对象的__proto__属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链),直到最高级Object的__proto__为Null为止。 5-如果通过person对象添加了一个属性name,则通过person访问name时,就相当于屏蔽了原型中的属性name,输出的是person对象中的name值 6-通过person对象只能读取构造函数的原型中的属性name值,而不能修改原型中的属性name值。person.name = “purplet”; 并不是修改了原型中的值,而是在person对象中给添加了一个属性name。
下面可以把原型、原型链的关系当作一个公式一般去记忆:
由于__proto__是任何对象都有的属性,而JavaScript里万物皆对象,所以会形成一条__proto__连起来的链条,但递归访问__proto__必须最终到头,其终点是Null 当JavaScript引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype,如图所示。
在看懂原型链的那几点内容后,其实就应该可以理解什么是原型链污染了,就是修改其构造函数的原型中的属性值,使其他通过该构造函数实例出的对象也具有该属性值。
可以看到我们修改成功了,新生成的 foo2
对象也具有hacker
属性,如果给foo1再往上加一个__proto__就可以修改(添加)Object的属性了。
那么在哪些情况下原型链会存在污染?
这里我引用郁离歌师傅的博客内容了。
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
以对象merge为例,我们想象一个简单的merge函数:
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
https://www.zhihu.com/tardis/sogou/art/44035916
https://www.jianshu.com/p/be7c95714586
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04