diff --git a/CHANGELOG b/CHANGELOG index 2dbce5228..224462efb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added broadcast-igmp-discovery script which discovers and outputs + interesting information from targets that have multicast groups memberships. + [Hani Benhabiles] + o Scripts can now return a structured name-value table so that results are queryable from XML output. Scripts can return a string as before, or a table, or a table and a string. In this last case, the table will @@ -18,7 +22,7 @@ o [NPING] Nping now prints out an error and exists when the user tries to use o [NSE] Added smb-print-text script which prints specified text using SMB shared printer. [Aleksandar Nikolic] -o [NSE] Added mrinfo script whiches queries a target router for multicast +o [NSE] Added mrinfo script which queries a target router for multicast information. [Hani Benhabiles] o [NSE] Added ssl-date script which gets server's time from SSL ServerHello diff --git a/scripts/broadcast-igmp-discovery.nse b/scripts/broadcast-igmp-discovery.nse new file mode 100644 index 000000000..27796fc0f --- /dev/null +++ b/scripts/broadcast-igmp-discovery.nse @@ -0,0 +1,360 @@ +local nmap = require "nmap" +local stdnse = require "stdnse" +local table = require "table" +local bin = require "bin" +local packet = require "packet" +local ipOps = require "ipOps" +local target = require "target" + +description = [[ +Discovers targets that have IGMP Multicast memberships and grabs interesting information. + +The scripts works by sending IGMP Membership Query message to the 224.0.0.1 All +Hosts multicast address and listening for IGMP Membership Report messages. The +script then extracts all the interesting information from the report messages +such as the version, group, mode, source addresses (depending on the version). + +The script defaults to sending an IGMPv2 Query but this could be changed to +another version (version 1 or 3) or to sending queries of all three version. If +no interface was specified as a script argument or with the -e option, the +script will proceed to sending queries through all the valid ethernet +interfaces. +]] + +--- +-- @args broadcast-igmp-discovery.timeout Time to wait for reports in seconds. +-- Defaults to 5 seconds. +-- +-- @args broadcast-igmp-discovery.version IGMP version to use. Could be +-- 1, 2, 3 or all. Defaults to 2 +-- +-- @args broadcast-igmp-discovery.interface Network interface to use. +-- +--@usage +-- nmap --script broadcast-igmp-discovery +-- nmap --script broadcast-igmp-discovery -e wlan0 +-- nmap --script broadcast-igmp-discovery +-- --script-args 'broadcast-igmp-discovery.version=all, broadcast-igmp-discovery.timeout=3' +-- +--@output +--Pre-scan script results: +-- | broadcast-igmp-discovery: +-- | 192.168.2.2 +-- | Interface: tap0 +-- | Version: 3 +-- | Group: 239.1.1.1 +-- | Mode: EXCLUDE +-- | Group: 239.1.1.2 +-- | Mode: EXCLUDE +-- | Group: 239.1.1.44 +-- | Mode: INCLUDE +-- | Sources: +-- | 192.168.31.1 +-- | 192.168.1.3 +-- | Interface: wlan0 +-- | Version: 2 +-- | Group: 239.255.255.250 +-- | 192.168.1.3 +-- | Interface: wlan0 +-- | Version: 2 +-- | Group: 239.255.255.253 +-- |_ Use the newtargets script-arg to add the results as targets +-- + + +prerule = function() + if nmap.address_family() ~= 'inet' then + stdnse.print_verbose("%s is IPv4 only.", SCRIPT_NAME) + return false + end + if ( not(nmap.is_privileged()) ) then + stdnse.print_verbose("%s not running due to lack of privileges.", SCRIPT_NAME) + return false + end + return true +end + +author = "Hani Benhabiles" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"discovery", "safe", "broadcast"} + +--- Parses a raw igmp packet and return a structred packet. +-- @param data string IGMP Raw packet. +-- @return response table Structured igmp packet. +local igmpParse = function(data) + local index + local response = {} + local group, source + -- Report type (0x12 == v1, 0x16 == v2, 0x22 == v3) + index, response.type = bin.unpack(">C", data, index) + if response.type == 0x12 or response.type == 0x16 then + -- Max response time + index, response.maxrt = bin.unpack(">C", data, index) + -- Checksum + index, response.checksum = bin.unpack(">S", data, index) + -- Multicast group + index, response.group = bin.unpack("= 12 then + -- Skip reserved byte + index = index + 1 + -- Checksum + index, response.checksum = bin.unpack(">S", data, index) + -- Skip reserved byte + index = index + 2 + -- Number of groups + index, response.ngroups = bin.unpack(">S", data, index) + response.groups = {} + for i=1,response.ngroups do + group = {} + -- Mode is either INCLUDE or EXCLUDE + index, group.mode = bin.unpack(">C", data, index) + -- Auxiliary data length in the group record (in 32bits units) + index, group.auxdlen = bin.unpack(">C", data, index) + -- Number of source addresses + index, group.nsrc = bin.unpack(">S", data, index) + index, group.address = bin.unpack(" 0 then + for i=1,group.nsrc do + index, source = bin.unpack("C", 0x11) -- Membership Query, same for all versions + if version == 1 then + igmp_raw = igmp_raw .. bin.pack(">C", 0x00) -- Unused, 0x00 for version 1 only + else + igmp_raw = igmp_raw .. bin.pack(">C", 0x16) -- Max response time: 10 Seconds, for version 2 and 3 + end + + igmp_raw = igmp_raw .. bin.pack(">S", 0x00) -- Checksum, calculated later + igmp_raw = igmp_raw .. bin.pack(">I", 0x00) -- Multicast Address: 0.0.0.0 + + if version == 3 then + -- Reserved = 4 bits (Should be zeroed) + -- Supress Flag = 1 bit + -- QRV (Querier's Robustness Variable) = 3 bits + -- all are set to 0 + igmp_raw = igmp_raw .. bin.pack(">C", 0x00) + -- QQIC (Querier's Query Interval Code) in seconds = Set to 0 to get insta replies. + igmp_raw = igmp_raw .. bin.pack(">C", 0x10) + -- Number of sources (in the next arrays) = 1 ( Our IP only) + igmp_raw = igmp_raw .. bin.pack(">S", 0x01) + -- Source = Our IP address + igmp_raw = igmp_raw .. bin.pack(">I", ipOps.todword(interface.address)) + end + + igmp_raw = igmp_raw:sub(1,2) .. bin.pack(">S", packet.in_cksum(igmp_raw)) .. igmp_raw:sub(5) + + return igmp_raw +end + + +local igmpQuery; +--- Sends an IGMP Membership query. +-- @param interface Network interface to send on. +-- @param vesion IGMP version. Could be 1, 2, 3 or all. +igmpQuery = function(interface, version) + local srcip = interface.address + local dstip = "224.0.0.1" + + if version == 'all' then + -- Small pause to let listener begin and not miss reports. + stdnse.sleep(0.5) + igmpQuery(interface, 3) + igmpQuery(interface, 2) + igmpQuery(interface, 1) + else + local igmp_raw = igmpRaw(interface, version) + + local ip_raw = bin.pack("H", "45c00040ed780000010218bc0a00c8750a00c86b") .. igmp_raw + local igmp_packet = packet.Packet:new(ip_raw, ip_raw:len()) + igmp_packet:ip_set_bin_src(ipOps.ip_to_str(srcip)) + igmp_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip)) + igmp_packet:ip_set_len(#igmp_packet.buf) + igmp_packet:ip_count_checksum() + + local sock = nmap.new_dnet() + sock:ethernet_open(interface.device) + + -- Ethernet IPv4 multicast, our ethernet address and type IP + local eth_hdr = bin.pack("HAH", "01 00 5e 00 00 01", interface.mac, "08 00") + sock:ethernet_send(eth_hdr .. igmp_packet.buf) + sock:ethernet_close() + end +end + +-- Function to compare wieght of an IGMP response message. +-- Used to sort elements in responses table. +local respCompare = function(a,b) + return ipOps.todword(a.src) + a.type + (a.ngroups or ipOps.todword(a.group)) + < ipOps.todword(b.src) + b.type + (b.ngroups or ipOps.todword(b.group)) +end + + +action = function(host, port) + local timeout = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 7 + local version = stdnse.get_script_args(SCRIPT_NAME .. ".version") or 2 + local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") + timeout = timeout * 1000 + if version ~= 'all' then + version = tonumber(version) + end + + local responses, results, interfaces, lthreads = {}, {}, {}, {} + local result, grouptable, sourcetable + + -- Check the interface + interface = interface or nmap.get_interface() + if interface then + -- Get the interface information + interface = nmap.get_interface_info(interface) + if not interface then + return ("ERROR: Failed to retreive %s interface information."):format(interface) + end + interfaces = {interface} + stdnse.print_debug("%s: Will use %s interface.", SCRIPT_NAME, interface.shortname) + else + local ifacelist = nmap.list_interfaces() + for _, iface in ipairs(ifacelist) do + -- Match all ethernet interfaces + if iface.address and iface.link=="ethernet" and + iface.address:match("%d+%.%d+%.%d+%.%d+") then + + stdnse.print_debug("%s: Will use %s interface.", SCRIPT_NAME, iface.shortname) + table.insert(interfaces, iface) + end + end + end + + + -- We should iterate over interfaces + for _, interface in pairs(interfaces) do + local co = stdnse.new_thread(igmpListener, interface, timeout, responses) + igmpQuery(interface, version) + lthreads[co] = true + end + + local condvar = nmap.condvar(responses) + -- Wait for the listening threads to finish + repeat + condvar("wait") + for thread in pairs(lthreads) do + if coroutine.status(thread) == "dead" then lthreads[thread] = nil end + end + until next(lthreads) == nil; + + -- Output useful info from the responses + if #responses > 0 then + -- We should sort our list here. + -- This is useful to have consistent results for tools such as Ndiff. + table.sort(responses, respCompare) + + for _, response in pairs(responses) do + result = {} + result.name = response.src + table.insert(result, "Interface: " .. response.interface) + -- Add to new targets if newtargets script arg provided + if target.ALLOW_NEW_TARGETS then target.add(response.src) end + if response.type == 0x12 then + table.insert(result, "Version: 1") + table.insert(result, "Multicast group: ".. response.group) + elseif response.type == 0x16 then + table.insert(result, "Version: 2") + table.insert(result, "Group: ".. response.group) + elseif response.type == 0x22 then + table.insert(result, "Version: 3") + for _, group in pairs(response.groups) do + grouptable = {} + grouptable.name = "Group: " .. group.address + if group.mode == 0x01 then + table.insert(grouptable, "Mode: INCLUDE") + elseif group.mode == 0x02 then + table.insert(grouptable, "Mode: EXCLUDE") + end + if group.nsrc > 0 then + sourcetable = {} + sourcetable.name = "Sources:" + table.insert(sourcetable, group.src) + table.insert(grouptable, sourcetable) + end + table.insert(result, grouptable) + end + end + table.insert(results, result) + end + if #results>0 and not target.ALLOW_NEW_TARGETS then + table.insert(results,"Use the newtargets script-arg to add the results as targets") + end + return stdnse.format_output(true, results) + end +end diff --git a/scripts/script.db b/scripts/script.db index 2cf6f2c06..9c5d34f31 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -30,6 +30,7 @@ Entry { filename = "broadcast-dhcp-discover.nse", categories = { "broadcast", "s Entry { filename = "broadcast-dhcp6-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } } +Entry { filename = "broadcast-igmp-discovery.nse", categories = { "discovery", "safe", "broadcast", } } Entry { filename = "broadcast-listener.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-netbios-master-browser.nse", categories = { "broadcast", "safe", } }