summaryrefslogtreecommitdiffstats
path: root/scripts/broadcast-wpad-discover.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/broadcast-wpad-discover.nse')
-rw-r--r--scripts/broadcast-wpad-discover.nse242
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