diff --git a/CHANGELOG b/CHANGELOG
index c6f651ad7..daaa757b0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added the script broadcast-dhcp-discover that sends a DHCP discover
+ message to the broadcast address and collects and reports the network
+ information received from the DHCP server. [Patrik]
+
o [NSE] Added the script smtp-brute that performs brute force password
auditing against SMTP servers. [Patrik]
diff --git a/nselib/dhcp.lua b/nselib/dhcp.lua
index 034b1df0c..89a1efd0a 100644
--- a/nselib/dhcp.lua
+++ b/nselib/dhcp.lua
@@ -402,7 +402,31 @@ local function dhcp_send(interface, host, packet, transaction_id)
return true, data
end
-local function dhcp_build(request_type, ip_address, mac_address, request_options, overrides, lease_time, transaction_id)
+--- Builds a DHCP packet
+--
+--@param request_type The type of request as an integer (use the request_types table at the
+-- top of this file).
+--@param ip_address Your ip address (as a dotted-decimal string). This tells the DHCP server where to
+-- send the response. Setting it to "255.255.255.255" or "0.0.0.0" is generally acceptable (if not,
+-- host.ip_src can work).
+--@param mac_address Your mac address (as a string up to 16 bytes) where the server will send the response. Like
+-- ip_address, setting to the broadcast address (FF:FF:FF:FF:FF:FF) is
+-- common (host.mac_addr_src works).
+--@param request_options [optional] The options to request from the server, as an array of integers. For the
+-- acceptable options, see the actions table above or have a look at rfc2132.
+-- Some DHCP servers (such as my Linksys WRT54g) will ignore this list and send whichever
+-- information it wants. Default: all options marked as 'default' in the actions
+-- table above are requested (the typical interesting ones) if no verbosity is given.
+-- If any level of verbosity is on, get all types.
+--@param overrides [optional] A table of overrides. If a field in the table matches a field in the DHCP
+-- packet (see rfc2131 section 2 for a list of possible fields), the value in the table
+-- will be sent instead of the default value.
+--@param lease_time [optional] The lease time used when requestint an IP. Default: 1 second.
+--@param transaction_id The identity of the transaction.
+--
+--@return status (true or false)
+--@return The parsed response, as a table.
+function dhcp_build(request_type, ip_address, mac_address, request_options, overrides, lease_time, transaction_id)
local packet = ''
-- Set up the default overrides
@@ -455,7 +479,7 @@ end
--
--@param data The DHCP packet data. Any padding at the end of the packet will be ignored (by default,
-- DHCP packets are padded with \x00 bytes).
-local function dhcp_parse(data, transaction_id)
+function dhcp_parse(data, transaction_id)
local pos = 1
local result = {}
diff --git a/scripts/broadcast-dhcp-discover.nse b/scripts/broadcast-dhcp-discover.nse
new file mode 100644
index 000000000..20f4b8f82
--- /dev/null
+++ b/scripts/broadcast-dhcp-discover.nse
@@ -0,0 +1,197 @@
+description = [[
+Sends a DHCP request to the broadcast address (255.255.255.255) and reports
+the results. The script uses a static MAC address (DE:AD:CO:DE:CA:FE) while
+doing so in order to prevent scope 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.
+]]
+
+---
+-- @usage
+-- sudo nmap --script broadcast-dhcp-discover
+--
+-- @output
+-- | broadcast-dhcp-discover:
+-- | 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
+--
+-- @args broadcast-dhcp-discover.timeout time in seconds to wait for a response
+-- (default: 10s)
+--
+
+-- Version 0.1
+-- Created 07/14/2011 - v0.1 - created by Patrik Karlsson
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+prerule = function() return not( nmap.address_family() == "inet6") end
+
+require 'dhcp'
+require 'ipOps'
+require 'packet'
+
+-- Creates a random MAC address
+--
+-- @return mac_addr string containing a random MAC
+local function randomizeMAC()
+ mac_addr = ""
+ for j=1, 6 do
+ mac_addr = mac_addr .. string.char(math.random(1, 255))
+ end
+ return mac_addr
+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 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(iface, timeout, xid, result)
+ local sock = nmap.new_socket()
+ local condvar = nmap.condvar(result)
+
+ sock:set_timeout(100)
+ sock:pcap_open(iface, 1500, false, "ip && udp && port 68")
+
+ local start_time = nmap.clock_ms()
+ while( nmap.clock_ms() - start_time < timeout ) do
+ local status, _, _, data = sock:pcap_receive()
+ -- abort, once another thread has picked up our response
+ if ( #result > 0 ) then
+ sock:close()
+ condvar "signal"
+ return
+ end
+
+ 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
+ table.insert( result, response )
+ sock:close()
+ condvar "signal"
+ return
+ end
+ end
+ end
+ end
+ sock:close()
+ condvar "signal"
+end
+
+
+action = function()
+
+ if not nmap.is_privileged() then
+ return ("\n ERROR: %s needs to be run as a privileged user (root)."):format(SCRIPT_NAME)
+ end
+
+ local host, port = "255.255.255.255", 67
+ local timeout = stdnse.get_script_args("broadcast-dhcp-discover.timeout")
+ timeout = tonumber(timeout) or 10
+
+ -- convert from seconds to ms
+ timeout = timeout * 1000
+
+ -- randomizing the MAC could exhaust dhcp servers with small scopes
+ -- if ran multiple times, so we should probably refrain from doing
+ -- this?
+ local mac = string.char(0xDE,0xAD,0xC0,0xDE,0xCA,0xFE)--randomizeMAC()
+
+ 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 "\n ERROR: Failed to retrieve interfaces (try setting one explicitly using -e)" end
+
+ local transaction_id = bin.pack("I", ipOps.todword("0.0.0.0"))
+
+ -- we nead to set the flags to broadcast
+ local request_options, overrides, lease_time = nil, { flags = 0x8000 }, nil
+ local status, packet = dhcp.dhcp_build(request_type, ip_address, mac, request_options, overrides, lease_time, transaction_id)
+ if (not(status)) then return "\n ERROR: Failed to build packet" end
+
+ local socket = nmap.new_socket("udp")
+ socket:bind(nil, 68)
+ socket:sendto( host, port, packet )
+ socket:close()
+
+ local threads = {}
+ local result = {}
+ local condvar = nmap.condvar(result)
+
+ -- start a listening thread for each interface
+ for iface, _ in pairs(interfaces) do
+ local co = stdnse.new_thread( dhcp_listener, iface, timeout, transaction_id, result )
+ threads[co] = true
+ end
+
+ -- wait until all threads are 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
+
+ local response = {}
+ -- Display the results
+ for i, r in ipairs(result) do
+ table.insert(response, string.format("IP Offered: %s", r.yiaddr_str))
+ for _, v in ipairs(r.options) do
+ if(type(v['value']) == 'table') then
+ table.insert(response, string.format("%s: %s", v['name'], stdnse.strjoin(", ", v['value'])))
+ else
+ table.insert(response, string.format("%s: %s\n", v['name'], v['value']))
+ end
+ end
+ end
+ return stdnse.format_output(true, response)
+end
diff --git a/scripts/script.db b/scripts/script.db
index 2b7362ac2..07203b8f1 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -11,6 +11,7 @@ Entry { filename = "backorifice-info.nse", categories = { "default", "discovery"
Entry { filename = "banner.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-dns-service-discovery.nse", categories = { "broadcast", "safe", } }
Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } }
Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "broadcast", "safe", } }