diff --git a/CHANGELOG b/CHANGELOG index 388e34a48..61fb69aa9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script duplicates which attempts to determine duplicate + hosts by analyzing information collected by other scripts. [Patrik Karlsson] + Nmap 5.61TEST5 [2012-03-09] o Integrated all of your IPv4 OS fingerprint submissions since June diff --git a/scripts/duplicates.nse b/scripts/duplicates.nse new file mode 100644 index 000000000..a6340e7dc --- /dev/null +++ b/scripts/duplicates.nse @@ -0,0 +1,261 @@ +description = [[ +The duplicates script attempts to discover multihomed systems by analysing and +comparing information collected by other scripts. The information analyzed +currently includes: + - SSL Certificates + - SSH Host keys + - MAC Address + - Netbios Server Name + +In order for the script to be able to analyze the data it has dependencies to +the following scripts: ssl-cert,ssh-hostkey,nbtstat. + +One or more of these scripts have to be run in order to allow the duplicates +script to analyze the data. +]] + +--- +-- @usage +-- sudo nmap -PN -p445,443 --script duplicates,nbstat,ssl-cert +-- +-- @output +-- | duplicates: +-- | ARP +-- | MAC: 01:23:45:67:89:0a +-- | 192.168.99.10 +-- | 192.168.99.11 +-- | Netbios +-- | Server Name: WIN2KSRV001 +-- | 192.168.0.10 +-- |_ 192.168.1.10 +-- + + +-- +-- While the script provides basic duplicate functionality, here are some ideas +-- on improvements. +-- +-- Possible additional information sources: +-- * Microsoft SQL Server instance names (Match hostname, version, instance +-- names and ports) - Reliable given several instances +-- * Oracle TNS names - Not very reliable +-- +-- Possible enhancements: +-- * Compare hosts across information sources and create a global category +-- in which system duplicates are reported based on more than one source. +-- * Add a reliability index for each information source that indicates how +-- reliable the duplicate match was. This could be an index compared to +-- other information sources as well as an indicator of how good the match +-- was for a particular information source. + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"safe"} +dependencies = {"ssl-cert", "ssh-hostkey", "nbstat"} + +require 'ipOps' + +hostrule = function() return true end +postrule = function() return true end + +--- check for the presence of a value in a table +--@param tab the table to search into +--@param item the searched value +--@return a boolean indicating whether the value has been found or not +local function contains(tab, item) + for _, val in pairs(tab) do + if val == item then + return true + end + end + return false +end + +local function processSSLCerts(tab) + + -- Handle SSL-certificates + -- We create a new table using the SHA1 digest as index + local ssl_certs = {} + for host, v in pairs(tab) do + for port, sha1 in pairs(v) do + ssl_certs[sha1] = ssl_certs[sha1] or {} + if ( not contains(ssl_certs[sha1], host.ip) ) then + table.insert(ssl_certs[sha1], host.ip) + end + end + end + + local results = {} + for sha1, hosts in pairs(ssl_certs) do + table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end) + if ( #hosts > 1 ) then + table.insert(results, { name = ("Certficate (%s)"):format(sha1), hosts } ) + end + end + + return results +end + +local function processSSHKeys(tab) + + local hostkeys = {} + + -- create a reverse mapping key_fingerprint -> host(s) + for ip, keys in pairs(tab) do + for _, key in ipairs(keys) do + local fp = ssh1.fingerprint_hex(key.fingerprint, key.algorithm, key.bits) + if not hostkeys[fp] then + hostkeys[fp] = {} + end + -- discard duplicate IPs + if not contains(hostkeys[fp], ip) then + table.insert(hostkeys[fp], ip) + end + end + end + + -- look for hosts using the same hostkey + local results = {} + for key, hosts in pairs(hostkeys) do + if #hostkeys[key] > 1 then + table.sort(hostkeys[key], function(a, b) return ipOps.compare_ip(a, "lt", b) end) + local str = 'Key ' .. key .. ':' + table.insert( results, { name = str, hostkeys[key] } ) + end + end + + return results +end + +local function processNBStat(tab) + + local results, mac_table, name_table = {}, {}, {} + for host, v in pairs(tab) do + mac_table[v.mac] = mac_table[v.mac] or {} + if ( not(contains(mac_table[v.mac], host.ip)) ) then + table.insert(mac_table[v.mac], host.ip) + end + + name_table[v.server_name] = name_table[v.server_name] or {} + if ( not(contains(name_table[v.server_name], host.ip)) ) then + table.insert(name_table[v.server_name], host.ip) + end + end + + for mac, hosts in pairs(mac_table) do + if ( #hosts > 1 ) then + table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end) + table.insert(results, { name = ("MAC: %s"):format(mac), hosts }) + end + end + + for srvname, hosts in pairs(name_table) do + if ( #hosts > 1 ) then + table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end) + table.insert(results, { name = ("Server Name: %s"):format(srvname), hosts }) + end + end + + return results +end + +local function processMAC(tab) + + local function format_mac(mac) + local octets = {} + for _, v in ipairs({ string.byte(mac, 1, #mac) }) do + octets[#octets + 1] = string.format("%02x", v) + end + return stdnse.strjoin(":", octets) + end + + local mac + local mac_table = {} + + for host in pairs(tab) do + if ( host.mac_addr ) then + mac = format_mac(host.mac_addr) + mac_table[mac] = mac_table[mac] or {} + if ( not(contains(mac_table[mac], host.ip)) ) then + table.insert(mac_table[mac], host.ip) + end + end + end + + local results = {} + for mac, hosts in pairs(mac_table) do + if ( #hosts > 1 ) then + table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end) + table.insert(results, { name = ("MAC: %s"):format(mac), hosts }) + end + end + + return results +end + +postaction = function() + + local handlers = { + ['ssl-cert'] = { func = processSSLCerts, name = "SSL" }, + ['sshhostkey'] = { func = processSSHKeys, name = "SSH" }, + ['nbstat'] = { func = processNBStat, name = "Netbios" }, + ['mac'] = { func = processMAC, name = "ARP" } + } + + -- temporary re-allocation code for SSH keys + for k, v in pairs(nmap.registry.sshhostkey or {}) do + nmap.registry['duplicates'] = nmap.registry['duplicates'] or {} + nmap.registry['duplicates']['sshhostkey'] = nmap.registry['duplicates']['sshhostkey'] or {} + nmap.registry['duplicates']['sshhostkey'][k] = v + end + + if ( not(nmap.registry['duplicates']) ) then + return + end + + local results = {} + for key, handler in pairs(handlers) do + if ( nmap.registry['duplicates'][key] ) then + local result_part = handler.func( nmap.registry['duplicates'][key] ) + if ( result_part and #result_part > 0 ) then + table.insert(results, { name = handler.name, result_part } ) + end + end + end + + return stdnse.format_output(true, results) +end + +-- we have no real action in here. In essence we move information from the +-- host based registry to the global one, so that our postrule has access to +-- it when we need it. +hostaction = function(host) + + nmap.registry['duplicates'] = nmap.registry['duplicates'] or {} + + for port, cert in pairs(host.registry["ssl-cert"] or {}) do + nmap.registry['duplicates']['ssl-cert'] = nmap.registry['duplicates']['ssl-cert'] or {} + nmap.registry['duplicates']['ssl-cert'][host] = nmap.registry['duplicates']['ssl-cert'][host] or {} + nmap.registry['duplicates']['ssl-cert'][host][port] = stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 }) + end + + if ( host.registry['nbstat'] ) then + nmap.registry['duplicates']['nbstat'] = nmap.registry['duplicates']['nbstat'] or {} + nmap.registry['duplicates']['nbstat'][host] = host.registry['nbstat'] + end + + if ( host.mac_addr_src ) then + nmap.registry['duplicates']['mac'] = nmap.registry['duplicates']['mac'] or {} + nmap.registry['duplicates']['mac'][host] = true + end + + return +end + +local Actions = { + hostrule = hostaction, + postrule = postaction +} + +-- execute the action function corresponding to the current rule +action = function(...) return Actions[SCRIPT_TYPE](...) end diff --git a/scripts/nbstat.nse b/scripts/nbstat.nse index e4d07330f..61ecf015f 100644 --- a/scripts/nbstat.nse +++ b/scripts/nbstat.nse @@ -110,6 +110,10 @@ action = function(host) if manuf == nil then manuf = "unknown" end + host.registry['nbstat'] = { + server_name = server_name, + mac = ("%02x:%02x:%02x:%02x:%02x:%02x"):format( statistics:byte(1), statistics:byte(2), statistics:byte(3), statistics:byte(4), statistics:byte(5), statistics:byte(6) ) + } mac = string.format("%02x:%02x:%02x:%02x:%02x:%02x (%s)", statistics:byte(1), statistics:byte(2), statistics:byte(3), statistics:byte(4), statistics:byte(5), statistics:byte(6), manuf) -- Samba doesn't set the Mac address, and nmap-mac-prefixes shows that as Xerox if(mac == "00:00:00:00:00:00 (Xerox)") then diff --git a/scripts/script.db b/scripts/script.db index 652863dde..1ac3cf8ca 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -78,6 +78,7 @@ Entry { filename = "domino-enum-users.nse", categories = { "auth", "intrusive", Entry { filename = "dpap-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "drda-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "drda-info.nse", categories = { "discovery", "safe", "version", } } +Entry { filename = "duplicates.nse", categories = { "safe", } } Entry { filename = "eap-info.nse", categories = { "broadcast", "safe", } } Entry { filename = "epmd-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } }