summaryrefslogtreecommitdiffstats
path: root/scripts/broadcast-ospf2-discover.nse
blob: ad3fca08b394a84f818a51af541f74b593b946d4 (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
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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
local ipOps  = require "ipOps"
local nmap   = require "nmap"
local ospf   = require "ospf"
local packet = require "packet"
local stdnse = require "stdnse"
local target = require "target"
local os = require "os"
local string = require "string"
local table = require "table"

local have_ssl, openssl = pcall(require,'openssl')

description = [[
Discover IPv4 networks using Open Shortest Path First version 2(OSPFv2) protocol.

The script works by listening for OSPF Hello packets from the 224.0.0.5
multicast address. The script then replies and attempts to create a neighbor
relationship, in order to discover network database.

If no interface was provided as a script argument or through the -e option,
the script will fail unless a single interface is present on the system.
]]

---
-- @usage
-- nmap --script=broadcast-ospf2-discover
-- nmap --script=broadcast-ospf2-discover -e wlan0
--
-- @args broadcast-ospf2-discover.md5_key MD5 digest key to use if message digest
-- authentication is disclosed.
--
-- @args broadcast-ospf2-discover.router_id Router ID to use. Defaults to 0.0.0.1
--
-- @args broadcast-ospf2-discover.timeout Time in seconds that the script waits for
-- hello from other routers. Defaults to 10 seconds, matching OSPFv2 default
-- value for hello interval.
--
-- @args broadcast-ospf2-discover.interface Interface to send on (overrides -e). Mandatory
-- if not using -e and multiple interfaces are present.
--
-- @output
-- Pre-scan script results:
-- | broadcast-ospf2-discover:
-- |   Area ID: 0.0.0.0
-- |   External Routes
-- |     192.168.24.0/24
-- |_  Use the newtargets script-arg to add the results as targets
--

author     = "Emiliano Ticci"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"broadcast", "discovery", "safe"}

prerule = function()
  if nmap.address_family() ~= "inet" then
    stdnse.print_verbose("is IPv4 only.")
    return false
  end
  if not nmap.is_privileged() then
    stdnse.print_verbose("not running for lack of privileges.")
    return false
  end
  return true
end

-- Script constants
OSPF_ALL_ROUTERS = "224.0.0.5"
OSPF_MSG_HELLO   = 0x01
OSPF_MSG_DBDESC  = 0x02
OSPF_MSG_LSREQ   = 0x03
OSPF_MSG_LSUPD   = 0x04
local md5_key, router_id

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

-- Print OSPFv2 LSA Header packet details to debug output.
-- @param hello OSPFv2 LSA Header packet
local ospfDumpLSAHeader = function(lsa_h)
  if 2 > nmap.debugging() then
    return
  end
  stdnse.print_debug(2, "|   LS Age: %s", lsa_h.age)
  stdnse.print_debug(2, "|   Options: %s", lsa_h.options)
  stdnse.print_debug(2, "|   LS Type: %s", lsa_h.type)
  stdnse.print_debug(2, "|   Link State ID: %s", lsa_h.id)
  stdnse.print_debug(2, "|   Advertising Router: %s", lsa_h.adv_router)
  stdnse.print_debug(2, "|   Sequence: 0x%s", lsa_h.sequence)
  stdnse.print_debug(2, "|   Checksum: 0x%s", lsa_h.checksum)
  stdnse.print_debug(2, "|   Length: %s", lsa_h.length)
end

-- Print OSPFv2 Database Description packet details to debug output.
-- @param hello OSPFv2 Database Description packet
local ospfDumpDBDesc = function(db_desc)
  if 2 > nmap.debugging() then
    return
  end
  stdnse.print_debug(2, "| MTU:      %s", db_desc.mtu)
  stdnse.print_debug(2, "| Options:  %s", db_desc.options)
  stdnse.print_debug(2, "| Init:     %s", db_desc.init)
  stdnse.print_debug(2, "| More:     %s", db_desc.more)
  stdnse.print_debug(2, "| Master:   %s", db_desc.master)
  stdnse.print_debug(2, "| Sequence: %s", db_desc.sequence)
  if #db_desc.lsa_headers > 0 then
    stdnse.print_debug(2, "| LSA Headers:")
    for i, lsa_h in ipairs(db_desc.lsa_headers) do
      ospfDumpLSAHeader(lsa_h)
      if i < #db_desc.lsa_headers then
        stdnse.print_debug(2, "|")
      end
    end
  end
end

-- Print OSPFv2 Hello packet details to debug output.
-- @param hello OSPFv2 Hello packet
local ospfDumpHello = function(hello)
  if 2 > nmap.debugging() then
    return
  end
  stdnse.print_debug(2, "| Router ID:         %s", hello.header.router_id)
  stdnse.print_debug(2, "| Area ID:           %s", ipOps.fromdword(hello.header.area_id))
  stdnse.print_debug(2, "| Checksum:          %s", hello.header.chksum)
  stdnse.print_debug(2, "| Auth Type:         %s", hello.header.auth_type)
  if hello.header.auth_type == 0x01 then
    stdnse.print_debug(2, "| Auth Password:     %s", hello.header.auth_data.password)
  elseif hello.header.auth_type == 0x02 then
    stdnse.print_debug(2, "| Auth Crypt Key ID: %s", hello.header.auth_data.keyid)
    stdnse.print_debug(2, "| Auth Data Length:  %s", hello.header.auth_data.length)
    stdnse.print_debug(2, "| Auth Crypt Seq:    %s", hello.header.auth_data.seq)
  end
  stdnse.print_debug(2, "| Netmask:           %s", hello.netmask)
  stdnse.print_debug(2, "| Hello interval:    %s", hello.interval)
  stdnse.print_debug(2, "| Options:           %s", hello.options)
  stdnse.print_debug(2, "| Priority:          %s", hello.prio)
  stdnse.print_debug(2, "| Dead interval:     %s", hello.router_dead_interval)
  stdnse.print_debug(2, "| Designated Router: %s", hello.DR)
  stdnse.print_debug(2, "| Backup Router:     %s", hello.BDR)
  stdnse.print_debug(2, "| Neighbors:         %s", table.concat(hello.neighbors, ","))
end

-- Print OSPFv2 LS Request packet details to debug output.
-- @param ls_req OSPFv2 LS Request packet
local ospfDumpLSRequest = function(ls_req)
  if 2 > nmap.debugging() then
    return
  end
  for i, req in ipairs(ls_req.ls_requests) do
    stdnse.print_debug(2, "| LS Type:           %s", req.type)
    stdnse.print_debug(2, "| Link State ID:     %s", req.id)
    stdnse.print_debug(2, "| Avertising Router: %s", req.adv_router)
    if i < #ls_req.ls_requests then
      stdnse.print_debug(2, "|")
    end
  end
end

-- Print OSPFv2 LS Update packet details to debug output.
-- @param ls_upd OSPFv2 LS Update packet
local ospfDumpLSUpdate = function(ls_upd)
  stdnse.print_debug(2, "| Number of LSAs: %s", ls_upd.num_lsas)
  for i, lsa in ipairs(ls_upd.lsas) do
    -- Only Type 1 (Router-LSA) and Type 5 (AS-External-LSA) are supported at the moment
    ospfDumpLSAHeader(lsa.header)
    if lsa.header.type == 1 then
      stdnse.print_debug(2, "|   Flags: %s", lsa.flags)
      stdnse.print_debug(2, "|   Number of links: %s", lsa.num_links)
      for j, link in ipairs(lsa.links) do
        stdnse.print_debug(2, "|     Link ID: %s", link.id)
        stdnse.print_debug(2, "|     Link Data: %s", link.data)
        stdnse.print_debug(2, "|     Link Type: %s", link.type)
        stdnse.print_debug(2, "|     Number of Metrics: %s", link.num_metrics)
        stdnse.print_debug(2, "|     0 Metric: %s", link.metric)
        if j < #lsa.links then
          stdnse.print_debug(2, "|")
        end
      end
      if i < #ls_upd.lsas then
        stdnse.print_debug(2, "|")
      end
    elseif lsa.header.type == 5 then
      stdnse.print_debug(2, "|   Netmask: %s", lsa.netmask)
      stdnse.print_debug(2, "|   External Type: %s", lsa.ext_type)
      stdnse.print_debug(2, "|   Metric: %s", lsa.metric)
      stdnse.print_debug(2, "|   Forwarding Address: %s", lsa.fw_address)
      stdnse.print_debug(2, "|   External Route Tag: %s", lsa.ext_tag)
    end
  end
end

-- Send OSPFv2 packet to specified destination.
-- @param interface   Source interface to use
-- @param ip_dst      Destination IP address
-- @param mac_dst     Destination MAC address
-- @param ospf_packet Raw OSPF packet
local ospfSend = function(interface, ip_dst, mac_dst, ospf_packet)
  local dnet  = nmap.new_dnet()
  local probe = packet.Frame:new()

  probe.mac_src    = interface.mac
  probe.mac_dst    = mac_dst
  probe.ip_bin_src = ipOps.ip_to_str(interface.address)
  probe.ip_bin_dst = ipOps.ip_to_str(ip_dst)
  probe.l3_packet  = ospf_packet
  probe.ip_dsf     = 0xc0
  probe.ip_p       = 89
  probe.ip_ttl     = 1

  probe:build_ip_packet()
  probe:build_ether_frame()

  dnet:ethernet_open(interface.device)
  dnet:ethernet_send(probe.frame_buf)
  dnet:ethernet_close()
end

-- Prepare OSPFv2 packet header for reply.
-- @param packet_in  Source packet
-- @param packet_out Destination packet
local ospfSetHeader = function(packet_in, packet_out)
  packet_out.header:setRouterId(router_id)
  packet_out.header:setAreaID(packet_in.header.area_id)
  if packet_in.header.auth_type == 0x01 then
    packet_out.header.auth_type = 0x01
    packet_out.header.auth_data.password = packet_in.header.auth_data.password
  elseif packet_in.header.auth_type == 0x02 then
    packet_out.header.auth_type = 0x02
    packet_out.header.auth_data.key = md5_key
    packet_out.header.auth_data.keyid = packet_in.header.auth_data.keyid
    packet_out.header.auth_data.length = 16
    packet_out.header.auth_data.seq = os.time()
  end

  return packet_out
end

-- Reply to OSPFv2 Database Description with an LS Request.
-- @param interface  Source interface
-- @param mac_dst    Destination MAC address
-- @param db_desc_in OSPFv2 Database Description packet to reply for
local ospfSendLSRequest = function(interface, mac_dst, db_desc_in)
  local ls_req_out = ospf.OSPF.LSRequest:new()
  ls_req_out = ospfSetHeader(db_desc_in, ls_req_out)

  for i, lsa_h in ipairs(db_desc_in.lsa_headers) do
    ls_req_out:addRequest(lsa_h.type, lsa_h.id, lsa_h.adv_router)
  end

  stdnse.print_debug(2, "Crafted OSPFv2 LS Request packet with the following parameters:")
  ospfDumpLSRequest(ls_req_out)
  ospfSend(interface, db_desc_in.header.router_id, mac_dst, tostring(ls_req_out))
end

-- Reply to given OSPFv2 Database Description packet.
-- @param interface  Source interface
-- @param mac_dst    Destination MAC address
-- @param db_desc_in OSPFv2 Database Description packet to reply for
local ospfReplyDBDesc = function(interface, mac_dst, db_desc_in)
  local reply       = false
  local db_desc_out = ospf.OSPF.DBDescription:new()
  db_desc_out = ospfSetHeader(db_desc_in, db_desc_out)

  if db_desc_in.init == true then
    db_desc_out.init     = false
    db_desc_out.more     = db_desc_in.more
    db_desc_out.master   = false
    db_desc_out.sequence = db_desc_in.sequence
    reply = true
  elseif #db_desc_in.lsa_headers > 0 then
    ospfSendLSRequest(interface, mac_dst, db_desc_in)
    return true
  end

  if reply then
    stdnse.print_debug(2, "Crafted OSPFv2 Database Description packet with the following parameters:")
    ospfDumpDBDesc(db_desc_out)
    ospfSend(interface, db_desc_in.header.router_id, mac_dst, tostring(db_desc_out))
  end

  return reply
end

-- Reply to given OSPFv2 Hello packet sending another Hello to
-- "All OSPF Routers" multicast address (224.0.0.5).
-- @param interface Source interface
-- @param hello_in  OSPFv2 Hello packet to reply for
local ospfReplyHello = function(interface, hello_in)
  local hello_out = ospf.OSPF.Hello:new()
  hello_out = ospfSetHeader(hello_in, hello_out)
  hello_out.interval             = hello_in.interval
  hello_out.router_dead_interval = hello_in.router_dead_interval
  hello_out:setDesignatedRouter(hello_in.header.router_id)
  hello_out:setNetmask(hello_in.netmask)
  hello_out:addNeighbor(hello_in.header.router_id)

  stdnse.print_debug(2, "Crafted OSPFv2 Hello packet with the following parameters:")
  ospfDumpHello(hello_out)

  ospfSend(interface, OSPF_ALL_ROUTERS, "\x01\x00\x5e\x00\x00\x05", tostring(hello_out))
end

-- Listen for OSPF packets on a specified interface.
-- @param interface Interface to use
-- @param timeout   Amount of time to listen in seconds
local ospfListen = function(interface, timeout)
  local status, l2_data, l3_data, ospf_raw, _
  local start  = nmap.clock_ms()

  stdnse.print_debug("Start listening on interface %s...", interface.shortname)
  local listener = nmap.new_socket()
  listener:set_timeout(1000)
  listener:pcap_open(interface.device, 1500, true, "ip proto 89 and not (ip src host " .. interface.address .. ")")
  while (nmap.clock_ms() - start) < (timeout * 1000) do
    status, _, l2_data, l3_data = listener:pcap_receive()
    if status then
      stdnse.print_debug(2, "Packet received on interface %s.", interface.shortname)
      local p = packet.Packet:new(l3_data, #l3_data)
      local ospf_raw = string.sub(l3_data, p.ip_hl * 4 + 1)
      if ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_HELLO then
        stdnse.print_debug(2, "OSPFv2 Hello packet detected.")

        local ospf_hello = ospf.OSPF.Hello.parse(ospf_raw)
        stdnse.print_debug(2, "Captured OSPFv2 Hello packet with the following parameters:")
        ospfDumpHello(ospf_hello)

        -- Additional checks required for message digest authentication
        if ospf_hello.header.auth_type == 0x02 then
          if not md5_key then
            return fail("Argument md5_key must be present when message digest authentication is disclosed.")
          elseif not have_ssl then
            return fail("Cannot handle message digest authentication unless openssl is compiled in.")
          end
        end

        ospfReplyHello(interface, ospf_hello)
        start = nmap.clock_ms()
      elseif ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_DBDESC then
        stdnse.print_debug(2, "OSPFv2 Database Description packet detected.")

        local ospf_db_desc = ospf.OSPF.DBDescription.parse(ospf_raw)
        stdnse.print_debug(2, "Captured OSPFv2 Database Description packet with the following parameters:")
        ospfDumpDBDesc(ospf_db_desc)

        if not ospfReplyDBDesc(interface, string.sub(l2_data, 7, 12), ospf_db_desc) then
          return
        end
      elseif ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_LSUPD then
        stdnse.print_debug(2, "OSPFv2 LS Update packet detected.")

        local ospf_ls_upd = ospf.OSPF.LSUpdate.parse(ospf_raw)
        stdnse.print_debug(2, "Captured OSPFv2 LS Update packet with the following parameters:")
        ospfDumpLSUpdate(ospf_ls_upd)

        local targets = {}
        for i, lsa in ipairs(ospf_ls_upd.lsas) do
          -- Only Type 1 (Router-LSA) and Type 5 (AS-External-LSA) are supported at the moment
          if lsa.header.type == 1 then
            for j, link in ipairs(lsa.links) do
              if link.type == 3 then
                local target = link.id .. ipOps.subnet_to_cidr(link.data)
                targets[target] = 1
              end
            end
          elseif lsa.header.type == 5 then
            local target = lsa.header.id .. ipOps.subnet_to_cidr(lsa.netmask)
            targets[target] = 1
          end
        end
        local output = stdnse.output_table()
        if next(targets) then
          local out_links = {}
          output["Area ID"] = ipOps.fromdword(ospf_ls_upd.header.area_id)
          output["External Routes"] = out_links
          for t, _ in pairs(targets) do
            table.insert(out_links, t)
            if target.ALLOW_NEW_TARGETS then
              target.add(t)
            end
          end
          if not target.ALLOW_NEW_TARGETS then
            stdnse.verbose("Use the newtargets script-arg to add the results as targets")
          end
        end
        return output
      end
    end
  end
  listener:pcap_close()
end

action = function()
  -- Get script arguments
  md5_key   = stdnse.get_script_args(SCRIPT_NAME .. ".md5_key") or false
  router_id = stdnse.get_script_args(SCRIPT_NAME .. ".router_id") or "0.0.0.1"
  local timeout   = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 10
  local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
  stdnse.print_debug("Value for router ID argument: %s.", router_id)
  stdnse.print_debug("Value for timeout argument: %s.", timeout)

  -- Determine interface to use
  interface  = interface or nmap.get_interface()
  if interface then
    interface = nmap.get_interface_info(interface)
    if not interface then
      return fail(("Failed to retrieve %s interface information."):format(interface))
    end
    stdnse.print_debug("Will use %s interface.", interface.shortname)
  else
    local interface_list = nmap.list_interfaces()
    local interface_good = {}
    for _, os_interface in ipairs(interface_list) do
      if os_interface.address and os_interface.link == "ethernet" and
        os_interface.address:match("%d+%.%d+%.%d+%.%d+") then

        stdnse.print_debug(2, "Found usable interface: %s.", os_interface.shortname)
        table.insert(interface_good, os_interface)
      end
    end
    if #interface_good == 1 then
      interface = interface_good[1]
      stdnse.print_debug("Will use %s interface.", interface.shortname)
    elseif #interface_good == 0 then
      return fail("Source interface not found.")
    else
      return fail("Ambiguous source interface, please specify it with -e or interface parameter.")
    end
  end

  return ospfListen(interface, timeout)
end