From 5ef9f2a70d9e52a9f55888948585559b584f0cba Mon Sep 17 00:00:00 2001 From: patrik Date: Tue, 24 Jan 2012 19:54:50 +0000 Subject: [PATCH] o [NSE] Added script broadcast-dhcp6-discover and supporting DHCPv6 library. The script retrieves and prints an IPv6 address and some of the DHCP6 options. [Patrik] --- CHANGELOG | 4 + nselib/dhcp6.lua | 617 +++++++++++++++++++++++++++ scripts/broadcast-dhcp6-discover.nse | 111 +++++ scripts/script.db | 1 + 4 files changed, 733 insertions(+) create mode 100644 nselib/dhcp6.lua create mode 100644 scripts/broadcast-dhcp6-discover.nse diff --git a/CHANGELOG b/CHANGELOG index 230a65c3a..1a8135451 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added script broadcast-dhcp6-discover and supporting DHCPv6 library. + The script retrieves and prints an IPv6 address and some of the DHCP6 + options. [Patrik] + o IPv6 OS detection now includes a novelty detection phase that avoids printing a match when an observed fingerprint is too different from fingerprints seen before. As the OS database is still small, this diff --git a/nselib/dhcp6.lua b/nselib/dhcp6.lua new file mode 100644 index 000000000..ef2bab078 --- /dev/null +++ b/nselib/dhcp6.lua @@ -0,0 +1,617 @@ +--- +-- Minimalistic DHCP6 implementation supporting basic DHCP6 Solicit requests +-- The library is structured around the following classes: +-- * DHCP6.Option - DHCP6 options encoders (for requests) and decoders +-- (for responses) +-- * DHCP6.Request - DHCP6 request encoder and decoder +-- * DHCP6.Response - DHCP6 response encoder and decoder +-- * Helper - The helper class, primary script interface +-- +-- The following sample code sends a DHCP6 Solicit request and returns a +-- response suitable for script output: +-- +-- local helper = DHCP6.Helper:new("eth0") +-- local status, response = helper:solicit() +-- if ( status ) then +-- return stdnse.format_output(true, response) +-- end +-- +-- +-- @author "Patrik Karlsson " +-- +module(... or "dhcp6", package.seeall) + +require 'bin' +require 'bit' +require 'ipOps' + +DHCP6 = {} + +-- DHCP6 request and response types +DHCP6.Type = { + SOLICIT = 1, + ADVERTISE = 2, + REQUEST = 3, +} + +-- DHCP6 type as string +DHCP6.TypeStr = { + [DHCP6.Type.SOLICIT] = "Solicit", + [DHCP6.Type.ADVERTISE] = "Advertise", + [DHCP6.Type.REQUEST] = "Request", +} + +-- DHCP6 option types +DHCP6.OptionTypes = { + OPTION_CLIENTID = 0x01, + OPTION_SERVERID = 0x02, + OPTION_IA_NA = 0x03, + OPTION_IAADDR = 0x05, + OPTION_ELAPSED_TIME = 0x08, + OPTION_STATUS_CODE = 0x0d, + OPTION_DNS_SERVERS = 0x17, + OPTION_DOMAIN_LIST = 0x18, + OPTION_IA_PD = 0x19, + OPTION_SNTP_SERVERS = 0x1f, + OPTION_CLIENT_FQDN = 0x27, +} + +-- DHCP6 options +DHCP6.Option = { + + [DHCP6.OptionTypes.OPTION_ELAPSED_TIME] = { + + -- Create a new class instance + -- @param time in ms since last request + -- @return o new instance of class + new = function(self, time) + local o = { + type = DHCP6.OptionTypes.OPTION_ELAPSED_TIME, + time = time, + -- in case no time was created, we need this to be able to + -- calculate time since instantiation + created = os.time(), + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local data + if ( self.time ) then + data = bin.pack(">S", self.time) + else + data = bin.pack(">S", (os.time() - self.created) * 1000) + end + return bin.pack(">SP", self.type, data) + end, + + }, + + [DHCP6.OptionTypes.OPTION_CLIENTID] = { + + -- Create a new class instance + -- @param mac string containing the mac address + -- @param duid number the duid of the client + -- @param hwtype number the hwtype of the client + -- @param time number time since 2000-01-01 00:00:00 + -- @return o new instance of class + new = function(self, mac, duid, hwtype, time) + local o = { + type = DHCP6.OptionTypes.OPTION_CLIENTID, + duid = duid or 1, + hwtype = hwtype or 1, + time = time or os.time() - os.time({year=2000, day=1, month=1, hour=0, min=0, sec=0}), + mac = mac, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID]:new() + local pos + pos, opt.duid = bin.unpack(">S", data, pos) + if ( 1 ~= opt.duid ) then + stdnse.print_debug("Unexpected DUID type (%d)", opt.duid) + return + end + pos, opt.hwtype, opt.time, opt.mac = bin.unpack(">SIA" .. (#data - pos - 4 - 2 + 1), data, pos) + opt.time = opt.time + os.time({year=2000, day=1, month=1, hour=0, min=0, sec=0}) + return opt + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local data = bin.pack(">SSIA", self.duid, self.hwtype, self.time, self.mac) + return bin.pack(">SP", self.type, data) + end, + }, + + [DHCP6.OptionTypes.OPTION_SERVERID] = { + -- Create a new class instance + -- @param mac string containing the mac address + -- @param duid number the duid of the client + -- @param hwtype number the hwtype of the client + -- @param time number time since 2000-01-01 00:00:00 + -- @return o new instance of class + new = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].new(...) end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].parse(...) end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].__tostring(...) end, + }, + + [DHCP6.OptionTypes.OPTION_STATUS_CODE] = { + + -- Create a new class instance + -- @param code number containing the error code + -- @param msg string containing the error message + -- @return o new instance of class + new = function(self, code, msg) + local o = { + type = DHCP6.OptionTypes.OPTION_STATUS_CODE, + code = code, + msg = msg, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_STATUS_CODE]:new() + local pos + + pos, opt.code, opt.msg = bin.unpack(">SA" .. (#data - 2), data) + return opt + end, + + }, + + [DHCP6.OptionTypes.OPTION_DNS_SERVERS] = { + + -- Create a new class instance + -- @param servers table containing DNS servers + -- @return o new instance of class + new = function(self, servers) + local o = { + type = DHCP6.OptionTypes.OPTION_DNS_SERVERS, + servers = servers or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_DNS_SERVERS]:new() + local pos, count = 1, #data/16 + + for i=1,count do + local srv + pos, srv = bin.unpack(">B16", data, pos) + table.insert(opt.servers, srv) + end + return opt + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local len = #self.servers * 16 + local data= bin.pack(">SS", self.type, self.len) + for _, ipv6 in ipairs(self.servers) do + data = data .. ipOps.ip_to_str(ipv6) + end + return data + end + }, + + [DHCP6.OptionTypes.OPTION_DOMAIN_LIST] = { + + -- Create a new class instance + -- @param domain table containing the search domains + -- @return o new instance of class + new = function(self, domains) + local o = { + type = DHCP6.OptionTypes.OPTION_DOMAIN_LIST, + domains = domains or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_DOMAIN_LIST]:new() + local pos = 1 + + repeat + local domain = {} + repeat + local part + pos, part = bin.unpack("p", data, pos) + if ( part ~= "" ) then + table.insert(domain, part) + end + until( part == "" ) + table.insert(opt.domains, stdnse.strjoin(".", domain)) + until( pos > #data ) + return opt + end, + + + }, + + [DHCP6.OptionTypes.OPTION_IA_PD] = { + + -- Create a new class instance + -- @param iad number containing iad + -- @param t1 number containing t1 + -- @param t2 number containing t2 + -- @param option string containing any options + -- @return o new instance of class + new = function(self, iaid, t1, t2, options) + local o = { + type = DHCP6.OptionTypes.OPTION_IA_PD, + iaid = iaid, + t1 = t1 or 0, + t2 = t2 or 0, + options = options or "", + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local data = bin.pack(">IIIA", self.iaid, self.t1, self.t2, self.options) + return bin.pack(">SP", self.type, data) + end, + + }, + + [DHCP6.OptionTypes.OPTION_IA_NA] = { + + -- Create a new class instance + -- @param iad number containing iad + -- @param t1 number containing t1 + -- @param t2 number containing t2 + -- @param option table containing any options + -- @return o new instance of class + new = function(self, iaid, t1, t2, options) + local o = { + type = DHCP6.OptionTypes.OPTION_IA_NA, + iaid = iaid, + t1 = t1 or 0, + t2 = t2 or 0, + options = options or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_IA_NA]:new() + local pos + + pos, opt.iaid, opt.t1, opt.t2 = bin.unpack(">III", data) + + -- do we have any options + while ( pos < #data ) do + local typ, len, ipv6, pref_lt, valid_lt, options + pos, typ, len = bin.unpack(">SS", data, pos) + + if ( 5 == DHCP6.OptionTypes.OPTION_IAADDR ) then + local addr = { type = DHCP6.OptionTypes.OPTION_IAADDR } + pos, addr.ipv6, addr.pref_lt, addr.valid_lt = bin.unpack(">A16II", data, pos) + table.insert(opt.options, addr) + else + pos = pos + len + end + end + return opt + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local data = bin.pack(">III", self.iaid, self.t1, self.t2) + + -- TODO: we don't cover self.options here, we should probably add that + return bin.pack(">SP", self.type, data) + end, + }, + + [DHCP6.OptionTypes.OPTION_SNTP_SERVERS] = { + + -- Create a new class instance + -- @param servers table containing the NTP servers + -- @return o new instance of class + new = function(self, servers) + local o = { + type = DHCP6.OptionTypes.OPTION_SNTP_SERVERS, + servers = servers or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_SNTP_SERVERS]:new() + local pos, server = 1 + + repeat + pos, server = bin.unpack(">B16", data, pos) + table.insert( opt.servers, ipOps.bin_to_ip(server) ) + until( pos > #data ) + return opt + end, + }, + +} + + +DHCP6.Request = { + + -- Create a new class instance + -- @param msgtype number containing the message type + -- @param xid number containing the transaction id + -- @param opts table containing any request options + -- @return o new instance of class + new = function(self, msgtype, xid, opts) + local o = { + type = msgtype, + xid = xid or math.random(1048575), + opts = opts or {} + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Adds a new DHCP6 option to the request + -- @param opt instance of object to add to the request + addOption = function(self, opt) + table.insert(self.opts, opt) + end, + + -- Converts option to a string + -- @return str string containing the class instance as string + __tostring = function(self) + local tmp = bit.lshift(self.type, 24) + self.xid + local data = "" + + for _, opt in ipairs(self.opts) do + data = data .. tostring(opt) + end + return bin.pack(">IA", tmp, data) + end, + +} + +-- The Response class handles responses from the server +DHCP6.Response = { + + -- Creates a new instance of the response class + -- @param msgtype number containing the type of DHCP6 message + -- @param xid number containing the transaction ID + new = function(self, msgtype, xid, opts) + local o = { + msgtype = msgtype, + xid = xid, + opts = opts or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parse the data string and create an instance of the class + -- @param data string containing the data as received over the socket + -- @return opt new instance of option + parse = function(data) + local resp = DHCP6.Response:new() + local pos, tmp = bin.unpack(">I", data) + + resp.msgtype = bit.band(tmp, 0xFF000000) + resp.msgtype = bit.rshift(resp.msgtype, 24) + resp.xid = bit.band(tmp, 0x00FFFFFF) + while( pos < #data ) do + local opt = {} + pos, opt.type, opt.data = bin.unpack(">SP", data, pos) + if ( DHCP6.Option[opt.type] and DHCP6.Option[opt.type].parse ) then + local opt_parsed = DHCP6.Option[opt.type].parse(opt.data) + if ( not(opt_parsed) ) then + table.insert(resp.opts, { type = opt.type, raw = opt.data }) + else + table.insert(resp.opts, { type = opt.type, resp = opt_parsed }) + end + else + stdnse.print_debug(2, "No option decoder for type: %d; len: %d", opt.type, #(opt.data or "")) + table.insert(resp.opts, { type = opt.type, raw = opt.data }) + end + end + return resp + end + +} + +-- Table of option to string converters +-- Each option should have it's own function to convert an instance of option +-- to a printable string. +-- +-- TODO: These functions could eventually be moved to a method in it's +-- respective class. +OptionToString = { + + [DHCP6.OptionTypes.OPTION_CLIENTID] = function(opt) + local HWTYPE_ETHER = 1 + if ( HWTYPE_ETHER == opt.hwtype ) then + local mac = stdnse.tohex(opt.mac):upper() + mac = mac:gsub("..", "%1:"):sub(1, -2) + local tm = os.date("%Y-%m-%d %H:%M:%S", opt.time) + return "Client identifier", ("MAC: %s; Time: %s"):format(mac, tm) + end + end, + + [DHCP6.OptionTypes.OPTION_SERVERID] = function(opt) + local topic, str = OptionToString[DHCP6.OptionTypes.OPTION_CLIENTID](opt) + return "Server identifier", str + end, + + [DHCP6.OptionTypes.OPTION_IA_NA] = function(opt) + if ( opt.options and 1 == #opt.options ) then + local ipv6 = opt.options[1].ipv6 + ipv6 = select(2, bin.unpack("B" .. #ipv6, ipv6)) + ipv6 = ipOps.bin_to_ip(ipv6) + return "Non-temporary Address", ipv6 + end + end, + + [DHCP6.OptionTypes.OPTION_DNS_SERVERS] = function(opt) + local servers = {} + for _, srv in ipairs(opt.servers) do + local ipv6 = ipOps.bin_to_ip(srv) + table.insert(servers, ipv6) + end + return "DNS Servers", stdnse.strjoin(",", servers) + end, + + [DHCP6.OptionTypes.OPTION_DOMAIN_LIST] = function(opt) + return "Domain Search", stdnse.strjoin(", ", opt.domains) + end, + + [DHCP6.OptionTypes.OPTION_STATUS_CODE] = function(opt) + return "Error", ("Code: %d; Message: %s"):format(opt.code, opt.msg) + end, + + [DHCP6.OptionTypes.OPTION_SNTP_SERVERS] = function(opt) + return "NTP Servers", stdnse.strjoin(", ", opt.servers) + end, +} + +-- The Helper class serves as the main interface to scripts +Helper = { + + -- Creates a new Helper class instance + -- @param iface string containing the interface name + -- @param options table containing any options, currently + -- timeout - socket timeout in ms + -- @return o new instance of Helper + new = function(self, iface, options) + local o = { + iface = iface, + options = options or {}, + } + setmetatable(o, self) + self.__index = self + + local info, err = nmap.get_interface_info(iface) + -- if we faile to get interface info, don't return a helper + -- this is true on OS X for interfaces like: p2p0 and vboxnet0 + if ( not(info) and err ) then + return + end + o.mac = info.mac + o.socket = nmap.new_socket("udp") + o.socket:bind(nil, 546) + o.socket:set_timeout(o.options.timeout or 5000) + return o + end, + + -- Sends a DHCP6 Solicit message to the server, essentiall requesting a new + -- IPv6 non-temporary address + -- @return table of results suitable for use with + -- stdnse.format_output + solicit = function(self) + local req = DHCP6.Request:new( DHCP6.Type.SOLICIT ) + local option = DHCP6.Option + req:addOption(option[DHCP6.OptionTypes.OPTION_ELAPSED_TIME]:new()) + req:addOption(option[DHCP6.OptionTypes.OPTION_CLIENTID]:new(self.mac)) + + local iaid = select(2, bin.unpack(">I", self.mac:sub(3))) + req:addOption(option[DHCP6.OptionTypes.OPTION_IA_NA]:new(iaid, 3600, 5400)) + + self.host, self.port = { ip = "ff02::1:2" }, { number = 547, protocol = "udp"} + local status, err = self.socket:sendto( self.host, self.port, tostring(req) ) + if ( not(status) ) then + self.host.ip = ("%s%%%s"):format(self.host.ip, self.iface) + status, err = self.socket:sendto( self.host, self.port, tostring(req) ) + if ( not(status) ) then + return false, "Failed to send DHCP6 request to server" + end + end + + local resp, retries = {}, 3 + repeat + retries = retries - 1 + local status, data = self.socket:receive() + if ( not(status) ) then + return false, "Failed to receive DHCP6 request from server" + end + + resp = DHCP6.Response.parse(data) + if ( not(resp) ) then + return false, "Failed to decode DHCP6 response from server" + end + until( req.xid == resp.xid or retries == 0 ) + + if ( req.xid ~= resp.xid ) then + return false, "Failed to receive DHCP6 response from server" + end + + local result, result_options = {}, { name = "Options" } + local resptype = DHCP6.TypeStr[resp.msgtype] or ("Unknown (%d)"):format(resp.msgtype) + + table.insert(result, ("Message type: %s"):format(resptype)) + table.insert(result, ("Transaction id: %d"):format(resp.xid)) + + for _, opt in ipairs(resp.opts or {}) do + if ( OptionToString[opt.type] ) then + local topic, str = OptionToString[opt.type](opt.resp) + if ( topic and str ) then + table.insert(result_options, ("%s: %s"):format(topic, str)) + end + else + stdnse.print_debug(2, "No decoder for option type: %d", opt.type) + end + end + table.insert(result, result_options) + return true, result + end, +} + diff --git a/scripts/broadcast-dhcp6-discover.nse b/scripts/broadcast-dhcp6-discover.nse new file mode 100644 index 000000000..5e71ed8eb --- /dev/null +++ b/scripts/broadcast-dhcp6-discover.nse @@ -0,0 +1,111 @@ +description = [[ +Sends a DHCPv6 request (Solicit) to the DHCPv6 multicast address. It parses the +response and extracts the address along with any options returned by the +server. + +The script requires Nmap to be run in privileged mode as it binds the socket +to a privileged port (udp/546). +]] + +--- +-- @usage +-- nmap -6 --script broadcast-dhcp6-discover +-- +-- @output +-- | broadcast-dhcp6-discover: +-- | Interface: en0 +-- | Message type: Advertise +-- | Transaction id: 74401 +-- | Options +-- | Client identifier: MAC: 68:AB:CD:EF:AB:CD; Time: 2012-01-24 20:36:48 +-- | Server identifier: MAC: 08:FE:DC:BA:98:76; Time: 2012-01-20 11:44:58 +-- | Non-temporary Address: 2001:db8:1:2:0:0:0:1000 +-- | DNS Servers: 2001:db8:0:0:0:0:0:35 +-- | Domain Search: example.com, sub.example.com +-- |_ NTP Servers: 2001:db8:1111:0:0:0:0:123, 2001:db8:1111:0:0:0:0:124 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"broadcast", "safe"} + +require 'dhcp6' + +prerule = function() + if not nmap.is_privileged() then + stdnse.print_verbose("%s not running for lack of privileges.", SCRIPT_NAME) + return false + end + + if nmap.address_family() ~= 'inet6' then + stdnse.print_debug("%s is IPv6 compatible only.", SCRIPT_NAME) + return false + end + return true +end + +-- Gets a list of available interfaces based on link and up filters +-- +-- @param link string containing the link type to filter +-- @param up string containing the interface status to filter +-- @return result table containing the matching interfaces +local function getInterfaces(link, up) + if( not(nmap.list_interfaces) ) then return end + local interfaces, err = nmap.list_interfaces() + local result + if ( not(err) ) then + for _, iface in ipairs(interfaces) do + if ( iface.link == link and iface.up == up ) then + result = result or {} + result[iface.device] = true + end + end + end + return result +end + +local function solicit(iface, result) + local condvar = nmap.condvar(result) + local helper = dhcp6.Helper:new(iface) + if ( not(helper) ) then + condvar "signal" + return + end + + local status, response = helper:solicit() + if ( status ) then + response.name=("Interface: %s"):format(iface) + table.insert(result, response ) + end + condvar "signal" +end + +action = function(host, port) + + local iface = nmap.get_interface() + local ifs, result, threads = {}, {}, {} + local condvar = nmap.condvar(result) + + if ( iface ) then + ifs[iface] = true + else + ifs = getInterfaces("ethernet", "up") + end + + for iface in pairs(ifs) do + local co = stdnse.new_thread( solicit, iface, result ) + threads[co] = true + end + + -- wait until the probes are all done + repeat + condvar "wait" + for thread in pairs(threads) do + if coroutine.status(thread) == "dead" then + threads[thread] = nil + end + end + until next(threads) == nil + + return stdnse.format_output(true, result) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index db050f160..82111db09 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -18,6 +18,7 @@ Entry { filename = "bittorrent-discovery.nse", categories = { "discovery", "safe Entry { filename = "broadcast-avahi-dos.nse", categories = { "broadcast", "dos", "intrusive", "vuln", } } Entry { filename = "broadcast-db2-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dhcp-discover.nse", categories = { "broadcast", "safe", } } +Entry { filename = "broadcast-dhcp6-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-listener.nse", categories = { "broadcast", "safe", } }