File: mtrace.nse

package info (click to toggle)
nmap 6.47-3%2Bdeb8u2
  • links: PTS, VCS
  • area: main
  • in suites: jessie
  • size: 44,788 kB
  • ctags: 25,108
  • sloc: ansic: 89,741; cpp: 62,412; sh: 19,492; python: 17,323; xml: 11,413; perl: 2,529; makefile: 2,503; yacc: 608; lex: 469; asm: 372; java: 45
file content (393 lines) | stat: -rw-r--r-- 12,960 bytes parent folder | download | duplicates (2)
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
local nmap = require "nmap"
local packet = require "packet"
local ipOps = require "ipOps"
local bin = require "bin"
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 <code>0.0.0.0</code> which represents all group addresses.
--
--@args mtrace.firsthop Host to which the query is sent. If not set, the
-- query will be sent to <code>224.0.0.2</code>.
--
--@args mtrace.timeout Time to wait for responses.
-- Defaults to <code>7s</code>.
--
--@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
    local 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, 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
  index, response.hops = bin.unpack(">C", data, 2)

  -- Checksum
  index, response.checksum = bin.unpack(">S", data, index)

  -- Group
  index, response.group = bin.unpack("<I", data, index)
  response.group = ipOps.fromdword(response.group)

  -- Source address
  index, response.source = bin.unpack("<I", data, index)
  response.source = ipOps.fromdword(response.source)

  -- Destination address
  index, response.destination = bin.unpack("<I", data, index)
  response.receiver = ipOps.fromdword(response.destination)

  -- Response address
  index, response.response = bin.unpack("<I", data, index)
  response.response = ipOps.fromdword(response.response)

  -- Response TTL
  index, response.ttl = 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 malformed 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)
    block.inaddr = ipOps.fromdword(block.inaddr)

    -- Out itf address
    index, block.outaddr = bin.unpack("<I", data, index)
    block.outaddr = ipOps.fromdword(block.outaddr)

    -- Previous rtr address
    index, block.prevaddr = bin.unpack("<I", data, index)
    block.prevaddr = ipOps.fromdword(block.prevaddr)

    -- In packets
    index, block.inpkts = 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 = 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.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