1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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
|