corCTF 2022 - Msfrog Generator

  • Author: jazzpizazz
  • Date:

Msfrog Generator

Msfrog Generator is a baby web challenge that was created for corCTF 2022. The challenge consists of a webapp that can be used to create your own version of the msfrog emoji:


When the generate button is clicked the app performs an XHR request to /api/generate containing the selected items and their positions in JSON format:

1[{ "type": "mskiss.png", "pos": { "x": 0, "y": 105 } }]

The server responds with a base64 encoded result image. The fact that a filename is passed to the server looks rather unusual yet when altering the filename the server responds with: I wont pass a non existing image to a shell command lol. Any tampering with the filename results in this error indicating sufficient input sanitization is being applied by the server.

Playing around with the pos variable we can get some more information: When removing either x or y or the entire pos key altogether the server responds with Ehh I need the position to supply to imagemagick. Wikipedia states the following about ImageMagick:

ImageMagick, invoked from the command line as magick, is a free and open-source […snip…]

It is important to note here that ImageMagick is a command line application, and from earlier responses from the server we know that our user input is passed to a shell command. We know the server checks if the image filename has been tampered with, but will it do the same for the positions?

When attempting to pass it a string instead: [{"type":"mstongue.png","pos":{"x":"test","y":124}}] the server responds with: Something went wrong : b"convert-im6.q16: invalid argument for option -geometry': +test+124 @ error/convert.c/ConvertImageCommand/1672.\n"The string “test” is indeed used in the shell command despite not being a valid input. When attempting to inject a command by sending [{"type":"mstongue.png","pos":{"x":"$(id)","y":124}}] the server responds with: […snip…] invalid argument for option -geometry': +uid=33(www-data)[…snip…]

The uid=33(www-data) is in fact the first part of the output of the id command meaning we have command injection.

To get the full output of a command we can simply wrap the command in semicolons, making the return value of the shell command equal zero causing the server to send us the command output instead of a base64 encoded image: [{"type":"mstongue.png","pos":{"x":";ls -la /;","y":124}}]

2  "msfrog": "[...snip...] -rw-r--r--   1 root root   50 Aug  5 11:51 flag.txt [...snip...]"

We find a file called /flag.txt and by changing our injected command to cat /flag.txt we can read its contents:

1{ "msfrog": "corctf{sh0uld_h4ve_r3nder3d_cl13nt_s1de_:msfrog:}\n" }

Alternative route

Player remy_o#5324 was the first one to tell me about this alternative route. The server decided if the type was valid using the following check:

1# Anti haxxor check
2if not os.path.exists("./img/" + os.path.basename(type)):
3    return "I wont pass a non existing image to a shell command lol", 400

os.path.basename basically only gets the name of a file, stripping the entire path. So for example:

1>>> os.path.basename("../../../../../etc/passwd")

When later constructing the shell command the server however used the non-escaped type:

1composites.append(f"img/{type} -geometry +{x}+{y} -composite")

making remy’s payload {"type": "msnose.png; cat ../flag.txt; ./msnose.png", ...} bypass the “anti haxxor” check while also resulting in command injection.