From 84a824c2cf6b30fdaf209fbeabee8254fc36f00f Mon Sep 17 00:00:00 2001 From: dmiller Date: Tue, 14 Mar 2017 00:15:22 +0000 Subject: [PATCH] New script broadcast-ospf2-discover. Closes #743 --- CHANGELOG | 3 + nselib/ipOps.lua | 29 ++ nselib/ospf.lua | 267 +++++++++++++++-- scripts/broadcast-ospf2-discover.nse | 431 +++++++++++++++++++++++++++ scripts/script.db | 1 + 5 files changed, 714 insertions(+), 17 deletions(-) create mode 100644 scripts/broadcast-ospf2-discover.nse diff --git a/CHANGELOG b/CHANGELOG index d810734ad..32d05f8cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#743] New script broadcast-ospf2-discover discovers OSPF 2 routers + and neighbors. OSPFv2 authentication is supported. [Emiliano Ticci] + o [NSE][GH#740] New script http-vuln-cve2017-5638 checks for the RCE bug in Apache Struts. [Seth Jackson] diff --git a/nselib/ipOps.lua b/nselib/ipOps.lua index 54c30b7e8..0ec3bfefa 100644 --- a/nselib/ipOps.lua +++ b/nselib/ipOps.lua @@ -3,6 +3,7 @@ -- -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html +local math = require "math" local stdnse = require "stdnse" local string = require "string" local table = require "table" @@ -674,6 +675,32 @@ hex_to_bin = function( hex ) return status, result end +--- +-- Convert a CIDR subnet mask to dotted decimal notation. +-- +-- @param subnet CIDR string representing the subnet mask. +-- @usage +-- local netmask = ipOps.cidr_to_subnet( "/16" ) +-- @return Dotted decimal representation of the suppliet subnet mask (e.g. "255.255.0.0") +cidr_to_subnet = function( subnet ) + local bits = subnet:match("/(%d%d)$") + if not bits then return nil end + return fromdword((0xFFFFFFFF >> tonumber(bits)) ~ 0xFFFFFFFF) +end + +--- +-- Convert a dotted decimal subnet mask to CIDR notation. +-- +-- @param subnet Dotted decimal string representing the subnet mask. +-- @usage +-- local cidr = ipOps.subnet_to_cidr( "255.255.0.0" ) +-- @return CIDR representation of the supplied subnet mask (e.g. "/16"). +subnet_to_cidr = function( subnet ) + local dword, err = todword(subnet) + if not dword then return nil, err end + return "/" .. tostring(32 - (math.tointeger(math.log((dword ~ 0xFFFFFFFF) + 1, 2)))) +end + --Ignore the rest if we are not testing. if not unittest.testing() then return _ENV @@ -801,5 +828,7 @@ do test_suite:add_test(unittest.is_nil(expand_ip("2001:db8::1", "ipv4")), "IPv6 to IPv4") end +test_suite:add_test(unittest.equal(cidr_to_subnet("/16"), "255.255.0.0"), "cidr_to_subnet") +test_suite:add_test(unittest.equal(subnet_to_cidr("255.255.0.0"), "/16"), "subnet_to_cidr") return _ENV; diff --git a/nselib/ospf.lua b/nselib/ospf.lua index d8da41990..69213f98b 100644 --- a/nselib/ospf.lua +++ b/nselib/ospf.lua @@ -1,21 +1,25 @@ --- --- A minimalistic OSPF (Open Shortest Path First routing protocol) library, currently supporting IPv4 and the following --- OSPF message types: HELLO +-- A limited OSPF (Open Shortest Path First routing protocol) library, currently supporting IPv4 and the following +-- OSPF message types: HELLO, DB_DESCRIPTION, LS_REQUEST, LS_UPDATE -- -- The library consists of an OSPF class that contains code to handle OSPFv2 packets. -- -- @author Patrik Karlsson +-- @author Emiliano Ticci -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html local bin = require "bin" local bit = require "bit" local math = require "math" local stdnse = require "stdnse" +local string = require "string" local table = require "table" local ipOps = require "ipOps" local packet = require "packet" _ENV = stdnse.module("ospf", stdnse.seeall) +local have_ssl, openssl = pcall(require, "openssl") + -- The OSPF class. OSPF = { @@ -23,13 +27,10 @@ OSPF = { Message = { HELLO = 1, DB_DESCRIPTION = 2, + LS_REQUEST = 3, LS_UPDATE = 4, }, - LSUpdate = { - - }, - Header = { size = 24, new = function(self, type, area_id, router_id, auth_type, auth_data) @@ -68,7 +69,7 @@ OSPF = { local _ _, header.auth_data.keyid = bin.unpack(">C", data, pos+2) _, header.auth_data.length = bin.unpack(">C", data, pos+3) - _, header.auth_data.seq = bin.unpack(">C", data, pos+4) + _, header.auth_data.seq = bin.unpack(">I", data, pos+4) _, header.auth_data.hash = bin.unpack(">H"..header.auth_data.length, data, header.length+1) else -- Shouldn't happen @@ -102,9 +103,9 @@ OSPF = { if self.auth_type == 0x00 then auth = bin.pack(">L", 0x00) elseif self.auth_type == 0x01 then - auth = bin.pack(">A8", self.auth_data.password) + auth = bin.pack(">A", self.auth_data.password) elseif self.auth_type == 0x02 then - auth = bin.pack(">A".. self.auth_data.length, self.auth_data.hash) + auth = bin.pack(">SCCI", 0, self.auth_data.keyid, self.auth_data.length, self.auth_data.seq) end local hdr = bin.pack(">CCS", self.ver, self.type, self.length ) .. bin.pack(">IISS", ipOps.todword(self.router_id), self.area_id, self.chksum, self.auth_type) @@ -169,10 +170,20 @@ OSPF = { data = data .. bin.pack(">I", ipOps.todword(n)) end self.header:setLength(#data) + if self.header.auth_data.hash then + data = data .. self.header.auth_data.hash + end return tostring(self.header) .. data end local data = tostr() - self.header.chksum = packet.in_cksum(data:sub(1,12) .. data:sub(25)) + if have_ssl and self.header.auth_type == 0x02 then + while string.len(self.header.auth_data.key) < 16 do + self.header.auth_data.key = self.header.auth_data.key .. "\0" + end + self.header.auth_data.hash = openssl.md5(data .. bin.pack(">A", self.header.auth_data.key)) + else + self.header.chksum = packet.in_cksum(data:sub(1,16) .. data:sub(25)) + end return tostr() end, @@ -208,10 +219,9 @@ OSPF = { }, - DBDescription = { - - LSAHeader = { - + LSA = { + Header = { + size = 20, new = function(self) local o = { age = 0, @@ -228,8 +238,113 @@ OSPF = { return o end, + parse = function(data) + local lsa_h = OSPF.LSA.Header:new() + local pos = 1 + pos, lsa_h.age, lsa_h.options, lsa_h.type, lsa_h.id, lsa_h.adv_router, lsa_h.sequence, lsa_h.checksum, lsa_h.length = bin.unpack(">SCCIIH4H2S", data, pos) + + lsa_h.id = ipOps.fromdword(lsa_h.id) + lsa_h.adv_router = ipOps.fromdword(lsa_h.adv_router) + return lsa_h + end, + }, + Link = { + new = function(self) + local o = { + id = 0, + data = 0, + type = 2, + num_metrics = 0, + metric = 10, + } + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local lsa_link = OSPF.LSA.Link:new() + local pos = 1 + pos, lsa_link.id, lsa_link.data, lsa_link.type, lsa_link.num_metrics, lsa_link.metric = bin.unpack(">IICCS", data, pos) + lsa_link.id = ipOps.fromdword(lsa_link.id) + lsa_link.data = ipOps.fromdword(lsa_link.data) + return lsa_link + end, + }, + + Router = { + new = function(self) + local o = { + header = OSPF.LSA.Header:new(), + flags = 0, + num_links = 0, + links = {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local router = OSPF.LSA.Router:new() + local pos = OSPF.LSA.Header.size + 1 + router.header = OSPF.LSA.Header.parse(data) + pos, router.flags, router.num_links = bin.unpack(">CxS", data, pos) + + while ( pos < router.header.length ) do + table.insert(router.links, OSPF.LSA.Link.parse(data:sub(pos, pos + 12))) + pos = pos + 12 + end + + return router + end, + }, + + ASExternal = { + new = function(self) + local o = { + header = OSPF.LSA.Header:new(), + netmask = 0, + ext_type = 1, + metric = 1, + fw_address = 0, + ext_tag = 0, + } + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local as_ext = OSPF.LSA.ASExternal:new() + local pos = OSPF.LSA.Header.size + 1 + as_ext.header = OSPF.LSA.Header.parse(data) + + pos, as_ext.netmask, as_ext.metric, as_ext.fw_address, as_ext.ext_tag = bin.unpack(">IIII", data, pos) + as_ext.netmask = ipOps.fromdword(as_ext.netmask) + as_ext.ext_type = 1 + bit.rshift(bit.band(as_ext.metric, 0xFF000000), 31) + as_ext.metric = bit.band(as_ext.metric, 0x00FFFFFF) + as_ext.fw_address = ipOps.fromdword(as_ext.fw_address) + + return as_ext + end, + }, + + parse = function(data) + local header = OSPF.LSA.Header.parse(data) + if header.type == 1 then + return OSPF.LSA.Router.parse(data) + elseif header.type == 5 then + return OSPF.LSA.ASExternal.parse(data) + end + return header.length + end, + }, + + DBDescription = { + new = function(self) local o = { header = OSPF.Header:new(OSPF.Message.DB_DESCRIPTION), @@ -238,7 +353,8 @@ OSPF = { init = true, more = true, master = true, - sequence = math.random(123456789) + sequence = math.random(123456789), + lsa_headers = {} } setmetatable(o, self) self.__index = self @@ -254,10 +370,20 @@ OSPF = { local data = bin.pack(">SCCI", self.mtu, self.options, flags, self.sequence) self.header:setLength(#data) + if self.header.auth_data.hash then + data = data .. self.header.auth_data.hash + end return tostring(self.header) .. data end local data = tostr() - self.header.chksum = packet.in_cksum(data:sub(1,12) .. data:sub(25)) + if have_ssl and self.header.auth_type == 0x02 then + while string.len(self.header.auth_data.key) < 16 do + self.header.auth_data.key = self.header.auth_data.key .. "\0" + end + self.header.auth_data.hash = openssl.md5(data .. bin.pack(">A", self.header.auth_data.key)) + else + self.header.chksum = packet.in_cksum(data:sub(1,16) .. data:sub(25)) + end return tostr() end, @@ -265,7 +391,7 @@ OSPF = { local desc = OSPF.DBDescription:new() local pos = OSPF.Header.size + 1 desc.header = OSPF.Header.parse(data) - assert( #data == desc.header.length, "OSPF packet too short") + assert( #data >= desc.header.length, "OSPF packet too short") local flags = 0 pos, desc.mtu, desc.options, flags, desc.sequence = bin.unpack(">SCCI", data, pos) @@ -274,6 +400,11 @@ OSPF = { desc.more = ( bit.band(flags, 2) == 2 ) desc.master = ( bit.band(flags, 1) == 1 ) + while ( pos < desc.header.length ) do + table.insert(desc.lsa_headers, OSPF.LSA.Header.parse(data:sub(pos, pos + 20))) + pos = pos + 20 + end + if ( desc.init or not(desc.more) ) then return desc end @@ -283,6 +414,104 @@ OSPF = { }, + LSRequest = { + new = function(self) + local o = { + header = OSPF.Header:new(OSPF.Message.LS_REQUEST), + ls_requests = {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + --- Adds a request to the list of requests. + -- @param type LS Type. + -- @param id Link State ID + -- @param adv_router Advertising Router + addRequest = function(self, type, id, adv_router) + local request = { + type = type, + id = id, + adv_router = adv_router + } + table.insert(self.ls_requests, request) + end, + + __tostring = function(self) + local function tostr() + local data = "" + for _, req in ipairs(self.ls_requests) do + data = data .. bin.pack(">III", req.type, ipOps.todword(req.id), ipOps.todword(req.adv_router)) + end + self.header:setLength(#data) + if self.header.auth_data.hash then + data = data .. self.header.auth_data.hash + end + return tostring(self.header) .. data + end + local data = tostr() + if have_ssl and self.header.auth_type == 0x02 then + while string.len(self.header.auth_data.key) < 16 do + self.header.auth_data.key = self.header.auth_data.key .. "\0" + end + self.header.auth_data.hash = openssl.md5(data .. bin.pack(">A", self.header.auth_data.key)) + else + self.header.chksum = packet.in_cksum(data:sub(1,16) .. data:sub(25)) + end + return tostr() + end, + + parse = function(data) + local ls_req = OSPF.LSRequest:new() + local pos = OSPF.Header.size + 1 + ls_req.header = OSPF.Header.parse(data) + assert( #data >= ls_req.header.length, "OSPF packet too short") + + while ( pos < #data ) do + local req = {} + pos, req.type, req.id, req.adv_router = bin.unpack(">III", data, pos) + table.insert(ls_req.ls_requests, req) + end + + return ls_req + end, + }, + + LSUpdate = { + new = function(self) + local o = { + header = OSPF.Header:new(OSPF.Message.LS_UPDATE), + num_lsas = 0, + lsas = {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local lsu = OSPF.LSUpdate:new() + local pos = OSPF.Header.size + 1 + lsu.header = OSPF.Header.parse(data) + assert( #data >= lsu.header.length, "OSPF packet too short") + + pos, lsu.num_lsas = bin.unpack(">I", data, pos) + + while ( pos < lsu.header.length ) do + local lsa = OSPF.LSA.parse(data:sub(pos)) + if ( type(lsa) == "table" ) then + table.insert(lsu.lsas, lsa) + pos = pos + lsa.header.length + else + pos = pos + lsa + end + end + + return lsu + end, + }, + Response = { parse = function(data) @@ -291,6 +520,10 @@ OSPF = { return OSPF.Hello.parse( data ) elseif( ospf_type == OSPF.Message.DB_DESCRIPTION ) then return OSPF.DBDescription.parse(data) + elseif( ospf_type == OSPF.Message.LS_REQUEST ) then + return OSPF.LSRequest.parse(data) + elseif( ospf_type == OSPF.Message.LS_UPDATE ) then + return OSPF.LSUpdate.parse(data) end return end, diff --git a/scripts/broadcast-ospf2-discover.nse b/scripts/broadcast-ospf2-discover.nse new file mode 100644 index 000000000..62b246efe --- /dev/null +++ b/scripts/broadcast-ospf2-discover.nse @@ -0,0 +1,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 http://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 diff --git a/scripts/script.db b/scripts/script.db index 15181094a..9b5f326eb 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -39,6 +39,7 @@ Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "broadcast", Entry { filename = "broadcast-netbios-master-browser.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-networker-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-novell-locate.nse", categories = { "broadcast", "safe", } } +Entry { filename = "broadcast-ospf2-discover.nse", categories = { "broadcast", "discovery", "safe", } } Entry { filename = "broadcast-pc-anywhere.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-pc-duo.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-pim-discovery.nse", categories = { "broadcast", "discovery", "safe", } }