224 lines
6.4 KiB
Lua
224 lines
6.4 KiB
Lua
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 <glenn@sensepost.com> (idea: Charl van der Walt <charl@sensepost.com>)"
|
|
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
|
|
-- <table key="hostnames">
|
|
-- <elem>scanme.nmap.org</elem>
|
|
-- </table>
|
|
-- <table key="ports">
|
|
-- <table>
|
|
-- <elem key="protocol">tcp</elem>
|
|
-- <elem key="number">22</elem>
|
|
-- </table>
|
|
-- <table>
|
|
-- <elem key="version">2.4.7</elem>
|
|
-- <elem key="product">Apache httpd</elem>
|
|
-- <elem key="protocol">tcp</elem>
|
|
-- <elem key="number">80</elem>
|
|
-- </table>
|
|
-- </table>
|
|
|
|
-- 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
|