From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/broadcast-igmp-discovery.nse | 412 +++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 scripts/broadcast-igmp-discovery.nse (limited to 'scripts/broadcast-igmp-discovery.nse') diff --git a/scripts/broadcast-igmp-discovery.nse b/scripts/broadcast-igmp-discovery.nse new file mode 100644 index 0000000..9359ee5 --- /dev/null +++ b/scripts/broadcast-igmp-discovery.nse @@ -0,0 +1,412 @@ +local nmap = require "nmap" +local stdnse = require "stdnse" +local table = require "table" +local packet = require "packet" +local ipOps = require "ipOps" +local target = require "target" +local coroutine = require "coroutine" +local string = require "string" +local stringaux = require "stringaux" +local io = require "io" + +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. +-- +-- @args broadcast-igmp-discovery.mgroupnamesdb Database with multicast group names +-- +--@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 +-- | Description: Organization-Local Scope (rfc2365) +-- | Group: 239.1.1.2 +-- | Mode: EXCLUDE +-- | Description: Organization-Local Scope (rfc2365) +-- | Group: 239.1.1.44 +-- | Mode: INCLUDE +-- | Description: Organization-Local Scope (rfc2365) +-- | Sources: +-- | 192.168.31.1 +-- | 192.168.1.3 +-- | Interface: wlan0 +-- | Version: 2 +-- | Group: 239.255.255.250 +-- | Description: Organization-Local Scope (rfc2365) +-- | 192.168.1.3 +-- | Interface: wlan0 +-- | Version: 2 +-- | Group: 239.255.255.253 +-- | Description: Organization-Local Scope (rfc2365) +-- |_ Use the newtargets script-arg to add the results as targets +-- + +-- +-- The Multicast Group names DB can be created by the following script: +-- +-- #!/usr/bin/awk -f +-- BEGIN { FS="<|>"; } +-- /.*-.*<\/addr>/ { T=$3; FS="-"; $0=T; addr1=$1; addr2=$2; FS="<|>"; } +-- /[^-]*<\/addr>/ { addr1=$3; addr2=$3; } +-- // { desc=$3; } +-- /nselib/data/mgroupnames.db + + +prerule = function() + if nmap.address_family() ~= 'inet' then + stdnse.verbose1("is IPv4 only.") + return false + end + if ( not(nmap.is_privileged()) ) then + stdnse.verbose1("not running due to lack of privileges.") + return false + end + return true +end + +author = "Hani Benhabiles" + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"discovery", "safe", "broadcast"} + +--- Parses a raw igmp packet and return a structured 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) + response.type, index = string.unpack(">B", data, index) + if response.type == 0x12 or response.type == 0x16 then + -- Max response time, Checksum, Multicast group + response.maxrt, response.checksum, response.group, index = string.unpack(">B I2 c4", data, index) + response.group = ipOps.str_to_ip(response.group) + return response + elseif response.type == 0x22 and #data >= 12 then + -- Skip reserved byte, Checksum, Skip reserved bytes, Number of groups + response.checksum, response.ngroups, index = string.unpack(">x I2 xx I2", data, index) + response.groups = {} + for i=1,response.ngroups do + group = {} + -- Mode is either INCLUDE or EXCLUDE + group.mode, + -- Auxiliary data length in the group record (in 32bits units) + group.auxdlen, + -- Number of source addresses + group.nsrc, + group.address, index = string.unpack(">BB I2 c4", data, index) + group.address = ipOps.str_to_ip(group.address) + group.src = {} + for i=1,group.nsrc do + source, index = string.unpack(">c4", data, index) + table.insert(group.src, ipOps.str_to_ip(source)) + end + -- Skip auxiliary data + index = index + group.auxdlen + -- Insert group + table.insert(response.groups, group) + end + return response + end +end + +--- Listens for IGMP Membership reports packets. +-- @param interface Interface to listen on. +-- @param timeout Amount of time to listen for. +-- @param responses table to put valid responses into. +local igmpListener = function(interface, timeout, responses) + local condvar = nmap.condvar(responses) + local start = nmap.clock_ms() + local listener = nmap.new_socket() + local p, igmp_raw, status, l3data, response, _ + local devices = {} + listener:set_timeout(100) + listener:pcap_open(interface.device, 1024, true, 'ip proto 2') + + while (nmap.clock_ms() - start) < timeout do + status, _, _, l3data = listener:pcap_receive() + if status then + p = packet.Packet:new(l3data, #l3data) + igmp_raw = string.sub(l3data, p.ip_hl*4 + 1) + if p then + -- check the first byte before sending to the parser + -- response 0x12 == Membership Response version 1 + -- response 0x16 == Membership Response version 2 + -- response 0x22 == Membership Response version 3 + local igmptype = igmp_raw:byte(1) + if igmptype == 0x12 or igmptype == 0x16 or igmptype == 0x22 then + response = igmpParse(igmp_raw) + if response then + response.src = p.ip_src + response.interface = interface.shortname + -- Many hosts return more than one same response message + -- this is to not output duplicates + if not devices[response.src..response.type..(response.group or response.ngroups)] then + devices[response.src..response.type..(response.group or response.ngroups)] = true + table.insert(responses, response) + end + end + end + end + end + end + condvar("signal") +end + +--- Crafts a raw IGMP packet. +-- @param interface Source interface of the packet. +-- @param version IGMP version. Could be 1, 2 or 3. +-- @return string Raw IGMP packet. +local igmpRaw = function(interface, version) + -- Only 1, 2 and 3 are valid IGMP versions + if version ~= 1 and version ~= 2 and version ~= 3 then + stdnse.debug1("IGMP version %s doesn't exist.", version) + return + end + + -- Let's craft an IGMP Membership Query + local igmp_raw = string.pack(">BB I2 I4", + 0x11, -- Membership Query, same for all versions + version == 1 and 0 or 0x16, -- Max response time: 10 Seconds, for version 2 and 3 + 0, -- Checksum, calculated later + 0 -- Multicast Address: 0.0.0.0 + ) + + if version == 3 then + igmp_raw = igmp_raw .. string.pack(">BB I2", + 0, -- Reserved = 4 bits (Should be zeroed) + -- Supress Flag = 1 bit + -- QRV (Querier's Robustness Variable) = 3 bits + -- all are set to 0 + 0x10, -- QQIC (Querier's Query Interval Code) in seconds = Set to 0 to get insta replies. + 0x0001 -- Number of sources (in the next arrays) = 1 ( Our IP only) + ) + .. ipOps.ip_to_str(interface.address) -- Source = Our IP address + end + + igmp_raw = igmp_raw:sub(1,2) .. string.pack(">I2", 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 version 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 = stdnse.fromhex( "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 = "\x01\x00\x5e\x00\x00\x01" .. interface.mac .. "\x08\x00" + sock:ethernet_send(eth_hdr .. igmp_packet.buf) + sock:ethernet_close() + end +end + +-- Function to compare weight 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 + +local mgroup_names_fetch = function(filename) + local groupnames_db = {} + + local file = io.open(filename, "r") + if not file then + return false + end + + for l in file:lines() do + groupnames_db[#groupnames_db + 1] = stringaux.strsplit("\t", l) + end + + file:close() + return groupnames_db +end + +local mgroup_name_identify = function(db, ip) + --stdnse.debug1("'%s'", ip) + for _, mg in ipairs(db) do + local ip1 = mg[1] + local ip2 = mg[2] + local desc = mg[3] + --stdnse.debug1("try: %s <= %s <= %s (%s)", ip1, ip, ip2, desc) + if (not ipOps.compare_ip(ip, "lt", ip1) and not ipOps.compare_ip(ip2, "lt", ip)) then + --stdnse.debug1("found! %s <= %s <= %s (%s)", ip1, ip, ip2, desc) + return desc + end + end + return false +end + +action = function(host, port) + local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + local version = stdnse.get_script_args(SCRIPT_NAME .. ".version") or 2 + local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") + timeout = (timeout or 7) * 1000 + if version ~= 'all' then + version = tonumber(version) + end + + local responses, results, interfaces, lthreads = {}, {}, {}, {} + local result, grouptable, sourcetable + + local group_names_fname = stdnse.get_script_args(SCRIPT_NAME .. ".mgroupnamesdb") or + nmap.fetchfile("nselib/data/mgroupnames.db") + local mg_names_db = group_names_fname and mgroup_names_fetch(group_names_fname) + + -- 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 stdnse.format_output(false, ("Failed to retrieve %s interface information."):format(interface)) + end + interfaces = {interface} + stdnse.debug1("Will use %s interface.", 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.debug1("Will use %s interface.", 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 + for thread in pairs(lthreads) do + if coroutine.status(thread) == "dead" then lthreads[thread] = nil end + end + if ( next(lthreads) ) then + condvar("wait") + 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) + local mg_desc = mgroup_name_identify(mg_names_db, response.group) + if mg_desc then + table.insert(result, "Description: ".. mg_desc) + end + 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 + local mg_desc = mgroup_name_identify(mg_names_db, group.address) + if mg_desc then + table.insert(grouptable, "Description: ".. mg_desc) + 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 -- cgit v1.2.3