summaryrefslogtreecommitdiffstats
path: root/scripts/broadcast-eigrp-discovery.nse
blob: 5b47ab4417f4a2d77c1f418606348b45db6a9884 (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
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
local eigrp = require "eigrp"
local nmap = require "nmap"
local stdnse = require "stdnse"
local table = require "table"
local packet = require "packet"
local ipOps = require "ipOps"
local target = require "target"
local coroutine = require "coroutine"
local string = require "string"

description = [[
Performs network discovery and routing information gathering through
Cisco's Enhanced Interior Gateway Routing Protocol (EIGRP).

The script works by sending an EIGRP Hello packet with the specified Autonomous
System value to the 224.0.0.10 multicast address and listening for EIGRP Update
packets. The script then parses the update responses for routing information.

If no A.S value was provided by the user, the script will listen for multicast
Hello packets to grab an A.S value. If no interface was provided as a script
argument or through the -e option, the script will send packets and listen
through all valid ethernet interfaces simultaneously.

]]

---
-- @usage
-- nmap --script=broadcast-eigrp-discovery <targets>
-- nmap --script=broadcast-eigrp-discovery <targets> -e wlan0
--
-- @args broadcast-eigrp-discovery.as Autonomous System value to announce on.
-- If not set, the script will listen for announcements on 224.0.0.10 to grab
-- an A.S value.
--
-- @args broadcast-eigrp-discovery.timeout Max amount of time to listen for A.S
-- announcements and updates. Defaults to <code>10</code> seconds.
--
-- @args broadcast-eigrp-discovery.kparams the K metrics.
-- Defaults to <code>101000</code>.
-- @args broadcast-eigrp-discovery.interface Interface to send on (overrides -e)
--
--@output
-- Pre-scan script results:
-- | broadcast-eigrp-discovery:
-- | 192.168.2.2
-- |   Interface: eth0
-- |   A.S: 1
-- |   Virtual Router ID: 0
-- |   Internal Route
-- |     Destination: 192.168.21.0/24
-- |     Next hop: 0.0.0.0
-- |   Internal Route
-- |     Destination: 192.168.31.0/24
-- |     Next hop: 0.0.0.0
-- |   External Route
-- |     Protocol: Static
-- |     Originating A.S: 0
-- |     Originating Router ID: 192.168.31.1
-- |     Destination: 192.168.60.0/24
-- |     Next hop: 0.0.0.0
-- |   External Route
-- |     Protocol: OSPF
-- |     Originating A.S: 1
-- |     Originating Router ID: 192.168.31.1
-- |     Destination: 192.168.24.0/24
-- |     Next hop: 0.0.0.0
-- |_  Use the newtargets script-arg to add the results as targets
--

author = "Hani Benhabiles"

license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"discovery", "broadcast", "safe"}

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


-- Sends EIGRP raw packet to EIGRP multicast address.
--@param interface Network interface to use.
--@param eigrp_raw Raw eigrp packet.
local eigrpSend = function(interface, eigrp_raw)
  local srcip = interface.address
  local dstip = "224.0.0.10"

  local ip_raw = stdnse.fromhex( "45c00040ed780000015818bc0a00c8750a00c86b") .. eigrp_raw
  local eigrp_packet = packet.Packet:new(ip_raw, ip_raw:len())
  eigrp_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
  eigrp_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
  eigrp_packet:ip_set_len(#eigrp_packet.buf)
  eigrp_packet:ip_count_checksum()

  local sock = nmap.new_dnet()
  sock:ethernet_open(interface.device)
  -- Ethernet IPv4 multicast, our ethernet address and packet type IP
  local eth_hdr = stdnse.fromhex("01 00 5e 00 00 0a") .. interface.mac .. stdnse.fromhex("08 00")
  sock:ethernet_send(eth_hdr .. eigrp_packet.buf)
  sock:ethernet_close()
end


-- Listens for EIGRP updates
--@param interface Network interface to listen on.
--@param timeout Ammount of time to listen for.
--@param responses Table to put valid responses into.
local eigrpListener = function(interface, timeout, responses)
  local condvar = nmap.condvar(responses)
  local routers = {}
  local status, l3data, response, p, eigrp_raw, _
  local start = nmap.clock_ms()
  -- Filter for EIGRP packets that are sent either to us or to multicast
  local filter =  "ip proto 88 and (ip dst host " .. interface.address .. " or 224.0.0.10)"
  local listener = nmap.new_socket()
  listener:set_timeout(500)
  listener:pcap_open(interface.device, 1024, true, filter)

  -- For each EIGRP packet captured until timeout
  while (nmap.clock_ms() - start) < timeout do
    response = {}
    response.tlvs = {}
    status, _, _, l3data = listener:pcap_receive()
    if status then
      p = packet.Packet:new(l3data, #l3data)
      eigrp_raw = string.sub(l3data, p.ip_hl*4 + 1)
      -- Check if it is an EIGRPv2 Update
      if eigrp_raw:byte(1) == 0x02 and eigrp_raw:byte(2) == 0x01 then
        -- Skip if did get the info from this router before
        if not routers[p.ip_src] then
          -- Parse header
          response = eigrp.EIGRP.parse(eigrp_raw)
          response.src = p.ip_src
          response.interface = interface.shortname
        end
        if response then
          -- See, if it has routing information
          for _,tlv in pairs(response.tlvs) do
            if eigrp.EIGRP.isRoutingTLV(tlv.type) then
              routers[p.ip_src] = true
              table.insert(responses, response)
              break
            end
          end
        end
      end
    end
  end
  condvar("signal")
  return
end

-- Listens for EIGRP announcements to grab a valid Autonomous System value.
--@param interface Network interface to listen on.
--@param timeout Max amount of time to listen for.
--@param astab Table to put result into.
local asListener = function(interface, timeout, astab)
  local condvar = nmap.condvar(astab)
  local status, l3data, p, eigrp_raw, eigrp_hello, _
  local start = nmap.clock_ms()
  local filter =  "ip proto 88 and ip dst host 224.0.0.10"
  local listener = nmap.new_socket()
  listener:set_timeout(500)
  listener:pcap_open(interface.device, 1024, true, filter)
  while (nmap.clock_ms() - start) < timeout do
    -- Check if another listener already found an A.S value.
    if #astab > 0 then break end

    status, _, _, l3data = listener:pcap_receive()
    if status then
      p = packet.Packet:new(l3data, #l3data)
      eigrp_raw = string.sub(l3data, p.ip_hl*4 + 1)
      -- Listen for EIGRPv2 Hello packets
      if eigrp_raw:byte(1) == 0x02 and eigrp_raw:byte(2) == 0x05 then
        eigrp_hello = eigrp.EIGRP.parse(eigrp_raw)
        if eigrp_hello and eigrp_hello.as then
          table.insert(astab, eigrp_hello.as)
          break
        end
      end
    end
  end
  condvar("signal")
end

local function fail (err) return stdnse.format_output(false, err) end

action = function()
  -- Get script arguments
  local as = stdnse.get_script_args(SCRIPT_NAME .. ".as")
  local kparams = stdnse.get_script_args(SCRIPT_NAME .. ".kparams") or "101000"
  local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
  local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
  local output, responses, interfaces, lthreads = {}, {}, {}, {}
  local result, response, route, eigrp_hello, k
  local timeout = (timeout or 10) * 1000

  -- K params should be of length 6
  -- Cisco routers ignore eigrp packets that don't have matching K parameters
  if #kparams < 6 or #kparams > 6 then
    return fail("kparams should be of size 6.")
  else
    k = {}
    k[1] = string.sub(kparams, 1,1)
    k[2] = string.sub(kparams, 2,2)
    k[3] = string.sub(kparams, 3,3)
    k[4] = string.sub(kparams, 4,4)
    k[5] = string.sub(kparams, 5,5)
    k[6] = string.sub(kparams, 6)
  end

  interface = interface or nmap.get_interface()
  if interface then
    -- If an interface was provided, get its information
    interface = nmap.get_interface_info(interface)
    if not interface then
      return fail(("Failed to retrieve %s interface information."):format(interface))
    end
    interfaces = {interface}
    stdnse.debug1("Will use %s interface.", interface.shortname)
  else
    local ifacelist = nmap.list_interfaces()
    for _, iface in ipairs(ifacelist) do
      -- Match all ethernet interfaces
      if iface.address and iface.link=="ethernet" and
        iface.address:match("%d+%.%d+%.%d+%.%d+") then

        stdnse.debug1("Will use %s interface.", iface.shortname)
        table.insert(interfaces, iface)
      end
    end
  end

  -- If user didn't provide an Autonomous System value, we listen fro multicast
  -- HELLO router announcements to get one.
  if not as then
    -- We use a table for condvar
    local astab = {}
    stdnse.debug1("No A.S value provided, will sniff for one.")
    -- We should iterate over interfaces
    for _, interface in pairs(interfaces) do
      local co = stdnse.new_thread(asListener, interface, timeout, astab)
      lthreads[co] = true
    end
    local condvar = nmap.condvar(astab)
    -- Wait for the listening threads to finish
    repeat
      for thread in pairs(lthreads) do
        if coroutine.status(thread) == "dead" then lthreads[thread] = nil end
      end
      if ( next(lthreads) ) then
        condvar("wait")
      end
    until next(lthreads) == nil;

    if #astab > 0 then
      stdnse.debug1("Will use %s A.S value.", astab[1])
      as = astab[1]
    else
      return fail("Couldn't get an A.S value.")
    end
  end

  -- Craft Hello packet
  eigrp_hello = eigrp.EIGRP:new(eigrp.OPCODE.HELLO, as)
  -- K params
  eigrp_hello:addTLV({ type = eigrp.TLV.PARAM, k = k, htime = 15})
  -- Software version
  eigrp_hello:addTLV({ type = eigrp.TLV.SWVER, majv = 12, minv = 4, majtlv = 1, mintlv = 2})

  -- On each interface, launch the listening thread and send the Hello packet.
  lthreads = {}
  for _, interface in pairs(interfaces) do
    local co = stdnse.new_thread(eigrpListener, interface, timeout, responses)
    -- We insert a small delay before sending the Hello so the listening
    -- thread doesn't miss updates.
    stdnse.sleep(0.5)
    lthreads[co] = true
    eigrpSend(interface, tostring(eigrp_hello))
  end

  local condvar = nmap.condvar(responses)
  -- Wait for the listening threads to finish
  repeat
    condvar("wait")
    for thread in pairs(lthreads) do
      if coroutine.status(thread) == "dead" then lthreads[thread] = nil end
    end
  until next(lthreads) == nil;

  -- Output the useful info from the responses
  if #responses > 0 then
    for _, response in pairs(responses) do
      result = {}
      result.name = response.src
      if target.ALLOW_NEW_TARGETS then target.add(response.src) end
      table.insert(result, "Interface: " .. response.interface)
      table.insert(result, ("A.S: %d"):format(response.as))
      table.insert(result, ("Virtual Router ID: %d"):format(response.routerid))
      -- Output routes information TLVs
      for _, tlv in pairs(response.tlvs) do
        route = {}
        -- We are only interested in Internal or external routes
        if tlv.type == eigrp.TLV.EXT then
          route.name = "External route"
          for name, value in pairs(eigrp.EXT_PROTO) do
            if value == tlv.eproto then
              table.insert(route, ("Protocol: %s"):format(name))
              break
            end
          end
          table.insert(route, ("Originating A.S: %s"):format(tlv.oas))
          table.insert(route, ("Originating Router ID: %s"):format(tlv.orouterid))
          if target.ALLOW_NEW_TARGETS then target.add(tlv.orouterid) end
          table.insert(route, ("Destination: %s/%d"):format(tlv.dst, tlv.mask))
          table.insert(route, ("Next hop: %s"):format(tlv.nexth))
          table.insert(result, route)
        elseif tlv.type == eigrp.TLV.INT then
          route.name = "Internal route"
          table.insert(route, ("Destination: %s/%d"):format(tlv.dst, tlv.mask))
          table.insert(route, ("Next hop: %s"):format(tlv.nexth))
          table.insert(result, route)
        end
      end
      table.insert(output, result)
    end
    if #output>0 and 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