JS原型链污染(0)

参考文档

https://xz.aliyun.com/t/10032

https://www.cnblogs.com/jofun/p/8727814.html

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

原型链继承

函数对象和实例对象

javascript中没有类的概念,但是,它的很多“面向对象”操作和那些有类的语言差不多。这是通过“原型对象”和“原型链”实现的。

javascript中的对象分为函数对象(构造函数)实例对象两类。

以下是一个函数对象的例子:

function wow(){
	this.a=1;
    this.b=2;
}

使用console.log();作用于原型对象的时候,它会直接打印出它的代码。

console.log(wow);
ƒ wow(){
	this.a=1;
    this.b=2;
}

以下是一些实例对象的例子:

var a=["hidden_er","onequiz","sjh"];
//默认构造方式,由 Array() 构造,构造函数自带。(数组)

let o=new f();
//一般构造方式,由上面那个函数对wow()构造,构造函数自定。

object1={"a":1,"b":2};
//JSON构造方式,解析成键值对的形式,允许嵌套。由Object()构造。

使用console.log();作用于实例对象的时候,它会打印出一堆层次性结构。

原型对象

原型对象是实例对象,而不是函数对象。

原型对象null是万物之源,即js原型继承链的源头。null对象没有任何属性。

我们介绍或熟知的js对象声明方式无法直接声明原型对象;原型对象是js自己创建的,通过一些属性和我们创建的对象相关联。

关键属性

prototype:指向其对应的原型对象。只有函数对象才拥有这个属性。

你每通过代码声明一个构造函数,js都会自动生成一个它的原型对象,并且在你的函数对象中添加该属性。

_proto_属性:指向它的构造函数的原型对象。只有实例对象才拥有这个属性。

constructor属性:指向它的构造函数本身。任何对象都有这个属性(因为任何对象都有相应的构造函数)。

需要注意的是,构造函数是函数对象,它有prototype属性;而prototype原型对象中的constructor又指向由它的构造函数本身。这是个套娃。

[[prototype]]:我觉得这玩意就是_proto_

证据:

函数对象Object

在上文中,我们已经知道,原型对象的万物之源是null

但这实际上还不够;实际上,null没有直接作为任何构造函数的原型对象。我们获取null的最快途径是Object.prototype._proto_ ;即,构造函数Object的原型对象的构造函数的原型对象 是null。

(构造函数本身属性我不会读取,要通过其原型对象的constructor属性读取;而null没有这玩意,所以 构造函数Object的原型对象的构造函数 这一层实际上也是没有东西的。)

这里不需要再深入理解了;我们只需要记住,构造函数的万物之源是Object,找到了Object就相当于找到了null。(实际上找null也没用,我们以后最深的地方也就是找Object)

还需要说明的是,以JSON生成的对象直接继承于Object,数组继承于Array-->Object,函数继承于Function-->Object。这些都是JS自带的原型对象。

总结0

在之后的利用中,

如果初始对象是函数对象,那就用一次prototype,然后开始_proto_一直向上跑;

如果初始对象是实例对象,那就直接_proto_一直向上跑。

总结1**

图中黑字部分是原型链的核心部分;其中,原型对象的源头是原型对象null(也可以理解为Object原型对象),构造函数(函数对象)的源头是构造函数Function

蓝色部分是最初的外延扩展部分。其中,JSON对象是最特殊的,它直接与Object原型对象和构造函数Object相关;而数组相关的内容实际上只需要理解为右边内容的特例,即,JS已经写好了相应的构造函数(并生成了相应原型对象),不需要我们自己去写。自定义构造函数的对象可以一直继承,从而把原型链一直延长下去。

箭头标注了这些东西相互之间的沟通情况及沟通方式。实例对象(包括但不限于原型对象)仅有1、2两种属性,而构造函数(函数对象)有1、2、3三种属性。

关于prototype

以上的很多分析实际上我们都不需要记;只需要记住一点:用prototype就是为了防止每次new的时候都运行一次方法。

具体的解释,就是:在有的面向对象编程语言中,对象的方法肯定写在类里面,而不是写在构造方法里面。而JS没有的概念,表面看起来想生成对象只能使用构造方法,于是只能把对象的方法写在构造函数里面。这就导致,每生成一个对象,对象方法就会被执行一次。我们往往不想这样,所以,我们可以把对象方法写入原型对象中,来避免这种情况。

//不使用prototype,每生成一个对象,都必须调用方法show。
function Foo() {
    this.bar = 1
    this.show = function() {
        console.log(this.bar)
    }
}
//使用prototype,避免了上述的尴尬情况
function Foo() {
    this.bar = 1
}

Foo.prototype.show = function show() {
    console.log(this.bar)
}

注意

原型链继承 只是JS对象继承中的一种方法;实际上,还有很多种方法可以实现继承,具体可参考 参考文档。

原型链污染

JS对象的属性检索

JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

看一些样例:

//原型链继承与修改
var Parent=function(){
	this.parent="this is an attribute in Parent";
}
var Children=function(){
    this.children="this is an attribute in Children";
}
Children.prototype=new Parent();//原型链继承的关键语句,把Children对应的原型对象接到了Parent对应的原型对象的前面。

//使用上述两个函数对象分别创建实例对象
var a=new Children();
var b=new Parent();
console.log(a.children);

//由于原型链继承,子实例对象具有父亲的属性。
console.log(a.parent);

//修改子实例对象的值;对其他任何东西都没有影响。
a.parent='???';
var c=new Children();
console.log(c.parent);

//修改父实例对象的值;对其他任何东西也没有影响。
b.parent='???';
console.log(c.parent);

//使用原型链;此处修改就是原型对象了,而不是实例对象。会对所有以 该原型对象对应的函数对象 生成的实例对象产生影响,!!包含已经实例化了的!!
a.__proto__.parent='???';
console.log(c.parent);

//不过,它不能对子类原有的属性产生影响;因为即使在父类原型对象里修改了该属性,在子类构造方法里,它也会被覆盖为正确的值。
a.__proto__.children='?????';
console.log(c.children);

回显:

this is an attribute in Children
this is an attribute in Parent
this is an attribute in Parent
this is an attribute in Parent
???
this is an attribute in Children

也就是说,我们只要能控制一个子实例对象的内容(属性),就可以控制所有其祖先原型对象,以及用它的祖先原型对象生成的实例对象

这是一个比较高的权限;很容易产生漏洞。

一个遗留问题

var Parent=function(){
	this.parent="this is an attribute in Parent";
}
var Children=function(){
    this.children="this is an attribute in Children";
}
Children.prototype=new Parent();
var a=new Children();
console.log(a.children);
console.log(a.constructor);
console.log(a.__proto__.constructor);
console.log(a.__proto__.__proto__.constructor);
console.log(a.__proto__.constructor.__proto__.constructor);
console.log(a.__proto__.__proto__.__proto__.constructor);

回显:

this is an attribute in Children
ƒ (){
	this.parent="this is an attribute in Parent";
}
ƒ (){
	this.parent="this is an attribute in Parent";
}
ƒ (){
	this.parent="this is an attribute in Parent";
}
ƒ Function() { [native code] } //构造函数Function
ƒ Object() { [native code] } //构造函数Object

我对于第二个回显很不理解,其他几个回显也没完全搞清楚。

实际污染样例01--Merge()

function merge(target,source){
    for (let key in source){
        console.log('key is: ',key,' ',source[key]);
        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}}')
console.log(object2)
merge(object1,object2)
console.log(object1.a,object1.b)
object3={}
console.log(object3)

在实际环境中,我们能够得到的权限往往比之前所述的要少。一般来说,我们最多有任意传参(JSON格式对象)的权限,而不能像之前那样直接对 对象相关属性 进行命令执行。

此处能够进行原型链污染的核心原因在于JSON和JS本身解析方式的不同:我们传入的参数,在经过JSON解析后,"__proto__"被认为是一个没有特殊意义的普通的键名(就跟"a"一样);但是,在Merge函数中,key扫到"__proto__"时,执行的target[key]=source[key]命令就变成了object1.__proto__=object2.__proto__,导致Object原型函数被修改,进而影响了所有实例对象。

posted @ 2022-04-29 12:17  hiddener  阅读(39)  评论(0编辑  收藏  举报