From 0f99596555b018a46d7bd244bc324795c26b987a Mon Sep 17 00:00:00 2001 From: dmiller Date: Wed, 16 Dec 2015 17:07:40 +0000 Subject: [PATCH] Fix a few bugs in targets-ipv6-multicast-mld. http://seclists.org/nmap-dev/2015/q2/250 --- nselib/multicast.lua | 181 +++++++++++++++++++++++++ scripts/targets-ipv6-multicast-mld.nse | 122 ++++++----------- 2 files changed, 222 insertions(+), 81 deletions(-) create mode 100644 nselib/multicast.lua diff --git a/nselib/multicast.lua b/nselib/multicast.lua new file mode 100644 index 000000000..52a14ff5b --- /dev/null +++ b/nselib/multicast.lua @@ -0,0 +1,181 @@ +--- +-- Utility functions for sending MLD requests and parsing reports. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html + +local bin = require "bin" +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 ipOps.ip_to_str(if_nfo.address) == nil -- validate IP address + or 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 = bin.pack("CA", + packet.IPPROTO_ICMPV6, -- next header + "\x00" .. -- length not including first 8 octets + "\x05" .. -- type is router alert + "\x02" .. -- length 2 bytes + "\x00\x00" .. -- router alert MLD + "\x01" .. -- padding type PadN + "\x00" -- 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] = { + device = device, + mac = stdnse.format_mac(l2reply.mac_src), + multicast_ips = {} + } + end + + -- 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_addresses[target_ip].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_addresses[target_ip].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 diff --git a/scripts/targets-ipv6-multicast-mld.nse b/scripts/targets-ipv6-multicast-mld.nse index 24b0c16be..a8db511b5 100644 --- a/scripts/targets-ipv6-multicast-mld.nse +++ b/scripts/targets-ipv6-multicast-mld.nse @@ -1,25 +1,25 @@ -local bin = require "bin" local ipOps = require "ipOps" local coroutine = require "coroutine" local nmap = require "nmap" -local packet = require "packet" local stdnse = require "stdnse" local tab = require "tab" local table = require "table" local target = require "target" +local multicast = require "multicast" description = [[ Attempts to discover available IPv6 hosts on the LAN by sending an MLD (multicast listener discovery) query to the link-local multicast address (ff02::1) and listening for any responses. The query's maximum response delay -set to 0 to provoke hosts to respond immediately rather than waiting for other +set to 1 to provoke hosts to respond immediately rather than waiting for other responses from their multicast group. ]] --- -- @usage --- nmap -6 --script=targets-ipv6-multicast-mld.nse --script-args 'newtargets,interface=eth0' -sP +-- nmap -6 --script=targets-ipv6-multicast-mld.nse --script-args 'newtargets,interface=eth0' -- +-- @output -- Pre-scan script results: -- | targets-ipv6-multicast-mld: -- | IP: fe80::5a55:abcd:ef01:2345 MAC: 58:55:ab:cd:ef:01 IFACE: en0 @@ -29,10 +29,25 @@ responses from their multicast group. -- -- @args targets-ipv6-multicast-mld.timeout timeout to wait for -- responses (default: 10s) --- @args targets-ipv6-multicast-mld.interface Interface to send on (overrides -e) +-- @args targets-ipv6-multicast-mld.interface Interface to send on (default: +-- the interface specified with -e or every available Ethernet interface +-- with an IPv6 address.) -- +-- @xmloutput +-- +--
+-- fe80::5a55:abcd:ef01:2345 +-- 58:55:ab:cd:ef:01 +-- en0 +--
+-- +-- fe80::9284:0123:4567:89ab +-- 90:84:01:23:45:67 +-- en0 +--
+-- -author = "niteesh" +author = "niteesh, alegen" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"discovery","broadcast"} @@ -54,19 +69,12 @@ local function get_interfaces() -- interfaces list (decide which interfaces to broadcast on) local interfaces = {} - if interface_name then - -- single interface defined - local if_table = nmap.get_interface_info(interface_name) - if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then - interfaces[#interfaces + 1] = if_table - else - stdnse.debug1("Interface not supported or not properly configured.") - end - else - for _, if_table in ipairs(nmap.list_interfaces()) do - if ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then - table.insert(interfaces, if_table) - end + for _, if_table in pairs(nmap.list_interfaces()) do + if (interface_name == nil or if_table.device == interface_name) -- check for correct interface + and ipOps.ip_to_str(if_table.address) -- validate IP address + and ipOps.ip_in_range(if_table.address, "fe80::/10") -- link local address + and if_table.link == "ethernet" then -- not the loopback interface + table.insert(interfaces, if_table) end end @@ -76,77 +84,29 @@ end local function single_interface_broadcast(if_nfo, results) stdnse.debug2("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device) local condvar = nmap.condvar(results) - 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 gen_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 = bin.pack("HA", "00 00 00 00", gen_qry) - probe:build_icmpv6_header() - probe.exheader = bin.pack("CH", packet.IPPROTO_ICMPV6, "00 05 02 00 00 01 00") - probe.ip6_nhdr = packet.IPPROTO_HOPOPTS - - probe:build_ipv6_packet() - probe:build_ether_frame() - - dnet:ethernet_send(probe.frame_buf) - - pcap:set_timeout(1000) - local pcap_timeout_count = 0 - local nse_timeout = arg_timeout or 10 - local start_time = nmap:clock() - local addrs = {} - - repeat - local status, length, layer2, layer3 = pcap:pcap_receive() - local cur_time = nmap:clock() - if ( status ) then - local l2reply = packet.Frame:new(layer2) - local reply = packet.Packet:new(layer3, length, true) - if ( reply.ip6_nhdr == packet.MLD_LISTENER_REPORT or - reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT ) then - local target_str = reply.ip_src - if not results[target_str] then - if target.ALLOW_NEW_TARGETS then - target.add(target_str) - end - results[target_str] = { address = target_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device } - end + local reports = multicast.mld_query(if_nfo, arg_timeout or 10) + for _, r in pairs(reports) do + local l2reply = r[2] + local l3reply = r[3] + local target_str = l3reply.ip_src + if not results[target_str] then + if target.ALLOW_NEW_TARGETS then + target.add(target_str) end + results[target_str] = { address = target_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device } end - until ( cur_time - start_time >= nse_timeout ) - - dnet:ethernet_close() - pcap:pcap_close() + end condvar("signal") end local function format_output(results) local output = tab.new() + local xmlout = {} - for _, record in pairs(results) do + for i, record in ipairs(table.sort(stdnse.keys(results))) do + xmlout[i] = record tab.addrow(output, "IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface) end @@ -156,7 +116,7 @@ local function format_output(results) table.insert(output, "") table.insert(output, "Use --script-args=newtargets to add the results as targets") end - return stdnse.format_output(true, output) + return xmlout, table.concat(output, "\n ") end end