rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/ftp_server.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: binary -*-
module Msf


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

  include Exploit::Remote::TcpServer

  #
  # 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(
      [
        OptPort.new('SRVPORT',    [ true, "The local port to listen on.", 21 ]),
        OptPort.new('PASVPORT',   [ false, "The local PASV data port to listen on (0 is random)", 0 ])
      ], Msf::Exploit::Remote::FtpServer)
  end

  # (see Msf::Exploit#setup)
  def setup
    super
    @state = {}
  end

  # (see TcpServer#on_client_connect)
  def on_client_connect(c)
    @state[c] = {
      :name => "#{c.peerhost}:#{c.peerport}",
      :ip   => c.peerhost,
      :port => c.peerport,
      :user => nil,
      :pass => nil,
      :cwd  => '/'
    }

    active_data_port_for_client(c, 20)

    c.put "220 FTP Server Ready\r\n"
  end

  # Dispatches client requests to command handlers.
  #
  # Handlers should be named +on_client_command_*+, ending with a
  # downcased FTP verb, e.g. +on_client_command_user+. If no handler
  # exists for the given command, returns a generic default response.
  #
  # @example Handle SYST requests
  #   class MetasploitModule < Msf::Exploit
  #     include Msf::Exploit::Remote::FtpServer
  #     ...
  #     def on_client_command_syst(cmd_conn, arg)
  #       print_status("Responding to SYST request")
  #       buf = build_exploit_buffer(cmd_conn)
  #       cmd_conn.put("215 Unix Type: #{buf}\r\n")
  #     end
  #   end
  #
  # @param  (see TcpServer#on_client_data)
  # @return (see TcpServer#on_client_data)
  def on_client_data(c)
    data = c.get_once
    return if not data

    cmd,arg = data.strip.split(/\s+/, 2)
    arg ||= ""

    return if not cmd

    # Allow per-command overrides
    if self.respond_to?("on_client_command_#{cmd.downcase}", true)
      return self.send("on_client_command_#{cmd.downcase}", c, arg)
    end

    case cmd.upcase
    when 'USER'
      @state[c][:user] = arg
      c.put "331 User name okay, need password...\r\n"
      return

    when 'PASS'
      @state[c][:pass] = arg
      print_status("#{@state[c][:name]} LOGIN #{@state[c][:user]} / #{@state[c][:pass]}")
      c.put "230 Login OK\r\n"
      return

    when 'QUIT'
      c.put "221 Logout\r\n"
      return

    when 'SYST'
      c.put "215 UNIX Type: L8\r\n"
      return

    when 'TYPE'
      c.put "200 Type is set\r\n"
      return

    when 'CWD'
      c.put "250 CWD command successful.\r\n"
      return

    when 'PWD'
      c.put "257 \"#{@state[c][:cwd]}\" is current directory.\r\n"
      return

    when 'SIZE'
      c.put "213 1\r\n"
      return

    when 'MDTM'
      c.put "213 #{Time.now.strftime("%Y%m%d%H%M%S")}\r\n"
      return

    when 'PORT'
      port = arg.split(',')[4,2]
      if(not port and port.length == 2)
        c.put("500 Illegal PORT command.\r\n")
        return
      end

      port = port.map{|x| x.to_i}.pack('C*').unpack('n')[0]
      active_data_port_for_client(c, port)

      c.put "200 PORT command successful.\r\n"
      return

    when 'PASV'
      daddr = Rex::Socket.source_address(c.peerhost)
      dport = passive_data_port_for_client(c)
      @state[c][:daddr] = daddr
      @state[c][:dport] = dport
      pasv  = (daddr.split('.') + [dport].pack('n').unpack('CC')).join(',')
      c.put "227 Entering Passive Mode (#{pasv})\r\n"
      return

    when /^(STOR|MKD|REM|DEL|RMD)$/
      c.put "500 Access denied\r\n"
      return

    else
      # Allow per-command overrides
      if(self.respond_to?("on_client_unknown_command"))
        return self.send("on_client_unknown_command", c, cmd.upcase, arg)
      end

      print_status("#{@state[c][:name]} UNKNOWN '#{cmd} #{arg}'")
      c.put("500 '#{cmd} #{arg}': command not understood.\r\n")
      return
    end

    return

  end

  def on_client_close(c)
    @state.delete(c)
  end

  def passive_data_port_for_client(c)
    @state[c][:mode] = :passive

    if(not @state[c][:passive_sock])
      s = Rex::Socket::TcpServer.create(
        'LocalHost' => '0.0.0.0',
        'LocalPort' => datastore['PASVPORT'].to_i,
        'Context'   => { 'Msf' => framework, 'MsfExploit' => self }
      )

      dport = s.getsockname[2]

      @state[c][:passive_sock] = s
      @state[c][:passive_port] = dport
      add_socket(s)
    end

    @state[c][:passive_port]
  end

  def active_data_port_for_client(c,port)
    @state[c][:mode] = :active

    connector = Proc.new {
      host = c.peerhost.dup
      sock = Rex::Socket::Tcp.create(
        'PeerHost' => host,
        'PeerPort' => port,
        'Context'   => { 'Msf' => framework, 'MsfExploit' => self }
      )
      add_socket(sock)
      sock
    }

    @state[c][:active_connector] = connector
    @state[c][:active_port]      = port
  end


  # Create a socket for the protocol data, either PASV or PORT,
  # depending on the client.
  #
  # @see http://tools.ietf.org/html/rfc3659 RFC 3659
  # @see http://tools.ietf.org/html/rfc959  RFC 959
  # @param c [Socket] Control connection socket
  #
  # @return [Socket] A connected socket for the data connection
  # @return [nil] on failure
  def establish_data_connection(c)
    begin
    Timeout.timeout(20) do
      if(@state[c][:mode] == :active)
        return @state[c][:active_connector].call()
      end
      if(@state[c][:mode] == :passive)
        c = @state[c][:passive_sock].accept
        add_socket(c)
        return c
      end
    end

    rescue ::Exception => e
      print_error("Failed to establish data connection: #{e.class} #{e}")
    end

    nil
  end

end
end