diff --git a/CHANGELOG b/CHANGELOG index 796170b61..250a6dacc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,13 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#606] Three new scripts render IP geolocation data as maps. + ip-geolocation-map-bing uses Bing Maps, ip-geolocation-map-google uses Google + Maps, and ip-geolocation-map-kml outputs KML map data for import into other + mapping software. [Mak Kolybabi] + +o [NSE][GH#606] New NSE library, geoip.lua, provides a common framework for + storing and retrieving IP geolocation results. [Mak Kolybabi] + o [NSE][GH#518] Brute scripts are faster and more accurate. New feedback and adaptivity mechanisms in brute.lua help brute scripts use resources more efficiently, dynamically changing number of threads based on protocol diff --git a/nselib/geoip.lua b/nselib/geoip.lua new file mode 100644 index 000000000..15f50519d --- /dev/null +++ b/nselib/geoip.lua @@ -0,0 +1,81 @@ +local nmap = require "nmap" +local stdnse = require "stdnse" +local table = require "table" + +_ENV = stdnse.module("geoip", stdnse.seeall) + +--- +-- Consolidation of GeoIP functions. +-- +-- @author "Mak Kolybabi " +-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html + +--- Add a geolocation to the registry +-- @param ip The IP that was geolocated +-- @param lat The latitude in degrees +-- @param lon The longitude in degrees +add = function(ip, lat, lon) + if not nmap.registry.geoip then + nmap.registry.geoip = {} + end + + if not nmap.registry.geoip[ip] then + nmap.registry.geoip[ip] = {} + end + + local lat_n = tonumber(lat) + if lat_n < -90 or lat_n > 90 then + stdnse.debug1("Invalid latitude for %s: %s.", ip, lat) + return + end + + local lon_n = tonumber(lon) + if lon_n < -180 or lon_n > 180 then + stdnse.debug1("Invalid longitude for %s: %s.", ip, lon) + return + end + + nmap.registry.geoip[ip]["latitude"] = lat + nmap.registry.geoip[ip]["longitude"] = lon +end + +--- Check if any coordinates have been stored in the registry +--@return True if any coordinates have been returned, false otherwise +empty = function() + return not nmap.registry.geoip +end + +--- Retrieve the table of coordinates by IP +--@return A table of coordinates keyed by IP. +get_all_by_ip = function() + if empty() then + return nil + end + + return nmap.registry.geoip +end + +--- Retrieve a table of IPs by coordinate +--@return A table of IPs keyed by coordinate in lat,lon format +get_all_by_gps = function(limit) + if empty() then + return nil + end + + local t = {} + for ip, coords in pairs(get_all_by_ip()) do + if limit and limit < #t then + break + end + + local key = coords["latitude"] .. "," .. coords["longitude"] + if not t[key] then + t[key] = {} + end + table.insert(t[key], ip) + end + + return t +end + +return _ENV; diff --git a/scripts/ip-geolocation-geoplugin.nse b/scripts/ip-geolocation-geoplugin.nse index a7a25920a..e3ec9ab04 100644 --- a/scripts/ip-geolocation-geoplugin.nse +++ b/scripts/ip-geolocation-geoplugin.nse @@ -1,3 +1,4 @@ +local geoip = require "geoip" local http = require "http" local ipOps = require "ipOps" local json = require "json" @@ -39,24 +40,35 @@ end local geoplugin = function(ip) local response = http.get("www.geoplugin.net", 80, "/json.gp?ip="..ip, {any_af=true}) local stat, loc = json.parse(response.body) - if not stat then return nil end + if not stat then + return false, loc + end local output = {} table.insert(output, "coordinates (lat,lon): "..loc.geoplugin_latitude..","..loc.geoplugin_longitude) local regionName = (loc.geoplugin_regionName == json.NULL) and "Unknown" or loc.geoplugin_regionName table.insert(output,"state: ".. regionName ..", ".. loc.geoplugin_countryName) - return output + geoip.add(ip, loc.geoplugin_latitude, loc.geoplugin_longitude) + + return true, output end action = function(host,port) - local output = geoplugin(host.ip) + local output = stdnse.output_table() - if(#output~=0) then - output.name = host.ip - if host.targetname then - output.name = output.name.." ("..host.targetname..")" + local status, result = geoplugin(host.ip) + if not status then + if result == "syntax error" then + result = "The geoPlugin service has likely blocked you due to excessive usage, but the response received was 'syntax error'." end + output.ERROR = result + return output, output.ERROR + end + + output.name = host.ip + if host.targetname then + output.name = output.name.." ("..host.targetname..")" end return stdnse.format_output(true,output) diff --git a/scripts/ip-geolocation-ipinfodb.nse b/scripts/ip-geolocation-ipinfodb.nse index 04620bf05..9deccb6c0 100644 --- a/scripts/ip-geolocation-ipinfodb.nse +++ b/scripts/ip-geolocation-ipinfodb.nse @@ -1,3 +1,4 @@ +local geoip = require "geoip" local http = require "http" local ipOps = require "ipOps" local json = require "json" @@ -70,6 +71,8 @@ local ipinfodb = function(ip) table.insert(output, "coordinates (lat,lon): "..loc.latitude..","..loc.longitude) table.insert(output,"city: ".. loc.cityName..", ".. loc.regionName..", ".. loc.countryName) + geoip.add(ip, loc.latitude, loc.longitude) + return output end diff --git a/scripts/ip-geolocation-map-bing.nse b/scripts/ip-geolocation-map-bing.nse new file mode 100644 index 000000000..cdb0498d5 --- /dev/null +++ b/scripts/ip-geolocation-map-bing.nse @@ -0,0 +1,177 @@ +local http = require "http" +local geoip = require "geoip" +local io = require "io" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local url = require "url" + +description = [[ +This script queries the Nmap registry for the GPS coordinates of targets stored +by previous geolocation scripts and renders a Bing Map of markers representing +the targets. + +Additional information for the Bing Maps REST Services API can be found at: +- https://msdn.microsoft.com/en-us/library/ff701724.aspx +]] + +--- +-- @usage +-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-bing --script-args ip-geolocation-map-bing.api_key=[redacted],ip-geolocation-map-bing.map_path=map.png +-- +-- @output +-- | ip-geolocation-map-bing: +-- |_ The map has been saved at 'map.png'. +-- +-- @args ip-geolocation-map-bing.api_key The required Bing Maps API key for your +-- account. An API key can be generated at https://www.bingmapsportal.com/ +-- +-- ip-geolocation-map-bing.center GPS coordinates defining the center of the +-- image. If omitted, Bing Maps will choose a center that shows all the +-- markers. +-- +-- @args ip-geolocation-map-bing.format The default value is 'jpeg', 'png', and +-- 'gif' are also allowed. +-- +-- @args ip-geolocation-map-bing.language The default value is 'en', but other +-- two-letter language codes are accepted. +-- +-- @args ip-geolocation-map-bing.layer The default value is 'Road', +-- 'Aerial', and 'AerialWithLabels' are also allowed. +-- +-- @args ip-geolocation-map-bing.map_path The path at which the rendered +-- Bing Map will be saved to the local filesystem. +-- +-- @args ip-geolocation-map-bing.marker_style This argument can apply styling +-- to the markers. +-- https://msdn.microsoft.com/en-us/library/ff701719.aspx +-- +-- @args ip-geolocation-map-bing.size The default value is '640x640' pixels, but +-- can be changed so long as the width is between 80 and 2000 pixels and the +-- height is between 80 and 1500 pixels. + +author = "Mak Kolybabi " +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"external", "safe"} + +local render = function(params, options) + -- Format marker style for inclusion in parameters. + local style = "" + if options["marker_style"] then + style = ";" .. options["marker_style"] + end + + -- Add in a marker for each host. + local markers = {} + for coords, ip in pairs(geoip.get_all_by_gps(100)) do + table.insert(markers, "pp=" .. coords .. style) + end + local body = table.concat(markers, "&") + + -- Format the parameters into a properly encoded URL. + local query = "/REST/v1/Imagery/Map/" .. options["layer"] .. "?" .. url.build_query(params) + stdnse.debug1("The query URL is: %s", query) + stdnse.debug1("The query body is: %s", body) + + local headers = { + ["header"] = { + ["Content-Type"] = "text/plain; charset=utf-8" + } + } + + local res = http.post("dev.virtualearth.net", 80, query, headers, nil, body) + if not res or res.status ~= 200 then + stdnse.debug1("Error %d from API: %s", res.status, res.body) + return false, ("Failed to recieve map using query '%s'."):format(query) + end + + local f = io.open(options["map_path"], "w") + if not f then + return false, ("Failed to open file '%s'."):format(options["map_path"]) + end + + if not f:write(res.body) then + return false, ("Failed to write file '%s'."):format(options["map_path"]) + end + + f:close() + + local msg + + return true, ("The map has been saved at '%s'."):format(options["map_path"]) +end + +local parse_args = function() + local options = {} + local params = {} + + local api_key = stdnse.get_script_args(SCRIPT_NAME .. '.api_key') + if not api_key then + return false, "Need to specify an API key, get one at https://www.bingmapsportal.com/." + end + params["key"] = api_key + + local center = stdnse.get_script_args(SCRIPT_NAME .. ".center") + if center then + params["centerPoint"] = center + end + + local format = stdnse.get_script_args(SCRIPT_NAME .. ".format") + if format then + params["format"] = format + end + + local language = stdnse.get_script_args(SCRIPT_NAME .. ".language") + if language then + params["language"] = language + end + + local layer = stdnse.get_script_args(SCRIPT_NAME .. ".layer") + if not layer then + layer = "Road" + end + options["layer"] = layer + + local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path') + if map_path then + options["map_path"] = map_path + else + return false, "Need to specify a path for the map." + end + + local size = stdnse.get_script_args(SCRIPT_NAME .. ".size") + if not size then + -- This size is arbitrary, and is chosen to match the default that Google + -- Maps will produce. + size = "640x640" + end + size = string.gsub(size, "x", ",") + params["mapSize"] = size + + return true, params, options +end + +postrule = function() + -- Only run if a previous script has registered geolocation data. + return not geoip.empty() +end + +action = function() + local output = stdnse.output_table() + + -- Parse and sanity check the command line arguments. + local status, params, options = parse_args() + if not status then + output.ERROR = params + return output, output.ERROR + end + + -- Render the map. + local status, msg = render(params, options) + if not status then + output.ERROR = msg + return output, output.ERROR + end + + return output, stdnse.format_output(true, msg) +end diff --git a/scripts/ip-geolocation-map-google.nse b/scripts/ip-geolocation-map-google.nse new file mode 100644 index 000000000..9ad4008cb --- /dev/null +++ b/scripts/ip-geolocation-map-google.nse @@ -0,0 +1,182 @@ +local http = require "http" +local geoip = require "geoip" +local io = require "io" +local stdnse = require "stdnse" +local table = require "table" +local url = require "url" + +description = [[ +This script queries the Nmap registry for the GPS coordinates of targets stored +by previous geolocation scripts and renders a Google Map of markers representing +the targets. + +Additional information for the Google Static Maps API can be found at: +- https://developers.google.com/maps/documentation/static-maps/intro +]] + +--- +-- @usage +-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-google --script-args ip-geolocation-map-google.api_key=[redacted],ip-geolocation-map-google.map_path=map.png +-- +-- @output +-- | ip-geolocation-map-google: +-- |_ The map has been saved at 'map.png'. +-- +-- @args ip-geolocation-map-google.api_key The required Google Maps API key for +-- your account. An API key can be generated at +-- https://developers.google.com/maps/documentation/static-maps/." @args +-- +-- ip-geolocation-map-google.center GPS coordinates defining the center of the +-- image. If omitted, Google Maps will choose a center that shows all the +-- markers. +-- +-- @args ip-geolocation-map-google.format The default value is 'png' (alias for +-- 'png8'), 'png32', 'gif', 'jpg', and 'jpg-baseline' are also allowed. +-- https://developers.google.com/maps/documentation/static-maps/intro#ImageFormats +-- +-- @args ip-geolocation-map-google.language The default value is 'en', but other +-- two-letter language codes are accepted. +-- +-- @args ip-geolocation-map-google.layer The default value is 'roadmap', +-- 'satellite', 'hybrid', and 'terrain' are also allowed. +-- https://developers.google.com/maps/documentation/static-maps/intro#MapTypes +-- +-- @args ip-geolocation-map-google.map_path The path at which the rendered +-- Google Map will be saved to the local filesystem. +-- +-- @args ip-geolocation-map-google.marker_style This argument can apply styling +-- to the markers. +-- https://developers.google.com/maps/documentation/static-maps/intro#MarkerStyles +-- +-- @args ip-geolocation-map-google.scale The default value is 1, but values 2 +-- and 4 are permitted. Scale level 4 is only available to Google Maps Premium +-- customers. +-- https://developers.google.com/maps/documentation/static-maps/intro#scale_values +-- +-- @args ip-geolocation-map-google.size The default value is '640x640' pixels, +-- but can be increased by Google Maps Premium customers. +-- https://developers.google.com/maps/documentation/static-maps/intro#Imagesizes + +author = "Mak Kolybabi " +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"external", "safe"} + +local render = function(params, options) + -- Add in a marker for each GPS coordinate. + local markers = {} + for coords, ips in pairs(geoip.get_all_by_gps()) do + table.insert(markers, coords) + end + params["markers"] = options["marker_style"] .. "|" .. table.concat(markers, "|") + + -- Format the parameters into a properly encoded URL. + local query = "/maps/api/staticmap?" .. url.build_query(params) + stdnse.debug1("The query URL is: %s", query) + + -- Check that the query string is below the 8192 character limit after + -- URL-encoding. + if #query > 8192 then + return false, ("Refused to send query since URL path is %d chararacters, but Google Maps limits to 8192."):format(#query) + end + + local res = http.get("maps.googleapis.com", 80, query) + if not res or res.status ~= 200 then + return false, ("Failed to recieve map using query '%s'."):format(query) + end + + local f = io.open(options["map_path"], "w") + if not f then + return false, ("Failed to open file '%s'."):format(options["map_path"]) + end + + if not f:write(res.body) then + return false, ("Failed to write file '%s'."):format(options["map_path"]) + end + + f:close() + + return true, ("The map has been saved at '%s'."):format(options["map_path"]) +end + +local parse_args = function() + local options = {} + local params = {} + + local api_key = stdnse.get_script_args(SCRIPT_NAME .. '.api_key') + if not api_key then + return false, "Need to specify an API key, get one at https://developers.google.com/maps/documentation/static-maps/." + end + params["key"] = api_key + + local center = stdnse.get_script_args(SCRIPT_NAME .. ".center") + if center then + params["center"] = center + end + + local format = stdnse.get_script_args(SCRIPT_NAME .. ".format") + if format then + params["format"] = format + end + + local language = stdnse.get_script_args(SCRIPT_NAME .. ".language") + if language then + params["language"] = language + end + + local layer = stdnse.get_script_args(SCRIPT_NAME .. ".layer") + if layer then + params["layer"] = layer + end + + local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path') + if map_path then + options["map_path"] = map_path + else + return false, "Need to specify a path for the map." + end + + local marker_style = stdnse.get_script_args(SCRIPT_NAME .. ".marker_style") + stdnse.debug1('--> [%s]', marker_style) + if not marker_style then + marker_style = "" + end + options["marker_style"] = marker_style + + local scale = stdnse.get_script_args(SCRIPT_NAME .. ".scale") + if scale then + params["scale"] = scale + end + + local size = stdnse.get_script_args(SCRIPT_NAME .. ".size") + if not size then + size = "640x640" + end + params["size"] = size + + return true, params, options +end + +postrule = function() + -- Only run if a previous script has registered geolocation data. + return not geoip.empty() +end + +action = function() + local output = stdnse.output_table() + + -- Parse and sanity check the command line arguments. + local status, params, options = parse_args() + if not status then + output.ERROR = params + return output, output.ERROR + end + + -- Render the map. + local status, msg = render(params, options) + if not status then + output.ERROR = msg + return output, output.ERROR + end + + return output, stdnse.format_output(true, msg) +end diff --git a/scripts/ip-geolocation-map-kml.nse b/scripts/ip-geolocation-map-kml.nse new file mode 100644 index 000000000..2cae31773 --- /dev/null +++ b/scripts/ip-geolocation-map-kml.nse @@ -0,0 +1,91 @@ +local http = require "http" +local geoip = require "geoip" +local io = require "io" +local stdnse = require "stdnse" +local table = require "table" + +description = [[ +This script queries the Nmap registry for the GPS coordinates of targets stored +by previous geolocation scripts and produces a KML file of points representing +the targets. +]] + +--- +-- @usage +-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-kml --script-args ip-geolocation-map-kml.map_path=map.kml +-- +-- @output +-- | ip-geolocation-map-kml: +-- |_ The map has been saved at 'map.kml'. +-- +-- @args ip-geolocation-map-kml.map_path (REQUIRED) + +author = "Mak Kolybabi " +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe"} + +local render = function(path) + local kml = {'\n\n '} + + for ip, coords in pairs(geoip.get_all_by_ip()) do + table.insert(kml, ([[ + + %s + + %s,%s + + ]]):format(ip, coords["longitude"], coords["latitude"]) + ) + end + + table.insert(kml, ' \n\n') + + kml = table.concat(kml, "\n") + + local f = io.open(path, "w") + if not f then + return false, ("Failed to open file '%s'."):format(path) + end + + if not f:write(kml) then + return false, ("Failed to write file '%s'."):format(path) + end + + f:close() + + return true, ("The map has been saved at '%s'."):format(path) +end + +local parse_args = function() + local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path') + if not map_path then + return false, "Need to specify a path for the map." + end + + return true, map_path +end + +postrule = function() + -- Only run if a previous script has registered geolocation data. + return not geoip.empty() +end + +action = function() + local output = stdnse.output_table() + + -- Parse and sanity check the command line arguments. + local status, path = parse_args() + if not status then + output.ERROR = path + return output, output.ERROR + end + + -- Render the map. + local status, msg = render(path) + if not status then + output.ERROR = msg + return output, output.ERROR + end + + return msg +end diff --git a/scripts/ip-geolocation-maxmind.nse b/scripts/ip-geolocation-maxmind.nse index 241184e56..5d6d8993f 100644 --- a/scripts/ip-geolocation-maxmind.nse +++ b/scripts/ip-geolocation-maxmind.nse @@ -1,4 +1,5 @@ local bit = require "bit" +local geoip = require "geoip" local io = require "io" local ipOps = require "ipOps" local math = require "math" @@ -492,6 +493,7 @@ local GeoIP = { output_record_by_addr = function(self,addr) local loc = self:record_by_addr(addr) if not loc then return nil end + geoip.add(addr, loc.latitude, loc.longitude) setmetatable(loc, record_metatable) return loc end, diff --git a/scripts/script.db b/scripts/script.db index e2d1363de..44dd9df9f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -279,6 +279,9 @@ Entry { filename = "informix-tables.nse", categories = { "auth", "intrusive", } Entry { filename = "ip-forwarding.nse", categories = { "discovery", "safe", } } Entry { filename = "ip-geolocation-geoplugin.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ip-geolocation-ipinfodb.nse", categories = { "discovery", "external", "safe", } } +Entry { filename = "ip-geolocation-map-bing.nse", categories = { "external", "safe", } } +Entry { filename = "ip-geolocation-map-google.nse", categories = { "external", "safe", } } +Entry { filename = "ip-geolocation-map-kml.nse", categories = { "safe", } } Entry { filename = "ip-geolocation-maxmind.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ip-https-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "ipidseq.nse", categories = { "discovery", "safe", } }