summaryrefslogtreecommitdiffstats
path: root/scripts/broadcast-dhcp-discover.nse
blob: c2f8a9cb0ea59217346fead35523982cc80b0102 (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
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