summaryrefslogtreecommitdiffstats
path: root/nselib/multicast.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/multicast.lua')
-rw-r--r--nselib/multicast.lua179
1 files changed, 179 insertions, 0 deletions
diff --git a/nselib/multicast.lua b/nselib/multicast.lua
new file mode 100644
index 0000000..ccdb72a
--- /dev/null
+++ b/nselib/multicast.lua
@@ -0,0 +1,179 @@
+---
+-- Utility functions for sending MLD requests and parsing reports.
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+
+local nmap = require "nmap"
+local ipOps = require "ipOps"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+_ENV = stdnse.module("multicast", stdnse.seeall)
+
+---
+-- Performs an MLD general query on the selected interface and caches the results such that
+-- subsequent calls to this function do not generate additional traffic.
+--
+-- @param if_nfo A table containing information about the interface to send the request on.
+-- Can be one of those returned by nmap.list_interfaces().
+-- @param arg_timeout The amount of time to wait for reports.
+--
+-- @return A list of tables, each table containing three items, namely device, layer 2 reply and layer 3 reply.
+--
+mld_query = function( if_nfo, arg_timeout )
+ -- check if the interface name is valid or if nmap can find one
+ if if_nfo == nil then
+ return nil
+ end
+
+ -- we need some ID for this interface & address combination to use as the
+ -- registry key and the object to lock the mutex on
+ local reg_entry = "mld_reports_" .. if_nfo.device .. "_" .. if_nfo.address
+ local mutex = nmap.mutex( reg_entry )
+ mutex('lock')
+
+ -- first check if nmap.registry contains reports for this interface from a previous call of this function
+ if nmap.registry[reg_entry] ~= nil then
+ mutex('done')
+ return nmap.registry[reg_entry]
+ end
+
+ if not ipOps.ip_in_range(if_nfo.address, "fe80::/10") -- link local address
+ or if_nfo.link ~= "ethernet" then -- not the loopback interface
+ mutex('done')
+ return nil
+ end
+
+ -- create the query packet
+ local src_mac = if_nfo.mac
+ local src_ip6 = ipOps.ip_to_str(if_nfo.address)
+ local dst_mac = packet.mactobin("33:33:00:00:00:01")
+ local dst_ip6 = ipOps.ip_to_str("ff02::1")
+ local general_qry = ipOps.ip_to_str("::")
+
+ local dnet = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+
+ dnet:ethernet_open(if_nfo.device)
+ pcap:pcap_open(if_nfo.device, 1500, false, "ip6[40:1] == 58")
+
+ local probe = packet.Frame:new()
+ probe.mac_src = src_mac
+ probe.mac_dst = dst_mac
+ probe.ip_bin_src = src_ip6
+ probe.ip_bin_dst = dst_ip6
+
+ probe.ip6_tc = 0
+ probe.ip6_fl = 0
+ probe.ip6_hlimit = 1
+
+ probe.icmpv6_type = packet.MLD_LISTENER_QUERY
+ probe.icmpv6_code = 0
+
+ -- Add a non-empty payload too.
+ probe.icmpv6_payload = (
+ "\x00\x01" .. -- maximum response delay 1 millisecond (if 0, virtualbox TCP/IP stack crashes)
+ "\x00\x00" .. -- reserved
+ ipOps.ip_to_str("::") -- empty address - general MLD query
+ )
+ probe:build_icmpv6_header()
+ probe.exheader = string.pack(">BBBB I2 BB",
+ packet.IPPROTO_ICMPV6, -- next header
+ 0x00, -- length not including first 8 octets
+ 0x05, -- type is router alert
+ 0x02, -- length 2 bytes
+ 0x00, -- router alert MLD
+ 0x01, -- padding type PadN
+ 0x00 -- padding length 0
+ )
+ probe.ip6_nhdr = packet.IPPROTO_HOPOPTS
+ probe:build_ipv6_packet()
+ probe:build_ether_frame()
+
+ -- send the query packet
+ dnet:ethernet_send(probe.frame_buf)
+
+ -- wait for responses to the query packet
+ pcap:set_timeout(1000)
+ local pcap_timeout_count = 0
+ local nse_timeout = arg_timeout or 10
+ local start_time = nmap:clock()
+ local addrs = {}
+ nmap.registry[reg_entry] = {}
+
+ repeat
+ local status, length, layer2, layer3 = pcap:pcap_receive()
+ local cur_time = nmap:clock()
+ if status then
+ local l2reply = packet.Frame:new(layer2)
+ local l3reply = packet.Packet:new(layer3, length, true)
+ local target_ip = l3reply.ip_src
+ if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT or l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then
+ table.insert(
+ nmap.registry[reg_entry],
+ { if_nfo.device, l2reply, l3reply }
+ )
+ end
+ end
+ until ( cur_time - start_time >= nse_timeout )
+
+ -- clean up
+ dnet:ethernet_close()
+ pcap:pcap_close()
+
+ mutex('done')
+ return nmap.registry[reg_entry]
+end
+
+---
+-- Extracts IP addresses from MLD reports captured by the mld_query function.
+--
+-- @param reports The output of the mld_query function.
+--
+-- @return A list of tables, each table containing three items, namely device, mac and a list of addresses.
+--
+mld_report_addresses = function(reports)
+ local rep_addresses = {}
+ for _, report in pairs(reports) do
+ local device = report[1]
+ local l2reply = report[2]
+ local l3reply = report[3]
+
+ local target_ip = l3reply.ip_src
+ if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT or l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then
+
+ -- if this is the first reply from the target, make an entry for it
+ if not rep_addresses[target_ip] then
+ rep_addresses[target_ip] = stdnse.output_table()
+ end
+ local rep = rep_addresses[target_ip]
+ rep.device = device
+ rep.mac = stdnse.format_mac(l2reply.mac_src)
+ rep.multicast_ips = rep.multicast_ips or {}
+
+ -- depending on the MLD version of the report, add appropriate IP addresses
+ if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT then
+ local multicast_ip = ipOps.str_to_ip( l3reply:raw(0x38, 16) ) -- IP starts at byte 0x38 and is 16 bytes long
+ table.insert(rep.multicast_ips, multicast_ip)
+ elseif l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then
+ local no_records = l3reply:u16(0x36)
+ local record_offset = 0
+ local records_start = 0x38
+ for i = 1, no_records do
+ -- for the format description, see RFC3810 (ch. 5.2)
+ local aux_data_len = l3reply:u8(records_start + record_offset + 1)
+ local no_sources = l3reply:u16(records_start + record_offset + 2)
+ local multicast_ip = ipOps.str_to_ip(l3reply:raw(records_start + record_offset + 4, 16))
+ table.insert(rep.multicast_ips, multicast_ip)
+ record_offset = record_offset + 4 + 16 + no_sources * 16 + aux_data_len * 4
+ end
+ end
+
+ end
+ end
+ return rep_addresses
+end
+
+return _ENV