1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00

Update upnp-info: structured output, correct targets added, etc.

This commit is contained in:
dmiller
2024-06-07 16:34:07 +00:00
parent f43878f0f5
commit 74b2b6fc05
3 changed files with 121 additions and 76 deletions

View File

@@ -41,6 +41,9 @@ local stdnse = require "stdnse"
local string = require "string" local string = require "string"
local table = require "table" local table = require "table"
local target = require "target" local target = require "target"
local slaxml = require "slaxml"
local url = require "url"
local outlib = require "outlib"
_ENV = stdnse.module("upnp", stdnse.seeall) _ENV = stdnse.module("upnp", stdnse.seeall)
Util = { Util = {
@@ -56,6 +59,17 @@ Util = {
} }
local device_elements = {
deviceType = true,
serviceType = true,
friendlyName = true,
manufacturer = true,
modelDescription = true,
modelName = true,
modelNumber = true,
UDN = true,
}
Comm = { Comm = {
--- Creates a new Comm instance --- Creates a new Comm instance
@@ -130,7 +144,6 @@ Comm = {
receiveResponse = function( self ) receiveResponse = function( self )
local status, response local status, response
local result = {} local result = {}
local host_responses = {}
repeat repeat
status, response = self.socket:receive() status, response = self.socket:receive()
@@ -140,31 +153,19 @@ Comm = {
break break
end end
local status, _, _, ip, _ = self.socket:get_info() local status = self:decodeResponse( response, result )
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 if ( not(status) ) then
return false, "Failed to decode UPNP response" return false, "Failed to decode UPNP response"
end end
output = { output }
output.name = ip
table.insert( result, output )
host_responses[ip] = true
end
until ( not( self.mcast ) ) until ( not( self.mcast ) )
if ( self.mcast ) then if ( self.mcast ) then
table.sort(result, Util.ipCompare) return true, outlib.sorted_by_key(result, Util.ipCompare)
return true, result
end end
if ( status and #result > 0 ) then if status then
return true, result[1] local i, v = next(result)
return (not not i), v
else else
return false, "Received no responses" return false, "Received no responses"
end end
@@ -175,35 +176,60 @@ Comm = {
-- @param response as received over the socket -- @param response as received over the socket
-- @return status boolean true on success, false on failure -- @return status boolean true on success, false on failure
-- @return response table or string suitable for output or error message if status is false -- @return response table or string suitable for output or error message if status is false
decodeResponse = function( self, response ) decodeResponse = function( self, response, results )
local output = {} local output = stdnse.output_table()
local key
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 -- 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 -- these match any combination of upper and lower case responses
local server, location local usn = string.match(response, "\n[Uu][Ss][Nn]:%s*([Uu][Uu][Ii][Dd]:[%x-]+)")
server = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:%s*(.-)\r?\n") if usn then
if server ~= nil then table.insert(output, "Server: " .. server ) end key = usn
location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:%s*(.-)\r?\n") output.usn = usn
if location ~= nil then end
table.insert(output, "Location: " .. location ) 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
local v = nmap.verbosity() 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 -- 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 ) local status, result = self:retrieveXML( location )
if status then if status then
table.insert(output, result) 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 end
end end
if #output > 0 then if #output > 0 then
return true, output results[key] = output
return true
else else
return false, "Could not decode response" return false, "Could not decode response"
end end
end
end, end,
--- Retrieves the XML file that describes the UPNP device --- Retrieves the XML file that describes the UPNP device
@@ -223,55 +249,74 @@ Comm = {
response = http.get_url( location, options ) response = http.get_url( location, options )
else else
-- otherwise, split the location into an IP address, port, and path name for the xml file -- otherwise, split the location into an IP address, port, and path name for the xml file
local xhost, xport, xfile local loc_url = url.parse(location)
xhost = string.match(location, "http://(.-)/") options.scheme = loc_url.scheme
-- check to see if the host portion of the location specifies a port local xhost = loc_url.host
-- if not, use port 80 as a standard web server port local xport = loc_url.port or url.get_default_port(loc_url.scheme) or 80
if xhost ~= nil and string.match(xhost, ":") then local xfile = loc_url.path
xport = string.match(xhost, ":(.*)") if loc_url.query then
xhost = string.match(xhost, "(.*):") xfile = xfile .. "?" .. loc_url.query
end end
-- check to see if the IP address returned matches the IP address we scanned -- check to see if the IP address returned matches the IP address we scanned
if xhost ~= self.host.ip then 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) stdnse.debug1("IP addresses did not match! Found %s, using %s instead.", xhost, self.host.ip)
xhost = self.host.ip xhost = self.host.ip
end end
if xport == nil then if xhost and xport and xfile 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 ) response = http.get( xhost, xport, xfile, options )
end end
end end
if response ~= nil then if response.body then
local output = {} local output = stdnse.output_table()
-- extract information about the webserver that is handling responses for the UPnP system -- extract information about the webserver that is handling responses for the UPnP system
local webserver = response['header']['server'] local webserver = response['header']['server']
if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end if webserver then output.webserver = webserver end
-- the schema for UPnP includes a number of <device> entries, which can a number of interesting fields -- 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 element
local fn, mnf, mdl, nm, ver local devices = {}
local depth = 0
fn = string.match(device, "<friendlyName>(.-)</friendlyName>") local parser = slaxml.parser:new({
mnf = string.match(device, "<manufacturer>(.-)</manufacturer>") startElement = function(name)
mdl = string.match(device, "<modelDescription>(.-)</modelDescription>") if name == "device" then
nm = string.match(device, "<modelName>(.-)</modelName>") depth = depth + 1
ver = string.match(device, "<modelNumber>(.-)</modelNumber>") devices[depth] = stdnse.output_table()
elseif devices[depth] and device_elements[name] then
if fn ~= nil then table.insert(output, "Name: " .. fn) end assert(not element, "nested element unexpected")
if mnf ~= nil then table.insert(output,"Manufacturer: " .. mnf) end element = name
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 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 return true, output
else else
return false, "Could not retrieve XML file" return false, "Could not retrieve XML file"

View File

@@ -45,7 +45,7 @@ action = function()
local status, result = helper:queryServices() local status, result = helper:queryServices()
if ( status ) then if ( status ) then
return stdnse.format_output(true, result) return result
end end
end end

View File

@@ -50,6 +50,6 @@ action = function(host, port)
if ( status ) then if ( status ) then
nmap.set_port_state(host, port, "open") nmap.set_port_state(host, port, "open")
return stdnse.format_output(true, result) return result
end end
end end