4 minute read

Today FireShell CTF 2019 wrapped up and ended. I had fun and i learned a lot. Coming into the ctf, i was expecting something a little more complicated than trivial, however it proved to be quite a challenge for my first ever serious ctf attempt. I decided to take on the most solved Web Application challenge called “Bad Injections”. With the help of my amazing friends and fellow contenders i came close to solving it, but ran out of energy at 2AM, 2 hours away from the end of the ctf.

The website greets us with 4 tabs and has a great deal of red herrings. The about page has SQL code hidden in its source file and the contact-us page has a non-functional feedback form. Really the only tab of interest is the List tab. It displays two images with one appearing to be broken. Upon closer inspection, it turns out that it’s not an image, but a text file.

It’s particularly interesting how it is retrieved:

<img src="download?file=files/test.txt&amp;hash=293d05cb2ced82858519bdec71a0354b" height="500">

The website uses /download and passes it two parameters: file path and name + a hash. Quick look in a hash analyzer tells us it’s most likely MD5, which can easily be broken.


This hash when broken reveals the first parameter,


Lets explore how we can use this newfound knowledge and issue a manual GET request to


We get an error in the response body:

<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />

Local File Inclusion

Conveniently this error gives us a path for Download.php. Knowing the file path, we can get the second parameter by hashing it and indeed, this works when we issue the request. We get the php source of Download.php. I researched directory structures of a couple php frameworks and this structure looked to me like CakePHP 2.x and to confirm it i tried getting .htaccess on the same level as the app folder. Indeed i got it. Knowing it is CakePHP, there’s no need to bruteforce or do directory discovery (and both were banned by the ctf organizers anyways), as we can just try to get the routing rules of the server from Routes.php. This gives us the list of resources for the website:

AboutUs.php, Admin.php, ContactUs.php, Custom.php, Download.php, Lista.php, Routes.php, Verify.php, index.php

Here again we encouter a bunch of red herrings: Verify.php issues a Curl command and some of the people i collaborated with thought this was exploitable, but it wasn’t. There’s this SQL code


in AboutUs.php, which is useless. ContactUS.php doesn’t do anything, neither does Index.php or Lista.php. The only two files of interest here are Admin.php and Custom.php.

First lets look how Custom is invoked in Routes.php:

  $handler = fopen('php://input','r');
  $data = stream_get_contents($handler);
  if(strlen($data) > 1){

It takes user input and calls the Test function. Now onto Custom.php:

public static function Test($string){
    $root = simplexml_load_string($string,'SimpleXMLElement',LIBXML_NOENT);
    $test = $root->name;
    echo $test;

XML External Entity Injection

The Test function takes in the input and parses it as XML. Certainly issuing a GET request to /custom doesn’t do anything and as was suggested to me by TheZakMan on the FireShell discord, i tried issuing a POST request to it and it worked. He sent it a piece of XML and discovered that it is vulnerable to Xml External Entity injection. I’ve never muddled in anything related to XML before, so it was a learning experience for me. I managed to inject the XMl code and read the first line of etc/passwd, but the rest was cut off by a million errors. Not before sixcross on discord showed me his XML code, i realized that i only got a messy response with mine because of some issue about single and double quotes.

This XML payload gave me a working injection:

<?xml version='1.0'?> 
<!DOCTYPE foo [<!ELEMENT name ANY >
<!ENTITY result SYSTEM 'file:///etc/passwd'>]>

Server-Side Request Forgery

Now onto Routes.php to take a look at how the admin page is invoked:

  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    if($_SERVER['REMOTE_ADDR'] == '' || $_SERVER['REMOTE_ADDR'] == '::1'){
     echo ";(";

As you can see here, a condition prevents us from directly accessing the /admin page:

if($_SERVER['REMOTE_ADDR'] == '' || $_SERVER['REMOTE_ADDR'] == '::1'){

Only if we come from IPV4 or IPV6 localhost can we call Admin::sort.

Now lets see what Admin.php does:

$uri = parse_url($url);
    $file = file_get_contents($url);
    $dom = new DOMDocument();
    $dom->loadXML($file,LIBXML_NOENT | LIBXML_DTDLOAD);
    $xml = simplexml_import_dom($dom);

It accepts XML input. sixcross suggested we try injecting a command to get remote code execution through this part:

usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));

Remote Code Execution

He was absolutely right. I also noticed that the first parameter “rss” is accepted as an XML, however this was the furthest i got before my brain ceased to function at 2AM.

The credit for the rest of it goes to the writeup by OmniscientSec. He discovered that not only does the first parameter accept XML, but that the second one accepts arbitrary code.

With the following payload he managed to exfil the flag with netcat:'

Funny enough, the flag fully describes my experience with this challenge:



In conclusion, this challenge has proven to be a challenge and i learned a lot when trying to solve it. I ultimately failed, but i learned so much about PHP, XML and URL parameters that i feel like in my next CTF i will certainly be able to solve a challenge of this difficulty, given enough time.

Thanks go to:

kronie for helping me understand PHP, TheZakMan for figuring out there is an XML injection, sixcross for his XML payload and figuring out RCE, OmniscientSec for solving the challenge, unlike the rest of us peasants and most importantly to FireShell CTF for making an awesome challenge.