From 663271f95d34087dcad792107f325eb2cb959d26 Mon Sep 17 00:00:00 2001 From: dmiller Date: Wed, 16 Mar 2016 05:47:58 +0000 Subject: [PATCH] New shodan-api script for querying Shodan internet scan data --- CHANGELOG | 4 + scripts/script.db | 1 + scripts/shodan-api.nse | 223 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 scripts/shodan-api.nse diff --git a/CHANGELOG b/CHANGELOG index 967fe8bb6..b19d1583e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#333] Added shodan-api for querying the Shodan API + (https://www.shodan.io) and retrieving open port and service info from their + Internet-wide scan data. [Glenn Wilkinson] + o Allow the -4 option for Nmap to indicate IPv4 address family. This is the default, and using the option doesn't change anything, but does make it more explicit which address family you want to scan. Using -4 with -6 is an error. diff --git a/scripts/script.db b/scripts/script.db index a875cd298..b1af3bc63 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -411,6 +411,7 @@ Entry { filename = "rusers.nse", categories = { "discovery", "safe", } } Entry { filename = "s7-info.nse", categories = { "discovery", "version", } } Entry { filename = "samba-vuln-cve-2012-1182.nse", categories = { "intrusive", "vuln", } } Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "shodan-api.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "sip-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "sip-call-spoof.nse", categories = { "discovery", "intrusive", } } Entry { filename = "sip-enum-users.nse", categories = { "auth", "intrusive", } } diff --git a/scripts/shodan-api.nse b/scripts/shodan-api.nse new file mode 100644 index 000000000..8dab22981 --- /dev/null +++ b/scripts/shodan-api.nse @@ -0,0 +1,223 @@ +local http = require "http" +local io = require "io" +local ipOps = require "ipOps" +local json = require "json" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local tab = require "tab" +local table = require "table" +local openssl = stdnse.silent_require "openssl" + + +-- Set your Shodan API key here to avoid typing it in every time: +local apiKey = "" + +author = "Glenn Wilkinson (idea: Charl van der Walt )" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe", "external"} + +description = [[ +Queries Shodan API for given targets and produces similar output to +a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script +argument, or hardcoded in the .nse file itself. You can get a free key from +https://developer.shodan.io + +N.B if you want this script to run completely passively make sure to +include the -sn -Pn -n flags. +]] + +--- +-- @usage +-- nmap --script shodan-api x.y.z.0/24 -sn -Pn -n --script-args 'shodan-api.outfile=potato.csv,shodan-api.apikey=SHODANAPIKEY' +-- nmap --script shodan-api --script-args 'shodan-api.target=x.y.z.a,shodan-api.apikey=SHODANAPIKEY' +-- +-- @output +-- | shodan-api: Report for 2600:3c01::f03c:91ff:fe18:bb2f (scanme.nmap.org) +-- | PORT PROTO PRODUCT VERSION +-- | 80 tcp Apache httpd +-- | 3306 tcp MySQL 5.5.40-0+wheezy1 +-- | 22 tcp OpenSSH 6.0p1 Debian 4+deb7u2 +-- |_443 tcp +-- +--@args shodan-api.outfile Write the results to the specified CSV file +--@args shodan-api.apikey Specify the ShodanAPI key. This can also be hardcoded in the nse file. +--@args shodan-api.target Specify a single target to be scanned. +-- +--@xmloutput +-- +-- scanme.nmap.org +--
+-- +--
+-- tcp +-- 22 +--
+-- +-- 2.4.7 +-- Apache httpd +-- tcp +-- 80 +--
+-- + +-- ToDo: * Have an option to complement non-banner scans with shodan data (e.g. -sS scan, but +-- grab service info from Shodan +-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of +-- scan, verbose port output (e.g. smb share info) +-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host +-- * Accept IP ranges via the script argument 'target' parameter + + +-- Begin +if not nmap.registry[SCRIPT_NAME] then + nmap.registry[SCRIPT_NAME] = { + apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey") or apiKey, + count = 0 + } +end +local registry = nmap.registry[SCRIPT_NAME] +local outFile = stdnse.get_script_args(SCRIPT_NAME .. ".outfile") +local arg_target = stdnse.get_script_args(SCRIPT_NAME .. ".target") + +local function lookup_target (target) + local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. registry.apiKey, {any_af = true}) + if response.status == 404 then + stdnse.debug1("Host not found: %s", target) + return nil + elseif (response.status ~= 200) then + stdnse.debug1("Bad response from Shodan for IP %s : %s", target, response.status) + return nil + end + + local stat, resp = json.parse(response.body) + if not stat then + stdnse.debug1("Error parsing Shodan response: %s", resp) + return nil + end + + return resp +end + +local function format_output(resp) + if resp.error then + return resp.error + end + + if resp.data then + registry.count = registry.count + 1 + local out = { hostnames = resp.hostnames, ports = {} } + local ports = out.ports + local tab_out = tab.new() + tab.addrow(tab_out, "PORT", "PROTO", "PRODUCT", "VERSION") + + for key, e in ipairs(resp.data) do + ports[#ports+1] = { + number = e.port, + protocol = e.transport, + product = e.product, + version = e.version, + } + tab.addrow(tab_out, e.port, e.transport, e.product or "", e.version or "") + end + return out, tab.dump(tab_out) + else + return "Unable to query data" + end +end + +prerule = function () + if (outFile ~= nil) then + local file = io.open(outFile, "w") + io.output(file) + io.write("IP,Port,Proto,Product,Version\n") + end + + if registry.apiKey == "" then + registry.apiKey = nil + end + + if not registry.apiKey then + stdnse.verbose1("Error: Please specify your ShodanAPI key with the %s.apikey argument", SCRIPT_NAME) + return false + end + + local response = http.get("api.shodan.io", 443, "/api-info?key=" .. registry.apiKey, {any_af=true}) + if (response.status ~= 200) then + stdnse.verbose1("Error: Your ShodanAPI key (%s) is invalid", registry.apiKey) + -- Prevent further stages from running + registry.apiKey = nil + return false + end + + if arg_target then + local is_ip, err = ipOps.expand_ip(arg_target) + if not is_ip then + stdnse.verbose1("Error: %s.target must be an IP address", SCRIPT_NAME) + return false + end + return true + end +end + +generic_action = function(ip) + local resp = lookup_target(ip) + if not resp then return nil end + local out, tabular = format_output(resp) + if type(out) == "string" then + -- some kind of error + return out + end + local result = string.format( + "Report for %s (%s)\n%s", + ip, + table.concat(out.hostnames, ", "), + tabular + ) + if (outFile ~= nil) then + for _, port in ipairs(out.ports) do + io.write( string.format("%s,%s,%s,%s,%s\n", + ip, port.number, port.protocol, port.product or "", port.version or "") + ) + end + end + return out, result +end + +preaction = function() + return generic_action(arg_target) +end + +hostrule = function(host) + return registry.apiKey and not ipOps.isPrivate(host.ip) +end + +hostaction = function(host) + return generic_action(host.ip) +end + +postrule = function () + return registry.apiKey +end + +postaction = function () + local out = { "Shodan done: ", registry.count, " hosts up." } + if outFile then + io.close() + out[#out+1] = "\nWrote Shodan output to: " + out[#out+1] = outFile + end + return table.concat(out) +end + +local ActionsTable = { + -- prerule: scan target from script-args + prerule = preaction, + -- hostrule: look up a host in Shodan + hostrule = hostaction, + -- postrule: report results + postrule = postaction +} + +-- execute the action function corresponding to the current rule +action = function(...) return ActionsTable[SCRIPT_TYPE](...) end