From bb7d5a526fc611cdf57b1a89773129013c1c59cf Mon Sep 17 00:00:00 2001 From: patrik Date: Mon, 26 Dec 2011 14:22:25 +0000 Subject: [PATCH] o [NSE] Added script dns-blacklist that performs DNSBL checks of given or scanned IP addresses against multiple DNSBL services. [Patrik] --- CHANGELOG | 3 + nselib/dnsbl.lua | 360 ++++++++++++++++++++++++++++++++++++++ scripts/dns-blacklist.nse | 157 +++++++++++++++++ scripts/script.db | 1 + 4 files changed, 521 insertions(+) create mode 100644 nselib/dnsbl.lua create mode 100644 scripts/dns-blacklist.nse diff --git a/CHANGELOG b/CHANGELOG index 901d109a0..78f124293 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added script dns-blacklist that performs DNSBL checks of given or + scanned IP addresses against multiple DNSBL services. [Patrik] + o [NSE] Applied patch to snmp-brute that solves problems with handling errors that occur when parsing files with community lists. [Duarte Silva] diff --git a/nselib/dnsbl.lua b/nselib/dnsbl.lua new file mode 100644 index 000000000..9f4188019 --- /dev/null +++ b/nselib/dnsbl.lua @@ -0,0 +1,360 @@ +--- 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: +-- +-- +-- local helper = dnsbl.Helper:new("SPAM", "short") +-- helper:setFilter('dnsbl.inps.de') +-- local status, result = helper:checkBL(host.ip) +-- ... formatting code ... +-- +-- +-- @author "Patrik Karlsson " +-- + +module(... or "dnsbl", package.seeall) + +require 'bit' + +-- 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: +-- ns_type - 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. +-- +-- resp_parser - A function to parse the response received from +-- the DNS query. The function should take two arguments: +-- * response - the DNS response received by the server, +-- typically a code represented by an IP. +-- * mode - a string representing what mode (long|short) that +-- the function should parse. If ns_type 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 state field, or nil if the IP wasn't listed. When long +-- mode is used, the function should return additional information using the +-- details field. Eg: +-- return { state = "SPAM" } -- short mode +-- return { state = "PROXY", details = { +-- "Proxy is working", +-- "Proxy was scanned" +-- } -- long mode +-- +-- fmt_query - A function responsible for formatting the DNS +-- query. When the default format is being used . +-- eg: 4.3.2.1.spam.dnsbl.sorbs.net, this function can be omitted. +-- +SERVICES = { + + SPAM = { + + ["dnsbl.inps.de"] = { + -- This service supports both long and short mode + ns_type = { + ["short"] = "A", + ["long"] = "TXT", + }, + -- 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(ip) + local rev_ip = dns.reverse(ip):match("^(.*)%.in%-addr%.arpa$") + return ("%s.spam.dnsbl.sorbs.net"):format(rev_ip) + end, + -- This function parses the response and supports borth long and + -- short mode. + resp_parser = function(r, mode) + local responses = { + ["127.0.0.2"] = "SPAM", + } + if ( ("short" == 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" + }, + resp_parser = function(r) + return ( r[1] == "127.0.0.6" and { state = "SPAM" } ) + end, + }, + + ["bl.nszones.com"] = { + resp_parser = function(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"] = { + resp_parser = function(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"] = { + resp_parser = function(r) + return ( ( r[1] and r[1] == "127.0.0.2" ) and { state = "SPAM" } ) + end + }, + + ["sbl.spamhaus.org"] = { + resp_parser = function(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"] = { + resp_parser = function(r) + local responses = { + ["127.0.0.2"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["dnsbl.ahbl.org"] = { + resp_parser = function(r) + local responses = { + ["127.0.0.4"] = "SPAM", + ["127.0.0.5"] = "SPAM", + ["127.0.0.6"] = "SPAM", + ["127.0.0.7"] = "SPAM", + ["127.0.0.8"] = "SPAM", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["l2.apews.org"] = { + resp_parser = function(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"] = { + resp_parser = function(r, mode) + if ( "short" == mode and r[1] ) then + return { state = "PROXY" } + elseif ( "long" == 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 ( bit.band( code, k ) == k ) then + table.insert(result, v) + end + end + return { state = "PROXY", details = result } + end + end, + }, + + ["dnsbl.ahbl.org"] = { + resp_parser = function(r) + local responses = { + ["127.0.0.3"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + end, + }, + + ["http.dnsbl.sorbs.net"] = { + resp_parser = function(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"] = { + resp_parser = function(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"] = { + resp_parser = function(r) + local responses = { + ["127.0.0.4"] = "PROXY", + } + return ( r[1] and responses[r[1]] ) and { state = responses[r[1]] } + 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 svc in pairs(SERVICES[self.category]) do + table.insert(services, svc) + 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(stdnse.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, + + -- 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 = {} + + for name, svc in pairs(self:getServices()) do + --local ns_type = ( self.mode == "long" and (tabcontains(svc.ns_type or {}, 'TXT') and 'TXT' or 'A') or 'A') + local ns_type = ( svc.ns_type and svc.ns_type[self.mode] ) and svc.ns_type[self.mode] or "A" + local query + + if ( svc.fmt_query ) then + query = svc.fmt_query(ip) + else + local rev_ip = dns.reverse(ip):match("^(.*)%.in%-addr%.arpa$") + query = ("%s.%s"):format(rev_ip, name) + end + + local status, answer = dns.query(query, {dtype=ns_type, retAll=true} ) + if ( status ) then + local svc_result = svc.resp_parser(answer, self.mode) + + if ( not(svc_result) ) then + local resp = ( #answer > 0 and ("UNKNOWN (%s)"):format(answer[1]) or "UNKNOWN" ) + stdnse.print_debug(2, ("%s received %s"):format(name, resp)) + end + + -- only add a record if the response could be parsed, some + -- services, such as list.quorum.to, incorrectly return + -- 127.0.0.0 when all is good. + 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, + + +} + + + diff --git a/scripts/dns-blacklist.nse b/scripts/dns-blacklist.nse new file mode 100644 index 000000000..36d57294f --- /dev/null +++ b/scripts/dns-blacklist.nse @@ -0,0 +1,157 @@ +description = [[ +Checks an IP address against a number of different DNS spam blacklists and returns a list of services where the IP has been blacklisted. +Checks may be limited by service category (eg: SPAM, PROXY) or to a specific service name. +]] + +--- +-- @usage +-- nmap --script dns-blacklist --script-args='dns-blacklist.ip=' +-- or +-- nmap -sn --script dns-blacklist +-- +-- @output +-- Pre-scan script results: +-- | dns-blacklist: +-- | 1.2.3.4 +-- | PROXY +-- | dnsbl.ahbl.org - PROXY +-- | dnsbl.tornevall.org - PROXY +-- | - IP marked as "abusive host". +-- | - Proxy is working +-- | - Proxy has been scanned +-- | SPAM +-- | dnsbl.inps.de - SPAM +-- | - Spam Received See: http://www.sorbs.net/lookup.shtml?1.2.3.4 +-- | l2.apews.org - SPAM +-- | list.quorum.to - SPAM +-- | bl.spamcop.net - SPAM +-- |_ spam.dnsbl.sorbs.net - SPAM +-- +-- @args dns-blacklist.ip string containing the IP to check only needed if +-- running the script as a prerule. + +-- @args dns-blacklist.mode string containing either "short" or "long" +-- long mode can sometimes provide additional information to why an IP +-- has been blacklisted. (default: long) +-- +-- @args dns-blacklist.list lists all services that are available for a +-- certain category. +-- +-- @args dns-blacklist.services string containing a comma-separated list of +-- services to query. (default: all) +-- +-- @args dns-blacklist.category string containing the service category to query +-- eg. spam or proxy (default: all) +-- +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"external", "safe"} + +require 'dns' +require 'dnsbl' +require 'tab' +require 'ipOps' + +-- The script can be run either as a host- or pre-rule +hostrule = function() return true end +prerule = function() return true end + +local arg_IP = stdnse.get_script_args(SCRIPT_NAME .. ".ip") +local arg_mode = stdnse.get_script_args(SCRIPT_NAME .. ".mode") or "long" +local arg_list = stdnse.get_script_args(SCRIPT_NAME .. ".list") +local arg_services = stdnse.get_script_args(SCRIPT_NAME .. ".services") +local arg_category = stdnse.get_script_args(SCRIPT_NAME .. ".category") or "all" + +local function listServices() + local result = {} + if ( "all" == arg_category ) then + for cat in pairs(dnsbl.SERVICES) do + local helper = dnsbl.Helper:new(cat, arg_mode) + local cat_res= helper:listServices() + cat_res.name = cat + table.insert(result, cat_res) + end + else + result = dnsbl.Helper:new(arg_category, arg_mode):listServices() + end + return stdnse.format_output(true, result) +end + +local function formatResult(result) + local output = {} + for _, svc in ipairs(result) do + if ( svc.result.details ) then + svc.result.details.name = ("%s - %s"):format(svc.name, svc.result.state) + table.insert(output, svc.result.details) + else + table.insert(output, ("%s - %s"):format(svc.name, svc.result.state)) + end + end + return output +end + +dnsblAction = function(host) + + -- if the list argument was given, just list the services and abort + if ( arg_list ) then + return listServices() + end + + local helper + if ( arg_services and ( not(arg_category) or "all" == arg_category:lower() ) ) then + return "\n ERROR: A service filter can't be used without a specific category" + elseif( "all" ~= arg_category ) then + helper = dnsbl.Helper:new(arg_category, arg_mode) + helper:setFilter(arg_services) + local status, err = helper:validateFilter() + if ( not(status) ) then + return ("\n ERROR: %s"):format(err) + end + end + + local output = {} + if ( helper ) then + local result = helper:checkBL(host.ip) + if ( #result == 0 ) then return end + output = formatResult(result) + else + for cat in pairs(dnsbl.SERVICES) do + helper = dnsbl.Helper:new(cat, arg_mode) + local result = helper:checkBL(host.ip) + local out_part = formatResult(result) + if ( #out_part > 0 ) then + out_part.name = cat + table.insert(output, out_part) + end + end + if ( #output == 0 ) then return end + end + + if ( "prerule" == SCRIPT_TYPE ) then + output.name = host.ip + end + + return stdnse.format_output(true, output) +end + + +-- execute the action function corresponding to the current rule +action = function(...) + + if ( arg_mode ~= "short" and arg_mode ~= "long" ) then + return "\n ERROR: Invalid argument supplied, mode should be either 'short' or 'long'" + end + + if ( not(ipOps.todword(arg_IP)) ) then + return "\n ERROR: Invalid IP address was supplied" + end + + if ( arg_IP and "prerule" == SCRIPT_TYPE ) then + return dnsblAction( { ip = arg_IP } ) + elseif ( "hostrule" == SCRIPT_TYPE ) then + return dnsblAction(...) + end + +end diff --git a/scripts/script.db b/scripts/script.db index 7176ec6b2..57c45747f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -47,6 +47,7 @@ Entry { filename = "daytime.nse", categories = { "discovery", "safe", } } Entry { filename = "db2-das-info.nse", categories = { "discovery", "safe", "version", } } Entry { filename = "db2-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "dhcp-discover.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "dns-blacklist.nse", categories = { "external", "safe", } } Entry { filename = "dns-brute.nse", categories = { "discovery", "intrusive", } } Entry { filename = "dns-cache-snoop.nse", categories = { "discovery", "intrusive", } } Entry { filename = "dns-fuzz.nse", categories = { "fuzzer", "intrusive", } }