diff --git a/CHANGELOG b/CHANGELOG index 816beed85..ea3566fd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a new library upnp that provides UPnP support to the scripts + upnp-info and broadcast-upnp-info. The library is largely based on code + taken from Thomas Buchanan's upnp-info script. [Patrik] + o [NSE] Added a new library dnssd with supporting functions for DNS Service Discovery. Moved multicast prerule from dns-service-discovery to a new script called broadcast-dns-service-discovery. [Patrik] diff --git a/nselib/upnp.lua b/nselib/upnp.lua new file mode 100644 index 000000000..61c3c66c2 --- /dev/null +++ b/nselib/upnp.lua @@ -0,0 +1,328 @@ +--- 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 in order to support +-- multicast requests. +-- +-- The library supports sending UPnP requests and decoding the responses +-- +-- The library contains the following classes +-- * Comm +-- ** A class that handles communication with the UPnP service +-- * Helper +-- ** The helper class wraps the Comm class using functions with a more descriptive name. +-- * Util +-- ** The Util class contains a number of static functions mainly used to convert and sort data. +-- +-- The following code snipplet queries all UPnP services on the network: +-- +-- local helper = upnp.Helper:new() +-- helper:setMulticast(true) +-- return stdnse.format_output(helper:queryServices()) +-- +-- +-- This next snipplet queries a specific host for the same information: +-- +-- local helper = upnp.Helper:new(host, port) +-- return stdnse.format_output(helper:queryServices()) +-- +-- +-- +-- @author "Thomas Buchanan, Patrik Karlsson " + +-- +-- Version 0.1 +-- + +module(... or "upnp", package.seeall) + +require("strbuf") +require("target") + +Util = { + + --- Converts a string ip to a numeric value suitable for comparing + -- + -- @param ip string containing the ip to convert + -- @return number containing the converted ip + ipToNumber = function(ip) + local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$") + return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4 + end, + + --- Compare function used for sorting IP-addresses + -- + -- @param a table containing first item + -- @param b table containing second item + -- @return true if the port of a is less than the port of b + ipCompare = function(a, b) + local ip_a = Util.ipToNumber(a.name) + local ip_b = Util.ipToNumber(b.name) + if ( tonumber(ip_a) < tonumber(ip_b) ) then + return true + end + return false + 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 ) + local payload = strbuf.new() + + -- for details about the UPnP message format, see http://upnp.org/resources/documents.asp + payload = payload .. "M-SEARCH * HTTP/1.1\r\n" + payload = payload .. "Host:239.255.255.250:1900\r\n" + payload = payload .. "ST:upnp:rootdevice\r\n" + payload = payload .. "Man:\"ssdp:discover\"\r\n" + payload = payload .. "MX:3\r\n\r\n" + + local status, err + + if ( self.mcast ) then + status, err = self.socket:sendto( self.host, self.port, strbuf.dump(payload) ) + else + status, err = self.socket:send( strbuf.dump(payload) ) + end + + if ( not(status) ) then return false, err end + + return true + end, + + --- Receives one or multiple UPNP responses depending on whether + -- setBroadcast 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, _, _, ip, _ = self.socket:get_info() + if target.ALLOW_NEW_TARGETS then target.add(ip) end + + 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 ) + until ( not( self.mcast ) ) + + if ( self.mcast ) then + table.sort(result, Util.ipCompare) + return true, result + end + + if ( #response > 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 true on success, false on failure + -- @return response suitable for output or error message if status is false + decodeResponse = function( 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*(.-)\010") + if server ~= nil then table.insert(output, server ) end + location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:(.-)\010") + 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 + -- 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 portionof 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 + + if xport == nil then + xport = 80 + end + + local peer = {} + local _ + + -- 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 + local payload = strbuf.new() + + strbuf.clear(payload) + -- create an HTTP request for the file, using the host and port we extracted earlier + payload = payload .. "GET /" .. xfile .. " HTTP/1.1\r\n" + payload = payload .. "Accept: text/xml, application/xml, text/html\r\n" + payload = payload .. "User-Agent: Mozilla/5.0 (compatible; Nmap Scripting Engine; http://nmap.org/book/nse.html)\r\n" + payload = payload .. "Host: " .. xhost .. ":" .. xport .. "\r\n" + payload = payload .. "Connection: Keep-Alive\r\n" + payload = payload .. "Cache-Control: no-cache\r\n" + payload = payload .. "Pragma: no-cache\r\n\r\n" + + local socket = nmap.new_socket() + socket:set_timeout(5000) + + local status = socket:connect(xhost, xport, "tcp") + if ( not(status) ) then return false, ("Failed to connect to: %s"):format(xhost) end + + status = socket:send(strbuf.dump(payload)) + if ( not(status) ) then return false, ("Failed to send data to: %s"):format(xhost) end + + -- we're expecting an xml file, and for UPnP purposes it should end in + status, response = socket:receive_buf("", true) + + if (status) and (response ~= "TIMEOUT") then + if string.match(response, "HTTP/1.%d 200") then + local webserver + -- extract information about the webserver that is handling responses for the UPnP system + webserver = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:(.-)\010") + if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end + + -- the schema for UPnP includes a number of entries, which can a number of interesting fields + for device in string.gmatch(response, "(.-)") do + local fn, mnf, mdl, nm, ver + + fn = string.match(device, "(.-)") + mnf = string.match(device, "(.-)") + mdl = string.match(device, "(.-)") + nm = string.match(device, "(.-)") + ver = string.match(device, "(.-)") + + 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 + end + end + end + end + end + return true, output + 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 + self.host = "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, + + --- 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, + +} \ No newline at end of file diff --git a/scripts/broadcast-upnp-info.nse b/scripts/broadcast-upnp-info.nse new file mode 100644 index 000000000..f3b8890a5 --- /dev/null +++ b/scripts/broadcast-upnp-info.nse @@ -0,0 +1,50 @@ +description = [[ +Attempts to extract system information from the UPnP service by running a multicast query. +]] + +--- +-- @output +-- | broadcast-upnp-info: +-- | 1.2.3.50 +-- | Debian/4.0 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.0 +-- | Location: http://1.2.3.50:8200/rootDesc.xml +-- | Webserver: Debian/4.0 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.0 +-- | Name: BUBBA|TWO DLNA Server +-- | Manufacturer: Justin Maggard +-- | Model Descr: MiniDLNA on Debian +-- | Model Name: Windows Media Connect compatible (MiniDLNA) +-- | Model Version: 1 +-- | 1.2.3.114 +-- | Linux/2.6 UPnP/1.0 KDL-32EX701/1.7 +-- | Location: http://1.2.3.114:52323/dmr.xml +-- | Webserver: Linux/2.6 UPnP/1.0 KDL-32EX701/1.7 +-- | Name: BRAVIA KDL-32EX701 +-- | Manufacturer: Sony Corporation +-- |_ Model Name: KDL-32EX701 + +-- Version 0.1 + +-- Created 10/29/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +require("shortport") +require("upnp") + +prerule = function() return true end + +--- +-- Sends UPnP discovery packet to host, +-- and extracts service information from results +action = function() + local helper = upnp.Helper:new() + helper:setMulticast(true) + local status, result = helper:queryServices() + + if ( status ) then + return stdnse.format_output(true, result) + end +end + diff --git a/scripts/script.db b/scripts/script.db index 37644a4c1..417bdb85f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -7,6 +7,7 @@ Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } } Entry { filename = "banner.nse", categories = { "discovery", "safe", } } Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "broadcast-upnp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } } Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } } Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "safe", } } diff --git a/scripts/upnp-info.nse b/scripts/upnp-info.nse index 047a0dd1c..7bea3ea64 100644 --- a/scripts/upnp-info.nse +++ b/scripts/upnp-info.nse @@ -9,6 +9,7 @@ Attempts to extract system information from the UPnP service. -- 2010-10-05 - add prerule support -- 2010-10-10 - add newtarget support +-- 2010-10-29 - factored out all of the code to upnp.lua author = "Thomas Buchanan" @@ -16,226 +17,22 @@ license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "discovery", "safe"} -require("stdnse") require("shortport") -require("strbuf") -require("target") - -prerule = function() return true end +require("upnp") --- -- Runs on UDP port 1900 portrule = shortport.portnumber(1900, "udp", {"open", "open|filtered"}) -local function process_response( response ) - - local catch = function() socket:close() end - local try = nmap.new_try(catch) - 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*(.-)\010") - if server ~= nil then table.insert(output, server ) end - location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:(.-)\010") - 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 - -- 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 portionof 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 - - if xport == nil then - xport = 80 - end - - local peer = {} - local _ - - -- 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 - local payload = strbuf.new() - - strbuf.clear(payload) - -- create an HTTP request for the file, using the host and port we extracted earlier - payload = payload .. "GET /" .. xfile .. " HTTP/1.1\r\n" - payload = payload .. "Accept: text/xml, application/xml, text/html\r\n" - payload = payload .. "User-Agent: Mozilla/5.0 (compatible; Nmap Scripting Engine; http://nmap.org/book/nse.html)\r\n" - payload = payload .. "Host: " .. xhost .. ":" .. xport .. "\r\n" - payload = payload .. "Connection: Keep-Alive\r\n" - payload = payload .. "Cache-Control: no-cache\r\n" - payload = payload .. "Pragma: no-cache\r\n\r\n" - - local socket = nmap.new_socket() - socket:set_timeout(5000) - - try(socket:connect(xhost, xport, "tcp")) - try(socket:send(strbuf.dump(payload))) - -- we're expecting an xml file, and for UPnP purposes it should end in - status, response = socket:receive_buf("", true) - - if (status) and (response ~= "TIMEOUT") then - if string.match(response, "HTTP/1.%d 200") then - local webserver - -- extract information about the webserver that is handling responses for the UPnP system - webserver = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:(.-)\010") - if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end - - -- the schema for UPnP includes a number of entries, which can a number of interesting fields - for device in string.gmatch(response, "(.-)") do - local fn, mnf, mdl, nm, ver - - fn = string.match(device, "(.-)") - mnf = string.match(device, "(.-)") - mdl = string.match(device, "(.-)") - nm = string.match(device, "(.-)") - ver = string.match(device, "(.-)") - - 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 - end - end - - socket:close() - end - end - end - return output - end -end - ---- Converts a string ip to a numeric value suitable for comparing --- --- @param ip string containing the ip to convert --- @return number containing the converted ip -local function ipToNumber(ip) - local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$") - return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4 -end - ---- Compare function used for sorting IP-addresses --- --- @param a table containing first item --- @param b table containing second item --- @return true if the port of a is less than the port of b -local function ipCompare(a, b) - local ip_a = ipToNumber(a.name) - local ip_b = ipToNumber(b.name) - if ( tonumber(ip_a) < tonumber(ip_b) ) then - return true - end - return false -end - - --- -- Sends UPnP discovery packet to host, -- and extracts service information from results -preaction = function(host, port) - - -- create the socket used for our connection - local socket = nmap.new_socket("udp") - - -- set a reasonable timeout value - socket:set_timeout(5000) - - local payload = strbuf.new() - - -- for details about the UPnP message format, see http://upnp.org/resources/documents.asp - payload = payload .. "M-SEARCH * HTTP/1.1\r\n" - payload = payload .. "Host:239.255.255.250:1900\r\n" - payload = payload .. "ST:upnp:rootdevice\r\n" - payload = payload .. "Man:\"ssdp:discover\"\r\n" - payload = payload .. "MX:3\r\n\r\n" +action = function(host, port) + local helper = upnp.Helper:new( host, port ) + local status, result = helper:queryServices() - local status, err = socket:sendto("239.255.255.250", 1900, strbuf.dump(payload)) - if (not(status)) then return err end - - local response, output - local result = {} - - while(true) do - -- read in any response we might get - status, response = socket:receive() - if (not status) then break end - - local status, _, _, peer_ip, _ = socket:get_info() - - if target.ALLOW_NEW_TARGETS then - target.add(peer_ip) - end - - output = process_response( response ) - output = { output } - output.name = peer_ip - table.insert( result, output ) + if ( status ) then + nmap.set_port_state(host, port, "open") + return stdnse.format_output(true, result) end - socket:close() - - table.sort(result, ipCompare) - return stdnse.format_output(true, result) end - -scanaction = function( host, port ) - - -- create the socket used for our connection - local socket = nmap.new_socket() - - -- set a reasonable timeout value - socket:set_timeout(5000) - - local payload = strbuf.new() - - -- for details about the UPnP message format, see http://upnp.org/resources/documents.asp - payload = payload .. "M-SEARCH * HTTP/1.1\r\n" - payload = payload .. "Host:239.255.255.250:1900\r\n" - payload = payload .. "ST:upnp:rootdevice\r\n" - payload = payload .. "Man:\"ssdp:discover\"\r\n" - payload = payload .. "MX:3\r\n\r\n" - - local status, err = socket:connect(host, port, "udp" ) - if ( not(status) ) then return err end - - status, err = socket:send( strbuf.dump(payload) ) - if ( not(status) ) then return err end - - local response - status, response = socket:receive() - - if (not status) then - socket:close() - return response - end - - -- since we got something back, the port is definitely open - nmap.set_port_state(host, port, "open") - - return stdnse.format_output(true, process_response( response )) -end - --- Function dispatch table -local actions = { - prerule = preaction, - hostrule = scanaction, - portrule = scanaction, -} - -function action (...) return actions[SCRIPT_TYPE](...) end -