时间: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重绑定

这里的解题详细过程:

  1. 将一个域名绑定一次到他docker.js里给定的ip地址来通过check
  2. 在他sleep(5)期间,以小于5的TTL(最好是极小)来绑定到你自己的ip地址
  3. 在你自己的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路由即可:


我啥也不会!