Outcast
Description
Author: YesWeHack
YesWeHack has provided this CTF challenge, and they state: “This challenge is meant to be run as a black-box environment. The source code is intentionally not provided.”
Light enumeration is permitted for this challenge.
NOTE, the flag for this challenge is not in the standard flag format. The format of the flag is with a flag{} wrapper but with l33tsp3@k! inside the curly braces.
Special thanks to YesWeHack for the sponsorship and support of the NahamCon CTF!
Exploitation
Enumeration
Looking into the source code, we disclosed another endpoint /test
Here is what we find inside the test
endpoint that was commented out but developer forgot to remove it:
As mentioned, we got access to two methods so far: getusers
and getversion
. Remembering that we are in the api caller
endpoint that is not supposed to be accessible. The version is => v1.3.37
but that lead us no where.
We also found this php
code following the Download template code
link here below:
Here is the code we downloaded:
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
43
44
45
46
47
48
49
50
<?php
class APICaller {
private $url = 'http://localhost/api/';
private $path_tmp = '/tmp/';
private $id;
public function __construct($id, $path_tmp = '/tmp/') {
$this->id = $id;
$this->path_tmp = $path_tmp;
}
public function __call($apiMethod, $data = array()) {
$url = $this->url . $apiMethod;
$data['id'] = $this->id;
foreach ($data as $k => &$v) {
if ( ($v) && (is_string($v)) && str_starts_with($v, '@') ) {
$file = substr($v, 1);
if ( str_starts_with($file, $this->path_tmp) ) {
$v = file_get_contents($file);
}
}
if (is_array($v) || is_object($v)) {
$v = json_encode($v);
}
}
// Call the API server using the given configuraions
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array('Accept: application/json'),
));
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if (!empty($error)) {
throw new Exception($error);
}
return $response;
}
}
Code review
Now it’s time to review the code of the API caller. We noticed that the APICaller
class is a callable class that can be called using the __call
method as the main method. We also noticed that the APICaller
class has a url
property that is set to http://localhost/api/
and a path_tmp
property that is set to /tmp/
. Let’s add to that the unsanitized $apiMethod
and $data
parameter that is passed to the __call
method. We also see a dangerous file_get_contents
call that is used to read files from the local file system, which arises a security concern since it allows arbitrary file access when the input is not properly sanitized. To add to our findings, in order to trigger the file_get_contents
call, we can use the @
symbol to inject a file path that starts with /tmp/
. We can then read the contents of the file. Finally, we see a curl call that is used to make a POST request to the API server. We can use this to send a request to the server and extract the response from the server with a file_get_contents
call in order to read the files from the local file system.
Login page Enumeration
We find out that we could not send a login request to the server, so looking into the source code of the login page, we find the following:
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
<form class="space-y-6" action="#" method="POST">
<div>
<label for="username" class="block text-sm/6 font-medium text-gray-900">Username</label>
<div class="mt-2">
<input
type="text"
name="username"
id="username"
value=""
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-red-700 sm:text-sm/6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label>
</div>
<div class="mt-2">
<input
type="password"
name="password"
value=""
id="password"
required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-red-700 sm:text-sm/6">
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-red-500 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-red-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-700">
Sign in</button>
</div>
</form>
The action is marked as #
in the form, so wathever we put in the username and password fields will be sent to the server as a POST request but not the login request.
API caller Exploitation
Now that we can try to use the API and start injecting some input to the server and see what happens, we injected the following:
After some trials and errors, we remembered that we had an /api/
endpoint:
So we discovered a path traversal vulnerability in the $apiMethod
parameter that we were using to call the API.
Here we confirmed the vulnerability by injecting the following: ../api/
endpoint.
Then we realized from our assumptions about the login page that our request were not being submitted, so we could actually use the API to send a POST request to the server and get the response on the API caller endpoint. This is for a writeup purpose but honestly, I spent hours trying to figure out how to do this and I was not sure if it was even possible. But after some trials and errors, we found that we could use the file_get_contents
function to read the contents of the file and send it as a POST request to the server using one of the parameters of the login form. First we needed to confirm if we could access the login page as we did with the /api/
endpoint.
We found that we could access the login page by injecting the following: /login/
endpoint. Now, we can abuse the file_get_contents
function to read the contents of the file and send it as a POST request to the server using one of the parameters of the login form.
We injected the following to test => username=@/tmp/../etc/passwd
in the parameters of the API caller. It worked as we were able to leak the contents of the /etc/passwd
file.
Then finally we injected the following payload to get the flag:
This was interesting because we had to chain vulnerabilities to get the flag. I really thought this was a clever attack and I was impressed by the challenge as it was ranked as easy. I would rank it as medium if I had to do it again. Because of the time spent figuring it out. Overall I am happy with the challenge and I learned a lot from it.
Here is the result in clear text:
Flag
flag{ch41n1ng_bug$_1s_w0nd3rful!}
Happy hacking ! 😊