local nmap = require "nmap"
local packet = require "packet"
local ipOps = require "ipOps"
local stdnse = require "stdnse"
local string = require "string"
local target = require "target"
local table = require "table"
description = [[
Queries targets for multicast routing information.
This works by sending a DVMRP Ask Neighbors 2 request to the target and
listening for DVMRP Neighbors 2 responses that are sent back and which contain
local addresses and the multicast neighbors on each interface of the target. If
no specific target is specified, the request will be sent to the 224.0.0.1 All
Hosts multicast address.
This script is similar somehow to the mrinfo utility included with Windows and
Cisco IOS.
]]
---
-- @args mrinfo.target Host to which the request is sent. If not set, the
-- request will be sent to 224.0.0.1
.
--
-- @args mrinfo.timeout Time to wait for responses.
-- Defaults to 5s
.
--
--@usage
-- nmap --script mrinfo
-- nmap --script mrinfo -e eth1
-- nmap --script mrinfo --script-args 'mrinfo.target=172.16.0.4'
--
--@output
-- Pre-scan script results:
-- | mrinfo:
-- | Source: 224.0.0.1
-- | Version 12.4
-- | Local address: 172.16.0.2
-- | Neighbor: 172.16.0.4
-- | Neighbor: 172.16.0.3
-- | Local address: 172.17.0.1
-- | Neighbor: 172.17.0.2
-- | Local address: 172.18.0.1
-- | Neighbor: 172.18.0.2
-- | Source: 224.0.0.1
-- | Version 12.4
-- | Local address: 172.16.0.4
-- | Neighbor: 172.16.0.3
-- | Neighbor: 172.16.0.2
-- | Local address: 172.17.0.2
-- | Neighbor: 172.17.0.1
-- | Source: 224.0.0.1
-- | Version 12.4
-- | Local address: 172.16.0.3
-- | Neighbor: 172.16.0.4
-- | Neighbor: 172.16.0.2
-- | Local address: 172.18.0.2
-- | Neighbor: 172.18.0.1
-- |_ Use the newtargets script-arg to add the responses as targets
--
author = "Hani Benhabiles"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe", "broadcast"}
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
-- Parses a DVMRP Ask Neighbor 2 raw data and returns
-- a structured response.
-- @param data raw data.
local mrinfoParse = function(data)
local index, address, neighbor
local response = {}
-- first byte should be IGMP type == 0x13 (DVMRP)
if data:byte(1) ~= 0x13 then return end
-- DVMRP Code
response.code,
-- Checksum
response.checksum,
-- Capabilities (Skip one reserved byte)
response.capabilities,
-- Major and minor version
response.minver,
response.majver, index = string.unpack(">B I2 x B B B", data, 2)
response.addresses = {}
-- Iterate over target local addresses (interfaces)
while index < #data do
if data:byte(index) == 0x00 then break end
address = {}
-- Local address
address.ip,
-- Link metric
address.metric,
-- Threshold
address.threshold,
-- Flags
address.flags,
-- Number of neighbors
address.ncount, index = string.unpack(">c4BBBB", data, index)
address.ip = ipOps.str_to_ip(address.ip)
address.neighbors = {}
-- Iterate over neighbors
for i = 1, address.ncount do
neighbor, index = string.unpack(">c4", data, index)
table.insert(address.neighbors, ipOps.str_to_ip(neighbor))
end
table.insert(response.addresses, address)
end
return response
end
-- Listens for DVMRP Ask Neighbors 2 responses
--@param interface Network interface to listen on.
--@param timeout Time to listen for a response.
--@param responses table to insert responses into.
local mrinfoListen = function(interface, timeout, responses)
local condvar = nmap.condvar(responses)
local start = nmap.clock_ms()
local listener = nmap.new_socket()
local p, mrinfo_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)
mrinfo_raw = string.sub(l3data, p.ip_hl*4 + 1)
if p then
-- Check that IGMP Type == DVMRP (0x13) and DVMRP code == Neighbor 2 (0x06)
if mrinfo_raw:byte(1) == 0x13 and mrinfo_raw:byte(2) == 0x06 then
response = mrinfoParse(mrinfo_raw)
if response then
response.srcip = p.ip_src
table.insert(responses, response)
end
end
end
end
end
condvar("signal")
end
-- Function that generates a raw DVMRP Ask Neighbors 2 request.
local mrinfoRaw = function()
local mrinfo_raw = string.pack(">BB I2 I2 BB",
0x13, -- Type: DVMRP
0x05, -- Code: Ask Neighbor v2
0x0000, -- Checksum: Calculated later
0x000a, -- Reserved
-- Version == Cisco IOS 12.4
0x04, -- Minor version: 4
0x0c) -- Major version: 12
-- Calculate checksum
mrinfo_raw = mrinfo_raw:sub(1,2) .. string.pack(">I2", packet.in_cksum(mrinfo_raw)) .. mrinfo_raw:sub(5)
return mrinfo_raw
end
-- Function that sends a DVMRP query.
--@param interface Network interface to use.
--@param dstip Destination IP to send to.
local mrinfoQuery = function(interface, dstip)
local mrinfo_packet, sock, eth_hdr
local srcip = interface.address
local mrinfo_raw = mrinfoRaw()
local ip_raw = stdnse.fromhex( "45c00040ed780000400218bc0a00c8750a00c86b") .. mrinfo_raw
mrinfo_packet = packet.Packet:new(ip_raw, ip_raw:len())
mrinfo_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
mrinfo_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
mrinfo_packet:ip_set_len(ip_raw:len())
if dstip == "224.0.0.1" then
-- Doesn't affect results, but we should respect RFC 3171 :)
mrinfo_packet:ip_set_ttl(1)
end
mrinfo_packet:ip_count_checksum()
sock = nmap.new_dnet()
if dstip == "224.0.0.1" then
sock:ethernet_open(interface.device)
-- Ethernet IPv4 multicast, our ethernet address and packet type IP
eth_hdr = "\x01\x00\x5e\x00\x00\x01" .. interface.mac .. "\x08\x00"
sock:ethernet_send(eth_hdr .. mrinfo_packet.buf)
sock:ethernet_close()
else
sock:ip_open()
sock:ip_send(mrinfo_packet.buf, dstip)
sock:ip_close()
end
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 timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
timeout = (timeout or 5) * 1000
local target = stdnse.get_script_args(SCRIPT_NAME .. ".target") or "224.0.0.1"
local responses = {}
local interface, result
interface = nmap.get_interface()
if interface then
interface = nmap.get_interface_info(interface)
else
interface = getInterface(target)
end
if not interface then
return stdnse.format_output(false, ("Couldn't get interface for %s"):format(target))
end
stdnse.debug1("will send to %s via %s interface.", target, interface.shortname)
-- Thread that listens for responses
stdnse.new_thread(mrinfoListen, interface, timeout, responses)
-- Send request after small wait to let Listener start
stdnse.sleep(0.1)
mrinfoQuery(interface, target)
local condvar = nmap.condvar(responses)
condvar("wait")
if #responses > 0 then
local output, ifoutput = {}
for _, response in pairs(responses) do
result = {}
result.name = "Source: " .. response.srcip
table.insert(result, ("Version %s.%s"):format(response.majver, response.minver))
for _, address in pairs(response.addresses) do
ifoutput = {}
ifoutput.name = "Local address: " .. address.ip
for _, neighbor in pairs(address.neighbors) do
if target.ALLOW_NEW_TARGETS then target.add(neighbor) end
table.insert(ifoutput, "Neighbor: " .. neighbor)
end
table.insert(result, ifoutput)
end
table.insert(output, result)
end
if not target.ALLOW_NEW_TARGETS then
table.insert(output,"Use the newtargets script-arg to add the results as targets")
end
return stdnse.format_output(true, output)
end
end