diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
commit | 0d47952611198ef6b1163f366dc03922d20b1475 (patch) | |
tree | 3d840a3b8c0daef0754707bfb9f5e873b6b1ac13 /nselib/dnsbl.lua | |
parent | Initial commit. (diff) | |
download | nmap-0d47952611198ef6b1163f366dc03922d20b1475.tar.xz nmap-0d47952611198ef6b1163f366dc03922d20b1475.zip |
Adding upstream version 7.94+git20230807.3be01efb1+dfsg.upstream/7.94+git20230807.3be01efb1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | nselib/dnsbl.lua | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/nselib/dnsbl.lua b/nselib/dnsbl.lua new file mode 100644 index 0000000..b551799 --- /dev/null +++ b/nselib/dnsbl.lua @@ -0,0 +1,624 @@ +--- A minimalistic DNS BlackList library implemented to facilitate querying +-- various DNSBL services. The current list of services has been implemented +-- based on the following compilations of services: +-- * http://en.wikipedia.org/wiki/Comparison_of_DNS_blacklists +-- * http://www.robtex.com +-- * http://www.sdsc.edu/~jeff/spam/cbc.html +-- +-- The library implements a helper class through which script may access +-- the BL services. A typical script implementation could look like this: +-- +-- <code> +-- local helper = dnsbl.Helper:new("SPAM", "short") +-- helper:setFilter('dnsbl.inps.de') +-- local status, result = helper:checkBL(host.ip) +-- ... formatting code ... +-- </code> +-- +-- @author Patrik Karlsson <patrik@cqure.net> +-- + +local coroutine = require "coroutine" +local dns = require "dns" +local ipOps = require "ipOps" +local nmap = require "nmap" +local stdnse = require "stdnse" +local stringaux = require "stringaux" +local table = require "table" +_ENV = stdnse.module("dnsbl", stdnse.seeall) + + +-- Creates a new Service instance +-- @param ip host that needs to be checked +-- @param mode string (short|long) specifying whether short or long +-- results are to be returned +-- @param config service configuration in case this service provider +-- needs user supplied configuration +-- @return o instance of Helper +local function service_new (self, ip, mode, config) + local o = { ip = ip, mode = mode, config = config } + setmetatable(o, self) + self.__index = self + return o +end + +-- The services table contains a list of valid DNSBL providers +-- Providers are categorized in categories that should contain services that +-- do DNS blacklist checks for that particular category. +-- +-- Each service should be stored under a key that specifies the service name +-- and should contain: +-- <code>ns_type</code> - A table with a record type as key and mode as value +-- eg: { ["A"] = "short", ["TXT"] = "long" }. +-- If only short queries are supported using A records, this argument may be +-- omitted. +-- +-- <code>resp_parser</code> - A function to parse the response received from +-- the DNS query. The function should take two arguments: +-- * <code>response</code> - the DNS response received by the server, +-- typically a code represented by an IP. +-- * <code>mode</code> - a string representing what mode (long|short) that +-- the function should parse. If <code>ns_type</code> does not contain +-- the TXT record, this argument and check can be omitted. +-- When the short mode is used, the function should return a table containing +-- the <code>state</code> field, or nil if the IP wasn't listed. When long +-- mode is used, the function should return additional information using the +-- <code>details</code> field. Eg: +-- return { state = "SPAM" } -- short mode +-- return { state = "PROXY", details = { +-- "Proxy is working", +-- "Proxy was scanned" +-- } -- long mode +-- +-- <code>fmt_query</code> - A function responsible for formatting the DNS +-- query. When the default format is being used <reverse ip>.<servicename> +-- eg: 4.3.2.1.spam.dnsbl.sorbs.net, this function can be omitted. But if +-- this function is defined, it must return the query to be executed, +-- otherwise the library will assume that the provider needs configuration +-- that failed to be provided. +-- +-- <code>configuration</code> - If the service requires the user to provide +-- configurations, this function will have to return a list with the name +-- and description of the arguments that provide the configuration/options. +-- If this function isn't specified, the library will assume the service +-- doesn't require configuration. +-- +SERVICES = { + + SPAM = { + + ["dnsbl.inps.de"] = { + -- This service supports both long and short <code>mode</code> + ns_type = { + ["short"] = "A", + ["long"] = "TXT", + }, + new = service_new, + -- Sample fmt_query function, if no function is specified, the library + -- will assume that the IP should be reversed add suffixed with the + -- service name. + fmt_query = function(self) + local rev_ip = dns.reverse(self.ip):match("^(.*)%.in%-addr%.arpa$") + return ("%s.spam.dnsbl.sorbs.net"):format(rev_ip) + end, + -- This function parses the response and supports both long and + -- short mode. + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "SPAM", + } + if ( ("short" == self.mode and r[1]) ) then + return responses[r[1]] + else + return { state = "SPAM", details = { r[1] } } + end + end, + }, + + ["spam.dnsbl.sorbs.net"] = { + ns_type = { + ["short"] = "A" + }, + new = service_new, + resp_parser = function(self, r) + return ( r[1] == "127.0.0.6" and { state = "SPAM" } ) + end, + }, + + ["bl.nszones.com"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "SPAM", + ["127.0.0.3"] = "DYNAMIC" + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["all.spamrats.com"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.36"] = "DYNAMIC", + ["127.0.0.38"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["list.quorum.to"] = { + new = service_new, + resp_parser = function(self, r) + -- this service appears to return 127.0.0.0 when the service is + -- "blocked because it has never been seen to send mail". + -- This would essentially return every host as SPAM and we + -- don't want that. + return ( ( r[1] and r[1] ~= "127.0.0.0" ) and { state = "SPAM" } ) + end + }, + + ["sbl.spamhaus.org"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "SPAM", + ["127.0.0.3"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["bl.spamcop.net"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["l2.apews.org"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + }, + + PROXY = { + + ["dnsbl.tornevall.org"] = { + new = service_new, + resp_parser = function(self, r) + if ( "short" == self.mode and r[1] ) then + return { state = "PROXY" } + elseif ( "long" == self.mode ) then + local responses = { + [1] = "Proxy has been scanned", + [2] = "Proxy is working", + [4] = "?", + [8] = "Proxy was tested, but timed out on connection", + [16] = "Proxy was tested but failed at connection", + [32] = "Proxy was tested but the IP was different", + [64] = "IP marked as \"abusive host\"", + [128] = "Proxy has a different anonymous-state" + } + + local code = tonumber(r[1]:match("%.(%d*)$")) + local result = {} + + for k, v in pairs(responses) do + if ( ( code & k ) == k ) then + table.insert(result, v) + end + end + return { state = "PROXY", details = result } + end + end, + }, + + ["ip-port.exitlist.torproject.org"] = { + configuration = { + ["port"] = "the port to which the target can relay to", + ["ip"] = "the IP address to which the target can relay to" + }, + new = service_new, + fmt_query = function(self) + if ( not(self.config.port) or not(self.config.ip) ) then + return + end + + local rev_ip = dns.reverse(self.ip):match("^(.*)%.in%-addr%.arpa$") + return ("%s.%s.%s.ip-port.exitlist.torproject.org"):format(rev_ip, + self.config.port, self.config.ip) + end, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["tor.dan.me.uk"] = { + ns_type = { + ["short"] = "A", + ["long"] = "TXT", + }, + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.100"] = "PROXY", + } + if ( "short" == self.mode and r[1] ) then + return { state = responses[r[1]] } + else + local flagsinfo = { + ["E"] = "Exit", + ["A"] = "Authority", + ["B"] = "BadExit", + ["D"] = "V2Dir", + ["F"] = "Fast", + ["G"] = "Guard", + ["H"] = "HSDir", + ["N"] = "Named", + ["R"] = "Running", + ["S"] = "Stable", + ["U"] = "Unnamed", + ["V"] = "Valid" + } + + local name, ports, flagsfound = r[1]:match( + "N:(.+)/P:([%d,]+)/F:([EABDFGHNRSUV]+)") + + local flags = {} + flags['name'] = "Flags" + + for k, v in pairs(flagsinfo) do + if flagsfound:match(k) then + table.insert(flags, v) + end + end + + local result = { + ("Name: %s"):format(name), + ("Ports: %s"):format(ports), + flags + } + + return { state = "PROXY", details = result } + end + end, + }, + + ["http.dnsbl.sorbs.net"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["socks.dnsbl.sorbs.net"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.3"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["misc.dnsbl.sorbs.net"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.4"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + } + + }, + + ATTACK = { + ["dnsbl.httpbl.org"] = { + configuration = { + ["apikey"] = "the http:BL API key" + }, + new = service_new, + fmt_query = function(self) + if ( not(self.config.apikey) ) then + return + end + + local rev_ip = dns.reverse(self.ip):match("^(.*)%.in%-addr%.arpa$") + return ("%s.%s.dnsbl.httpbl.org"):format(self.config.apikey, rev_ip) + end, + resp_parser = function(self, r) + if ( not(r[1]) ) then + return + end + + local parts, err = ipOps.get_parts_as_number(r[1]) + + if ( not(parts) or err ) then + -- TODO Should we return failure in the result? + stdnse.debug1("The dnsbl.httpbl.org provider failed to return a valid address") + return + end + + local octet1, octet2, octet3, octet4 = table.unpack(parts) + + if ( octet1 ~= 127 ) then + -- This shouldn't happen :P + stdnse.debug1( + "The request made to dnsbl.httpbl.org was considered invalid (%i)", + octet1) + elseif ( "short" == self.mode ) then + return { state = "ATTACK" } + else + local search = { + [0] = "Undocumented", + [1] = "AltaVista", + [2] = "Ask", + [3] = "Baidu", + [4] = "Excite", + [5] = "Google", + [6] = "Looksmart", + [7] = "Lycos", + [8] = "MSN", + [9] = "Yahoo", + [10] = "Cuil", + [11] = "InfoSeek", + [12] = "Miscellaneous" + } + + local result = {} + + -- Search engines are a special case. + if ( octet4 == 0 ) then + table.insert(result, ("Search engine: %s"):format( + search[octet3])) + else + table.insert(result, ("Last activity: %i days"):format( + octet2)) + table.insert(result, ("Threat score: %i"):format( + octet3)) + + local activity = {} + activity['name'] = "Activity" + -- Suspicious activity + if ( (octet4 & 1) == 1) then + table.insert(activity, "Suspicious") + end + + -- Harvester + if ( (octet4 & 2) == 2) then + table.insert(activity, "Harvester") + end + + -- Comment spammer + if ( (octet4 & 4) == 4) then + table.insert(activity, "Comment spammer") + end + + table.insert(result, activity) + end + + return { state = "ATTACK", details = result } + end + end, + }, + + ["all.bl.blocklist.de"] = { + new = service_new, + resp_parser = function(self, r) + local responses = { + ["127.0.0.2"] = "Amavis", + ["127.0.0.3"] = "DDoS", + ["127.0.0.4"] = "Asterisk, SIP, VoIP", + ["127.0.0.5"] = "Badbot", + ["127.0.0.6"] = "FTP", + ["127.0.0.7"] = "IMAP", + ["127.0.0.8"] = "IRC bot", + ["127.0.0.9"] = "Mail", + ["127.0.0.10"] = "POP3", + ["127.0.0.11"] = "Registration bot", + ["127.0.0.12"] = "Remote file inclusion", + ["127.0.0.13"] = "SASL", + ["127.0.0.14"] = "SSH", + ["127.0.0.15"] = "w00tw00t", + ["127.0.0.16"] = "Port flood", + } + if ( "short" == self.mode and r[1] ) then + return "ATTACK" + else + return ( r[1] and responses[r[1]] ) and { state = "ATTACK", + details = { + ("Type: %s"):format(responses[r[1]]) + } + } + end + end, + } +}, + +} + + + +Helper = { + + -- Creates a new Helper instance + -- @param category string containing a valid DNSBL service category + -- @param mode string (short|long) specifying whether short or long + -- results are to be returned + -- @return o instance of Helper + new = function(self, category, mode) + local o = { category = category:upper(), mode = mode } + assert(category and SERVICES[category:upper()], "Invalid category was supplied, aborting") + setmetatable(o, self) + self.__index = self + return o + end, + + -- Lists all DNSBL services for the category + -- @return services table of service names + listServices = function(self) + local services = {} + for name, svc in pairs(SERVICES[self.category]) do + if ( svc.configuration ) then + local service = {} + service['name'] = name + + for config, description in pairs(svc.configuration) do + table.insert(service, ("config: %s.%s - %s"):format( + name, config, description)) + end + + table.insert(services, service ) + else + table.insert(services, name) + end + end + return services + end, + + -- Validates the filter set by setFilter to make sure it contains only + -- valid service names. + -- @return status boolean, true on success false on failure + -- @return err string containing an error message on failure + validateFilter = function(self) + + if ( not(self.filterstr) ) then + return true + end + + local all = SERVICES[self.category] + self.filter = {} + for _, f in pairs(stringaux.strsplit(",%s*", self.filterstr)) do + if ( not(SERVICES[self.category][f]) ) then + self.filter = nil + return false, ("Service does not exist '%s'"):format(f) + end + self.filter[f] = true + end + return true + end, + + -- Sets a new service filter to choose only a limited subset of services + -- within a category. + -- @param filter string containing a comma separated list of service names + setFilter = function(self, filter) self.filterstr = filter end, + + -- Gets a list of filtered services, or all services if no filter is in use + -- @return services table containing a list of services + getServices = function(self) + if ( not(self:validateFilter()) ) then + return nil + end + + if ( self.filter ) then + local filtered = {} + for name, svc in pairs(SERVICES[self.category]) do + if ( self.filter[name] ) then + filtered[name] = svc + end + end + return filtered + else + return SERVICES[self.category] + end + end, + + doQuery = function(self, ip, name, svc, answers) + + local condvar = nmap.condvar(answers) + local config = {} + + if ( svc.configuration ) then + for key in pairs(svc.configuration) do + config[key] = stdnse.get_script_args(("%s.%s"):format(name, key)) + end + end + + svc = svc:new(ip, self.mode, config) + + local ns_type = ( svc.ns_type and svc.ns_type[self.mode] ) and svc.ns_type[self.mode] or "A" + local query + + if ( not(svc.fmt_query) ) then + local rev_ip = dns.reverse(ip):match("^(.*)%.in%-addr%.arpa$") + query = ("%s.%s"):format(rev_ip, name) + else + query = svc:fmt_query() + end + + if ( query ) then + local status, answer = dns.query(query, {dtype=ns_type, retAll=true} ) + answers[name] = { status = status, answer = answer, svc = svc } + else + stdnse.debug1("Query function returned nothing, skipping '%s'", name) + end + + condvar "signal" + end, + + -- Runs the DNS blacklist check for the given IP against all non-filtered + -- services in the given category. + -- @param ip string containing the IP address to check + -- @return result table containing the results of the BL checks + checkBL = function(self, ip) + local result, answers, threads = {}, {}, {} + local condvar = nmap.condvar(answers) + + for name, svc in pairs(self:getServices()) do + local co = stdnse.new_thread(self.doQuery, self, ip, name, svc, answers) + threads[co] = true + end + + repeat + for t in pairs(threads) do + if ( coroutine.status(t) == "dead" ) then threads[t] = nil end + end + if ( next(threads) ) then + condvar "wait" + end + until( next(threads) == nil ) + + for name, answer in pairs(answers) do + local status, answer, svc = answer.status, answer.answer, answer.svc + if ( status ) then + local svc_result = svc:resp_parser(answer) + if ( not(svc_result) ) then + local resp = ( #answer > 0 and ("UNKNOWN (%s)"):format(answer[1]) or "UNKNOWN" ) + stdnse.debug2("%s received %s", name, resp) + end + + if ( svc_result ) then + table.insert(result, { name = name, result = svc_result }) + end + + -- if status is false, and the response was "No Such Name", it + -- simply means that the IP isn't listed, we haven't failed at + -- this point. It would obviously be better to check this against + -- an error code, or in some other way, but this is what we've got. + elseif ( answer ~= "No Such Name" ) then + table.insert(result, { name = name, result = { state = "FAIL" }}) + end + end + return result + end, + +} + +return _ENV; |