原型链
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现
1、prototype 属性的作用
JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象
function f() {}
typeof f.prototype // "object"
函数`f`默认具有`prototype`属性,指向一个对象
js中类的建立
js 中,定义一个类,需以定义“构造函数”的方式来定义:
function Foo() {this.bar = 1;
}new Foo();
解析:
Foo函数的内容,就是Foo类的构造函数,this.bar就表示Foo类中的一个属性
(为简化编写js的代码,ECmAScript6 后增加了class语法,但class其实只是一个语法塘)
js中的类中方法的建立
一个类中必然有一些方法,类似属性this.bar,也可将方法定义在构造函数内部
function Foo() {this.bar = 1;this.show = function() {console.log(this.bar);}
}(new Foo()).show() // 1
解析:
出现问题:新建Foo对象时,this.show = function()... 就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中
js中原型prototype的引用
在创建类时只创建一次show方法,需要使用原型(prototype)
function Foo() {this.bar = 1;
}Foo.prototype.show = function show() {console.log(this.bar);
}let foo = new Foo();
foo.show();
解析:
原型prototype是类Foo的一个属性,所有用Foo类实例化的对象,都有这个属性的所有内容,包括变量和方法。foo对象,天生具有foo.show()方法
此时Foo.prototype访问Foo类的原型,但是Foo实例化出来的对象,不能够通过prototype访问原型
2、__proto__
是 JavaScript 中一个对象的内部属性,它指向该对象的原型。原型是另一个对象,包含共享的属性和方法,对象可以通过原型继承这些属性和方法。
js 中__proto__的引用
一个 Foo 类实例化出来的 foo 对象,可通过foo.__proto__属性来访问Foo类中的原型
prototype和__proto__的定义
- prototype:一个类的属性,所有类对象在实例化的时候会拥有prototype中的属性和方法
- __proto__:一个对象的__proto__属性,指向这个对象所在的类的prototype属性
3、原型链继承
所有类对象在实例化的时候将会拥有 prototype 的属性和方法,这个特性被用来实现 js 中的继承机制
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}`)
// Name:Melania Trump
解析:
Son类继承了Father类的last_name属性
主要流程:
- 在对象son中寻找last_name
- 无法找出,则在son.__proto__中寻找last_name
- 如果仍然无法找到,则继续在son.__proto__.__proto__中寻找last_name
- 依次寻找,直到null结束。如:object.prototype的__proto__就是null
(js 中的这个查找机制,被运用在面向对象的继承中,被称为是prototype继承链)
PS:
- 每个构造函数(constructor)都有一个原型对象(prototype)
- 对象的__proto__属性,指向类的原型对象prototype
- js 使用prototype链实现继承机制
4、原型链污染
实例:
foo.__proto__指向的是Foo类的prototype。若修改foo.__proto__中的值,就可修改Foo类?
// 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);// 此时用objecr创建一个空的zoo对象
let zoo = {};// 查看zoo.bar
console.log(zoo.bar);
解析:
修改 foo 原型foo.__proto__.bar = 2,而 foo 是一个object类的实例,所以实际上是修改了object这个类,给这个类增加了一个属性bar,值为2
后来用object类创建了一个zoo对象,let zoo = {},zoo对象自然也有一个bar属性了
原型链污染定义:
如果攻击者控制并修改了一个对象的原型,那将可以影响所有和这个对象来自同一个类、父类的对象,这种攻击方式就是原型链污染
哪些情况原型链会被污染?
哪些情况可以设置__proto__的值?找到能够控制数组(对象)的“键名”的操作即可:
使用megre测试
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];}}
}
merge操作是最常见可能控制键名的操作,也最能被原型链攻击
在合并过程中,存在赋值的操作 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);
结果:合并成功,原型链未被污染
解析:
js 创建 o2 的过程(let o2 = {a:2,"__proto__":{b:2}})中,__proto__代表o2的原型了,此时遍历o2的所有键名,拿到的是[a,b],__proto__并不是一个key,也不会修改object的原型
修改代码
let o1 = {};
let o2 = JSON.parse('{"a":1,"__proto__":{"b":2}}');
merge(o1,o2);
console.log(o1.a,o2.b);o3 = {};
console.log(o3.b);
解析:
JSON解析的情况下,__proto__会被认为是一个真正的“键名”,不代表原型,所以在遍历o2的时候存在这个键,
新建的o3对象,也存在b属性,说明object已被污染
实例
案例一
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
//目前user并没有admintoken
var user = []; //empty for nowvar matrix = [];
for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];
}function draw(mat) {var count = 0;for (var i = 0; i < 3; i++){for (var j = 0; j < 3; j++){if (matrix[i][j] !== null){count += 1;}}}return count === 9;
}app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);app.get('/', (req, res) => {for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];}res.render('index');
})app.get('/admin', (req, res) => { /*this is under development I guess ??*/console.log(user.admintoken);if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');} else {res.status(403).send('Forbidden');}
}
)app.post('/api', (req, res) => {var client = req.body;var winner = null;if (client.row > 3 || client.col > 3){client.row %= 3;client.col %= 3;}matrix[client.row][client.col] = client.data;for(var i = 0; i < 3; i++){if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){if (matrix[i][0] === 'X') {winner = 1;}else if(matrix[i][0] === 'O') {winner = 2;}}if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){if (matrix[0][i] === 'X') {winner = 1;}else if(matrix[0][i] === 'O') {winner = 2;}}}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){winner = 1;}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){winner = 2;} if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){winner = 1;}if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){winner = 2;}if (draw(matrix) && winner === null){res.send(JSON.stringify({winner: 0}))}else if (winner !== null) {res.send(JSON.stringify({winner: winner}))}else {res.send(JSON.stringify({winner: -1}))}})
app.listen(3000, () => {console.log('app listening on port 3000!')
})
解析:
获取 flag 的条件是传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在;全文没有对user.afmintoken进行赋值,理论上这个值不存在,但存在以下赋值语句
matrix[client.row][client.col] = client.data;
其中data、row、col,都是post传入的值,都是可控的。所以可构造原型链污染