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", } }