rapid7/metasploit-framework

View on GitHub
modules/post/multi/manage/autoroute.rb

Summary

Maintainability
D
2 days
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Multi Manage Network Route via Meterpreter Session',
        'Description' => %q{
          This module manages session routing via an existing
          Meterpreter session. It enables other modules to 'pivot' through a
          compromised host when connecting to the named NETWORK and SUBMASK.
          Autoadd will search a session for valid subnets from the routing table
          and interface list then add routes to them. Default will add a default
          route so that all TCP/IP traffic not specified in the MSF routing table
          will be routed through the session when pivoting. See documentation for more
          'info -d' and click 'Knowledge Base'
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'todb',
          'Josh Hale "sn0wfa11" <jhale85446[at]gmail.com>'
        ],
        'SessionTypes' => [ 'meterpreter'],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_net_config_get_interfaces
              stdapi_net_config_get_routes
            ]
          }
        }
      )
    )

    register_options(
      [
        OptString.new('SUBNET', [false, 'Subnet (IPv4, for example, 10.10.10.0)', nil]),
        OptString.new('NETMASK', [false, 'Netmask (IPv4 as "255.255.255.0" or CIDR as "/24"', '255.255.255.0']),
        OptEnum.new('CMD', [true, 'Specify the autoroute command', 'autoadd', ['add', 'autoadd', 'print', 'delete', 'default']])
      ]
    )
  end

  # Get the CMD string vs ACTION
  #
  # Backwards compatability: This was changed because the option name of "ACTION"
  # is special for some things, and indicates the :action attribute, not a datastore option.
  # However, this is a semi-popular module, though, so I'd prefer not to break people's
  # RC scripts that set ACTION. Note that ACTION is preferred over CMD.
  #
  # TODO: The better solution is to use 'Action' and 'DefaultAction' info elements,
  # but there are some squirelly problems right now with rendering these for post modules.
  #
  # @return [string class] cmd string
  def route_cmd
    if datastore['ACTION'].to_s.empty?
      datastore['CMD'].to_s.downcase.to_sym
    else
      wlog("Warning, deprecated use of 'ACTION' datastore option for #{fullname}'. Use 'CMD' instead.")
      datastore['ACTION'].to_s.downcase.to_sym
    end
  end

  # Run Method for when run command is issued
  #
  # @return [void] A useful return value is not expected here
  def run
    return unless session_good?

    print_status("Running module against #{sysinfo['Computer']}")

    case route_cmd
    when :print
      print_routes
    when :add
      if validate_cmd(datastore['SUBNET'], netmask)
        print_status('Adding a route to %s/%s...' % [datastore['SUBNET'], netmask])
        add_route(datastore['SUBNET'], netmask)
      end
    when :autoadd
      autoadd_routes
    when :default
      add_default
    when :delete
      if datastore['SUBNET']
        print_status('Deleting route to %s/%s...' % [datastore['SUBNET'], netmask])
        delete_route(datastore['SUBNET'], netmask)
      else
        delete_all_routes
      end
    end
  end

  # Delete all routes from framework routing table.
  #
  # @return [void] A useful return value is not expected here
  def delete_all_routes
    if !Rex::Socket::SwitchBoard.routes.empty?
      print_status("Deleting all routes associated with session: #{session.sid}.")
      loop do
        count = 0
        Rex::Socket::SwitchBoard.each do |route|
          if route.comm == session
            print_status("Deleting: #{route.subnet}/#{route.netmask}")
            delete_route(route.subnet, route.netmask)
          end
        end
        Rex::Socket::SwitchBoard.each do |route|
          count += 1 if route.comm == session
        end
        break if count == 0
      end
      print_status('Deleted all routes')
    else
      print_status('No routes associated with this session to delete.')
    end
  end

  # Print all of the active routes defined on the framework
  #
  # Identical functionality to command_dispatcher/core.rb, and
  # nearly identical code
  #
  # @return [void] A useful return value is not expected here
  def print_routes
    # IPv4 Table
    tbl_ipv4 = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'IPv4 Active Routing Table',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Columns' =>
        [
          'Subnet',
          'Netmask',
          'Gateway',
        ],
      'ColProps' =>
        {
          'Subnet' => { 'Width' => 17 },
          'Netmask' => { 'Width' => 17 }
        }
    )

    # IPv6 Table
    tbl_ipv6 = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'IPv6 Active Routing Table',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Columns' =>
        [
          'Subnet',
          'Netmask',
          'Gateway',
        ],
      'ColProps' =>
        {
          'Subnet' => { 'Width' => 17 },
          'Netmask' => { 'Width' => 17 }
        }
    )

    # Populate Route Tables
    Rex::Socket::SwitchBoard.each do |route|
      if route.comm.is_a?(Msf::Session)
        gw = "Session #{route.comm.sid}"
      else
        gw = route.comm.name.split(/::/)[-1]
      end

      tbl_ipv4 << [ route.subnet, route.netmask, gw ] if Rex::Socket.is_ipv4?(route.netmask)
      tbl_ipv6 << [ route.subnet, route.netmask, gw ] if Rex::Socket.is_ipv6?(route.netmask)
    end

    # Print Route Tables
    print_status(tbl_ipv4.to_s) if !tbl_ipv4.rows.empty?
    print_status(tbl_ipv6.to_s) if !tbl_ipv6.rows.empty?
    if (tbl_ipv4.rows.length + tbl_ipv6.rows.length) < 1
      print_status('There are currently no routes defined.')
    elsif tbl_ipv4.rows.empty? && !tbl_ipv6.rows.empty?
      print_status('There are currently no IPv4 routes defined.')
    elsif !tbl_ipv4.rows.empty? && tbl_ipv6.rows.empty?
      print_status('There are currently no IPv6 routes defined.')
    end
  end

  # Validation check on an IPv4 address
  #
  # Yet another IP validator. I'm sure there's some Rex
  # function that can just do this.
  #
  # @return [string class] IPv4 subnet
  def check_ip(ip = nil)
    return false if (ip.nil? || ip.strip.empty?)

    begin
      rw = Rex::Socket::RangeWalker.new(ip.strip)
      (rw.valid? && rw.length == 1) ? true : false
    rescue StandardError
      false
    end
  end

  # Converts a CIDR value to a netmask
  #
  # @return [string class] IPv4 netmask
  def cidr_to_netmask(cidr)
    int = cidr.gsub(/\x2f/, '').to_i
    Rex::Socket.addr_ctoa(int)
  end

  # Validates the user input 'NETMASK'
  #
  # @return [string class] IPv4 netmask
  def netmask
    case datastore['NETMASK']
    when /^\x2f[0-9]{1,2}/
      cidr_to_netmask(datastore['NETMASK'])
    when /^[0-9]{1,3}\.[0-9]/ # Close enough, if it's wrong it'll fail out later.
      datastore['NETMASK']
    else
      '255.255.255.0'
    end
  end

  # This function adds a route to the framework routing table
  #
  # @subnet [string class] subnet to add
  # @netmask [string class] netmask
  # @origin [string class] where route is coming from. Nill for none.
  #
  # @return [true]  If added
  # @return [false] If not
  def add_route(subnet, netmask, origin = nil)
    if origin
      origin = " from #{origin}"
    else
      origin = ''
    end

    begin
      if Rex::Socket::SwitchBoard.add_route(subnet, netmask, session)
        print_good("Route added to subnet #{subnet}/#{netmask}#{origin}.")
        return true
      else
        print_error("Could not add route to subnet #{subnet}/#{netmask}#{origin}.")
        return false
      end
    rescue ::Rex::Post::Meterpreter::RequestError => e
      print_error("Could not add route to subnet #{subnet}/#{netmask}#{origin}.")
      print_error("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
      return false
    end
  end

  # This function removes a route to the framework routing table
  #
  # @subnet [string class] subnet to add
  # @netmask [string class] netmask
  # @origin [string class] where route is coming from.
  #
  # @return [true]  If removed
  # @return [false] If not
  def delete_route(subnet, netmask)
    Rex::Socket::SwitchBoard.remove_route(subnet, netmask, session)
  rescue ::Rex::Post::Meterpreter::RequestError => e
    print_error("Could not remove route to subnet #{subnet}/#{netmask}")
    print_error("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
    return false
  end

  # This function will exclude loopback, multicast, and default routes
  #
  # @subnet [string class] IPv4 subnet or address to check
  # @netmask [string class] IPv4 netmask to check
  #
  # @return [true]  If good to add
  # @return [false] If not
  def is_routable?(subnet, netmask)
    if subnet =~ /^224\.|^127\./
      return false
    elsif subnet == '0.0.0.0'
      return false
    elsif netmask == '255.255.255.255'
      return false
    end

    return true
  end

  # Search for valid subnets on the target and attempt
  # add a route to each. (Operation from auto_add_route plugin.)
  #
  # @return [void] A useful return value is not expected here
  def autoadd_routes
    return unless route_compatible?

    print_status('Searching for subnets to autoroute.')
    found = false

    begin
      session.net.config.each_route do |route|
        next unless (Rex::Socket.is_ipv4?(route.subnet) && Rex::Socket.is_ipv4?(route.netmask)) # Pick out the IPv4 addresses

        subnet = get_subnet(route.subnet, route.netmask) # Make sure that the subnet is actually a subnet and not an IP address. Android phones like to send over their IP.
        next unless is_routable?(subnet, route.netmask)

        if !Rex::Socket::SwitchBoard.route_exists?(subnet, route.netmask) && add_route(subnet, route.netmask, "host's routing table")
          found = true
        end
      end
    rescue ::Rex::Post::Meterpreter::RequestError => e
      print_status('Unable to get routes from session, trying interface list.')
    end

    if !autoadd_interface_routes && !found # Check interface list for more possible routes
      print_status('Did not find any new subnets to add.')
    end
  end

  # Look at network interfaces as options for additional routes.
  # If the routes are not already included they will be added.
  #
  # @return [true] A route from the interface list was added
  # @return [false] No additional routes were added
  def autoadd_interface_routes
    return unless interface_compatible?

    found = false

    begin
      session.net.config.each_interface do |interface| # Step through each of the network interfaces
        (0..(interface.addrs.size - 1)).each do |index| # Step through the addresses for the interface
          ip_addr = interface.addrs[index]
          netmask = interface.netmasks[index]

          next unless (Rex::Socket.is_ipv4?(ip_addr) && Rex::Socket.is_ipv4?(netmask)) # Pick out the IPv4 addresses
          next unless is_routable?(ip_addr, netmask)

          subnet = get_subnet(ip_addr, netmask)

          if subnet && !Rex::Socket::SwitchBoard.route_exists?(subnet, netmask) && add_route(subnet, netmask, interface.mac_name)
            found = true
          end
        end
      end
    rescue ::Rex::Post::Meterpreter::RequestError => e
      print_error('Unable to get interface information from session.')
    end
    return found
  end

  # Take an IP address and a netmask and return the appropreate subnet "Network"
  #
  # @ip_addr [string class] Input IPv4 Address
  # @netmask [string class] Input IPv4 Netmask
  #
  # @return [string class] The subnet related to the IP address and netmask
  # @return [nil class] Something is out of range
  def get_subnet(ip_addr, netmask)
    return nil if !validate_cmd(ip_addr, netmask) # make sure IP and netmask are valid

    nets = ip_addr.split('.')
    masks = netmask.split('.')
    output = ''

    4.times do |index|
      octet = get_subnet_octet(int_or_nil(nets[index]), int_or_nil(masks[index]))
      return nil if !octet

      output << octet.to_s
      output << '.' if index < 3
    end
    return output
  end

  # Input an octet of an IPv4 address and the cooresponding octet of the
  # IPv4 netmask then return the appropreate subnet octet.
  #
  # @net  [integer class] IPv4 address octet
  # @mask [integer class] Ipv4 netmask octet
  #
  # @return [integer class] Octet of the subnet
  # @return [nil class] If an input is nil
  def get_subnet_octet(net, mask)
    return nil if !net || !mask

    subnet_range = 256 - mask  # This is the address space of the subnet octet

    multi = net / subnet_range # Integer division to get the multiplier needed to determine subnet octet

    return(subnet_range * multi) # Multiply to get subnet octet
  end

  # Take a string of numbers and converts it to an integer.
  #
  # @string [string class] Input string, needs to be all numbers (0..9)
  #
  # @return [integer class] Integer representation of the number string
  # @return [nil class] string contains non-numbers, cannot convert
  def int_or_nil(string)
    num = string.to_i
    num if num.to_s == string
  end

  # Add a default route to the routing table
  #
  # @return [void] A useful return value is not expected here
  def add_default
    subnet = '0.0.0.0'
    mask = '0.0.0.0'

    switch_board = Rex::Socket::SwitchBoard.instance
    print_status('Attempting to add a default route.')

    if !switch_board.route_exists?(subnet, mask)
      add_route(subnet, mask)
    end
  end

  # Checks to see if the session is ready.
  #
  # Some Meterpreter types, like python, can take a few seconds to
  # become fully established. This gracefully exits if the session
  # is not ready yet.
  #
  # @return [true class] Session is good
  # @return [false class] Session is not
  def session_good?
    if !session.info
      print_error('Session is not yet fully established. Try again in a bit.')
      return false
    end
    return true
  end

  # Checks to see if the session has routing capabilities
  #
  # @return [true class] Session has routing capabilities
  # @return [false class] Session does not
  def route_compatible?
    session.respond_to?(:net) &&
      session.net.config.respond_to?(:each_route)
  end

  # Checks to see if the session has capabilities of accessing network interfaces
  #
  # @return [true class] Session has ability to access network interfaces
  # @return [false class] Session does not
  def interface_compatible?
    session.respond_to?(:net) &&
      session.net.config.respond_to?(:each_interface)
  end

  # Validates the command options
  #
  # @return [true class] Everything is good
  # @return [false class] Not so much
  def validate_cmd(subnet = nil, netmask = nil)
    if subnet.nil?
      print_error 'Missing subnet option'
      return false
    end

    unless check_ip(subnet)
      print_error 'Subnet invalid (must be IPv4)'
      return false
    end

    if (netmask && !Rex::Socket.addr_atoc(netmask))
      print_error 'Netmask invalid (must define contiguous IP addressing)'
      return false
    end

    if (netmask && !check_ip(netmask))
      print_error 'Netmask invalid'
      return false
    end
    return true
  end
end