参考两位大佬的博客:
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
继承与原型链 - JavaScript | MDN (mozilla.org)
1.在js中,定义类是用定义构造函数的方法定义的,比如:
其中,foo叫做实例化对象,Foo叫做类。
js中有一个叫做原型对象的东西,就是一个对象模板,可以理解成实例化对象都是根据这个模板先“克隆”出来,然后再投入使用。它记录了这个类实例化的对象的最初属性及属性值和方法。同时,它也有一个“模板”,也就是它的父类型。
原型对象是可变的,可以改变其属性值,添加方法等。想要访问它,可以使用类名.prototype属性,或者实例化对象.__proto__属性
例:
我们可以看到,Foo.prototype和foo.__proto__是相等的,都访问到了Foo类的原型对象,而原型对象中存储了构造器,属性值和它的原型对象。
其实,大家这时候可能会发现,这种原型对象有原型对象的模型非常类似于——继承。没错,js中就是利用原型链来实现继承的效果。A类proto(原型对象)中的属性,属性值以及方法都会被A类的所有实例化对象以及其子类(proto属性是A类的类)继承(包括之前创建的)
原型链:
我们可以看到,即使你是在后面对原型对象进行修改,之前创建的对象也会被影响。
js中,Object类是几乎所有对象的父类,Object类没有原型,定义为null
2.关于属性
在js中,有一个属性遮蔽的特性。简单来说,在某一个对象(实例化对象或原型对象)访问一个属性的时候,会首先寻找该对象本身有没有这个属性,有的话直接采用其本身的属性值,没有的话寻找其原型中有无这个属性,一直寻找直到寻找到null为止
例:
我们可以看到,在Foo类原型对象新增temp2属性,值是1之后,f.temp2同样变成了1,其实f中没有temp2属性,于是解析器去寻找f.__proto__中是否有这个属性,就显示了f.__proto__.temp2的值
后面,实例化对象f中新增了temp2属性,值是2,于是再访问f.temp2时,就直接显示了f中的temp2属性
3.原型链污染
简单来说,就是当原型对象被修改时,对应的所有实例化对象都会被影响。举个例子:
foo.__proto__.__proto__的值是Object类的原型对象,我们在里面加入了test属性之后,发现之后生成的Object类实例化对象test也带有了test属性
那么,如果我们可以控制某些类,然后这些可控类又与一些其它类进行了赋值操作,我们就可以间接性通过赋值,修改其它类的原型对象,从而改变这些类的实例化对象的属性。
具体操作实例可以参照:深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com),我做一些补充
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操作,作用是把source中的属性全部赋值到target中,如果属性名已经存在,target值不变,如果target中属性名不存在,那么新建属性,对应赋值
这种操作失败的原因是,o2中的__proto__被视为了已经存在的键值而不是一个新的键值,所以遍历键值只得到了[a,b],执行的是o1.b=o2.__proto__.b,o2的属性b并没有赋值到o1的__proto__原型对象中,而是直接以实例化对象的特有属性在o1中存在
这里要注意o2.__proto__={b:2}与o2.__proto__.b=2的区别,或许体现在o2中是一样的,但是,前者只影响o2,只改变o2一个实例化对象的属性值,所以这里创建o2并不能影响到原型对象;而后者是影响o2.__proto__,可以影响Object类的原型对象
在json解析中,o2中的__proto__会被视为一个新的键值,所以遍历键值得到了[a,__proto__],执行的是o1.__proto__.b=o2.__proto__.b,改变了o1.__proto__,所以改变了Object类的原型对象
这种漏洞在出现属性赋值的时候比较容易出现,特别是merge操作。在CTF中,js里面出现对某个变量的一个不存在的属性做校验的时候可以考虑原型链污染,利用json把__proto__看做新的键值的特性可以进行污染,http发包中把Content-Type设置成application/json也可