mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
394 lines
12 KiB
Lua
394 lines
12 KiB
Lua
--- 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"
|
|
local slaxml = require "slaxml"
|
|
local url = require "url"
|
|
local outlib = require "outlib"
|
|
_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,
|
|
|
|
}
|
|
|
|
local device_elements = {
|
|
deviceType = true,
|
|
serviceType = true,
|
|
friendlyName = true,
|
|
manufacturer = true,
|
|
modelDescription = true,
|
|
modelName = true,
|
|
modelNumber = true,
|
|
UDN = true,
|
|
}
|
|
|
|
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 = {}
|
|
|
|
repeat
|
|
status, response = self.socket:receive()
|
|
if ( not(status) and #response == 0 ) then
|
|
return false, response
|
|
elseif( not(status) ) then
|
|
break
|
|
end
|
|
|
|
local status = self:decodeResponse( response, result )
|
|
if ( not(status) ) then
|
|
return false, "Failed to decode UPNP response"
|
|
end
|
|
until ( not( self.mcast ) )
|
|
|
|
if ( self.mcast ) then
|
|
return true, outlib.sorted_by_key(result, Util.ipCompare)
|
|
end
|
|
|
|
if status then
|
|
local i, v = next(result)
|
|
return (not not i), v
|
|
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, results )
|
|
local output = stdnse.output_table()
|
|
local key
|
|
|
|
-- 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 usn = string.match(response, "\n[Uu][Ss][Nn]:%s*([Uu][Uu][Ii][Dd]:[%x-]+)")
|
|
if usn then
|
|
key = usn
|
|
output.usn = usn
|
|
end
|
|
local location = string.match(response, "\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:%s*(.-)\r?\n")
|
|
if location then
|
|
local loc_url = url.parse(location)
|
|
if loc_url.host then
|
|
key = loc_url.host
|
|
if target.ALLOW_NEW_TARGETS then target.add(loc_url.host) end
|
|
end
|
|
output.location = location
|
|
end
|
|
|
|
if key and results[key] then
|
|
return false, "Already recorded a response for this host"
|
|
end
|
|
|
|
local server = string.match(response, "\n[Ss][Ee][Rr][Vv][Ee][Rr]:%s*(.-)\r?\n")
|
|
if server ~= nil then output.server = server end
|
|
|
|
if location and nmap.verbosity() > 0 then
|
|
-- the following check can output quite a lot of information, so we require at least one -v flag
|
|
local status, result = self:retrieveXML( location )
|
|
if status then
|
|
if result.webserver ~= output.server then
|
|
output.webserver = result.webserver
|
|
end
|
|
result.webserver = nil
|
|
if usn and result[usn] then
|
|
for k, v in pairs(result[usn]) do
|
|
output[k] = v
|
|
end
|
|
result[usn] = nil
|
|
end
|
|
if #result > 0 then
|
|
output.devices = result
|
|
end
|
|
end
|
|
end
|
|
|
|
if #output > 0 then
|
|
results[key] = output
|
|
return true
|
|
else
|
|
return false, "Could not decode response"
|
|
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 loc_url = url.parse(location)
|
|
options.scheme = loc_url.scheme
|
|
local xhost = loc_url.host
|
|
local xport = loc_url.port or url.get_default_port(loc_url.scheme) or 80
|
|
local xfile = loc_url.path
|
|
if loc_url.query then
|
|
xfile = xfile .. "?" .. loc_url.query
|
|
end
|
|
|
|
-- check to see if the IP address returned matches the IP address we scanned
|
|
if not ipOps.compare_ip(xhost, "eq", 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 xhost and xport and xfile then
|
|
response = http.get( xhost, xport, xfile, options )
|
|
end
|
|
end
|
|
|
|
if response.body then
|
|
local output = stdnse.output_table()
|
|
|
|
-- extract information about the webserver that is handling responses for the UPnP system
|
|
local webserver = response['header']['server']
|
|
if webserver then output.webserver = webserver end
|
|
|
|
-- the schema for UPnP includes a number of <device> entries, which can a number of interesting fields
|
|
local element
|
|
local devices = {}
|
|
local depth = 0
|
|
local parser = slaxml.parser:new({
|
|
startElement = function(name)
|
|
if name == "device" then
|
|
depth = depth + 1
|
|
devices[depth] = stdnse.output_table()
|
|
elseif devices[depth] and device_elements[name] then
|
|
assert(not element, "nested element unexpected")
|
|
element = name
|
|
end
|
|
end,
|
|
closeElement = function(name)
|
|
if element then
|
|
assert(name == element, "close tag unexpected")
|
|
element = nil
|
|
elseif name == "device" then
|
|
local dev = devices[depth]
|
|
assert(dev and dev.UDN, "missing device or UDN")
|
|
output[dev.UDN] = dev
|
|
dev.UDN = nil
|
|
devices[depth] = nil
|
|
depth = depth - 1
|
|
end
|
|
end,
|
|
text = function(content)
|
|
if element then
|
|
local dev = devices[depth]
|
|
if element == "serviceType" then
|
|
local services = dev.services or {}
|
|
services[#services+1] = content
|
|
dev.services = services
|
|
else
|
|
dev[element] = content
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
parser:parseSAX(response.body, {stripWhitespace=true})
|
|
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;
|