diff --git a/CHANGELOG b/CHANGELOG index 46b48bd9e..816beed85 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a new library dnssd with supporting functions for DNS Service + Discovery. Moved multicast prerule from dns-service-discovery to a new + script called broadcast-dns-service-discovery. [Patrik] + o [NSE] Added the rmi-dumpregistry script, which shows the contents of Java RMI registry. [Martin Holst Swende] diff --git a/nselib/dnssd.lua b/nselib/dnssd.lua new file mode 100644 index 000000000..d06142e67 --- /dev/null +++ b/nselib/dnssd.lua @@ -0,0 +1,400 @@ +--- Library for supporting DNS Service Discovery +-- +-- The library supports +-- * Unicast and Multicast requests +-- * Decoding responses +-- * Running requests in parallell using Lua coroutines +-- +-- The library contains the following classes +-- * Comm +-- ** A class with static functions that handle communication using the dns library +-- * Helper +-- ** The helper class wraps the Comm class using functions with a more descriptive name. +-- ** The purpose of this class is to give developers easy access to some of the common DNS-SD tasks. +-- * Util +-- ** The Util class contains a number of static functions mainly used to convert data. +-- +-- The following code snipplet queries all mDNS resolvers on the network for a +-- full list of their supported services and returns the formated output: +-- +-- local helper = dnssd.Helper:new( ) +-- helper:setMulticast(true) +-- return stdnse.format_output(helper:queryServices()) +-- +-- +-- This next snipplet queries a specific host for the same information: +-- +-- local helper = dnssd.Helper:new( host, port ) +-- return stdnse.format_output(helper:queryServices()) +-- +-- +-- In order to query for a specific service a string or table with service +-- names can be passed to the Helper.queryServices method. +-- +-- @args dnssd.services string or table containing services to query +-- +-- @author Patrik Karlsson +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- + +module(... or "dnssd", package.seeall) + +require 'dns' +require 'target' + +Util = { + + --- Converts a string ip to a numeric value suitable for comparing + -- + -- @param ip string containing the ip to convert + -- @return number containing the converted ip + ipToNumber = function(ip) + local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$") + return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4 + end, + + --- Compare function used for sorting IP-addresses + -- + -- @param a table containing first item + -- @param b table containing second item + -- @return true if the port of a is less than the port of b + ipCompare = function(a, b) + local ip_a = Util.ipToNumber(a.name) or 0 + local ip_b = Util.ipToNumber(b.name) or 0 + + if ( tonumber(ip_a) < tonumber(ip_b) ) then + return true + end + return false + end, + + --- Function used to compare discovered DNS services so they can be sorted + -- + -- @param a table containing first item + -- @param b table containing second item + -- @return true if the port of a is less than the port of b + serviceCompare = function(a, b) + -- if no port is found use 999999 for comparing, this way all services + -- without ports and device information gets printed at the end + local port_a = a.name:match("^(%d+)") or 999999 + local port_b = b.name:match("^(%d+)") or 999999 + + if ( tonumber(port_a) < tonumber(port_b) ) then + return true + end + return false + end, + + --- Creates a service host table + -- + -- ['_ftp._tcp.local'] = {10.10.10.10,20.20.20.20} + -- ['_http._tcp.local'] = {30.30.30.30,40.40.40.40} + -- + -- @param response containing multiple responses from dns.query + -- @return services table containing the service name as a key and all host addresses as value + createSvcHostTbl = function( response ) + local services = {} + -- Create unique table of services + for _, r in ipairs( response ) do + -- do we really have mutliple responses? + if ( not(r.output) ) then return end + for _, svc in ipairs(r.output ) do + services[svc] = services[svc] or {} + table.insert(services[svc], r.peer) + end + end + + return services + end, + + --- Creates a unique list of services + -- + -- @param response containing a single or multiple responses from + -- dns.query + -- @return array of strings containing service names + getUniqueServices = function( response ) + local services = {} + + for _, r in ipairs(response) do + if ( r.output ) then + for _, svc in ipairs(r.output) do services[svc] = true end + else + services[r] = true + end + end + + return services + end, + + --- Returns the amount of currenlty active threads + -- + -- @param threads table containing the list of threads + -- @return count number containing the number of non-dead threads + threadCount = function( threads ) + local count = 0 + + for thread in pairs(threads) do + if ( coroutine.status(thread) == "dead" ) then + threads[thread] = nil + else + count = count + 1 + end + end + return count + end + +} + +Comm = { + + --- Gets a record from both the Answer and Additional section + -- + -- @param dtype DNS resource record type. + -- @param response Decoded DNS response. + -- @param retAll If true, return all entries, not just the first. + -- @return True if one or more answers of the required type were found - otherwise false. + -- @return Answer according to the answer fetcher for dtype or an Error message. + getRecordType = function( dtype, response, retAll ) + + local result = {} + local status1, answers = dns.findNiceAnswer( dtype, response, retAll ) + + if status1 then + if retAll then + for _, v in ipairs(answers) do + table.insert(result, string.format("%s", v) ) + end + else + return true, answers + end + end + + local status2, answers = dns.findNiceAdditional( dtype, response, retAll ) + + if status2 then + if retAll then + for _, v in ipairs(answers) do + table.insert(result, v) + end + else + return true, answers + end + end + + if not status1 and not status2 then + return false, answers + end + + return true, result + + end, + + --- Send a query for a particular service and store the response in a table + -- + -- @param host string containing the ip to connect to + -- @param port number containing the port to connect to + -- @param svc the service record to retrieve + -- @param multiple true if responses from multiple hosts are expected + -- @param svcresponse table to which results are stored + queryService = function( host, port, svc, multiple, svcresponse ) + local condvar = nmap.condvar(svcresponse) + local status, response = dns.query( svc, { port = port, host = host, dtype="PTR", retPkt=true, retAll=true, multiple=multiple, sendCount=1, timeout=2000} ) + if not status then + stdnse.print_debug("Failed to query service: %s; Error: %s", svc, response) + return + end + svcresponse[svc] = svcresponse[svc] or {} + if ( multiple ) then + for _, r in ipairs(response) do + table.insert( svcresponse[svc], r ) + end + else + svcresponse[svc] = response + end + condvar("broadcast") + end, + + --- Decodes a record received from the queryService function + -- + -- @param response as returned by queryService + -- @param result table into which the decoded output should be stored + decodeRecords = function( response, result ) + local service, deviceinfo = {}, {} + local txt = {} + local ipv6, srv, address, port, proto + + local record = ( #response.questions > 0 and response.questions[1].dname ) and response.questions[1].dname or "" + + local status, ip = Comm.getRecordType( dns.types.A, response, false ) + if status then address = ip end + + status, ipv6 = Comm.getRecordType( dns.types.AAAA, response, false ) + if status then address = address .. " " .. ipv6 end + + status, txt = Comm.getRecordType( dns.types.TXT, response, true ) + if status then + for _, v in ipairs(txt) do + if v:len() > 0 then + table.insert(service, v) + end + end + end + + status, srv = Comm.getRecordType( dns.types.SRV, response, false ) + if status then + local srvparams = stdnse.strsplit( ":", srv ) + + if #srvparams > 3 then + port = srvparams[3] + end + end + + if address then + table.insert( service, ("Address=%s"):format( address ) ) + end + + if record == "_device-info._tcp.local" then + service.name = "Device Information" + deviceinfo = service + table.insert(result, deviceinfo) + else + local serviceparams = stdnse.strsplit("[.]", record) + + if #serviceparams > 2 then + local servicename = serviceparams[1]:sub(2) + local proto = serviceparams[2]:sub(2) + + if port == nil or proto == nil or servicename == nil then + service.name = record + else + service.name = string.format( "%s/%s %s", port, proto, servicename) + end + end + table.insert( result, service ) + end + end, + + --- Query the mDNS resolvers for a list of their services + -- + -- @param host table as received by the action function + -- @param port number specifying the port to connect to + -- @param multiple receive multiple responses (multicast) + -- @return True if a dns response was received and contained an answer of + -- the requested type, or the decoded dns response was requested + -- (retPkt) and is being returned - or False otherwise. + -- @return String answer of the requested type, Table of answers or a + -- String error message of one of the following: + -- "No Such Name", "No Servers", "No Answers", + -- "Unable to handle response" + queryAllServices = function( host, port, multiple ) + local sendCount, timeout = 1, 2000 + if ( multiple ) then + sendCount, timeout = 2, 5000 + end + return dns.query( "_services._dns-sd._udp.local", { port = port, host = ( host.ip or host ), dtype="PTR", retAll=true, multiple=multiple, sendCount=sendCount, timeout=timeout } ) + end, + +} + +Helper = { + + --- Creates a new helper instance + -- + -- @param host string containing the host name or ip + -- @param port number containing the port to connect to + -- @return o a new instance of Helper + new = function( self, host, port ) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.mcast = false + return o + end, + + + --- Instructs the helper to use unconnected sockets supporting multicast + -- + -- @param mcast boolean true if multicast is to be used, false otherwise + setMulticast = function( self, mcast ) + assert( type(mcast)=="boolean", "mcast has to be either true or false") + self.mcast = mcast + end, + + --- Performs a DNS-SD query against a host + -- + -- @param host table as received by the action function + -- @param port number specifying the port to connect to + -- @param service string or table with the service(s) to query eg. + -- _ssh._tcp.local, _afpovertcp._tcp.local + -- if nil defaults to _services._dns-sd._udp.local (all) + -- @param mcast boolean true if a multicast query is to be done + -- @return status true on success, false on failure + -- @return response table suitable for stdnse.format_output + queryServices = function( self, service ) + local result = {} + local status, response + local mcast = self.mcast + local port = self.port or 5353 + local host = mcast and "224.0.0.251" or self.host + local service = service or stdnse.get_script_args('dnssd.services') + + if ( not(service) ) then + status, response = Comm.queryAllServices( host, port, mcast ) + if ( not(status) ) then return status, response end + else + if ( 'string' == type(service) ) then + response = { service } + elseif ( 'table' == type(service) ) then + response = service + end + end + + response = Util.getUniqueServices(response) + + local svcresponse = {} + local condvar = nmap.condvar( svcresponse ) + local threads = {} + + for svc in pairs(response) do + local co = stdnse.new_thread( Comm.queryService, (host.ip or host), port, svc, mcast, svcresponse ) + threads[co] = true + end + + -- Wait for all threads to finish running + while Util.threadCount(threads)>0 do condvar("wait") end + + local ipsvctbl = {} + if ( mcast ) then + -- Process all records that were returned + for svcname, response in pairs(svcresponse) do + for _, r in ipairs( response ) do + ipsvctbl[r.peer] = ipsvctbl[r.peer] or {} + Comm.decodeRecords( r.output, ipsvctbl[r.peer] ) + end + end + else + -- Process all records that were returned + for svcname, response in pairs(svcresponse) do + Comm.decodeRecords( response, result ) + end + end + + if ( mcast ) then + -- Restructure and build our output table + for ip, svctbl in pairs( ipsvctbl ) do + table.sort(svctbl, Util.serviceCompare) + svctbl.name = ip + if target.ALLOW_NEW_TARGETS then target.add(ip) end + table.insert( result, svctbl ) + end + table.sort( result, Util.ipCompare ) + else + -- sort the tables per port + table.sort( result, Util.serviceCompare ) + end + return true, result + end, + +} \ No newline at end of file diff --git a/scripts/broadcast-dns-service-discovery.nse b/scripts/broadcast-dns-service-discovery.nse new file mode 100644 index 000000000..ad9e66712 --- /dev/null +++ b/scripts/broadcast-dns-service-discovery.nse @@ -0,0 +1,58 @@ +description=[[ +Attempts to discover a hosts services using the DNS Service Discovery protocol. +It does so by sending a multicast query and collects responses from all +responding hosts. + +The script first sends a query for _services._dns-sd._udp.local to get a +list of services. It then sends a followup query for each one to try to +get more information. +]] + + +--- +-- @usage +-- nmap --script=broadcast-dns-service-discovery -p 5353 +-- +-- @output +-- | broadcast-dns-service-discovery: +-- | 1.2.3.1 +-- | _ssh._tcp.local +-- | _http._tcp.local +-- | 1.2.3.50 +-- | 22/tcp ssh +-- | org.freedesktop.Avahi.cookie=2292090182 +-- | Address=1.2.3.50 +-- | 80/tcp http +-- | path=/admin +-- | org.freedesktop.Avahi.cookie=2292090182 +-- | path=/ +-- | org.freedesktop.Avahi.cookie=2292090182 +-- | path=/pim +-- | org.freedesktop.Avahi.cookie=2292090182 +-- | Address=1.2.3.50 +-- | 1.2.3.116 +-- | 80/tcp http +-- |_ Address=1.2.3.116 + + +-- Version 0.1 +-- Created 10/29/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +require 'shortport' +require 'dnssd' + +prerule = function() return true end + +action = function() + local helper = dnssd.Helper:new( ) + helper:setMulticast(true) + + local status, result = helper:queryServices() + if ( status ) then + return stdnse.format_output(true, result) + end +end diff --git a/scripts/dns-service-discovery.nse b/scripts/dns-service-discovery.nse index fbbc419d3..b69859c62 100644 --- a/scripts/dns-service-discovery.nse +++ b/scripts/dns-service-discovery.nse @@ -33,317 +33,32 @@ get more information. -- |_ Address=192.168.0.2 fe80:0:0:0:223:6cff:1234:5678 --- Version 0.6 +-- Version 0.7 -- Created 01/06/2010 - v0.1 - created by Patrik Karlsson -- Revised 01/13/2010 - v0.2 - modified to use existing dns library instead of mdns, changed output to be less DNS like -- Revised 02/01/2010 - v0.3 - removed incorrect try/catch statements -- Revised 10/04/2010 - v0.4 - added prerule and add target support -- Revised 10/05/2010 - v0.5 - added ip sort function and -- Revised 10/10/2010 - v0.6 - multicast queries are now used in parallel to collect service information +-- Revised 10/29/2010 - v0.7 - factored out most of the code to dnssd library author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "discovery", "safe"} require 'shortport' -require 'dns' -require 'target' +require 'dnssd' portrule = shortport.portnumber(5353, "udp") -prerule = function() return true end ---- Gets a record from both the Answer and Additional section --- --- @param dtype DNS resource record type. --- @param response Decoded DNS response. --- @param retAll If true, return all entries, not just the first. --- @return True if one or more answers of the required type were found - otherwise false. --- @return Answer according to the answer fetcher for dtype or an Error message. -function getRecordType( dtype, response, retAll ) +action = function(host, port) + local helper = dnssd.Helper:new( host, port ) + local status, result = helper:queryServices() - local result = {} - local status1, answers = dns.findNiceAnswer( dtype, response, retAll ) - - if status1 then - if retAll then - for _, v in ipairs(answers) do - table.insert(result, string.format("%s", v) ) - end - else - return true, answers - end + if ( status ) then + -- set port to open + nmap.set_port_state(host, port, "open") + return stdnse.format_output(true, result) end - - local status2, answers = dns.findNiceAdditional( dtype, response, retAll ) - - if status2 then - if retAll then - for _, v in ipairs(answers) do - table.insert(result, v) - end - else - return true, answers - end - end - - if not status1 and not status2 then - return false, answers - end - - return true, result - end ---- Function used to compare discovered DNS services so they can be sorted --- --- @param a table containing first item --- @param b table containing second item --- @return true if the port of a is less than the port of b -local function serviceCompare(a, b) - -- if no port is found use 999999 for comparing, this way all services - -- without ports and device information gets printed at the end - local port_a = a.name:match("^(%d+)") or 999999 - local port_b = b.name:match("^(%d+)") or 999999 - - if ( tonumber(port_a) < tonumber(port_b) ) then - return true - end - return false -end - ---- Converts a string ip to a numeric value suitable for comparing --- --- @param ip string containing the ip to convert --- @return number containing the converted ip -local function ipToNumber(ip) - local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$") - return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4 -end - ---- Compare function used for sorting IP-addresses --- --- @param a table containing first item --- @param b table containing second item --- @return true if the port of a is less than the port of b -local function ipCompare(a, b) - local ip_a = ipToNumber(a.name) or 0 - local ip_b = ipToNumber(b.name) or 0 - - if ( tonumber(ip_a) < tonumber(ip_b) ) then - return true - end - return false -end - ---- Send a query for a particular service and store the response in a table --- --- @param host string containing the ip to connect to --- @param port number containing the port to connect to --- @param svc the service record to retrieve --- @param multiple true if responses from multiple hosts are expected --- @param svcresponse table to which results are stored -local function queryService( host, port, svc, multiple, svcresponse ) - local condvar = nmap.condvar(svcresponse) - local status, response = dns.query( svc, { port = port, host = host, dtype="PTR", retPkt=true, retAll=true, multiple=multiple, sendCount=1, timeout=2000} ) - if not status then - stdnse.print_debug("Failed to query service: %s; Error: %s", svc, response) - return - end - svcresponse[svc] = svcresponse[svc] or {} - if ( multiple ) then - for _, r in ipairs(response) do - table.insert( svcresponse[svc], r ) - end - else - svcresponse[svc] = response - end - condvar("broadcast") -end - ---- Sends a unicast query for each discovered service to each host --- --- @param host string containing the ip to connect to --- @param record string containing the DNS record to query --- @param result table to which the results are added -local function processRecords( response, result ) - local service, deviceinfo = {}, {} - local txt = {} - local ip, ipv6, srv, address, port, proto - - local record = ( #response.questions > 0 and response.questions[1].dname ) and response.questions[1].dname or "" - - status, ip = getRecordType( dns.types.A, response, false ) - if status then address = ip end - - status, ipv6 = getRecordType( dns.types.AAAA, response, false ) - if status then address = address .. " " .. ipv6 end - - status, txt = getRecordType( dns.types.TXT, response, true ) - if status then - for _, v in ipairs(txt) do - if v:len() > 0 then - table.insert(service, v) - end - end - end - - status, srv = getRecordType( dns.types.SRV, response, false ) - if status then - local srvparams = stdnse.strsplit( ":", srv ) - - if #srvparams > 3 then - port = srvparams[3] - end - end - - if address then - table.insert( service, ("Address=%s"):format( address ) ) - end - - if record == "_device-info._tcp.local" then - service.name = "Device Information" - deviceinfo = service - table.insert(result, deviceinfo) - else - local serviceparams = stdnse.strsplit("[.]", record) - - if #serviceparams > 2 then - local servicename = serviceparams[1]:sub(2) - local proto = serviceparams[2]:sub(2) - - if port == nil or proto == nil or servicename == nil then - service.name = record - else - service.name = string.format( "%s/%s %s", port, proto, servicename) - end - end - table.insert( result, service ) - end - -end - - ---- Returns the amount of currenlty active threads --- --- @param threads table containing the list of threads --- @return count number containing the number of non-dead threads -threadCount = function( threads ) - local count = 0 - - for thread in pairs(threads) do - if ( coroutine.status(thread) == "dead" ) then - threads[thread] = nil - else - count = count + 1 - end - end - return count -end - ---- Creates a service host table --- --- ['_ftp._tcp.local'] = {10.10.10.10,20.20.20.20} --- ['_http._tcp.local'] = {30.30.30.30,40.40.40.40} --- --- @param response containing the response from dns.query --- @return services table containing the service name as a key and all host addresses as value -local function createSvcHostTbl( response ) - local services = {} - -- Create unique table of services - for _, r in ipairs( response ) do - for _, svc in ipairs(r.output ) do - services[svc] = services[svc] or {} - table.insert(services[svc], r.peer) - end - end - - return services -end - -preaction = function() - local result = {} - local host, port = "224.0.0.251", 5353 - local status, response = dns.query( "_services._dns-sd._udp.local", { port = port, host = host, dtype="PTR", retAll=true, multiple=true, sendCount=1, timeout=2000} ) - if not status then return end - - local services = createSvcHostTbl(response) - local ipsvctbl = {} - local svcresponse = {} - local condvar = nmap.condvar( svcresponse ) - local threads = {} - - -- Start one collector thread for each service - for svc in pairs(services) do - local co = stdnse.new_thread( queryService, host, port, svc, true, svcresponse ) - threads[co] = true - end - - -- Wait for all threads to finish running - while threadCount(threads)>0 do - condvar("wait") - end - - -- Process all records that were returned - for svcname, response in pairs(svcresponse) do - for _, r in ipairs( response ) do - ipsvctbl[r.peer] = ipsvctbl[r.peer] or {} - processRecords( r.output, ipsvctbl[r.peer] ) - end - end - - -- Restructure and build our output table - for ip, svctbl in pairs( ipsvctbl ) do - table.sort(svctbl, serviceCompare) - svctbl.name = ip - if target.ALLOW_NEW_TARGETS then target.add(ip) end - table.insert( result, svctbl ) - end - table.sort( result, ipCompare ) - - return stdnse.format_output(true, result ) -end - -scanaction = function(host, port) - local result = {} - local status, response = dns.query( "_services._dns-sd._udp.local", { port = 5353, host = host.ip, dtype="PTR", retAll=true, sendCount=1, timeout=2000 } ) - if not status then return end - - local svcresponse = {} - local condvar = nmap.condvar( svcresponse ) - local threads = {} - - -- Start one collector thread for each service - for _, svc in ipairs(response) do - local co = stdnse.new_thread( queryService, host.ip, port, svc, false, svcresponse ) - threads[co] = true - end - - -- Wait for all threads to finish running - while threadCount(threads)>0 do - condvar("wait") - end - - -- Process all records that were returned - for svcname, response in pairs(svcresponse) do - processRecords( response, result ) - end - - -- sort the tables per port - table.sort( result, serviceCompare ) - - -- set port to open - nmap.set_port_state(host, port, "open") - - return stdnse.format_output(true, result ) - -end - --- Function dispatch table -local actions = { - prerule = preaction, - hostrule = scanaction, - portrule = scanaction, -} - -function action (...) return actions[SCRIPT_TYPE](...) end - diff --git a/scripts/script.db b/scripts/script.db index 3221583a1..390b97966 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -6,6 +6,7 @@ Entry { filename = "asn-query.nse", categories = { "discovery", "external", "saf Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } } Entry { filename = "banner.nse", categories = { "discovery", "safe", } } +Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } } Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } } Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "safe", } }