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/mtrace.nse | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 scripts/mtrace.nse (limited to 'scripts/mtrace.nse') diff --git a/scripts/mtrace.nse b/scripts/mtrace.nse new file mode 100644 index 0000000..4df7935 --- /dev/null +++ b/scripts/mtrace.nse @@ -0,0 +1,378 @@ +local nmap = require "nmap" +local packet = require "packet" +local ipOps = require "ipOps" +local stdnse = require "stdnse" +local table = require "table" +local math = require "math" +local string = require "string" + +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. +-- Defaults to 7s. +-- +--@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 https://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.verbose1("is IPv4 only.") + return false + end + if not nmap.is_privileged() then + stdnse.verbose1("not running for lack of privileges.") + 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 = string.pack(">BBI2 I4 I4 I4 I4 BBI2", + 0x1f, -- Type: Traceroute Query + 0x20, -- Hops: 32 + 0x0000, -- Checksum: To be set later + ipOps.todword(group), -- Multicast group + ipOps.todword(fromip), -- Source + ipOps.todword(toip), -- Destination + ipOps.todword(receiver), -- Receiver + 0x40, -- TTL + 0x00, math.random(123456) -- Query ID + ) + + -- We calculate checksum + data = data:sub(1,2) .. string.pack(">I2", 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 = stdnse.fromhex( "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 + local eth_hdr = "\x01\x00\x5e\x00\x00\x02" .. interface.mac .. "\x08\x00" + sock:ethernet_send(eth_hdr .. trace_packet.buf) + sock:ethernet_close() + else + sock:ip_open() + sock:ip_send(trace_packet.buf, destination) + 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 + response.hops, + -- Checksum + response.checksum, + -- Group + response.group, + -- Source address + response.source, + -- Destination address + response.destination, + -- Response address + response.response, + -- Response TTL + response.ttl, + -- Query ID + response.qid, index = string.unpack(">B I2 I4 I4 I4 I4 B I3", data, 2) + + response.group = ipOps.fromdword(response.group) + response.source = ipOps.fromdword(response.source) + response.receiver = ipOps.fromdword(response.destination) + response.response = ipOps.fromdword(response.response) + + 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.verbose1("malformed traceroute response.") + return + end + + block = {} + -- Query Arrival + block.query, + -- In itf address + block.inaddr, + -- Out itf address + block.outaddr, + -- Previous rtr address + block.prevaddr, + -- In packets + block.inpkts, + -- Out packets + block.outpkts, + -- S,G pkt count + block.sgpkt, + -- Protocol + block.proto, + -- Forward TTL + block.fwdttl, + -- Options + block.options, + -- Forwarding Code + block.code, index = string.unpack(">I4 I4 I4 I4 I4 I4 I4 BBBB", data, index) + + block.inaddr = ipOps.fromdword(block.inaddr) + block.outaddr = ipOps.fromdword(block.outaddr) + block.prevaddr = ipOps.fromdword(block.prevaddr) + + 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.verbose1("%s", err) + return + end + local status, address, _, _, _ = sock:get_info() + if not status then + stdnse.verbose1("%s", 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 = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + local responses = {} + timeout = (timeout or 7) * 1000 + + -- Source address from which to traceroute + if not fromip then + stdnse.verbose1("A source IP must be provided through fromip argument.") + 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 stdnse.format_output(false, ("Couldn't get interface for %s"):format(firsthop)) + end + + -- Destination defaults to our own host + toip = toip or interface.address + + stdnse.debug1("Traceroute group %s from %s to %s.", group, fromip, toip) + stdnse.debug1("will send to %s via %s interface.", 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 -- cgit v1.2.3