rapid7/metasploit-framework

View on GitHub
lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/net.rb

Summary

Maintainability
F
6 days
Test Coverage
# -*- coding: binary -*-
require 'rex/post/meterpreter'
require 'rex/post/meterpreter/extensions/stdapi/command_ids'

module Rex
module Post
module Meterpreter
module Ui

###
#
# The networking portion of the standard API extension.
#
###
class Console::CommandDispatcher::Stdapi::Net

  Klass = Console::CommandDispatcher::Stdapi::Net

  include Console::CommandDispatcher
  include Rex::Post::Meterpreter::Extensions::Stdapi

  #
  # This module is used to extend the meterpreter session
  # so that local port forwards can be tracked and cleaned
  # up when the meterpreter session goes away
  #
  module PortForwardTracker
    def cleanup
      super

      if pfservice
        pfservice.deref
      end
    end

    attr_accessor :pfservice
  end

  #
  # Options for the resolve command
  #
  @@resolve_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner.' ],
    '-f' => [true,  'Address family - IPv4 or IPv6 (default IPv4)'])

  #
  # Options for the route command.
  #
  @@route_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner.'])

  #
  # Options for the portfwd command.
  #
  @@portfwd_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner.'],
    '-i' => [true,  'Index of the port forward entry to interact with (see the "list" command).'],
    '-l' => [true,  'Forward: local port to listen on. Reverse: local port to connect to.'],
    '-r' => [true,  'Forward: remote host to connect to.'],
    '-p' => [true,  'Forward: remote port to connect to. Reverse: remote port to listen on.'],
    '-R' => [false, 'Indicates a reverse port forward.'],
    '-L' => [true,  'Forward: local host to listen on (optional). Reverse: local host to connect to.'])

  #
  # Options for the netstat command.
  #
  @@netstat_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner.'],
    '-S' => [true, 'Search string.'])

  #
  # Options for ARP command.
  #
  @@arp_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner.'],
    '-S' => [true, 'Search string.'])

  #
  # List of supported commands.
  #
  def commands
    all = {
      'ipconfig' => 'Display interfaces',
      'ifconfig' => 'Display interfaces',
      'route'    => 'View and modify the routing table',
      'portfwd'  => 'Forward a local port to a remote service',
      'arp'      => 'Display the host ARP cache',
      'netstat'  => 'Display the network connections',
      'getproxy' => 'Display the current proxy configuration',
      'resolve'  => 'Resolve a set of host names on the target',
    }

    reqs = {
      'ipconfig' => [COMMAND_ID_STDAPI_NET_CONFIG_GET_INTERFACES],
      'ifconfig' => [COMMAND_ID_STDAPI_NET_CONFIG_GET_INTERFACES],
      'route'    => [
        # Also uses these, but we don't want to be unable to list them
        # just because we can't alter them.
        #COMMAND_ID_STDAPI_NET_CONFIG_ADD_ROUTE,
        #COMMAND_ID_STDAPI_NET_CONFIG_REMOVE_ROUTE,
        COMMAND_ID_STDAPI_NET_CONFIG_GET_ROUTES
      ],
      # Only creates tcp channels, which is something whose availability
      # we can't check directly at the moment.
      'portfwd'  => [],
      'arp'      => [COMMAND_ID_STDAPI_NET_CONFIG_GET_ARP_TABLE],
      'netstat'  => [COMMAND_ID_STDAPI_NET_CONFIG_GET_NETSTAT],
      'getproxy' => [COMMAND_ID_STDAPI_NET_CONFIG_GET_PROXY],
      'resolve'  => [COMMAND_ID_STDAPI_NET_RESOLVE_HOST],
    }

    filter_commands(all, reqs)
  end

  #
  # Name for this dispatcher.
  #
  def name
    'Stdapi: Networking'
  end

  #
  # Displays network connections of the remote machine.
  #
  def cmd_netstat(*args)
    if args.include?('-h')
      @@netstat_opts.usage
      return 0
    end

    connection_table = client.net.config.netstat
    search_term = nil

    @@netstat_opts.parse(args) { |opt, idx, val|
      case opt
      when '-S'
        search_term = val
        if search_term.nil?
          print_error("Enter a search term")
          return true
        else
          search_term = /#{search_term}/nmi
        end

      end
    }

    tbl = Rex::Text::Table.new(
    'Header'  => 'Connection list',
    'Indent'  => 4,
    'Columns' => [
      'Proto',
      'Local address',
      'Remote address',
      'State',
      'User',
      'Inode',
      'PID/Program name'
    ],
   'SearchTerm' => search_term)

    connection_table.each { |connection|
      tbl << [ connection.protocol, connection.local_addr_str, connection.remote_addr_str,
        connection.state, connection.uid, connection.inode, connection.pid_name]
    }

    if tbl.rows.length > 0
      print_line("\n#{tbl.to_s}")
    else
      print_line('Connection list is empty.')
    end
  end

  #
  # Displays ARP cache of the remote machine.
  #
  def cmd_arp(*args)
    if args.include?('-h')
      @@arp_opts.usage
      return 0
    end

    arp_table = client.net.config.arp_table
    search_term = nil

    @@arp_opts.parse(args) { |opt, idx, val|
      case opt
      when '-S'
        search_term = val
        if search_term.nil?
          print_error("Enter a search term")
          return true
        else
          search_term = /#{search_term}/nmi
        end
      end
    }

    tbl = Rex::Text::Table.new(
    'Header'  => 'ARP cache',
    'Indent'  => 4,
    'Columns' => [
      'IP address',
      'MAC address',
      'Interface'
    ],
    'SearchTerm' => search_term)

    arp_table.each { |arp|
      tbl << [ arp.ip_addr, arp.mac_addr, arp.interface ]
    }

    if tbl.rows.length > 0
      print_line("\n#{tbl.to_s}")
    else
      print_line('ARP cache is empty.')
    end
  end


  #
  # Displays interfaces on the remote machine.
  #
  def cmd_ipconfig(*args)
    ifaces = client.net.config.interfaces

    if (ifaces.length == 0)
      print_line('No interfaces were found.')
    else
      ifaces.sort{|a,b| a.index <=> b.index}.each do |iface|
        print("\n" + iface.pretty + "\n")
      end
    end
  end

  alias :cmd_ifconfig :cmd_ipconfig

  #
  # Displays or modifies the routing table on the remote machine.
  #
  def cmd_route(*args)
    # Default to list
    if (args.length == 0)
      args.unshift('list')
    end

    # Check to see if they specified -h
    @@route_opts.parse(args) do |opt, idx, val|
      case opt
      when '-h'
        cmd_route_help
        return true
      end
    end

    cmd = args.shift

    # Process the commands
    case cmd
      when 'list'
        routes = client.net.config.routes

        # IPv4
        tbl = Rex::Text::Table.new(
          'Header'  => 'IPv4 network routes',
          'Indent'  => 4,
          'Columns' => [
            'Subnet',
            'Netmask',
            'Gateway',
            'Metric',
            'Interface'
          ])

        routes.select {|route|
          Rex::Socket.is_ipv4?(route.netmask)
        }.each { |route|
          tbl << [ route.subnet, route.netmask, route.gateway, route.metric, route.interface ]
        }

        if tbl.rows.length > 0
          print_line("\n#{tbl.to_s}")
        else
          print_line('No IPv4 routes were found.')
        end

        # IPv6
        tbl = Rex::Text::Table.new(
          'Header'  => 'IPv6 network routes',
          'Indent'  => 4,
          'Columns' => [
            'Subnet',
            'Netmask',
            'Gateway',
            'Metric',
            'Interface'
          ])

        routes.select {|route|
          Rex::Socket.is_ipv6?(route.netmask)
        }.each { |route|
          tbl << [ route.subnet, route.netmask, route.gateway, route.metric, route.interface ]
        }

        if tbl.rows.length > 0
          print("\n#{tbl.to_s}")
        else
          print_line('No IPv6 routes were found.')
        end

      when 'add'
        # Satisfy check to see that formatting is correct
        unless Rex::Socket.is_ip_addr?(args[0])
          print_error "Invalid subnet: #{args[0]}"
          return false
        end

        unless Rex::Socket.is_ip_addr?(args[1])
          print_error "Invalid subnet mask: #{args[1]}"
          return false
        end

        unless Rex::Socket.is_ip_addr?(args[2])
          print_error "Invalid gateway address: #{args[2]}"
          return false
        end

        print_line("Creating route #{args[0]}/#{args[1]} -> #{args[2]}")

        client.net.config.add_route(*args)
      when 'delete'
        # Satisfy check to see that formatting is correct
        unless Rex::Socket.is_ip_addr?(args[0])
          print_error "Invalid subnet: #{args[0]}"
          return false
        end

        unless Rex::Socket.is_ip_addr?(args[1])
          print_error "Invalid subnet mask: #{args[1]}"
          return false
        end

        unless Rex::Socket.is_ip_addr?(args[2])
          print_error "Invalid gateway address: #{args[2]}"
          return false
        end

        print_line("Deleting route #{args[0]}/#{args[1]} -> #{args[2]}")

        client.net.config.remove_route(*args)
      else
        print_error("Unsupported command: #{cmd}")
    end
  end

  def cmd_route_help
    print_line('Usage: route [-h] command [args]')
    print_line
    print_line('Display or modify the routing table on the remote machine.')
    print_line
    print_line('Supported commands:')
    print_line
    print_line('   add    [subnet] [netmask] [gateway]')
    print_line('   delete [subnet] [netmask] [gateway]')
    print_line('   list')
    print_line
    print_line
    print @@route_opts.usage
  end

  def cmd_route_tabs(str, words)
    return %w[add delete list] + @@route_opts.option_keys if words.length == 1
  end

  #
  # Starts and stops local port forwards to remote hosts on the target
  # network.  This provides an elementary pivoting interface.
  #
  def cmd_portfwd(*args)
    args.unshift('list') if args.empty?

    # For clarity's sake.
    lport = nil
    lhost = nil
    rport = nil
    rhost = nil
    reverse = false
    index = nil

    # Parse the options
    @@portfwd_opts.parse(args) { |opt, idx, val|
      case opt
        when '-h'
          cmd_portfwd_help
          return true
        when '-l'
          lport = val.to_i
        when '-L'
          lhost = val
        when '-p'
          rport = val.to_i
        when '-r'
          rhost = val
        when '-R'
          reverse = true
        when '-i'
          index = val.to_i
      end
    }

    # If we haven't extended the session, then do it now since we'll
    # need to track port forwards
    if client.kind_of?(PortForwardTracker) == false
      client.extend(PortForwardTracker)
      client.pfservice = Rex::ServiceManager.start(Rex::Services::LocalRelay)
    end

    # Build a local port forward in association with the channel
    service = client.pfservice

    # Process the command
    case args.shift
      when 'list'

        table = Rex::Text::Table.new(
          'Header'    => 'Active Port Forwards',
          'Indent'    => 3,
          'SortIndex' => -1,
          'Columns'   => ['Index', 'Local', 'Remote', 'Direction'])

        cnt = 0

        # Enumerate each TCP relay
        service.each_tcp_relay do |lhost, lport, rhost, rport, opts|
          next unless opts['MeterpreterRelay']

          if opts['Reverse']
            table << [cnt + 1, "#{netloc(rhost, rport)}", "#{netloc(lhost, lport)}", 'Reverse']
          else
            table << [cnt + 1, "#{netloc(lhost, lport)}", "#{netloc(rhost, rport)}", 'Forward']
          end

          cnt += 1
        end

        print_line
        if cnt > 0
          print_line(table.to_s)
          print_line("#{cnt} total active port forwards.")
        else
          print_line('No port forwards are currently active.')
        end
        print_line

      when 'add'
        if reverse
          # Validate parameters
          unless lport && lhost && rport
            print_error('You must supply a local port, local host, and remote port.')
            return
          end

          unless rhost.nil?
            print_warning('The remote host (-r) option is ignored for reverse port forwards.')
          end

          begin
            channel = client.net.socket.create(
              Rex::Socket::Parameters.new(
                'LocalHost' => '', # see: #17282, always bind to all interfaces
                'LocalPort' => rport,
                'Proto'     => 'tcp',
                'Server'    => true
              )
            )

            # Start the local TCP reverse relay in association with this stream
            relay = service.start_reverse_tcp_relay(channel,
              'LocalHost'         => channel.params.localhost,
              'LocalPort'         => channel.params.localport,
              'PeerHost'          => lhost,
              'PeerPort'          => lport,
              'MeterpreterRelay'  => true)
          rescue Exception => e
            print_error("Failed to create relay: #{e.to_s}")
            return false
          end

          print_status("Reverse TCP relay created: (remote) #{netloc(channel.params.localhost, channel.params.localport)} -> (local) #{netloc(lhost, lport)}")
        else
          # Validate parameters
          unless lport && rhost && rport
            print_error('You must supply a local port, remote host, and remote port.')
            return
          end

          # Start the local TCP relay in association with this stream
          relay = service.start_tcp_relay(lport,
            'LocalHost'         => lhost,
            'PeerHost'          => rhost,
            'PeerPort'          => rport,
            'MeterpreterRelay'  => true,
            'OnLocalConnection' => Proc.new { |relay, lfd| create_tcp_channel(relay) })
          lport = relay.opts['LocalPort']

          print_status("Forward TCP relay created: (local) #{netloc(lhost, lport)} -> (remote) #{netloc(rhost, rport)}")
        end
      # Delete local port forwards
      when 'delete', 'remove', 'del', 'rm'

        found = false
        unless index.nil?
          counter = 1
          service.each_tcp_relay do |lh, lp, rh, rp, opts|
            if counter == index
              lhost, lport, rhost, rport = lh, lp, rh, rp
              reverse = opts['Reverse'] == true
              found = true
              break
            end

            counter += 1
          end

          unless found
            print_error("Invalid index: #{index}")
          end
        end

        if reverse
          # No remote port, no love.
          unless rport
            print_error('You must supply a remote port.')
            return
          end

          if service.stop_reverse_tcp_relay(lport)
            print_status("Successfully stopped reverse TCP relay on :#{lport}")
          else
            print_error("Failed to stop reverse TCP relay on #{lport}")
          end
        else
          # No local port, no love.
          unless lport
            print_error('You must supply a local port.')
            return
          end

          # Stop the service
          if service.stop_tcp_relay(lport, lhost)
            print_status("Successfully stopped TCP relay on #{netloc(lhost || '0.0.0.0', lport)}")
          else
            print_error("Failed to stop TCP relay on #{netloc(lhost || '0.0.0.0', lport)}")
          end
        end

      when 'flush'

        counter = 0
        service.each_tcp_relay do |lhost, lport, rhost, rport, opts|
          next if (opts['MeterpreterRelay'] == nil)

          if opts['Reverse'] == true
            if service.stop_reverse_tcp_relay(lport)
              print_status("Successfully stopped reverse TCP relay on :#{lport}")
            else
              print_error("Failed to stop reverse TCP relay on #{lport}")
              next
            end
          else
            if service.stop_tcp_relay(lport, lhost)
              print_status("Successfully stopped TCP relay on #{netloc(lhost || '0.0.0.0', lport)}")
            else
              print_error("Failed to stop TCP relay on #{netloc(lhost || '0.0.0.0', lport)}")
              next
            end
          end

          counter += 1
        end
        print_status("Successfully flushed #{counter} rules")

      else
        cmd_portfwd_help
    end
  end

  def cmd_portfwd_help
    print_line 'Usage: portfwd [-h] [add | delete | list | flush] [args]'
    print_line
    print @@portfwd_opts.usage
  end

  def cmd_portfwd_tabs(str, words)
    return %w[add delete list flush] + @@portfwd_opts.option_keys if words.length == 1

    case words[-1]
    when '-L'
      return tab_complete_source_address
    when '-i'
      if client.respond_to?('pfservice')
        return (1..client.pfservice.each_tcp_relay { |lh, lp, rh, rp, opts| }.length).to_a.map!(&:to_s)
      end
    when 'add', 'delete', 'list', 'flush'
      return @@portfwd_opts.option_keys
    end

    []
  end

  def cmd_getproxy
    p = client.net.config.get_proxy_config
    print_line( "Auto-detect     : #{p[:autodetect] ? "Yes" : "No"}" )
    print_line( "Auto config URL : #{p[:autoconfigurl]}" )
    print_line( "Proxy URL       : #{p[:proxy]}" )
    print_line( "Proxy Bypass    : #{p[:proxybypass]}" )
  end

  #
  # Resolve 1 or more hostnames on the target session
  #
  def cmd_resolve(*args)
    args.unshift('-h') if args.length == 0

    hostnames = []
    family = AF_INET

    @@resolve_opts.parse(args) { |opt, idx, val|
      case opt
      when '-h'
        print_line('Usage: resolve host1 host2 .. hostN [-h] [-f IPv4|IPv6]')
        print_line
        print_line(@@resolve_opts.usage)
        return false
      when '-f'
        if val.downcase == 'ipv6'
          family = AF_INET6
        elsif val.downcase != 'ipv4'
          print_error("Invalid family: #{val}")
          return false
        end
      else
        hostnames << val
      end
    }

    response = client.net.resolve.resolve_hosts(hostnames, family)

    table = Rex::Text::Table.new(
      'Header'    => 'Host resolutions',
      'Indent'    => 4,
      'SortIndex' => 0,
      'Columns'   => ['Hostname', 'IP Address']
    )

    response.each do |result|
      if result[:ip].nil?
        table << [result[:hostname], '[Failed To Resolve]']
      else
        table << [result[:hostname], result[:ip]]
      end
    end

    print_line
    print_line(table.to_s)
  end

protected

  #
  # Creates a TCP channel using the supplied relay context.
  #
  def create_tcp_channel(relay)
    client.net.socket.create(
      Rex::Socket::Parameters.new(
        'PeerHost' => relay.opts['PeerHost'],
        'PeerPort' => relay.opts['PeerPort'],
        'Proto'    => 'tcp'
      )
    )
  end

  def netloc(host, port)
    host = "[#{host}]" if Rex::Socket.is_ipv6?(host)
    "#{host}:#{port}"
  end
end

end
end
end
end