summaryrefslogtreecommitdiffstats
path: root/scripts/mrinfo.nse
blob: fb7284f6ed69d30b306c7c231e68a2e7c1aeb538 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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 <code>224.0.0.1</code>.
--
-- @args mrinfo.timeout Time to wait for responses.
-- Defaults to <code>5s</code>.
--
--@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