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