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
|
local coroutine = require "coroutine"
local dhcp = require "dhcp"
local ipOps = require "ipOps"
local math = require "math"
local nmap = require "nmap"
local outlib = require "outlib"
local packet = require "packet"
local rand = require "rand"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Sends a DHCP request to the broadcast address (255.255.255.255) and reports
the results. By default, the script uses a static MAC address
(DE:AD:CO:DE:CA:FE) in order to prevent IP pool exhaustion.
The script reads the response using pcap by opening a listening pcap socket
on all available ethernet interfaces that are reported up. If no response
has been received before the timeout has been reached (default 10 seconds)
the script will abort execution.
The script needs to be run as a privileged user, typically root.
]]
---
-- @see broadcast-dhcp6-discover.nse
-- @see dhcp-discover.nse
--
-- @usage
-- sudo nmap --script broadcast-dhcp-discover
--
-- @output
-- | broadcast-dhcp-discover:
-- | Response 1 of 1:
-- | Interface: wlp1s0
-- | IP Offered: 192.168.1.114
-- | DHCP Message Type: DHCPOFFER
-- | Server Identifier: 192.168.1.1
-- | IP Address Lease Time: 1 day, 0:00:00
-- | Subnet Mask: 255.255.255.0
-- | Router: 192.168.1.1
-- | Domain Name Server: 192.168.1.1
-- |_ Domain Name: localdomain
--
-- @xmloutput
-- <table key="Response 1 of 1:">
-- <elem key="Interface">wlp1s0</elem>
-- <elem key="IP Offered">192.168.1.114</elem>
-- <elem key="DHCP Message Type">DHCPOFFER</elem>
-- <elem key="Server Identifier">192.168.1.1</elem>
-- <elem key="IP Address Lease Time">1 day, 0:00:00</elem>
-- <elem key="Subnet Mask">255.255.255.0</elem>
-- <elem key="Router">192.168.1.1</elem>
-- <elem key="Domain Name Server">192.168.1.1</elem>
-- <elem key="Domain Name">localdomain</elem>
-- </table>
--
-- @args broadcast-dhcp-discover.mac Set to <code>random</code> or a specific
-- client MAC address in the DHCP request. "DE:AD:C0:DE:CA:FE"
-- is used by default. Setting it to <code>random</code> will
-- possibly cause the DHCP server to reserve a new IP address
-- each time.
-- @args broadcast-dhcp-discover.clientid Client identifier to use in DHCP
-- option 61. The value is a string, while hardware type 0, appropriate
-- for FQDNs, is assumed. Example: clientid=kurtz is equivalent to
-- specifying clientid-hex=00:6b:75:72:74:7a (see below).
-- @args broadcast-dhcp-discover.clientid-hex Client identifier to use in DHCP
-- option 61. The value is a hexadecimal string, where the first octet
-- is the hardware type.
-- @args broadcast-dhcp-discover.timeout time in seconds to wait for a response
-- (default: 10s)
--
-- Created 04/22/2022 - v0.3 - updated by nnposter
-- o Implemented script arguments "clientid" and "clientid-hex" to allow
-- passing a specific client identifier (option 61)
--
-- Created 01/14/2020 - v0.2 - updated by nnposter
-- o Implemented script argument "mac" to force a specific MAC address
--
-- Created 07/14/2011 - v0.1 - created by Patrik Karlsson
author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"broadcast", "safe"}
prerule = function()
if not nmap.is_privileged() then
stdnse.verbose1("not running for lack of privileges.")
return false
end
if nmap.address_family() ~= 'inet' then
stdnse.debug1("is IPv4 compatible only.")
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
-- Listens for an incoming dhcp response
--
-- @param iface string with the name of the interface to listen to
-- @param macaddr client hardware address
-- @param options DHCP options to include in the request
-- @param timeout number of ms to wait for a response
-- @param xid the DHCP transaction id
-- @param result a table to which the result is written
local function dhcp_listener(sock, iface, macaddr, options, timeout, xid, result)
local condvar = nmap.condvar(result)
local srcip = ipOps.ip_to_str("0.0.0.0")
local dstip = ipOps.ip_to_str("255.255.255.255")
-- Build DHCP request
local status, pkt = dhcp.dhcp_build(
dhcp.request_types.DHCPDISCOVER,
srcip,
macaddr,
options,
nil, -- request options
{flags=0x8000}, -- override: broadcast
nil, -- lease time
xid)
if not status then
stdnse.debug1("Failed to build packet for %s: %s", iface, pkt)
condvar "signal"
return
end
-- Add UDP header
local udplen = #pkt + 8
local tmp = string.pack(">c4c4 xBI2 I2I2I2xx",
srcip, dstip,
packet.IPPROTO_UDP, udplen,
68, 67, udplen) .. pkt
pkt = string.pack(">I2 I2 I2 I2", 68, 67, udplen, packet.in_cksum(tmp)) .. pkt
-- Create a frame and add the IP header
local frame = packet.Frame:new()
frame:build_ip_packet(srcip, dstip, pkt, nil, --dsf
string.unpack(">I2", xid, 3), -- IPID, use 16 lsb of xid
nil, nil, nil, -- flags, offset, ttl
packet.IPPROTO_UDP)
-- Add the Ethernet header
frame:build_ether_frame(
"\xff\xff\xff\xff\xff\xff",
nmap.get_interface_info(iface).mac, -- can't use macaddr or we won't see response
packet.ETHER_TYPE_IPV4)
local dnet = nmap.new_dnet()
dnet:ethernet_open(iface)
local status, err = dnet:ethernet_send(frame.frame_buf)
dnet:ethernet_close()
if not status then
stdnse.debug1("Failed to send frame for %s: %s", iface, err)
condvar "signal"
return
end
local start_time = nmap.clock_ms()
local now = start_time
while( now - start_time < timeout ) do
sock:set_timeout(timeout - (now - start_time))
local status, _, _, data = sock:pcap_receive()
if ( status ) then
local p = packet.Packet:new( data, #data )
if ( p and p.udp_dport ) then
local data = data:sub(p.udp_offset + 9)
local status, response = dhcp.dhcp_parse(data, xid)
if ( status ) then
response.iface = iface
table.insert( result, response )
end
end
end
now = nmap.clock_ms()
end
sock:close()
condvar "signal"
end
local function fail (err) return stdnse.format_output(false, err) end
action = function()
local timeout = stdnse.parse_timespec(stdnse.get_script_args("broadcast-dhcp-discover.timeout"))
timeout = (timeout or 10) * 1000
local options = {}
local macaddr = (stdnse.get_script_args(SCRIPT_NAME .. ".mac") or "DE:AD:C0:DE:CA:FE"):lower()
if macaddr:find("^ra?nd") then
macaddr = rand.random_string(6)
else
macaddr = macaddr:gsub(":", "")
if not (#macaddr == 12 and macaddr:find("^%x+$")) then
return stdnse.format_output(false, "Invalid MAC address")
end
macaddr = stdnse.fromhex(macaddr)
end
local clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid")
if clientid then
clientid = "\x00" .. clientid -- hardware type 0 presumed
else
clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid-hex")
if clientid then
clientid = clientid:gsub(":", "")
if not clientid:find("^%x+$") then
return stdnse.format_output(false, "Invalid hexadecimal client ID")
end
clientid = stdnse.fromhex(clientid)
end
end
if clientid then
if #clientid == 0 or #clientid > 255 then
return stdnse.format_output(false, "Client ID must be between 1 and 255 characters long")
end
table.insert(options, {number = 61, type = "string", value = clientid })
end
local interfaces
-- first check if the user supplied an interface
if ( nmap.get_interface() ) then
interfaces = { [nmap.get_interface()] = true }
else
-- As the response will be sent to the "offered" ip address we need
-- to use pcap to pick it up. However, we don't know what interface
-- our packet went out on, so lets get a list of all interfaces and
-- run pcap on all of them, if they're a) up and b) ethernet.
interfaces = getInterfaces("ethernet", "up")
end
if( not(interfaces) ) then return fail("Failed to retrieve interfaces (try setting one explicitly using -e)") end
local transaction_id = math.random(0, 0x7F000000)
local threads = {}
local result = {}
local condvar = nmap.condvar(result)
-- start a listening thread for each interface
for iface, _ in pairs(interfaces) do
transaction_id = transaction_id + 1
local xid = string.pack(">I4", transaction_id)
local sock, co
sock = nmap.new_socket()
sock:pcap_open(iface, 1500, true, "ip && udp dst port 68")
co = stdnse.new_thread( dhcp_listener, sock, iface, macaddr, options, timeout, xid, result )
threads[co] = true
end
-- wait until all threads are done
repeat
for thread in pairs(threads) do
if coroutine.status(thread) == "dead" then threads[thread] = nil end
end
if ( next(threads) ) then
condvar "wait"
end
until next(threads) == nil
if not next(result) then
return nil
end
local response = stdnse.output_table()
-- Display the results
for i, r in ipairs(result) do
local result_table = stdnse.output_table()
result_table["Interface"] = r.iface
result_table["IP Offered"] = r.yiaddr_str
for _, v in ipairs(r.options) do
if(type(v.value) == 'table') then
outlib.list_sep(v.value)
end
result_table[ v.name ] = v.value
end
response[string.format("Response %d of %d", i, #result)] = result_table
end
return response
end
|