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