File Storer

AngstromCTF 2018 - Web.

AngstromCTF 2018 : File Storer

AngstromCTF Logo

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.

Login
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.

Home
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:

.git directory
Fig 3 - .git directory

Informations about the first commit
Fig 4 - Informations about the first commit

Versions and files
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/ path which could let us extract informations about User’s attributes. So what we basically tried is to ask for the admin’s password, but hey did not worked as intended because of the __ in front of password :)

Get username
Fig 6 - Get username

Trial for \_\_password
Fig 7 - Trial for __password

Trial for getPass function
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…

localhost /etc/passwd
Fig 9 - localhost /etc/passwd

Oups...
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

/etc/passwd extracted
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 !

Dirty Bruteforce Script
Fig 12 - Dirty Bruteforce Script

We retrieved the flag :) \o/

Flag
Fig 13 - Flag

The author of the challenge was also kinda suprised we successed that way ;)

kmh11, the author
Fig 14 - kmh11, the author

Flag: actf{2_und3rsc0res_h1des_n0th1ng}

_ACKNAK_