diff --git a/CHANGELOG b/CHANGELOG
index 1f82de2e0..2ca5d07b9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added mtrace script which queries for the multicast path from a source
+ to a destination host. [Hani Benhabiles]
+
o [NSE] Added broadcast-eigrp-discovery script which does network discovery and
information gathering through Cisco's EIGRP protocol. [Hani Benhabiles]
diff --git a/scripts/mtrace.nse b/scripts/mtrace.nse
new file mode 100644
index 000000000..f4e8a126e
--- /dev/null
+++ b/scripts/mtrace.nse
@@ -0,0 +1,391 @@
+local nmap = require "nmap"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local bin = require "bin"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Queries for the multicast path from a source to a destination host.
+
+This works by sending an IGMP Traceroute Query and listening for IGMP
+Traceroute responses. The Traceroute Query is sent to the first hop and
+contains information about source, destination and multicast group addresses.
+First hop defaults to the multicast All routers address. The default multicast
+group address is 0.0.0.0 and the default destination is our own host address. A
+source address must be provided. The responses are parsed to get interesting
+information about interface addresses, used protocols and error codes.
+
+This is similar to the mtrace utility provided in Cisco IOS.
+]]
+
+--
+--@args mtrace.fromip Source address from which to traceroute.
+--
+--@args mtrace.toip Destination address to which to traceroute.
+-- Defaults to our host address.
+--
+--@args mtrace.group Multicast group address for the traceroute.
+-- Defaults to 0.0.0.0 which represents all group addresses.
+--
+--@args mtrace.firsthop Host to which the query is sent. If not set, the
+-- query will be sent to 224.0.0.2.
+--
+--@args mtrace.timeout Time to wait for responses in seconds.
+-- Defaults to 7 seconds.
+--
+--@usage
+-- nmap --script mtrace --script-args 'mtrace.fromip=172.16.45.4'
+--
+--@output
+-- Pre-scan script results:
+-- | mtrace:
+-- | Group 0.0.0.0 from 172.16.45.4 to 172.16.0.1
+-- | Source: 172.16.45.4
+-- | In address: 172.16.34.3
+-- | Out address: 172.16.0.3
+-- | Protocol: PIM
+-- | In address: 172.16.45.4
+-- | Out address: 172.16.34.4
+-- | Protocol: PIM
+-- | Source: 172.16.45.4
+-- | In address: 172.16.13.1
+-- | Out address: 172.16.0.2
+-- | Protocol: PIM / Static
+-- | In address: 172.16.34.3
+-- | Out address: 172.16.13.3
+-- | Protocol: PIM
+-- | In address: 172.16.45.4
+-- | Out address: 172.16.34.4
+-- |_ Protocol: PIM
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+-- From: https://tools.ietf.org/id/draft-ietf-idmr-traceroute-ipm-07.txt
+PROTO = {
+ [0x01] = "DVMRP",
+ [0x02] = "MOSPF",
+ [0x03] = "PIM",
+ [0x04] = "CBT",
+ [0x05] = "PIM / Special table",
+ [0x06] = "PIM / Static",
+ [0x07] = "DVMRP / Static",
+ [0x08] = "PIM / MBGP",
+ [0x09] = "CBT / Special table",
+ [0x10] = "CBT / Static",
+ [0x11] = "PIM / state created by Assert processing",
+}
+
+FWD_CODE = {
+ [0x00] = "NO_ERROR",
+ [0x01] = "WRONG_IF",
+ [0x02] = "PRUNE_SENT",
+ [0x03] = "PRUNE_RCVD",
+ [0x04] = "SCOPED",
+ [0x05] = "NO_ROUTE",
+ [0x06] = "WRONG_LAST_HOP",
+ [0x07] = "NOT_FORWARDING",
+ [0x08] = "REACHED_RP",
+ [0x09] = "RPF_IF",
+ [0x0A] = "NO_MULTICAST",
+ [0x0B] = "INFO_HIDDEN",
+ [0x81] = "NO_SPACE",
+ [0x82] = "OLD_ROUTER",
+ [0x83] = "ADMIN_PROHIB",
+}
+
+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 for lack of privileges.", SCRIPT_NAME)
+ return false
+ end
+ return true
+end
+
+--- Generates a raw IGMP Traceroute Query.
+--@param fromip Source address.
+--@param toip Destination address.
+--@param group Multicast group address.
+--@param receiver Receiver of the response.
+--@return data Raw Traceroute Query.
+local traceRaw = function(fromip, toip, group, receiver)
+ local data = bin.pack(">C", 0x1f) -- Type: Traceroute Query
+ local data = data .. bin.pack(">C", 0x20) -- Hops: 32
+ local data = data .. bin.pack(">S", 0x0000) -- Checksum: To be set later
+ local data = data .. bin.pack(">I", ipOps.todword(group)) -- Multicast group
+ local data = data .. bin.pack(">I", ipOps.todword(fromip)) -- Source
+ local data = data .. bin.pack(">I", ipOps.todword(toip)) -- Destination
+ local data = data .. bin.pack(">I", ipOps.todword(receiver)) -- Receiver
+ local data = data .. bin.pack(">C", 0x40) -- TTL
+ local data = data .. bin.pack(">CS", 0x00, math.random(123456)) -- Query ID
+
+ -- We calculate checksum
+ data = data:sub(1,2) .. bin.pack(">S", packet.in_cksum(data)) .. data:sub(5)
+ return data
+end
+
+--- Sends a raw IGMP Traceroute Query.
+--@param interface Network interface to send through.
+--@param destination Target host to which the packet is sent.
+--@param trace_raw Traceroute raw Query.
+local traceSend = function(interface, destination, trace_raw)
+ local ip_raw = bin.pack("H", "45c00040ed780000400218bc0a00c8750a00c86b") .. trace_raw
+ local trace_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ trace_packet:ip_set_bin_src(ipOps.ip_to_str(interface.address))
+ trace_packet:ip_set_bin_dst(ipOps.ip_to_str(destination))
+ trace_packet:ip_set_len(#trace_packet.buf)
+ trace_packet:ip_count_checksum()
+
+ if destination == "224.0.0.2" then
+ -- Doesn't affect results as it is ignored but most routers, but RFC
+ -- 3171 should be respected.
+ trace_packet:ip_set_ttl(1)
+ end
+ trace_packet:ip_count_checksum()
+
+ local sock = nmap.new_dnet()
+ if destination == "224.0.0.2" then
+ sock:ethernet_open(interface.device)
+ -- Ethernet IPv4 multicast, our ethernet address and packet type IP
+ eth_hdr = bin.pack("HAH", "01 00 5e 00 00 02", interface.mac, "08 00")
+ sock:ethernet_send(eth_hdr .. trace_packet.buf)
+ sock:ethernet_close()
+ else
+ sock:ip_open()
+ sock:ip_send(trace_packet.buf)
+ sock:ip_close()
+ end
+end
+
+--- Parses an IGMP Traceroute Response and returns it in structured form.
+--@param data Raw Traceroute Response.
+--@return response Structured Traceroute Response.
+local traceParse = function(data)
+ local index
+ local response = {}
+
+ -- first byte should be IGMP type == 0x1e (Traceroute Response)
+ if data:byte(1) ~= 0x1e then return end
+
+ -- Hops
+ index, response.hops = bin.unpack(">C", data, 2)
+
+ -- Checksum
+ index, response.checksum = bin.unpack(">S", data, index)
+
+ -- Group
+ index, response.group = bin.unpack("C", data, index)
+
+ -- Query ID
+ index, response.qid = bin.unpack(">C", data, index)
+ index, response.qid = response.qid * 2^16 + bin.unpack(">S", data, index)
+
+ local block
+ response.blocks = {}
+ -- Now, parse data blocks
+ while true do
+ -- To end parsing and not get stuck in infinite loops.
+ if index >= #data then
+ break
+ elseif #data - index < 31 then
+ stdnse.print_verbose("%s malformated traceroute response.", SCRIPT_NAME)
+ return
+ end
+
+ block = {}
+ -- Query Arrival
+ index, block.query = bin.unpack(">I", data, index)
+
+ -- In itf address
+ index, block.inaddr = bin.unpack("I", data, index)
+
+ -- Out packets
+ index, block.outpkts = bin.unpack(">I", data, index)
+
+ -- S,G pkt count
+ index, block.sgpkt = bin.unpack(">I", data, index)
+
+ -- Protocol
+ index, block.proto = bin.unpack(">C", data, index)
+
+ -- Forward TTL
+ index, block.fwdttl = bin.unpack(">C", data, index)
+
+ -- Options
+ index, block.options = bin.unpack(">C", data, index)
+
+ -- Forwarding Code
+ index, block.code = bin.unpack(">C", data, index)
+
+ table.insert(response.blocks, block)
+ end
+ return response
+end
+
+-- Listens for IGMP Traceroute responses
+--@param interface Network interface to listen on.
+--@param timeout Amount of time to listen for in seconds.
+--@param responses table to insert responses into.
+local traceListener = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local p, trace_raw, status, l3data, response
+
+ -- IGMP packets that are sent to our host
+ local filter = 'ip proto 2 and dst host ' .. interface.address
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ trace_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ if p then
+ -- Check that IGMP Type == 0x1e (Traceroute Response)
+ if trace_raw:byte(1) == 0x1e then
+ response = traceParse(trace_raw)
+ if response then
+ response.srcip = p.ip_src
+ table.insert(responses, response)
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+-- Returns the network interface used to send packets to a target host.
+--@param target host to which the interface is used.
+--@return interface Network interface used for target host.
+local getInterface = function(target)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(target, "12345", "udp")
+ if not status then
+ stdnse.print_verbose("%s: %s", SCRIPT_NAME, err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.print_verbose("%s: %s", SCRIPT_NAME, err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+
+action = function()
+ local fromip = stdnse.get_script_args(SCRIPT_NAME .. ".fromip")
+ local toip = stdnse.get_script_args(SCRIPT_NAME .. ".toip")
+ local group = stdnse.get_script_args(SCRIPT_NAME .. ".group") or "0.0.0.0"
+ local firsthop = stdnse.get_script_args(SCRIPT_NAME .. ".firsthop") or "224.0.0.2"
+ local timeout = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 7
+ local responses = {}
+ timeout = timeout * 1000
+
+ -- Source address from which to traceroute
+ if not fromip then
+ stdnse.print_verbose("%s: A source IP must be provided through fromip argument.", SCRIPT_NAME)
+ return
+ end
+
+ -- Get network interface to use
+ local interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(firsthop)
+ end
+ if not interface then
+ return ("\n ERROR: Couldn't get interface for %s"):format(firsthop)
+ end
+
+ -- Destination defaults to our own host
+ toip = toip or interface.address
+
+ stdnse.print_debug("%s: Traceroute group %s from %s to %s.", SCRIPT_NAME, group, fromip, toip)
+ stdnse.print_debug("%s: will send to %s via %s interface.", SCRIPT_NAME, firsthop, interface.shortname)
+
+ -- Thread that listens for responses
+ stdnse.new_thread(traceListener, interface, timeout, responses)
+
+ -- Send request after small wait to let Listener start
+ stdnse.sleep(0.1)
+ local trace_raw = traceRaw(fromip, toip, group, interface.address)
+ traceSend(interface, firsthop, trace_raw)
+
+ local condvar = nmap.condvar(responses)
+ condvar("wait")
+ if #responses > 0 then
+ local outresp
+ local output, outblock = {}
+ table.insert(output, ("Group %s from %s to %s"):format(group, fromip, toip))
+ for _, response in pairs(responses) do
+ outresp = {}
+ outresp.name = "Source: " .. response.srcip
+ for _, block in pairs(response.blocks) do
+ outblock = {}
+ outblock.name = "In address: " .. block.inaddr
+ table.insert(outblock, "Out address: " .. block.outaddr)
+ -- Protocol
+ if PROTO[block.proto] then
+ table.insert(outblock, "Protocol: " .. PROTO[block.proto])
+ else
+ table.insert(outblock, "Protocol: Unknown")
+ end
+ -- Error Code, we ignore NO_ERROR which is the normal case.
+ if FWD_CODE[block.code] and block.code ~= 0x00 then
+ table.insert(outblock, "Error code: " .. FWD_CODE[block.code])
+ elseif block.code ~= 0x00 then
+ table.insert(outblock, "Error code: Unknown")
+ end
+ table.insert(outresp, outblock)
+ end
+ table.insert(output, outresp)
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/script.db b/scripts/script.db
index e77b91b70..0ee110570 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -251,6 +251,7 @@ Entry { filename = "mongodb-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "mrinfo.nse", categories = { "discovery", "safe", "broadcast"} }
+Entry { filename = "mtrace.nse", categories = { "discovery", "safe", "broadcast"} }
Entry { filename = "ms-sql-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "ms-sql-config.nse", categories = { "discovery", "safe", } }
Entry { filename = "ms-sql-dac.nse", categories = { "discovery", "safe", } }