0%

(转载)JavaScript原型链污染攻击学习笔记

Js面向对象的特点

d={"d":"ddd",dd:dd()}
->Object{d:"ddd",dd:dd)()}
d.dd->fuction dd()
d.d->"ddd"
d['d']->"ddd"

也就是说在JavaScript中能够使用多种方法去操作对象。

prototype和_proto_

  • JavaScript中要定义一个类需要使用构造函数的方式去定义:

  • 例如:

function foo(){
this.bar = 1;
}
new foo()
  • 当然和其他语言的类一样能够在类内部构造方法。
  • 例如:
function Foo() {
    this.bar = 1
    this.show = function() {
        console.log(this.bar)
    }
}

(new Foo()).show()
  • 但是每次实例化类时候都会执行一次this.show = function…,这个show是绑定在对象上而非在类中。如果希望new类时候希望只创建一次show方法,那么就需要使用prototype(原型):
function Foo() {
    this.bar = 1
}

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

let foo = new Foo()
foo.show()

img

  • 我们可以认为原型prototype是类Foo的一个属性,而所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo对象,其天生就具有foo.show()方法。在js中,所有的对象都是从各种基础对象继承下来的,所以每个对象都有他的父类,通过prototype可以直接操作修改父类的对象。

img

  • 一般函数默认的prototype是一个类型为"object"的对象,它有两个属性:constructor__proto__。其中constructor属性指向这个函数自身,__proto__属性指向Object.prototype,这说明一般函数的prototype属性是由Object函数生成的.

JavaScript原型链继承

img

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

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

Son.prototype = new Father()

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

最终出来的结果很明显,son中并没有last_name这个属性那么他会到原型链上查找,那么最终打印出来的son.last_name那会是Trump

img

原型链污染攻击

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)
//最后,虽然zoo是一个空对象{},但zoo.bar的结果居然是2:

img

原因也显而易见:因为前面我们修改了foo的原型foo.proto.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。

后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。

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

哪些情况下原型链会被污染?

在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?

我们思考一下,哪些情况下我们可以设置proto的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:

对象merge
对象clone(其实内核就是将待操作的对象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]
        }
    }
}
123456789

在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是proto,是不是就可以原型链污染呢?

我们用如下代码实验一下:

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

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

结果是,合并虽然成功了,但原型链没有被污染:

img

这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “proto”: {b: 2}})中,proto已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],proto并不是一个key,自然也不会修改Object的原型。

那么,如何让proto被认为是一个键名呢?

我们将代码改成如下:

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

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

可见,新建的o3对象,也存在b属性,说明Object已经被污染:

img

这是因为,JSON解析的情况下,proto会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

文章来源

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

https://blog.csdn.net/qq_41107295/article/details/95789944

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__