diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/broadcast-wpad-discover.nse | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/scripts/broadcast-wpad-discover.nse b/scripts/broadcast-wpad-discover.nse new file mode 100644 index 0000000..a9c6cba --- /dev/null +++ b/scripts/broadcast-wpad-discover.nse @@ -0,0 +1,242 @@ +local dhcp = require "dhcp" +local dns = require "dns" +local http = require "http" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local url = require "url" + +description = [[ +Retrieves a list of proxy servers on a LAN using the Web Proxy +Autodiscovery Protocol (WPAD). It implements both the DHCP and DNS +methods of doing so and starts by querying DHCP to get the address. +DHCP discovery requires nmap to be running in privileged mode and will +be skipped when this is not the case. DNS discovery relies on the +script being able to resolve the local domain either through a script +argument or by attempting to reverse resolve the local IP. +]] + +--- +-- @usage +-- nmap --script broadcast-wpad-discover +-- +-- @output +-- | broadcast-wpad-discover: +-- | 1.2.3.4:8080 +-- |_ 4.5.6.7:3128 +-- +-- @args broadcast-wpad-discover.domain the domain in which the WPAD host should be discovered +-- @args broadcast-wpad-discover.nodns instructs the script to skip discovery using DNS +-- @args broadcast-wpad-discover.nodhcp instructs the script to skip discovery using dhcp +-- @args broadcast-wpad-discover.getwpad instructs the script to retrieve the WPAD file instead of parsing it + +author = "Patrik Karlsson" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"broadcast", "safe"} + + +prerule = function() return true end + +local arg_domain = stdnse.get_script_args(SCRIPT_NAME .. ".domain") +local arg_nodns = stdnse.get_script_args(SCRIPT_NAME .. ".nodns") +local arg_nodhcp = stdnse.get_script_args(SCRIPT_NAME .. ".nodhcp") +local arg_getwpad= stdnse.get_script_args(SCRIPT_NAME .. ".getwpad") + +local function createRequestList(req_list) + local output = {} + for i, v in ipairs(req_list) do + output[i] = string.char(v) + end + return table.concat(output) +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 parseDHCPResponse(response) + for _, v in ipairs(response.options) do + if ( "WPAD" == v.name ) then + return true, v.value + end + end +end + +local function getWPAD(u) + local u_parsed = url.parse(u) + + if ( not(u_parsed) ) then + return false, ("Failed to parse url: %s"):format(u) + end + + local response = http.get(u_parsed.host, u_parsed.port or 80, u_parsed.path) + if ( response and response.status == 200 ) then + return true, response.body + end + + return false, ("Failed to retrieve wpad.dat (%s) from server"):format(u) +end + +local function parseWPAD(wpad) + local proxies = {} + for proxy in wpad:gmatch("PROXY%s*([^\";%s]*)") do + table.insert(proxies, proxy) + end + return proxies +end + +-- cache of all names we've already tried once. No point in wasting time. +local wpad_dns_tried = {} + +-- tries to discover WPAD for all domains and sub-domains +local function enumWPADNames(domain) + local d = domain + -- reduce domain until we only have a single dot left + -- there is a security problem in querying for wpad.tld like eg + -- wpad.com as this could be a rogue domain. This loop does not + -- account for domains with tld's containing two parts e.g. co.uk. + -- However, as the script just attempts to download and parse the + -- proxy values in the WPAD there should be no real harm here. + repeat + local name = ("wpad.%s"):format(d) + if wpad_dns_tried[name] then + -- We've been here before, stop. + d = nil + else + wpad_dns_tried[name] = true + d = d:match("^[^%.]-%.(.*)$") + local status, response = dns.query(name, { dtype = 'A', retAll = true }) + + -- get the first entry and return + if ( status and response[1] ) then + return true, { name = name, ip = response[1] } + end + end + until not d + +end + +local function dnsDiscover() + -- first try a domain if it was supplied + if ( arg_domain ) then + local status, response = enumWPADNames(arg_domain) + if ( status ) then + return status, response + end + end + + + -- if no domain was supplied, attempt to reverse lookup every ip on each + -- interface to find our FQDN hostname, once we do, try to query for WPAD + for i in pairs(getInterfaces("ethernet", "up") or {}) do + local iface, err = nmap.get_interface_info(i) + if ( iface ) then + local status, response = dns.query( dns.reverse(iface.address), { dtype = 'PTR', retAll = true } ) + + -- did we get a name back from dns? + if ( status ) then + local domains = {} + for _, name in ipairs(response) do + -- first get all unique domain names + if ( not(name:match("in%-addr.arpa$")) ) then + local domain = name:match("^[^%.]-%.(.*)$") + domains[domain or ""] = true + end + end + + -- attempt to discover the ip for WPAD in all domains + -- each domain is processed and reduced and ones the first + -- match is received it returns an IP + for domain in pairs(domains) do + status, response = enumWPADNames(domain) + if ( status ) then + return true, response + end + end + + end + + end + end + + return false, "Failed to find WPAD using DNS" + +end + +local function dhcpDiscover() + + -- send a DHCP discover on all ethernet interfaces that are up + for i in pairs(getInterfaces("ethernet", "up") or {}) do + local iface, err = nmap.get_interface_info(i) + if ( iface ) then + local req_list = createRequestList( { 1, 15, 3, 6, 44, 46, 47, 31, 33, 249, 43, 252 } ) + local status, response = dhcp.make_request("255.255.255.255", dhcp.request_types["DHCPDISCOVER"], "0.0.0.0", iface.mac, nil, req_list, { flags = 0x8000 } ) + + -- if we got a response, we're happy and don't need to continue + if (status) then + return status, response + end + end + end + +end + +local function fail (err) return stdnse.format_output(false, err) end + +action = function() + + local status, response, wpad + + if ( arg_nodhcp and arg_nodns ) then + stdnse.verbose1("Both nodns and nodhcp arguments were supplied") + return fail("Both nodns and nodhcp arguments were supplied") + end + + if ( nmap.is_privileged() and not(arg_nodhcp) ) then + status, response = dhcpDiscover() + if ( status ) then + status, wpad = parseDHCPResponse(response) + end + end + + -- if the DHCP did not get a result, fallback to DNS + if (not(status) and not(arg_nodns) ) then + status, response = dnsDiscover() + if ( not(status) ) then + local services = "DNS" .. ( nmap.is_privileged() and "/DHCP" or "" ) + return fail(("Could not find WPAD using %s"):format(services)) + end + wpad = ("http://%s/wpad.dat"):format( response.name ) + end + + if ( status ) then + status, response = getWPAD(wpad) + end + + if ( not(status) ) then + return status, response + end + + local output = ( arg_getwpad and response or parseWPAD(response) ) + + return stdnse.format_output(true, output) +end |