rapid7/metasploit-framework

View on GitHub
lib/msf/base/sessions/meterpreter.rb

Summary

Maintainability
F
3 days
Test Coverage
# -*- coding: binary -*-
require 'rex/post/meterpreter/client'
require 'rex/post/meterpreter/ui/console'

module Msf
module Sessions

###
#
# This class represents a session compatible interface to a meterpreter server
# instance running on a remote machine.  It provides the means of interacting
# with the server instance both at an API level as well as at a console level.
#
###

class Meterpreter < Rex::Post::Meterpreter::Client

  include Msf::Session
  #
  # The meterpreter session is interactive
  #
  include Msf::Session::Interactive
  include Msf::Session::Comm

  #
  # This interface supports interacting with a single command shell.
  #
  include Msf::Session::Provider::SingleCommandShell

  include Msf::Sessions::Scriptable

  # Override for server implementations that can't do SSL
  def supports_ssl?
    true
  end

  # Override for server implementations that can't do zlib
  def supports_zlib?
    true
  end

  def tunnel_to_s
    if self.pivot_session
      "Pivot via [#{self.pivot_session.tunnel_to_s}]"
    else
      super
    end
  end

  #
  # Initializes a meterpreter session instance using the supplied rstream
  # that is to be used as the client's connection to the server.
  #
  def initialize(rstream, opts={})
    super

    opts[:capabilities] = {
      :ssl => supports_ssl?,
      :zlib => supports_zlib?
    }

    # The caller didn't request to skip ssl, so make sure we support it
    if not opts[:skip_ssl]
      opts.merge!(:skip_ssl => (not supports_ssl?))
    end

    #
    # Parse options passed in via the datastore
    #

    # Extract the HandlerSSLCert option if specified by the user
    if opts[:datastore] and opts[:datastore]['HandlerSSLCert']
      opts[:ssl_cert] = opts[:datastore]['HandlerSSLCert']
    end

    # Extract the MeterpreterDebugBuild option if specified by the user
    if opts[:datastore]
      opts[:debug_build] = opts[:datastore]['MeterpreterDebugBuild']
    end

    # Don't pass the datastore into the init_meterpreter method
    opts.delete(:datastore)

    # Assume by default that 10 threads is a safe number for this session
    self.max_threads ||= 10

    #
    # Initialize the meterpreter client
    #
    self.init_meterpreter(rstream, opts)

    #
    # Create the console instance
    #
    self.console = Rex::Post::Meterpreter::Ui::Console.new(self)
  end

  def exit
    begin
      self.core.shutdown
    rescue StandardError
      nil
    end
    self.shutdown_passive_dispatcher
    self.console.stop
  end
  #
  # Returns the session type as being 'meterpreter'.
  #
  def self.type
    "meterpreter"
  end

  #
  # Calls the class method
  #
  def type
    self.class.type
  end

  def self.can_cleanup_files
    true
  end

  ##
  # :category: Msf::Session::Provider::SingleCommandShell implementors
  #
  # Create a channelized shell process on the target
  #
  def shell_init
    return true if @shell

    # COMSPEC is special-cased on all meterpreters to return a viable
    # shell.
    sh = sys.config.getenv('COMSPEC')
    @shell = sys.process.execute(sh, nil, { "Hidden" => true, "Channelized" => true })

  end

  def bootstrap(datastore = {}, handler = nil)
    session = self

    # Configure unicode encoding before loading stdapi
    session.encode_unicode = datastore['EnableUnicodeEncoding']

    session.init_ui(self.user_input, self.user_output)

    initialize_tlv_logging(datastore['SessionTlvLogging']) unless datastore['SessionTlvLogging'].nil?

    verification_timeout = datastore['AutoVerifySessionTimeout']&.to_i || session.comm_timeout
    begin
      session.tlv_enc_key = session.core.negotiate_tlv_encryption(timeout: verification_timeout)
    rescue Rex::TimeoutError
    end

    if session.tlv_enc_key.nil?
      # Fail-closed if TLV encryption can't be negotiated (close the session as invalid)
      dlog("Session #{session.sid} failed to negotiate TLV encryption")
      print_error("Meterpreter session #{session.sid} is not valid and will be closed")
      # Terminate the session without cleanup if it did not validate
      session.skip_cleanup = true
      session.kill
      return nil
    end

    # always make sure that the new session has a new guid if it's not already known
    guid = session.session_guid
    if guid == "\x00" * 16
      guid = [SecureRandom.uuid.gsub(/-/, '')].pack('H*')
      session.core.set_session_guid(guid)
      session.session_guid = guid
      # TODO: New stageless session, do some account in the DB so we can track it later.
    else
      # TODO: This session was either staged or previously known, and so we should do some accounting here!
    end

    session.commands.concat(session.core.get_loaded_extension_commands('core'))

    # Unhook the process prior to loading stdapi to reduce logging/inspection by any AV/PSP
    if datastore['AutoUnhookProcess'] == true
      console.run_single('load unhook')
      console.run_single('unhook_pe')
    end

    unless datastore['AutoLoadStdapi'] == false

      session.load_stdapi

      unless datastore['AutoSystemInfo'] == false
        session.load_session_info
      end

      # only load priv on native windows
      # TODO: abstract this too, to remove windows stuff
      if session.platform == 'windows' && [ARCH_X86, ARCH_X64].include?(session.arch)
        session.load_priv rescue nil
      end
    end

    # TODO: abstract this a little, perhaps a "post load" function that removes
    # platform-specific stuff?
    if session.platform == 'android'
      session.load_android
    end

    ['InitialAutoRunScript', 'AutoRunScript'].each do |key|
      unless datastore[key].nil? || datastore[key].empty?
        args = Shellwords.shellwords(datastore[key])
        print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'")
        session.execute_script(args.shift, *args)
      end
    end
  end

  ##
  # :category: Msf::Session::Provider::SingleCommandShell implementors
  #
  # Read from the command shell.
  #
  def shell_read(length=nil, timeout=1)
    shell_init

    length = nil if length.nil? or length < 0
    begin
      rv = nil
      # Meterpreter doesn't offer a way to timeout on the victim side, so
      # we have to do it here.  I'm concerned that this will cause loss
      # of data.
      Timeout.timeout(timeout) {
        rv = @shell.channel.read(length)
      }
      framework.events.on_session_output(self, rv) if rv
      return rv
    rescue ::Timeout::Error
      return nil
    rescue ::Exception => e
      shell_close
      raise e
    end
  end

  ##
  # :category: Msf::Session::Provider::SingleCommandShell implementors
  #
  # Write to the command shell.
  #
  def shell_write(buf)
    shell_init

    begin
      framework.events.on_session_command(self, buf.strip)
      len = @shell.channel.write("#{buf}\n")
    rescue ::Exception => e
      shell_close
      raise e
    end

    len
  end

  ##
  # :category: Msf::Session::Provider::SingleCommandShell implementors
  #
  # Terminate the shell channel
  #
  def shell_close
    @shell.close
    @shell = nil
  end

  def shell_command(cmd, timeout = 5)
    # Send the shell channel's stdin.
    shell_write(cmd + "\n")

    etime = ::Time.now.to_f + timeout
    buff = ""

    # Keep reading data until no more data is available or the timeout is
    # reached.
    while (::Time.now.to_f < etime)
      res = shell_read(-1, timeout)
      break unless res
      timeout = etime - ::Time.now.to_f
      buff << res
    end

    buff
  end

  #
  # Called by PacketDispatcher to resolve error codes to names.
  # This is the default version (return the number itself)
  #
  def lookup_error(code)
    "#{code}"
  end

  ##
  # :category: Msf::Session overrides
  #
  # Cleans up the meterpreter client session.
  #
  def cleanup
    cleanup_meterpreter

    super
  end

  ##
  # :category: Msf::Session overrides
  #
  # Returns the session description.
  #
  def desc
    "Meterpreter"
  end


  ##
  # :category: Msf::Session::Scriptable implementors
  #
  # Runs the Meterpreter script or resource file.
  #
  def execute_file(full_path, args)
    # Infer a Meterpreter script by .rb extension
    if File.extname(full_path) == '.rb'
      Rex::Script::Meterpreter.new(self, full_path).run(args)
    else
      console.load_resource(full_path)
    end
  end


  ##
  # :category: Msf::Session::Interactive implementors
  #
  # Initializes the console's I/O handles.
  #
  def init_ui(input, output)
    self.user_input = input
    self.user_output = output
    console.init_ui(input, output)
    console.set_log_source(log_source)

    super
  end

  ##
  # :category: Msf::Session::Interactive implementors
  #
  # Resets the console's I/O handles.
  #
  def reset_ui
    console.unset_log_source
    console.reset_ui
  end

  #
  # Terminates the session
  #
  def kill(reason='')
    begin
      cleanup_meterpreter
      self.sock.close if self.sock
    rescue ::Exception
    end
    # deregister will actually trigger another cleanup
    framework.sessions.deregister(self, reason)
  end

  #
  # Run the supplied command as if it came from suer input.
  #
  def queue_cmd(cmd)
    console.queue_cmd(cmd)
  end

  ##
  # :category: Msf::Session::Interactive implementors
  #
  # Explicitly runs a command in the meterpreter console.
  #
  def run_cmd(cmd,output_object=nil)
    stored_output_state = nil
    # If the user supplied an Output IO object, then we tell
    # the console to use that, while saving it's previous output/
    if output_object
      stored_output_state = console.output
      console.send(:output=, output_object)
    end
    success = console.run_single(cmd)
    # If we stored the previous output object of the channel
    # we restore it here to put everything back the way we found it
    # We re-use the conditional above, because we expect in many cases for
    # the stored state to actually be nil here.
    if output_object
      console.send(:output=,stored_output_state)
    end
    success
  end

  #
  # Load the stdapi extension.
  #
  def load_stdapi
    original = console.disable_output
    console.disable_output = true
    console.run_single('load stdapi')
    console.disable_output = original
  end

  #
  # Load the priv extension.
  #
  def load_priv
    original = console.disable_output
    console.disable_output = true
    console.run_single('load priv')
    console.disable_output = original
  end

  def update_session_info
    # sys.config.getuid, and fs.dir.getwd cache their results, so update them
    fs&.dir&.getwd
    username = self.sys.config.getuid
    sysinfo  = self.sys.config.sysinfo

    # when updating session information, we need to make sure we update the platform
    # in the UUID to match what the target is actually running on, but only for a
    # subset of platforms.
    if ['java', 'python', 'php'].include?(self.platform)
      new_platform = guess_target_platform(sysinfo['OS'])
      if self.platform != new_platform
        self.payload_uuid.platform = new_platform
        self.core.set_uuid(self.payload_uuid)
      end
    end

    safe_info = "#{username} @ #{sysinfo['Computer']}"
    safe_info.force_encoding("ASCII-8BIT") if safe_info.respond_to?(:force_encoding)
    # Should probably be using Rex::Text.ascii_safe_hex but leave
    # this as is for now since "\xNN" is arguably uglier than "_"
    # showing up in various places in the UI.
    safe_info.gsub!(/[\x00-\x08\x0b\x0c\x0e-\x19\x7f-\xff]+/n,"_")
    self.info = safe_info
  end

  def guess_target_platform(os)
    case os
    when /windows/i
      Msf::Module::Platform::Windows.realname.downcase
    when /darwin/i
      Msf::Module::Platform::OSX.realname.downcase
    when /mac os ?x/i
      # this happens with java on OSX (for real!)
      Msf::Module::Platform::OSX.realname.downcase
    when /freebsd/i
      Msf::Module::Platform::FreeBSD.realname.downcase
    when /openbsd/i, /netbsd/i
      Msf::Module::Platform::BSD.realname.downcase
    else
      Msf::Module::Platform::Linux.realname.downcase
    end
  end

  #
  # Populate the session information.
  #
  # Also reports a session_fingerprint note for host os normalization.
  #
  def load_session_info
    begin
      ::Timeout.timeout(60) do
        update_session_info

        hobj = nil

        nhost = find_internet_connected_address

        original_session_host = self.session_host
        # If we found a better IP address for this session, change it
        # up.  Only handle cases where the DB is not connected here
        if nhost && !(framework.db && framework.db.active)
          self.session_host = nhost
        end

        # The rest of this requires a database, so bail if it's not
        # there
        return if !(framework.db && framework.db.active)

        ::ApplicationRecord.connection_pool.with_connection {
          wspace = framework.db.find_workspace(workspace)

          # Account for finding ourselves on a different host
          if nhost and self.db_record
            # Create or switch to a new host in the database
            hobj = framework.db.report_host(:workspace => wspace, :host => nhost)
            if hobj
              self.session_host = nhost
              self.db_record.host_id = hobj[:id]
            end
          end

          sysinfo = sys.config.sysinfo
          host = Msf::Util::Host.normalize_host(self)

          framework.db.report_note({
            :type => "host.os.session_fingerprint",
            :host => host,
            :workspace => wspace,
            :data => {
              :name => sysinfo["Computer"],
              :os => sysinfo["OS"],
              :arch => sysinfo["Architecture"],
            }
          })

          if self.db_record
            framework.db.update_session(self)
          end

          # XXX: This is obsolete given the Mdm::Host.normalize_os() support for host.os.session_fingerprint
          # framework.db.update_host_via_sysinfo(:host => self, :workspace => wspace, :info => sysinfo)

          if nhost
            framework.db.report_note({
              :type      => "host.nat.server",
              :host      => original_session_host,
              :workspace => wspace,
              :data      => { :info   => "This device is acting as a NAT gateway for #{nhost}", :client => nhost },
              :update    => :unique_data
            })
            framework.db.report_host(:host => original_session_host, :purpose => 'firewall' )

            framework.db.report_note({
              :type      => "host.nat.client",
              :host      => nhost,
              :workspace => wspace,
              :data      => { :info => "This device is traversing NAT gateway #{original_session_host}", :server => original_session_host },
              :update    => :unique_data
            })
            framework.db.report_host(:host => nhost, :purpose => 'client' )
          end
        }

      end
    rescue ::Interrupt
      dlog("Interrupt while loading sysinfo: #{e.class}: #{e}")
      raise $!
    rescue ::Exception => e
      # Log the error but otherwise ignore it so we don't kill the
      # session if reporting failed for some reason
      elog('Error loading sysinfo', error: e)
      dlog("Call stack:\n#{e.backtrace.join("\n")}")
    end
  end

  ##
  # :category: Msf::Session::Interactive implementors
  #
  # Interacts with the meterpreter client at a user interface level.
  #
  def _interact
    framework.events.on_session_interact(self)

    console.framework = framework
    if framework.datastore['MeterpreterPrompt']
      console.update_prompt(framework.datastore['MeterpreterPrompt'])
    end
    # Call the console interaction subsystem of the meterpreter client and
    # pass it a block that returns whether or not we should still be
    # interacting.  This will allow the shell to abort if interaction is
    # canceled.
    console.interact { self.interacting != true }
    console.framework = nil

    # If the stop flag has been set, then that means the user exited.  Raise
    # the EOFError so we can drop this handle like a bad habit.
    raise EOFError if (console.stopped? == true)
  end


  ##
  # :category: Msf::Session::Comm implementors
  #
  # Creates a connection based on the supplied parameters and returns it to
  # the caller.  The connection is created relative to the remote machine on
  # which the meterpreter server instance is running.
  #
  def create(param)
    sock = nil

    # Notify handlers before we create the socket
    notify_before_socket_create(self, param)

    sock = net.socket.create(param)

    # Notify now that we've created the socket
    notify_socket_created(self, sock, param)

    # Return the socket to the caller
    sock
  end

  def supports_udp?
    true
  end

  #
  # Get a string representation of the current session platform
  #
  def platform
    if self.payload_uuid
      # return the actual platform of the current session if it's there
      self.payload_uuid.platform
    else
      # otherwise just use the base for the session type tied to this handler.
      # If we don't do this, storage of sessions in the DB dies
      self.base_platform
    end
  end

  #
  # Get a string representation of the current session architecture
  #
  def arch
    if self.payload_uuid
      # return the actual arch of the current session if it's there
      self.payload_uuid.arch
    else
      # otherwise just use the base for the session type tied to this handler.
      # If we don't do this, storage of sessions in the DB dies
      self.base_arch
    end
  end

  #
  # Get a string representation of the architecture of the process in which the
  # current session is running. This defaults to the same value of arch but can
  # be overridden by specific meterpreter implementations to add support.
  #
  def native_arch
    arch
  end

  #
  # Generate a binary suffix based on arch
  #
  def binary_suffix
    # generate a file/binary suffix based on the current arch and platform.
    # Platform-agnostic archs go first
    case self.arch
    when 'java'
      ['jar']
    when 'php'
      ['php']
    when 'python'
      ['py']
    else
      # otherwise we fall back to the platform
      case self.platform
      when 'windows'
        ["#{self.arch}.dll"]
      when 'linux' , 'aix' , 'hpux' , 'irix' , 'unix'
        ['bin', 'elf']
      when 'osx'
        ['elf']
      when 'android', 'java'
        ['jar']
      when 'php'
        ['php']
      when 'python'
        ['py']
      else
        nil
      end
    end
  end

  # These are the base arch/platform for the original payload, required for when the
  # session is first created thanks to the fact that the DB session recording
  # happens before the session is even established.
  attr_accessor :base_arch
  attr_accessor :base_platform

  attr_accessor :console # :nodoc:
  attr_accessor :skip_ssl
  attr_accessor :skip_cleanup
  attr_accessor :target_id
  attr_accessor :max_threads

protected

  attr_accessor :rstream # :nodoc:

  # Rummage through this host's routes and interfaces looking for an
  # address that it uses to talk to the internet.
  #
  # @see Rex::Post::Meterpreter::Extensions::Stdapi::Net::Config#get_interfaces
  # @see Rex::Post::Meterpreter::Extensions::Stdapi::Net::Config#get_routes
  # @return [String] The address from which this host reaches the
  #   internet, as ASCII. e.g.: "192.168.100.156"
  # @return [nil] If there is an interface with an address that matches
  #   {#session_host}
  def find_internet_connected_address

    ifaces = self.net.config.get_interfaces().flatten rescue []
    routes = self.net.config.get_routes().flatten rescue []

    # Try to match our visible IP to a real interface
    found = !!(ifaces.find { |i| i.addrs.find { |a| a == session_host } })
    nhost = nil

    # If the host has no address that matches what we see, then one of
    # us is behind NAT so we have to look harder.
    if !found
      # Grab all routes to the internet
      default_routes = routes.select { |r| r.subnet == "0.0.0.0" || r.subnet == "::" }

      default_routes.each do |route|
        # Now try to find an interface whose network includes this
        # Route's gateway, which means it's the one the host uses to get
        # to the interweb.
        ifaces.each do |i|
          # Try all the addresses this interface has configured
          addr_and_mask = i.addrs.zip(i.netmasks).find do |addr, netmask|
            bits = Rex::Socket.net2bitmask( netmask )
            range = Rex::Socket::RangeWalker.new("#{addr}/#{bits}") rescue nil

            !!(range && range.valid? && range.include?(route.gateway))
          end
          if addr_and_mask
            nhost = addr_and_mask[0]
            break
          end
        end
        break if nhost
      end

      if !nhost
        # No internal address matches what we see externally and no
        # interface has a default route. Fall back to the first
        # non-loopback address
        non_loopback = ifaces.find { |i| i.ip != "127.0.0.1" && i.ip != "::1" }
        if non_loopback
          nhost = non_loopback.ip
        end
      end
    end

    nhost
  end

end

end
end