Overview

The machine starts by discovering WingFTP portal that's vulnerable to improper neutralization of null bytes in the username parameter of the login interface that leads to lua RCE With foothold at the system we can get our hands on another user's hash and by cracking it we get in Finding that we can run binary as sudo, this binary is vulnerable to Zip Slip attack that leads to EoP to root

Enumeration

as usual start with nmap scanning

bash
jimmex@attacker  nmap -sC -sV -vv -oA results 10.129.17.81
# Nmap 7.95 scan initiated Sun Mar 29 15:21:16 2026 as: /usr/lib/nmap/nmap --privileged -sC -sV -vv -oA results -p80,22 10.129.17.81
Nmap scan report for wingdata.htb (10.129.17.81)
Host is up, received echo-reply ttl 63 (0.082s latency).
Scanned at 2026-03-29 15:21:17 EET for 13s

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 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 ttl 63 Apache httpd 2.4.66
| _http-title: WingData Solutions
| _http-server-header: Apache/2.4.66 (Debian)
| http-methods:
| _ Supported Methods: OPTIONS HEAD GET POST
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar 29 15:21:30 2026 -- 1 IP address (1 host up) scanned in 13.69 seconds

and we'll see we got only two ports open

  1. 22 running OpenSSH which isn't vulnerable to any public CVE (SSH rarely is)
  2. 80 which runs http and redirects to wingdata.htb

so lets add this to our hosts file and see what the site looks like

plaintext
10.129.17.81   wingdata.htb

got a static webpage that has mostly every thing static except that portal button Pasted image 20260329173912.png and when we click that portal button we get Pasted image 20260329174003.png

so it is a virtual hosting add it to your hosts file

plaintext
10.129.17.81   wingdata.htb ftp.wingdata.htb

and lets see what it is Pasted image 20260329174120.png and it is Wing FTP Server v7.4.3 this version is vulnerable to attack that was well-known a while back cause it affected a lot of environments so for this to work the anonymous login must be enabled so lets see if we can login Pasted image 20260329174307.png and we got in but we didn't need that i just needed to check something that i might get to later now we know that his version is vulnerable to CVE-2025-47812 which an unauthenticated remote code execution (RCE) flaw that allows attackers to execute arbitrary commands without valid login credentials. This vulnerability stems from improper neutralization of null bytes in the username parameter of the login interface, enabling a NULL-byte authentication bypass

CVE-2025-47812

The c_CheckUser() function uses strlen() to check the username, which stops at a NULL byte \0. But the session file writer uses the full unsanitized username including everything after the NULL byte

Once we login we are assigned a cookie called UID and this UID is used to store session in sessions folder under this syntax UID.session

now Wing FTP embeds a Lua interpreter that runs server-side scripts to generate dynamic HTML responses. Every page like /dir.html is essentially a Lua script and the normal session would look like this

lua
username = "anonymous"
homedir = "/files"

but if we tried to embed a malicious code inside that session Lua via that unsanitized username it will execute it as code and it would be something like this

lua
username = "anonymous"]]
local h = io.popen("id")
local r = h:read("*a")
h:close()
print(r)

and this will execute ID so lets try to do that manually Pasted image 20260329181705.png now if we used the same UID to request dir.html Pasted image 20260329181920.png now this didn't work and we got time out as a response because we'll find out later once we get foothold that the session expires in 5 seconds only and the copying and moving between tabs and the second request that takes like 2-3 seconds to respond we can't do it manually we either have to use burp macro or I'll just cheat a little and use the exploit Pasted image 20260329182116.png and as you can see we got command execution lets try to get a reverse shell if we tried to use something like bash -c "bash -i >& /dev/tcp/etc.. we probably gonna break some thing and we can't also use | pipes so lets hope the box has busybox and nc on it and we got a shell back lets get a full TTY and start enumeration Pasted image 20260329182356.png usually when i drop in a shell i try to see what are the available home directories on the system (in this case wacky) and try to find creds for these users after poking around i found this path /opt/wftpserver/Data/1/users/wacky.xml and we got a hash for that user so lets try to crack it Pasted image 20260329182735.png

as you can see here the timeout 5 seconds for the session

when i tried to crack the hash i couldn't know what is the hash format cause it might be a lot of things but i just went with SHA-256 but it didn't work so i tried to search how does WingFTP store passwords and i found this Pasted image 20260329183215.png there is default salt WingFTP and when i went back to the machine Pasted image 20260329183302.png it actually gave a hint about this so i went back to the configuration and did grep -r salt and found this Pasted image 20260329183610.png so lets see what is the value of this SALTING STRING OPTIONS and ENABLE PASS SALTING so now i know that there is a salt string configured and by looking for it i found it

plaintext
/opt/wftpserver/Data/1/settings.xml:    <SaltingString>WingFTP</SaltingString>

i mean yeah after all it was the default but we learned something now what we need to do is to put the hash in that format

plaintext
hashed_pass_we_found:WingFTP

and start cracking it

bash
jimmex@attacker ~/htb/labs/wingdata  hashcat -m 1410 wacky.hash /usr/share/wordlists/rockyou.txt --show
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5

and we got that so lets ssh in Pasted image 20260329185140.png

just note that i only use sshpass here to show you the password without the interactive shell but never use this on in real environment pentesting

and we got the user Pasted image 20260329185302.png so lets look at what we can do Pasted image 20260329185329.png we can run that file as a root, lets inspect it and see what it does the script uses a very dangerous function Pasted image 20260329185702.png that extractall() with filter="data" validates each tar member individually in isolation, but fails to account for symlink resolution at extraction time allowing a hardlink to follow a symlink outside the intended extraction directory, resulting in arbitrary file write so if we can get that to write to a file like the sudeors then we can run bash as root

CVE-2025-4517

Python's filter="data" works by calling os.path.realpath() on each tar member's path to check if it stays inside the extraction directory.

But os.path.realpath() has a bug when a resolved path exceeds 4096 characters (PATH_MAX), it stops resolving symlinks and switches to pure string manipulation String manipulation just removes .. components from the string it doesn't follow what they actually point to on the filesystem.

so here is what we need to

  1. Create Deep Nested Directories to confuse the extractor
  2. Create a Symlink That Escapes to /etc
  3. Create a Hardlink Through the Symlink to /etc/sudoers
  4. Write the Sudoers Entry to the Hardlink
  5. Pack Everything into a Tar (in our case we need it to call it in this format backup_0001.tar)
  6. then we'll trigger the script to do the dirty work

Using this PoC I modified the script with these

python
DEST_DIR = "/opt/backup_clients/restored_backups/restore_pwn/"
# Count depth cause it matters here opt(1) backup_clients(2) restored_backups(3) restore_pwn(4)
DEPTH_TO_ROOT = 4
TARGET_FILE = "etc/sudoers"
PAYLOAD = b"wacky ALL=(ALL) NOPASSWD: ALL\n"
OUTPUT = "backup_9999.tar"

now run the exploit Pasted image 20260329192045.png it will give you a file host it on web server and move it to the target Pasted image 20260329192124.png download it and move it to the backup folder in the script folder

now all what's left to do is to run that script Pasted image 20260329192201.png and as you can see now we can run any command with NOPASSWD so we'll just bash and get root Pasted image 20260329192249.png and just like that we got root

Back to Why i logged in in the form

when i first saw the exploit a while ago i was wondering how is it unauthenticated RCE when it needs anonymous to be enabled so i can do username=anonymous%00Payload_here so now i know that anonymous is a valid username (till now i don't know why didn't i try to replicate it when i first saw it xD) lets see what if we tried something like DoesnotExist as username Pasted image 20260329192616.png

and as you can see i changed the username and still exploits it then i went to reread that CVE details again and found that even though the Server receives username DOESNOTEXIST\x00<lua>and the auth fails but the session file was already written and the UID is returned regardless

Resources