学习JavaScript原型链污染攻击

0xGeekCat · 2020-8-25 · 次阅读


JavaScript原型链继承

在上一篇文章中对其已经进行过详细研究,再次就仅跟着文章简单温习一下,不再做详细解释

所有构造函数在实例化为对象的时候会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    this.first_name = 'Melania'
}

let father = new Father()
Son.prototype = father

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

Son类继承了Father类的last_name属性,最后输出的是Name: Melania Trump

对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是null

👇观察输出便于理解

截屏2020-08-25 下午8.40.45

JavaScript的这个查找机制运用在面向对象的继承中,被称作prototype继承链

不再深入研究更细节的内容,只要牢记👇几点

  1. 每个构造函数都有一个prototype原型对象
  2. 对象的__proto__属性,指向类的原型对象prototype
  3. JavaScript使用prototype链实现继承机制

原型链污染

在上一篇文章中提到过原型链继承会带来的问题之一便是父类属性被某个子类篡改,其余所有子类属性都将一同改变,在这篇文章中就会将如何利用这个理论基础实现原型链污染

let foo = {bar: 1}
foo["__proto__"].bar = 2 👈 与foo.__proto__.bar等效 此处书写目的为了方便对下文污染操作的理解
console.log(foo.bar) // 1
let zoo = {}
console.log(zoo.bar) // 2

👆修改foo的原型添加bar为2,而foo是Object的一个实例,所以实际上是修改Object,给这个Object增加了一个属性bar,值为2;后来又用Object创建了一个zoo对象,zoo对象自然也有一个bar属性

🔔在一个应用中,如果攻击者控制并修改一个对象的原型,就可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

哪些情况会被污染

这个问题等价于在哪些情况下可以设置__proto__的值

其实就是寻找能够控制数组(对象)的’’键名’’的操作

  • 对象merge
  • 对象clone

以对象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]
        }
    }
}

👆代码很简单,就是一个实现将source数组拼接到target数组后面的函数;在合并的过程中,存在赋值的操作target[key] = source[key],那么key如果是__proto__,是就可以实现原型链污染

👇做个试验

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)

o3 = {}
console.log(o3.b)

出现问题,原型链没有被污染

截屏2020-08-25 下午9.22.35

观察上文的等效写法,let o2 = {a: 1, "__proto__": {b: 2}}中插入__proto__的行为可以如👇理解

o2['__proto__'] = {b: 2} 👉 o2.__proto__ = {b: 2}

实际上是将{b: 2}设为o2的原型对象,很明显和修改Object.prototype的目的相违背;导致污染失败是因为o2__proto__被视为指向Object.prototype的原型链处理

❓如何让__proto__被认为是一个键名,使得污染生效

利用JSON格式字符串,JSON解析时,__proto__会被认为是真正的’’键名’’而不是原型链;这样便实现直接将{b: 2}写入Object.prototype的操作

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)

o3 = {}
console.log(o3.b)

截屏2020-08-25 下午9.55.30

reference

深入理解 JavaScript Prototype 污染攻击

郑重感谢