From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/shodan-api.nse | 223 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 scripts/shodan-api.nse (limited to 'scripts/shodan-api.nse') diff --git a/scripts/shodan-api.nse b/scripts/shodan-api.nse new file mode 100644 index 0000000..8dab229 --- /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 -- cgit v1.2.3