背景
2019年初,Snyk的安全研究人员披露了流行的JavaScript库Lodash中一个严重漏洞的详细信息,该漏洞使黑客能够攻击多个Web应用程序,这个安全漏洞就是一个“原型污染漏洞”(JavaScript Prototype Pollution),攻击者可以利用该漏洞利用JavaScript编程语言的规则并以各种方式破坏应用程序。
原型与原型链
Javascript中一切皆是对象, 其中对象之间是存在共同和差异的,比如对象的最终原型是Object
的原型null
,函数对象有prototype
属性,但是实例对象没有。
-
原型的定义:
原型是Javascript中继承的基础,Javascript的继承就是基于原型的继承
(1)所有引用类型(函数,数组,对象)都拥有
__proto__
属性(隐式原型(2)所有函数拥有
prototype
属性(显式原型)(仅限函数) -
原型链的定义:
原型链是javascript的实现的形式,递归继承原型对象的原型,原型链的顶端是Object的原型。
-
原型对象:
在JavaScript中,声明一个函数A的同时,浏览器在内存中创建一个对象B,然后A函数默认有一个属性
prototype
指向了这个对象B,这个B就是函数A的原型对象,简称为函数的原型。这个对象B默认会有个属性constructor
指向了这个函数A。
-
实例对象:
我们可以通过构造函数A创建一个实例对象A,A默认会有一个属性
__proto__
指向了构造函数A的原型对象B。 -
关系
function Foo(){}; undefined let foo = new Foo(); undefined Foo.prototype == foo.__proto__ true
3、原型链机制
回顾一下构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。——摘自《javascript高级程序设计》
感觉理解起来有点绕,不过引用图片可以很好理解。
这里person实例对象,Person.prototype是原型,原型通过
__proto__
访问原型对象,实例对象继承的就是原型及其原型对象的属性。继承的查找过程:
调用对象属性时, 会查找属性,如果本身没有,则会去
__proto__
中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有__proto__
,那么会去__proto__
的显式原型中查找,一直到null(很好说明了原型才是继承的基础)
原型链污染机制
javascript是种动态继承。与java两者的继承方式机制可以说完全不一样的,一个java是基于对象来继承, 一个javascript是基于原型来继承
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类继承了Father类的last_name属性
对于对象son,在调用last_name的时候,JavaScript引擎会进行的操作如下:
在对象son中寻找last_name
如果找不到,就到son.__proto__中寻找last_name
还找不到,就到son.__proto__.__proto__中寻找last_name
就这样一直往上找,一直找到null宣告结束
我们修改下代码:
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
son.__proto__['add_name'] = 'hehehe'
let son1 = new Son();
console.log(`son Name: ${son.add_name} `)
console.log(`son1 Name: ${son1.add_name} `)
发现一个对象son修改自身的原型的属性的时候会影响到另外一个具有相同原型的对象son1,同理
当我们修改上层的原型的时候,底层的实例会发生动态继承从而产生一些修改。
我们真正修改的其实是原型prototype
原型链污染(利用手段)
在JavaScript发展历史上,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto,构造函数和原型。攻击者可以通过注入其他值来覆盖或污染这些proto,构造函数和原型属性。然后,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。
原型链污染的发生主要有两种场景:不安全的对象递归合并和按路径定义属性。
我们先了解下什么情况下容易发生原型链污染
存在可控的对象键值
1.常发生在merge
等对象递归合并操作
2.对象克隆
3.路径查找属性然后修改属性的时候
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] } } }
let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b)
这样的话__proto__
才会被当作一个JSON格式的字符串被解析成键值,而不是上面之间被解析成了一个属性值。
参考博客:
浅析javascript原型链污染攻击 - 先知社区 (aliyun.com)
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
JavaScript原型链污染原理及相关CVE漏洞剖析 - FreeBuf网络安全行业门户
渗透攻击漏洞之——原型链污染-CSDN博客