0xGame碰到了原型链污染的题,记录一下学习过程
因为第一次接触,所以先了解了Javascript
这东西的特性,先看一段简单的构造类的方法:
1 2 3 4 5 6 7 8 9 10 11
| function Person(){ this.name="rendongjun"; this.test=function () { return 333;
} } Person.prototype.a=3; web=new Person(); console.log(web.test()); console.log(web.a)
|
什么是原型继承?
每个JavaScript对象都有一个原型对象,它是一个普通的对象,包含了一些共享的属性和方法。当我们创建一个新对象时,它会自动继承原型对象的属性和方法。
在JavaScript中,我们可以通过修改原型对象来实现属性和方法的继承。
Person.prototype.a = 3
这段代码就是在Person
构造函数的原型对象上添加了一个名为a
的属性,其值为3。这意味着通过Person
构造函数创建的所有对象实例都会继承这个a
属性,并且可以通过对象实例访问和使用它。
换句话说,当我们创建一个person
对象实例,比如web = new person()
,这个实例会继承person
构造函数的原型对象上的属性和方法。因此,web
对象可以访问和使用a
属性,即web.a
会返回3。
__proto__
属性指向它的构造函数的prototype
属性。
什么是原型链?
原型链是JavaScript中一种对象之间的关系模型,它用于实现对象的继承和属性查找。
在JavaScript中,每个对象都有一个原型对象(prototype),原型对象也是一个对象,它包含了一些共享的属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(即Object.prototype)。
原型链的关系可以通过对象的__proto__
属性来表示。每个对象都有一个__proto__
属性,它指向该对象的原型对象。原型对象也有自己的原型对象,这样就形成了一个链式结构,即原型链。
constructor
每个实例对象都有一个 constructor
属性指向对应的构造函数,即类。所以以下几种写法其实是相等的,都返回 Foo
类的原型对象。
1 2 3 4
| Foo.prototype foo["__proto__"] foo.__proto__ foo.constructor.prototype
|
大段大段的文字总是让人头疼,写个简单代码体会一下:
1 2 3 4 5 6
| function Foo(){}; undefined let foo = new Foo(); undefined Foo.prototype == foo.__proto__ true
|
以及
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function Foo() { this.first_name = 'Donald' this.last_name = 'Trump' }
function Son() { this.first_name = 'Melania' }
Son.prototype = new Foo()
let son = new Son() son.__proto__['add_name'] = 'abc' let son1 = new Son(); console.log(`son Name: ${son.add_name}`)
console.log(`son1 Name: ${son.add_name}`)
|
我们明明只修改了son
对象的__proto__
属性,但由于有其它对象的原型属性指向了相同的原型属性(即son1.__proto__
与其相同),导致另外一个具有相同原型的对象也受到了影响。
再看一段js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let foo = {bar: 1}
console.log(foo.bar)
foo.__proto__.bar = 2
console.log(foo.bar)
let zoo = {}
console.log(zoo.bar)
|
再通过一段代码加深加深印象 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function Foo() { this.first_name = 'D' this.last_name = 'K' function Son() { this.first_name = 'A' } Son.prototype = new Foo() let son = new Son() son.last_name
son.__proto__ Foo.add_name='viper' son.add_name Son.add_name Foo Foo.prototype['add_name']='notbad' son.add_name Son.add_name
JavaScript 的查找机制如下:
在对象son中寻找last_name 如果找不到,则在son.__proto__中寻找last_name 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name 依次寻找,直到找到null结束。比如,Object.prototype 的 __proto__就是 null
|
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
总结一下:
1.构造函数.prototype指向的是一个对象(原型)。
2.任何对象都有一个原型对象,这个原型对象由对象的内置属性__proto__指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的。
3.只有构造函数内才有ptorotype属性。
4.每个对象都内含有一个属性:proto,也就是说就算对象里面没有对这个属性进行赋值,那么也是有这个属性的。
5.原型链的核心就是依赖对象__proto__的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype的进行查找,直至查找到Object时,就没有指向了。如果最终查找失败会返回undefined或报错。
通过一段java
代码对比两者的异同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package Test;
class Father{ public String name; } class Son extends Father{ public Son(){ super.name = "father"; } void alert() { System.out.println("i am son"); } } public class Test { public static void main(String args[]) { Son s1 = new Son(); System.out.println(s1.name); s1.name = "son"; System.out.println(s1.name); Son s2 = new Son(); System.out.println(s2.name); } }
father son father
|
java是基于对象继承,javascript是基于原型继承
污染原理
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象
利用手段
现在我们大致可以知道:如果可以通过某种方法控制对象.__proto__
的值,那我们就可以间接修改继承该原型对象的所有对象。
1.对象merge 对象clone(其实内核就是将待操作的对象merge到一个空对象中) 以对象merge为例,我们想象一个简单的merge函数:
1 2 3 4 5 6 7 8 9 10
| 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] } } }
|
这里如果我们让key是__proto__
的话那么是不是就可以对原型造成影响,最终影响到实例化出来的类呢?
举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 = {"a": 1, "__proto__": {"b": 2}} merge(o1, o2) console.log(o1.a, o1.b) console.log(o1) console.log(o1.__proto__) o3 = {} console.log(o3.b) console.log(o3)
|
为什么o1
被污染了而o3
没有被污染?
打开node看下o2
的key
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Welcome to Node.js v18.18.0. Type ".help" for more information. > let o2 = {a:1,"__proto__":{b:2}} undefined > for ( let key in o2){console.log(key);} a b undefined > o2 { a: 1 } > o2.__proto__ { b: 2 } > o2.__proto__.__proto__ [Object: null prototype] {} //最上层的object,并没有任何属性 > o2.__proto__.__proto__.__proto__ null >可以看到o2的key只有a,b没有我们需要的__proto__,并注意最后几行!
|
原因很好理解:在o2 = {a:1,"__proto__":{b:2}}
中,"__proto__":{b:2}
等价于o2.__proto__={b:2}
,也就是o2
这个实例的__proto__
属性。__proto__
并没被当成键名解析。
我们需要这样修改(利用JSON.parse
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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) console.log(o1.__proto__) o3 = {}
console.log(o3.b) console.log(o3.__proto__)
|
简单分析下运行过程:
首先是o1[a]=02[a]=1
,然后进行o1.__proto__
和o2.__proto__
(注意这是共有的,所以进行循环merge
过程),即o1.__proto__.b=o2.__proto__.b=2
,即Object.prototype.b=2
。
也就是最顶层的Object.prototype
所指向的对象添加了属性,所以我们随便创建一个对象也就有了b这个属性(间接拥有,会向上查一个.proto__
)。
参考文章:
1 2 3
| https://jlkl.github.io/2020/11/06/Web_16/ https://blog.csdn.net/qq_51586883/article/details/119867720 https://xz.aliyun.com/t/7182
|