description = [[
Looks up geolocation information for BSSID (MAC) addresses of WiFi access points
in the Google geolocation database. Geolocation information in this databasea
usually includes information including coordinates, country, state, city,
street address etc. The MAC addresses can be supplied as an argument
macs
, or in the registry under
nmap.registry.[host.ip][mac-geolocation]
.
]]
---
-- @usage
-- nmap --script mac-geolocation --script-args 'mac-geolocation.macs="00:24:B2:1E:24:FE,00:23:69:2A:B1:27"'
--
-- @arg macs a list of MAC addresses separated by "," for which to do a geolocation lookup
-- @arg extra_info include additional information in the output such as lookup accuracy, street address etc.
--
-- @output Location info arranged by MAC and geolocation database
-- | mac-geolocation:
-- | 00:24:B2:1E:24:FE (Netgear)
-- | Google:
-- | coordinates (lat,lon): 44.9507415,-93.100682
-- | city: St Paul, Minnesota, United States
-- |_ Additional geolocation info may be available through --script-args mac-geolocation.extra_info
--
-- Important notice:
--
-- Google Geolocation lookup related information:
-- When given a wrong MAC address, or a nonexistant MAC the Google API for
-- geolocation of MAC addresses makes an IP geolocation of the host which is
-- making the geolookup request (which is us). This IP based geolookup generates
-- a response which has an accuracy field containing a high value (meaning low
-- accuracy). So, in order to separate the MAC-based responses from the IP-based
-- ones, we do a lookup of a non-valid MAC address "00", and compare all the
-- results with that one: if the results match, and the accuracy is larger than
-- 2000 (meters) then it's probably safe to say that the geolookup was made
-- based on our IP address.
-- Google Geolocation API Protocol:
-- http://code.google.com/apis/gears/geolocation_network_protocol.html
--
author = "Gorjan Petrovski"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery","external","safe"}
dependencies = {"snmp-interfaces"}
require "nmap"
require "stdnse"
require "http"
require "json"
require "nsedebug"
prerule = function()
local macs = stdnse.get_script_args(SCRIPT_NAME .. ".macs")
if macs and macs ~= "" then
return true
else
stdnse.print_debug(3,
"Skipping '%s' %s, 'mac-geolocation.macs' argument is missing.",
SCRIPT_NAME, SCRIPT_TYPE)
return false
end
end
hostrule = function(host)
if host.mac_addr or (nmap.registry[host.ip] and
nmap.registry[host.ip]["mac-geolocation"]) then
return true
else
return false
end
end
--- Pipes a HTTP POST request for one MAC addres to google geo database
--
-- @param mac_addr The MAC address for which to retrieve the location
-- @param pipe_q The current pipeline queue
-- @returns a table containing the location lookup information
local geo_google_pipe = function(mac_addr,pipe_q)
local postdata = [[{"version":"1.1.0","request_address":true,"wifi_towers":[{"mac_address":"]]..mac_addr..[["}]}]]
local options = {header={['Content-Type']='application/json'}, content = postdata}
pipe_q = http.pipeline_add("/loc/json", options, pipe_q, 'POST')
return pipe_q
end
--- Parses the combined Google geolocation lookup response for a list of MAC addresses
--
-- @param mac_list a list of MAC addresses which match the response param
-- @param response matching response to the geo lookup of mac_list
-- @param mac_geo_table output table in which the parsed response is inserted
local geo_google_parse = function(mac_list,response,mac_geo_table)
-- remove the null entries,
local mac_null
local loc_null
local loc_null_json = nil
if nmap.registry["mac-geolocation"].null_location then
loc_null = nmap.registry["mac-geolocation"].null_location
else
mac_null = table.remove(mac_list,1)
loc_null = table.remove(response,1)
nmap.registry["mac-geolocation"].null_location = loc_null
end
-- Just in case google doesn't return our default location we insert a nil value
-- for the comparison in the loop to fail (and the whole statement to succeed)
if (not loc_null) or (not loc_null["status-line"]) or
(not loc_null["status-line"]:match("HTTP/1.1 200 OK")) then
loc_null_json = {}
loc_null_json.location = nil
loc_null_json.location.accuracy = 0
else
local stat
stat, loc_null_json = json.parse(loc_null.body)
if not stat then
loc_null_json = {}
loc_null_json.location = nil
loc_null_json.location.accuracy = 0
end
end
for i,mac in ipairs(mac_list) do
if not mac_geo_table[mac] then
mac_geo_table[mac]={}
end
if response[i] and
response[i]["status-line"]:match("HTTP/1.1 200 OK") and
response[i].header["content-type"]:match("application/json")
then
local status, json_loc = json.parse(response[i].body)
-- Since Google returns the IP location of the origin of the request (which is us)
-- we compare it with the
if status and json_loc.location
and not ( json_loc.location.longitude == loc_null_json.location.longitude
and json_loc.location.latitude == loc_null_json.location.latitude
and json_loc.location.accuracy > 2000)
then
mac_geo_table[mac].google=json_loc["location"]
else
-- mac_geo_table[mac].google = {}
end
else
mac_geo_table[mac].google = {"Could not connect to API"}
end
end
end
--- Looks up a list of MAC addresses in the Google Geolocation database
--
-- @param mac_list a list of MAC addresses
-- @param mac_geo_table output table with the geo lookup results inserted
local geo_google = function(mac_list,mac_geo_table)
-- adding an invalid MAC address in front so we can detect locations
-- generated by our IP address, it is removed in geo_google_parse()
if not nmap.registry["mac-geolocation"].null_location then
table.insert(mac_list,1,"00")
end
local pipe_q = nil
for _,mac in ipairs(mac_list) do
pipe_q = geo_google_pipe(mac, pipe_q)
end
local response = http.pipeline_go('www.google.com',443,pipe_q)
geo_google_parse(mac_list,response,mac_geo_table)
end
local fill_output = function(src, dst, xtra)
if src.latitude and src.longitude then
table.insert(dst, "coordinates (lat,lon): "..src.latitude..","..src.longitude)
local city = "city: "
if src.address then
if src.address.city then
city = city..src.address.city
if src.address.region then
city = city..", "..src.address.region
end
if src.address.country then
city = city..", "..src.address.country
end
end
table.insert(dst,city)
if xtra then
local addr = "address: "
if src.address.street then
addr = addr..src.address.street
end
if src.address.street_number then
addr = addr..", "..src.address.street_number
end
if src.address.postal_code then
addr = addr..", "..src.address.postal_code
end
if src.address.county then
addr = addr..", "..src.address.county
end
table.insert(dst,addr)
if src.accuracy then
table.insert(dst,"accuracy: "..src.accuracy)
end
end
end
return true
end
return false
end
action = function(host,port)
local mac_list = {}
local catch = function() return end
local try = nmap.new_try(catch)
if not nmap.registry["mac-geolocation"] then
nmap.registry["mac-geolocation"] = {}
end
if (SCRIPT_TYPE == "prerule") then
local macs = stdnse.get_script_args(SCRIPT_NAME .. ".macs")
mac_list = stdnse.strsplit(",",macs:upper())
else
if (nmap.registry[host.ip] and nmap.registry[host.ip]["mac-geolocation"]) then
for _,mac in ipairs(nmap.registry[host.ip]["mac-geolocation"]) do
table.insert(mac_list,mac:upper())
end
-- del the table once we're done with it, so it doesn't bloat the registry
nmap.registry[host.ip]["mac-geolocation"] = nil
end
if host.mac_addr then
local m = host.mac_addr
table.insert(mac_list, (string.format( "%02x:%02x:%02x:%02x:%02x:%02x",
m:byte(1), m:byte(2), m:byte(3), m:byte(4), m:byte(5), m:byte(6))):upper())
end
end
if mac_list and #mac_list>0 then
local extra_info = stdnse.get_script_args(SCRIPT_NAME .. ".extra_info")
local mac_geo_table = {}
-- Google Geolocation Database
geo_google(mac_list,mac_geo_table)
local output = {} -- table in which we insert output
local entry_flag = false -- indicates if we should print anything (existence of atleast one entry
-- lookup manufacturer table based on MAC prefix
local mac_prefixes={}
if nmap.registry.snmp_interfaces and nmap.registry.snmp_interfaces.mac_prefixes then
mac_prefixes = nmap.registry.snmp_interfaces.mac_prefixes
elseif nmap.registry["mac-geolocation"].mac_prefixes then
mac_prefixes = nmap.registry["mac-geolocation"].mac_prefixes
else
nmap.registry["mac-geolocation"].mac_prefixes = try(datafiles.parse_mac_prefixes())
mac_prefixes = nmap.registry["mac-geolocation"].mac_prefixes
end
for mac in pairs(mac_geo_table) do
local tmp = {}
local manuf = "Unknown"
if mac_prefixes then
local prefix = (mac:match("^(%x+:%x+:%x+).*")):gsub(":",""):upper()
if mac_prefixes[prefix] then
manuf = mac_prefixes[prefix]
end
end
tmp.name = mac.." ("..manuf..")"
-- only fill output if there are entries in mac_geo_table
if mac_geo_table[mac].google then
table.insert(tmp, {name="Google:"})
if fill_output(mac_geo_table[mac].google, tmp[1], extra_info) then
entry_flag = true
end
end
table.insert(output,tmp)
end
if not extra_info then
table.insert(output,"Additional geolocation info may be available through --script-args mac-geolocation.extra_info")
end
if entry_flag then
return(stdnse.format_output(true,output))
else
return
end
end
end