1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-11 02:09:03 +00:00

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]
This commit is contained in:
patrik
2012-01-24 19:54:50 +00:00
parent 055ff59b94
commit 5ef9f2a70d
4 changed files with 733 additions and 0 deletions

View File

@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*- # 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 o IPv6 OS detection now includes a novelty detection phase that avoids
printing a match when an observed fingerprint is too different from printing a match when an observed fingerprint is too different from
fingerprints seen before. As the OS database is still small, this fingerprints seen before. As the OS database is still small, this

617
nselib/dhcp6.lua Normal file
View File

@@ -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:
-- <code>
-- local helper = DHCP6.Helper:new("eth0")
-- local status, response = helper:solicit()
-- if ( status ) then
-- return stdnse.format_output(true, response)
-- end
-- </code>
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
--
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
-- <code>timeout</code> - 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
-- <code>stdnse.format_output</code>
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,
}

View File

@@ -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

View File

@@ -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-avahi-dos.nse", categories = { "broadcast", "dos", "intrusive", "vuln", } }
Entry { filename = "broadcast-db2-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-db2-discover.nse", categories = { "broadcast", "safe", } }
Entry { filename = "broadcast-dhcp-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-dns-service-discovery.nse", categories = { "broadcast", "safe", } }
Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } }
Entry { filename = "broadcast-listener.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-listener.nse", categories = { "broadcast", "safe", } }