# -*- coding: binary -*-
module Msf


###
#
# This module exposes methods that may be useful to exploits that deal with
# servers that speak the File Transfer Protocol (FTP).
#
###
module Exploit::Remote::Ftp

  include Exploit::Remote::Tcp
  include Msf::Auxiliary::Report

  #
  # Creates an instance of an FTP exploit module.
  #
  def initialize(info = {})
    super

    # Register the options that all FTP exploits may make use of.
    register_options(
      [
        Opt::RHOST,
        Opt::RPORT(21),
        OptString.new('FTPUSER', [ false, 'The username to authenticate as', 'anonymous'], fallbacks: ['USERNAME']),
        OptString.new('FTPPASS', [ false, 'The password for the specified username', 'mozilla@example.com'], fallbacks: ['PASSWORD']),
      ], Msf::Exploit::Remote::Ftp)

    register_advanced_options(
      [
        OptInt.new('FTPTimeout', [ true, 'The number of seconds to wait for a reply from an FTP command', 16]),
        OptBool.new('FTPDEBUG', [ false, 'Whether or not to print verbose debug statements', false ]),
        OptBool.new('PassiveMode', [ false, 'Set true for extended passive (EPSV) ftp mode.', false])
      ], Msf::Exploit::Remote::Ftp)

    register_autofilter_ports([ 21, 2121])
    register_autofilter_services(%W{ ftp })

    @ftpbuff = ""

  end

  #
  # This method establishes an FTP connection to host and port specified by
  # the 'rhost' and 'rport' methods. After connecting, the banner
  # message is read in and stored in the 'banner' attribute.
  #
  def connect(global = true, verbose = nil)
    verbose = datastore['FTPDEBUG'] || datastore['VERBOSE'] if verbose.nil?

    print_status("Connecting to FTP server...") if verbose

    begin
      fd = super(global)
    rescue ::Rex::ConnectionRefused
      report_host(host: rhost)
      raise
    end

    # Wait for a banner to arrive...
    self.banner = recv_ftp_resp(fd)

    print_status('Connected to target FTP server') if verbose

    # Only record the service and banner when the greeting looks like FTP (RFC 959)
    if self.banner&.match?(/^(120|220)[\s-]/)
      # Cleaned up FTP banner
      report_service(
        host: rhost,
        port: rport,
        proto: 'tcp',
        name: 'ftp',
        info: Rex::Text.to_hex_ascii(banner_version),
        parents: {
          host: rhost,
          port: rport,
          proto: 'tcp',
          name: 'tcp'
        }
      )

      # Raw FTP banner
      report_note(
        host: rhost,
        port: rport,
        proto: 'tcp',
        sname: 'ftp',
        type: 'ftp.banner',
        data: { banner: Rex::Text.to_hex_ascii(self.banner.strip) }
      )
    end

    # Return the file descriptor to the caller
    fd
  end

  # Extracts a normalized version string from the FTP banner
  # 220 (vsFTPd 2.3.4)\x0d\x0a                                   -> vsFTPd 2.3.4
  # 220 ProFTPD 1.3.1 Server (Debian) [::ffff:10.0.0.10]\x0d\x0a -> ProFTPD 1.3.1 Server (Debian)
  def banner_version
    banner.to_s
          .sub(/^\d{3}[\s-]/, '')
          .strip
          .gsub(/\A\(|\)\z/, '')
          .gsub(/\s*\[(?:(?:\d{1,3}\.){3}\d{1,3}|[0-9A-Fa-f:]*:[0-9A-Fa-f:.]+)\]/, '')
  end

  #
  # This method handles establishing datasocket for data channel
  #
  def data_connect(mode = nil, nsock = self.sock)
    pass_mode = datastore['PassiveMode']

    if mode
      res = send_cmd([ 'TYPE' , mode ], true, nsock)
      return nil if not res =~ /^200/
    end

    # force datasocket to renegotiate
    self.datasocket.shutdown if self.datasocket != nil

    # Need to be able to do both extended and normal
    # passive modes.  normal passive mode is default
    # details of EPSV are in RFC2428
    # pass_mode = true is EPSV; false is PASV
    if pass_mode
      res = send_cmd(['EPSV'], true, nsock)
      return nil if not res =~ /^229/
      # 229 Entering Passive Mode (|||port|)
      if res =~ /\(\|\|\|(\d+)\|\)/
        # convert port to FTP syntax
        datahost = "#{rhost}"
        dataport = $1.to_i
        self.datasocket = Rex::Socket::Tcp.create(
          'PeerHost' => datahost,
          'PeerPort' => dataport,
          'Context'  => { 'Msf' => framework, 'MsfExploit' => self }
        )
      end
    else
      res = send_cmd(['PASV'], true, nsock)
      return nil if not res =~ /^227/
      # 227 Entering Passive Mode (127,0,0,1,196,5)
      if res =~ /\((\d+)\,(\d+),(\d+),(\d+),(\d+),(\d+)/
        # convert port to FTP syntax
        datahost = "#{$1}.#{$2}.#{$3}.#{$4}"
        dataport = ($5.to_i * 256) + $6.to_i
        self.datasocket = Rex::Socket::Tcp.create(
          'PeerHost' => datahost,
          'PeerPort' => dataport,
          'Context'  => { 'Msf' => framework, 'MsfExploit' => self }
        )
      end
    end

    self.datasocket
  end

  #
  # This method handles disconnecting our data channel
  #
  def data_disconnect
    begin
      if datasocket
        datasocket.shutdown
        datasocket.close
      end
    rescue IOError
    end
    datasocket = nil if datasocket
  end

  #
  # Connect and login to the remote FTP server using the credentials
  # that have been supplied in the exploit options.
  #
  def connect_login(global = true, verbose = nil)
    verbose = datastore['FTPDEBUG'] || datastore['VERBOSE'] if verbose.nil?

    ftpsock = ftp_connect(global, verbose)

    if !(user and pass)
      print_error("No username and password were supplied, unable to login")
      return false
    end

    print_status("Authenticating as #{user} with password #{pass}...") if verbose
    res = send_user(user, ftpsock)

    if (res !~ /^(331|2)/)
      print_error("The server rejected our username") if verbose
      return false
    end

    if (pass)
      print_status("Sending password...") if verbose
      res = send_pass(pass, ftpsock)
      if (res !~ /^2/)
        print_error("The server rejected our password") if verbose
        return false
      end
    end

    return true
  end

  #
  # This method logs in as the supplied user by transmitting the FTP
  # 'USER <user>' command.
  #
  def send_user(user, nsock = self.sock)
    raw_send("USER #{user}\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method completes user authentication by sending the supplied
  # password using the FTP 'PASS <pass>' command.
  #
  def send_pass(pass, nsock = self.sock)
    raw_send("PASS #{pass}\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method sends a QUIT command.
  #
  def send_quit(nsock = self.sock)
    raw_send("QUIT\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method sends one command with zero or more parameters
  #
  def send_cmd(args, recv = true, nsock = self.sock)
    cmd = args.join(" ") + "\r\n"
    ret = raw_send(cmd, nsock)
    if (recv)
      return recv_ftp_resp(nsock)
    end
    return ret
  end

  #
  # This method transmits the command in args and receives / uploads DATA via data channel
  # For commands not needing data, it will fall through to the original send_cmd
  #
  # For commands that send data only, the return will be the server response.
  # For commands returning both data and a server response, an array will be returned.
  #
  # NOTE: This function always waits for a response from the server.
  #
  def send_cmd_data(args, data, mode = 'a', nsock = self.sock)
    type = nil
    # implement some aliases for various commands
    if (args[0] =~ /^DIR$/i || args[0] =~ /^LS$/i)
      # TODO || args[0] =~ /^MDIR$/i || args[0] =~ /^MLS$/i
      args[0] = "LIST"
      type = "get"
    elsif (args[0] =~ /^GET$/i)
      args[0] = "RETR"
      type = "get"
    elsif (args[0] =~ /^PUT$/i)
      args[0] = "STOR"
      type = "put"
    end

    # fall back if it's not a supported data command
    if not type
      return send_cmd(args, true, nsock)
    end

    # Set the transfer mode and connect to the remove server
    return nil if not data_connect(mode)

    # Our pending command should have got a connection now.
    res = send_cmd(args, true, nsock)
    # make sure could open port
    return nil unless res =~ /^(150|125) /

    # dispatch to the proper method
    if (type == "get")
      # failed listings just disconnect..
      begin
        data = datasocket.get(ftp_timeout, ftp_data_timeout)
      rescue ::EOFError
        data = nil
      end
    else
      sent = self.datasocket.put(data)
    end

    # close data channel so command channel updates
    data_disconnect

    # get status of transfer
    ret = nil
    if (type == "get")
      ret = recv_ftp_resp(nsock)
      ret = [ ret, data ]
    else
      ret = recv_ftp_resp(nsock)
    end

    ret
  end

  #
  # This method transmits a FTP command and waits for a response.  If one is
  # received, it is returned to the caller.
  #
  def raw_send_recv(cmd, nsock = self.sock)
    nsock.put(cmd)
    nsock.get_once(-1, ftp_timeout)
  end

  #
  # This method reads an FTP response based on FTP continuation stuff
  #
  def recv_ftp_resp(nsock = self.sock)
    found_end = false
    resp = ""
    left = ""
    if !@ftpbuff.empty?
      left << @ftpbuff
      @ftpbuff = ""
    end
    while true
      data = nsock.get_once(-1, ftp_timeout)
      if not data
        @ftpbuff << resp
        @ftpbuff << left
        return data
      end

      got = left + data
      left = ""

      # handle the end w/o newline case
      enlidx = got.rindex(0x0a.chr)
      if enlidx != (got.length-1)
        if not enlidx
          left << got
          next
        else
          left << got.slice!((enlidx+1)..got.length)
        end
      end

      # split into lines
      rarr = got.split(/\r?\n/)
      rarr.each do |ln|
        if not found_end
          resp << ln
          resp << "\r\n"
          if ln.length > 3 and ln[3,1] == ' ' and ln[0,3] =~ /\A\d{3}\z/
            found_end = true
          end
        else
          left << ln
          left << "\r\n"
        end
      end
      if found_end
        @ftpbuff << left
        print_status("FTP recv: #{resp.inspect}") if datastore['FTPDEBUG']
        return resp
      end
    end
  end

  #
  # This method transmits a FTP command and does not wait for a response
  #
  def raw_send(cmd, nsock = self.sock)
    print_status("FTP send: #{cmd.inspect}") if datastore['FTPDEBUG']
    nsock.put(cmd)
  end

  ##
  #
  # Wrappers for getters
  #
  ##

  #
  # Returns the user string from the 'FTPUSER' option.
  #
  def user
    datastore['FTPUSER']
  end

  #
  # Returns the user string from the 'FTPPASS' option.
  #
  def pass
    datastore['FTPPASS']
  end

  #
  # Returns the number of seconds to wait for a FTP reply
  #
  def ftp_timeout
    (datastore['FTPTimeout'] || 10).to_i
  end

  #
  # Returns the number of seconds to wait to get more FTP data
  #
  def ftp_data_timeout
    (datastore['FTPDataTimeout'] || 1).to_i
  end

  alias ftp_connect connect

protected

  #
  # This attribute holds the banner that was read in after a successful call
  # to connect or connect_login.
  #
  attr_accessor :banner, :datasocket

end

end
