时间:2020年12月23
题目进来貌似是一个文件上传,传上文件会返回给你一个貌似路由的东西提供下载,而且只能下载文件,看来不能传马上去了。任意文件下载也没啥思路,返回题目一看,有源码。核心js源码如下
app.get('/admin', async (req, res) => {
let host = `http://${docker.ip}:${docker.port}/`
let html = ""
await req.session.files.forEach((file) => {
html += `<a href ='javascript:doPost("/admin", {"fileurl":"${host}download/${file}"})' target=''>${file}</a><br>` + "\n\n"
})
res.render("admin", {"files" : html})
})
app.post('/admin', (req, res) => {
if ( !req.body.fileurl || !check(req.body.fileurl) ) {
res.end("Invalid file link")
return
}
let file = req.body.fileurl;
//dont DOS attack, i will sleep before request
cp.execSync('sleep 5')
let options = {url : file, timeout : 3000}
request.get(options ,(error, httpResponse, body) => {
if (!error) {
res.set({"Content-Type" : "text/html; charset=utf-8"})
res.render("check", {"body" : body})
} else {
res.end( JSON.stringify({"code" : "-1", "message" : error.toString()}) )
}
});
})
在pannel.js有以上代码,以及在utils.js有如下check代码
const check = function(s) {
if (!typeof (s) == 'string' || !s.match(/^http\:\/\//))
return false
let blacklist = ['wrong', '127.', 'local', '@', 'flag']
let host, port, dns;
host = url.parse(s).hostname
port = url.parse(s).port
if ( host == null || port == null)
return false
dns = dnslookup(host);
if ( ip.isPrivate(dns) || dns != docker.ip || ['80','8080'].includes(port) )
return false
for (let i = 0; i < blacklist.length; i++)
{
let regex = new RegExp(blacklist[i], 'i');
try {
if (ip.fromLong(s.replace(/[^\d]/g,'').substr(0,10)).match(regex))
return false
} catch (e) {}
if (s.match(regex))
return false
}
return true
}
const dnslookup = function(s) {
if (typeof(s) == 'string' && !s.match(/[^\w-.]/)) {
let query = '';
try {
query = JSON.parse(cp.execSync(`curl http://ip-api.com/json/${s}`)).query
} catch (e) {
return 'wrong'
}
return checkip(query) ? query : 'wrong'
} else return 'wrong'
}
再看看其他代码,如上传下载代码的文件名都是经过不可逆hash过后的,所以不可控。
再次进行源码审计,大概能知道admin路由的功能是查看你上传的文件(而不需要经过下载),而且POST到admin的参数fileurl可控,再看POST的过程,参数fileurl会经过如下过程处理:
- 经过check函数检查一遍
- sleep(5)
- 请求一次你给的url并返回到页面中 –> request.get(fileurl)
而且注意到我们的最终目的
app.get('/flag', function(req, res){
if (req.ip === '127.0.0.1') {
res.status(200).send(env.parsed.flag)
} else res.status(403).end('not so simple');
});
那么目的就很明显了——-我们要利用ssrf来让服务器本地访问flag路由并且返回flag到admin页面中。
那么,我们只要过到check函数然后sleep(5)后就可以得到flag了。
于是check函数过滤的东西如下:
- s.match(/^http\:\/\//)
- blacklist = [‘wrong’, ‘127.’, ‘local’, ‘@’, ‘flag’]
- host == null || port == null
- ip.isPrivate(dns) || dns != docker.ip || [’80’,’8080′].includes(port)
第一点是http://开头,第二点是黑名单,第三点简而言之是必须要有端口
第四点是dnslookup过后必须是他自身的ip,而且不能是127/192等保留ip
解题
注意到sleep(5)上面有一条注释说的是防止DDOS攻击,其实并不是
这里很明显是配合dnslookup的检测来完成DNS-rebinding攻击,具体大家可以百度一下DNS重绑定
这里的解题详细过程:
- 将一个域名绑定一次到他docker.js里给定的ip地址来通过check
- 在他sleep(5)期间,以小于5的TTL(最好是极小)来绑定到你自己的ip地址
- 在你自己的VPS上开一个php -S 0.0.0.0:8000,并且在index.php写一个302 :
<?php
header("Location:http://127.0.0.1:80/flag");
4. 他的服务器第一次dnslookup的时候发现DNS会返回自己的ip,过check。5秒后再次访问这个域名,就会访问到你自己的VPS上,这时候你的index.php就会302重定向到127.0.0.1:80/flag从而返回flag
注意,极小TTL换绑定域名网上有个链接,可以以很小的TTL值一直随机绑定两个ip地址,贴给大家dns-rebinding。
POST到admin路由即可: