AngstromCTF 2018 : File Storer

This challenge was the 7 of the 8 proposed by Angstrom at this contest.
Challenge
We follow the given link (http://web2.angstromctf.com:8899/) and it shows a login page.

Fig 1 - Login page
Once we create an account and log into it, we see that the application is allowing us to add files through an URL.

Fig 2 - Home
Exploit
After several tries like Remote File Inclusion, with no good result we tried discovering directories (which also was a hint given further).
So we found a .git/HEAD file, here we go, we extract the whole directory with GitTools (https://github.com/internetwache/GitTools). Now we can simply extract all the informations we can find in the .git directory. As you can see we retrieve informations about commits, the tree and files’ content:

Fig 3 - .git directory

Fig 4 - Informations about the first commit

Fig 5 - Versions and files
We quickly notice that the interesting part of the challenge is the index.py file:
from flask import Flask, request, render_template, abort
import os, requests
app = Flask(__name__)
class user:
def __init__(self, username, password):
self.username = username
self.__password = password
self.files = []
def getPass(self):
return self.__password
users = {}
users["admin"] = user("admin", os.environ["FLAG"])
@app.errorhandler(500)
def custom500(error):
return str(error), 500
@app.route("/", methods=["GET", "POST"])
def mainpage():
if request.method == "POST":
if request.form["action"] == "Login":
if request.form["username"] in users:
if request.form["password"] == users[request.form["username"]].getPass():
return render_template("index.html", user=users[request.form["username"]])
return "wrong password"
return "user does not exist"
elif request.form["action"] == "Signup":
if request.form["username"] not in users:
users[request.form["username"]] = user(request.form["username"], request.form["password"])
return render_template("index.html", user=users[request.form["username"]])
else:
return "user already exists"
elif request.form["action"] == "Add File":
return addfile()
return render_template("loggedout.html")
#beta feature for viewing info about other users - still testing
@app.route("/user/<username>", methods=['POST'])
def getInfo(username):
val = getattr(users[username], request.form['field'], None)
if val != None: return val
else: return "error"
@app.route("/files/<path:file>", methods=["GET"])
def getFile(file):
if "index.py" in file:
return "no! bad user! bad!"
return open(file, "rb").read()
def addfile():
if users[request.form["username"]].getPass() == request.form["password"]:
if request.form['url'][-1] == "/": downloadurl = request.form['url'][:-1]
else: downloadurl = request.form['url']
if downloadurl.split("/")[-1] in os.listdir("."):
return "file already exists"
file = requests.get(downloadurl, stream=True)
f = open(downloadurl.split("/")[-1], "wb")
first = True
for chunk in file.iter_content(chunk_size=1024*512):
if not first: break
f.write(chunk)
first = False
f.close()
users[request.form["username"]].files.append(downloadurl.split("/")[-1])
return render_template("index.html", user=users[request.form["username"]])
return "bad password"
if name == "__main__": app.run(host="0.0.0.0")
I guess you probably noticed the /user/

Fig 6 - Get username

Fig 7 - Trial for __password

Fig 8 - Trial for getPass function
We did not found how we could abuse it that way to properly assign arguments to the getPass function neither to extract the __password attributes. Now the CTF is over we can see that the wanted solution was to ask for _user__password to get the password :)
But that’s not how we did to get the flag…
After a good night of sleep, we went back on it, and discovered that we could eventually extract content files of the server because when we ask for localhost/etc/passwd it stated the files already exists ! And as shown in index.py’s code, that it because the code checked the file was there, even if we did not upload such a file…

Fig 9 - localhost /etc/passwd

Fig 10 - Oups…
But now the challenge for us was to retrieve that content ! Here we go browsing the /files/ path with a hex encoding of “/” => %2F

Fig 11 - /etc/passwd extracted
Ok that sounds good, we saw in the index.py’s code that the flag we’re looking for is an environment variable :)
Browsing files such as /etc/environment, /etc/profile, /root/.bashrc (which is also ~/.bashrc), /root/.profile, /root/.bash_profile, we supposed the environment variable was set at the docker’s deployment… So what we know is that process’ environment variables are saved in /proc/PID/environ (PID is an integer). And now comes the dirty CTF script to bruteforce the PID !

Fig 12 - Dirty Bruteforce Script
We retrieved the flag :) \o/

Fig 13 - Flag
The author of the challenge was also kinda suprised we successed that way ;)

Fig 14 - kmh11, the author
Flag: actf{2_und3rsc0res_h1des_n0th1ng}
_ACKNAK_