原型链污染攻击
prototype 和 _proto_是什么
JavaScript中的类的简历
在JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
function Foo() {
this.bar = 1
}
new Foo()
解析:
Foo函数的内容,就是Foo类的构造函数,而this.bar就表示Foo类中的一个属性
为了简化编写JavaScript代码,ECMAScript 6后增加了
class
语法,但class
其实只是一个语法糖
JavaScript中的类中方法的建立
一个类必然有一些方法,类似属性this.bar,我们也可以将方法定义在构造函数内部
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
解析:
这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function()... 就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中
JavaScript中原型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访问原型
JavaScript中__proto__的引用
一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo中类的原型
foo.__proto__ == Foo.prototype
prototype 和 _proto_的定义
prototype --- 是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
一个对象的__proto__属性,指向这个对象所在类的prototype属性
JavaScript中原型链继承
所有类对象在实例化的时候将会拥有中的prototype属性和方法,这个特性被用来实现JavaScript中的继承机制
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
JavaScript引擎会进行如下操作:
-
在对象son中寻找last_name
-
如果找不到,则在
son.__proto__
中寻找last_name -
如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name -
依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链
注意:
每个构造函数(constructor)都有一个原型对象(prototype)
对象的
__proto__
属性,指向类的原型对象prototype
JavaScript使用prototype链实现继承机制
原型链污染是什么
引入
在前面说过 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)
// 此时再用Object创建一个空的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__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
-
对象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]
}
}
}
在合并的过程中,存在赋值的操作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)
结果是,合并虽然成功了,但原型链没有被污染
解析:
这是因为,我们用JavaScript创建o2的过程(
let o2 = {a: 1, "__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, o1.b)
o3 = {}
console.log(o3.b)
解析:
这是因为,JSON解析的情况下,
__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键
案例
案例一
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 now
var 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.admintokn 进行赋值,所以理论上这个值是不存在的,但因为存在一下赋值语句
matrix[client.row][client.col] = client.data;
其中data
,row
,col
,都是我们post传入的值,都是可控的。所以可以构造原型链污染
本地测试