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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
--- A UPNP library based on code from upnp-info initially written by
-- Thomas Buchanan. The code was factored out from upnp-info and partly
-- re-written by Patrik Karlsson <patrik@cqure.net> in order to support
-- multicast requests.
--
-- The library supports sending UPnP requests and decoding the responses
--
-- The library contains the following classes
-- * <code>Comm</code>
-- ** A class that handles communication with the UPnP service
-- * <code>Helper</code>
-- ** The helper class wraps the <code>Comm</code> class using functions with a more descriptive name.
-- * <code>Util</code>
-- ** The <code>Util</code> class contains a number of static functions mainly used to convert and sort data.
--
-- The following code snippet queries all UPnP services on the network:
-- <code>
-- local helper = upnp.Helper:new()
-- helper:setMulticast(true)
-- return stdnse.format_output(helper:queryServices())
-- </code>
--
-- This next snippet queries a specific host for the same information:
-- <code>
-- local helper = upnp.Helper:new(host, port)
-- return stdnse.format_output(helper:queryServices())
-- </code>
--
--
-- @author Thomas Buchanan
-- @author Patrik Karlsson <patrik@cqure.net>
--
-- Version 0.1
--
local http = require "http"
local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local target = require "target"
_ENV = stdnse.module("upnp", stdnse.seeall)
Util = {
--- Compare function used for sorting IP-addresses
--
-- @param a table containing first item
-- @param b table containing second item
-- @return true if a is less than b
ipCompare = function(a, b)
return ipOps.compare_ip(a, "lt", b)
end,
}
Comm = {
--- Creates a new Comm instance
--
-- @param host string containing the host name or ip
-- @param port number containing the port to connect to
-- @return o a new instance of Comm
new = function( self, host, port )
local o = {}
setmetatable(o, self)
self.__index = self
o.host = host
o.port = port
o.mcast = false
return o
end,
--- Connect to the server
--
-- @return status true on success, false on failure
connect = function( self )
if ( self.mcast ) then
self.socket = nmap.new_socket("udp")
self.socket:set_timeout(5000)
else
self.socket = nmap.new_socket()
self.socket:set_timeout(5000)
local status, err = self.socket:connect(self.host, self.port, "udp" )
if ( not(status) ) then return false, err end
end
return true
end,
--- Send the UPNP discovery request to the server
--
-- @return status true on success, false on failure
sendRequest = function( self )
-- for details about the UPnP message format, see http://upnp.org/resources/documents.asp
local payload = 'M-SEARCH * HTTP/1.1\r\n\z
Host:239.255.255.250:1900\r\n\z
ST:upnp:rootdevice\r\n\z
Man:"ssdp:discover"\r\n\z
MX:3\r\n\r\n'
local status, err
if ( self.mcast ) then
status, err = self.socket:sendto( self.host, self.port, payload )
else
status, err = self.socket:send( payload )
end
if ( not(status) ) then return false, err end
return true
end,
--- Receives one or multiple UPNP responses depending on whether
-- <code>setBroadcast</code> was enabled or not.
--
-- The function returns the
-- status and a response containing:
-- * an array (table) of responses if broadcast is used
-- * a single response if broadcast is not in use
-- * an error message if status was false
--
-- @return status true on success, false on failure
-- @return result table or string containing results or error message
-- on failure.
receiveResponse = function( self )
local status, response
local result = {}
local host_responses = {}
repeat
status, response = self.socket:receive()
if ( not(status) and #response == 0 ) then
return false, response
elseif( not(status) ) then
break
end
local status, _, _, ip, _ = self.socket:get_info()
if ( not(status) ) then
return false, "Failed to retrieve socket information"
end
if target.ALLOW_NEW_TARGETS then target.add(ip) end
if ( not(host_responses[ip]) ) then
local status, output = self:decodeResponse( response )
if ( not(status) ) then
return false, "Failed to decode UPNP response"
end
output = { output }
output.name = ip
table.insert( result, output )
host_responses[ip] = true
end
until ( not( self.mcast ) )
if ( self.mcast ) then
table.sort(result, Util.ipCompare)
return true, result
end
if ( status and #result > 0 ) then
return true, result[1]
else
return false, "Received no responses"
end
end,
--- Processes a response from a upnp device
--
-- @param response as received over the socket
-- @return status boolean true on success, false on failure
-- @return response table or string suitable for output or error message if status is false
decodeResponse = function( self, response )
local output = {}
if response ~= nil then
-- We should get a response back that has contains one line for the server, and one line for the xml file location
-- these match any combination of upper and lower case responses
local server, location
server = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:%s*(.-)\r?\n")
if server ~= nil then table.insert(output, "Server: " .. server ) end
location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:%s*(.-)\r?\n")
if location ~= nil then
table.insert(output, "Location: " .. location )
local v = nmap.verbosity()
-- the following check can output quite a lot of information, so we require at least one -v flag
if v > 0 then
local status, result = self:retrieveXML( location )
if status then
table.insert(output, result)
end
end
end
if #output > 0 then
return true, output
else
return false, "Could not decode response"
end
end
end,
--- Retrieves the XML file that describes the UPNP device
--
-- @param location string containing the location of the XML file from the UPNP response
-- @return status boolean true on success, false on failure
-- @return response table or string suitable for output or error message if status is false
retrieveXML = function( self, location )
local response
local options = {}
options['header'] = {}
options['header']['Accept'] = "text/xml, application/xml, text/html"
-- if we're in multicast mode, or if the user doesn't want us to override the IP address,
-- just use the HTTP library to grab the XML file
if ( self.mcast or ( not self.override ) ) then
response = http.get_url( location, options )
else
-- otherwise, split the location into an IP address, port, and path name for the xml file
local xhost, xport, xfile
xhost = string.match(location, "http://(.-)/")
-- check to see if the host portion of the location specifies a port
-- if not, use port 80 as a standard web server port
if xhost ~= nil and string.match(xhost, ":") then
xport = string.match(xhost, ":(.*)")
xhost = string.match(xhost, "(.*):")
end
-- check to see if the IP address returned matches the IP address we scanned
if xhost ~= self.host.ip then
stdnse.debug1("IP addresses did not match! Found %s, using %s instead.", xhost, self.host.ip)
xhost = self.host.ip
end
if xport == nil then
xport = 80
end
-- extract the path name from the location field, but strip off the \r that HTTP servers return
xfile = string.match(location, "http://.-(/.-)\013")
if xfile ~= nil then
response = http.get( xhost, xport, xfile, options )
end
end
if response ~= nil then
local output = {}
-- extract information about the webserver that is handling responses for the UPnP system
local webserver = response['header']['server']
if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end
-- the schema for UPnP includes a number of <device> entries, which can a number of interesting fields
for device in string.gmatch(response['body'], "<deviceType>(.-)</UDN>") do
local fn, mnf, mdl, nm, ver
fn = string.match(device, "<friendlyName>(.-)</friendlyName>")
mnf = string.match(device, "<manufacturer>(.-)</manufacturer>")
mdl = string.match(device, "<modelDescription>(.-)</modelDescription>")
nm = string.match(device, "<modelName>(.-)</modelName>")
ver = string.match(device, "<modelNumber>(.-)</modelNumber>")
if fn ~= nil then table.insert(output, "Name: " .. fn) end
if mnf ~= nil then table.insert(output,"Manufacturer: " .. mnf) end
if mdl ~= nil then table.insert(output,"Model Descr: " .. mdl) end
if nm ~= nil then table.insert(output,"Model Name: " .. nm) end
if ver ~= nil then table.insert(output,"Model Version: " .. ver) end
end
return true, output
else
return false, "Could not retrieve XML file"
end
end,
--- Enables or disables multicast support
--
-- @param mcast boolean true if multicast is to be used, false otherwise
setMulticast = function( self, mcast )
assert( type(mcast)=="boolean", "mcast has to be either true or false")
self.mcast = mcast
local family = nmap.address_family()
self.host = (family=="inet6" and "FF02::C" or "239.255.255.250")
self.port = 1900
end,
--- Closes the socket
close = function( self ) self.socket:close() end
}
Helper = {
--- Creates a new helper instance
--
-- @param host string containing the host name or ip
-- @param port number containing the port to connect to
-- @return o a new instance of Helper
new = function( self, host, port )
local o = {}
setmetatable(o, self)
self.__index = self
o.comm = Comm:new( host, port )
return o
end,
--- Enables or disables multicast support
--
-- @param mcast boolean true if multicast is to be used, false otherwise
setMulticast = function( self, mcast ) self.comm:setMulticast(mcast) end,
--- Enables or disables whether the script will override the IP address is the Location URL
--
-- @param override boolean true if override is to be enabled, false otherwise
setOverride = function( self, override )
assert( type(override)=="boolean", "override has to be either true or false")
self.comm.override = override
end,
--- Sends a UPnP queries and collects a single or multiple responses
--
-- @return status true on success, false on failure
-- @return result table or string containing results or error message
-- on failure.
queryServices = function( self )
local status, err = self.comm:connect()
local response
if ( not(status) ) then return false, err end
status, err = self.comm:sendRequest()
if ( not(status) ) then return false, err end
status, response = self.comm:receiveResponse()
self.comm:close()
return status, response
end,
}
return _ENV;
|