From 734f938b04d56f5c24bded02aee60841045bd684 Mon Sep 17 00:00:00 2001 From: patrik Date: Wed, 10 Nov 2010 22:35:13 +0000 Subject: [PATCH] o [NSE] Added a new Web Service Dynamic Discovery library (wsdd) and the two scripts broadcast-wsdd-discover and wsdd-discover. [Patrik] --- CHANGELOG | 3 + nselib/wsdd.lua | 373 ++++++++++++++++++++++++++++ scripts/broadcast-wsdd-discover.nse | 102 ++++++++ scripts/script.db | 2 + scripts/wsdd-discover.nse | 90 +++++++ 5 files changed, 570 insertions(+) create mode 100644 nselib/wsdd.lua create mode 100644 scripts/broadcast-wsdd-discover.nse create mode 100644 scripts/wsdd-discover.nse diff --git a/CHANGELOG b/CHANGELOG index f2956a983..15e0d9f70 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a new Web Service Dynamic Discovery library (wsdd) and the two + scripts broadcast-wsdd-discover and wsdd-discover. [Patrik] + o [Zenmap] Upgraded to the newer gtk.Tooltip API to avoid deprecation messages about gtk.Tooltip. [Rob Nicholls] diff --git a/nselib/wsdd.lua b/nselib/wsdd.lua new file mode 100644 index 000000000..bcf1ed5fc --- /dev/null +++ b/nselib/wsdd.lua @@ -0,0 +1,373 @@ +--- A library that enables scripts to send Web Service Dynamic Discovery probes +-- and perform some very basic decoding of responses. The library is in no way +-- a full WSDD implementation it's rather the result of some packet captures +-- and some creative coding. +-- +-- The "general" probe was captured of the wire of a Windows 7 box while +-- connecting to the network. The "wcf" probe was captured from a custom tool +-- tool performing WCF discovery in .NET 4.0. +-- +-- More information about the protocol can be found here: +-- * http://docs.oasis-open.org/ws-dd/discovery/1.1/os/wsdd-discovery-1.1-spec-os.pdf +-- * http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf +-- +-- The library contains the following classes +-- * Comm +-- ** A class that handles most communication +-- * 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 data. +-- * Decoders +-- ** The Decoders class contains static functions used for decoding probe matches +-- +-- The following code snipplet shows how the library can be used: +-- +-- local helper = wsdd.Helper:new() +-- helper:setMulticast(true) +-- return stdnse.format_output( helper:discoverDevices() ) +-- +-- +-- @author "Patrik Karlsson " +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- + +module(... or "wsdd", package.seeall) + +require 'openssl' +require 'target' + +-- The different probes +local probes = { + + -- Detects devices supporting the WSDD protocol + { + name = 'general', + desc = 'Devices', + data = '' .. + '' .. + '' .. + 'urn:schemas-xmlsoap-org:ws:2005:04:discovery' .. + '' .. + 'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe' .. + 'urn:uuid:#uuid#' .. + '' + }, + + -- Detects Windows Communication Framework (WCF) web services + { + name = 'wcf', + desc = 'WCF Services', + data = '' .. + '' .. + '' .. + 'http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe' .. + '' .. + 'urn:uuid:#uuid#' .. + '' .. + 'urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01' .. + '' .. + '' .. + '' .. + '' .. + '' .. + 'PT20S' .. + '' .. + '' .. + '' .. + '', + } +} + +-- A table that keeps track of received probe matches +local probe_matches = {} + +Util = { + + --- Creates a UUID + -- + -- @return uuid string containing a uuid + generateUUID = function() + local rnd_bytes = select(2, bin.unpack( "H16", openssl.rand_bytes( 16 ) ) ):lower() + + return ("%s-%s-%s-%s-%s"):format( rnd_bytes:sub(1, 8), + rnd_bytes:sub(9, 12), rnd_bytes:sub( 13, 16 ), rnd_bytes:sub( 17, 20 ), + rnd_bytes:sub(21, 32) ) + end, + + --- Retrieves a probe from the probes table by name + -- + -- @param name string containing the name of the probe to retrieve + -- @return probe table containing the probe or nil if not found + getProbeByName = function( name ) + for _, probe in ipairs(probes) do + if ( probe.name == name ) then + return probe + end + end + return + end, + + getProbes = function() return probes end, + + sha1sum = function(data) return openssl.sha1(data) end + +} + +Decoders = { + + --- Decodes a wcf probe response + -- + -- @param data string containing the response as received over the wire + -- @return status true on success, false on failure + -- @return response table containing the following fields + -- msgid, xaddrs, types + -- err string containing the error message + ['wcf'] = function( data ) + local response = {} + + -- extracts the messagid, so we can check if we already got a response + response.msgid = data:match("\<.*:MessageID\>urn:uuid:(.*)\") + + -- if unable to parse msgid return nil + if ( not(response.msgid) ) then + return false, "No message id was found" + end + + response.xaddrs = data:match("\<.*:*XAddrs\>(.*)\") + response.types = data:match("\<.*:Types\>[wsdp:]*(.*)\") + + return true, response + end, + + --- Decodes a general probe response + -- + -- @param data string containing the response as received over the wire + -- @return status true on success, false on failure + -- @return response table containing the following fields + -- msgid, xaddrs, types + -- err string containing the error message + ['general'] = function( data ) + return Decoders['wcf'](data) + end, + + --- Decodes an error message received from the service + -- + -- @param data string containing the response as received over the wire + -- @return status true on success, false on failure + -- @return err string containing the error message + ['error'] = function( data ) + local response = "Failed to decode response from device: " + local err = data:match("\\(.-)\<") + response = response .. (err or "Unknown error") + + return true, response + 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, mcast ) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.mcast = mcast or false + o.sendcount = 2 + o.timeout = 5000 + return o + end, + + --- Sets the timeout for socket reads + setTimeout = function( self, timeout ) self.timeout = timeout end, + + --- Sends a probe over the wire + -- + -- @return status true on success, false on failure + sendProbe = function( self ) + local status, err + + -- replace all instances of #uuid# in the probe + local probedata = self.probe.data:gsub("#uuid#", Util.generateUUID()) + + if ( self.mcast ) then + self.socket = nmap.new_socket("udp") + self.socket:set_timeout(self.timeout) + else + self.socket = nmap.new_socket() + self.socket:set_timeout(self.timeout) + status, err = self.socket:connect( self.host, self.port, "udp" ) + if ( not(status) ) then return err end + end + + for i=1, self.sendcount do + if ( self.mcast ) then + status, err = self.socket:sendto( self.host, self.port, probedata ) + else + status, err = self.socket:send( probedata ) + end + if ( not(status) ) then return err end + end + return true + end, + + --- Sets a probe from the probes table to send + -- + -- @param probe table containing a probe from probes + setProbe = function( self, probe ) + self.probe = probe + end, + + --- Receives one or more responses for a Probe + -- + -- @return table containing decoded responses suitable for + -- stdnse.format_output + recvProbeMatches = function( self ) + local responses = {} + repeat + local data + + local status, data = self.socket:receive() + if ( not(status) ) then + if ( data == "TIMEOUT" ) then + break + else + return false, data + end + end + + local _, ip + status, _, _, ip, _ = self.socket:get_info() + if( not(status) ) then + stdnse.print_debug( 3, "wsdd.recvProbeMatches: ERROR: Failed to get socket info" ) + return false, "ERROR: Failed to get socket info" + end + + -- push the unparsed response to the response table + local status, response = Decoders[self.probe.name]( data ) + local id, output + -- if we failed to decode the response indicate this + if ( status ) then + output = {} + table.insert(output, "Message id: " .. response.msgid) + if ( response.xaddrs ) then + table.insert(output, "Address: " .. response.xaddrs) + end + if ( response.types ) then + table.insert(output, "Type: " .. response.types) + end + id = response.msgid + else + status, response = Decoders["error"](data) + output = response + id = Util.sha1sum(data) + end + + if ( self.mcast and not(probe_matches[id]) ) then + if target.ALLOW_NEW_TARGETS then target.add(ip) end + table.insert( responses, { name=ip, output } ) + elseif ( not(probe_matches[id]) ) then + responses = output + end + + -- avoid duplicates + probe_matches[id] = true + until( not(self.mcast) ) + + -- we're done with the socket + self.socket:close() + + return true, responses + 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.host = host + o.port = port + o.mcast = false + o.timeout = 5000 + return o + end, + + --- Instructs the helper to use unconnected sockets supporting multicast + -- + -- @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, self.port = "239.255.255.250", 3702 + end, + + --- Sets the timeout for socket reads + setTimeout = function( self, timeout ) self.timeout = timeout end, + + --- Sends a probe, receives and decodes a probematch + -- + -- @param probename string containing the name of the probe to send + -- check probes for available probes + -- @return status true on success, false on failure + -- @return matches table containing responses, suitable for printing using + -- the stdnse.format_output function + discoverServices = function( self, probename ) + local comm = Comm:new(self.host, self.port, self.mcast) + local probe = Util.getProbeByName(probename) + comm:setProbe( probe ) + comm:setTimeout( self.timeout ) + + local status = comm:sendProbe() + if ( not(status) ) then + return false, "ERROR: wcf.discoverServices failed" + end + + local status, matches = comm:recvProbeMatches() + if ( not(status) ) then + return false, "ERROR: wcf.recvProbeMatches failed" + end + + if ( #matches > 0 ) then matches.name = probe.desc end + return true, matches + end, + + --- Sends a general probe to attempt to discover WSDD supporting devices + -- + -- @return status true on success, false on failure + -- @return matches table containing responses, suitable for printing using + -- the stdnse.format_output function + discoverDevices = function( self ) + return self:discoverServices('general') + end, + + + --- Sends a probe that attempts to discover WCF web services + -- + -- @return status true on success, false on failure + -- @return matches table containing responses, suitable for printing using + -- the stdnse.format_output function + discoverWCFServices = function( self ) + return self:discoverServices('wcf') + end, + +} diff --git a/scripts/broadcast-wsdd-discover.nse b/scripts/broadcast-wsdd-discover.nse new file mode 100644 index 000000000..87ae8a79d --- /dev/null +++ b/scripts/broadcast-wsdd-discover.nse @@ -0,0 +1,102 @@ +description = [[ +Discovers devices supporting the Web Services Dynamic Discovery (WS-Discovery) +protocol. It also attempts to locate any published Windows Communication +Framework (WCF) web services (.NET 4.0 or later). +]] + +--- +-- @usage +-- sudo ./nmap --script broadcast-wsdd-discover +-- +-- @output +-- | broadcast-wsdd-discover: +-- | Devices +-- | 1.2.3.116 +-- | Message id: 9ea97e41-e874-faa7-fe28-deadbeefceb3 +-- | Address: http://1.2.3.116:50000 +-- | Type: Device wprt:PrintDeviceType +-- | 1.2.3.131 +-- | Message id: 4d971368-291c-1218-30f1-deadbeefceb3 +-- | Address: http://1.2.3.131:5357/deadbeef-ea5c-4b9a-a68d-deadbeefceb3/ +-- | Type: Device pub:Computer +-- | 1.2.3.110 +-- | Message id: f5a25a38-d61c-49e5-96c4-deadbeefceb3 +-- | Address: http://1.2.3.110:5357/deadbeef-469b-4da4-b413-deadbeefee90/ +-- | Type: Device pub:Computer +-- | WCF Services +-- | 1.2.3.131 +-- | Message id: c1767df8-43e5-4440-9e26--deadbeefceb3 +-- |_ Address: http://win-7:8090/discovery/scenarios/service2/deadbeef-3382-4668-86e7-deadbeefb935/ +-- +-- + +-- +-- Version 0.1 +-- Created 10/31/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"safe", "discovery"} + +require 'shortport' +require 'wsdd' + +prerule = function() return true end + +-- function used for running several discovery threads in parallell +-- +-- @param funcname string containing the name of the function to run +-- the name should be one of the discovery functions in wsdd.Helper +-- @param result table into which the results are stored +discoverThread = function( funcname, results ) + -- calculates a timeout based on the timing template (default: 5s) + local timeout = ( 20000 / ( nmap.timing_level() + 1 ) ) + local condvar = nmap.condvar( results ) + local helper = wsdd.Helper:new() + helper:setMulticast(true) + helper:setTimeout(timeout) + + local func = loadstring( "return helper:" .. funcname .. "()" ) + setfenv(func, setmetatable({ helper=helper; }, {__index = _G})) + + if ( func ) then + local status, result = func() + if ( status ) then table.insert(results, result) end + else + stdnse.print_debug("ERROR: Failed to call function: %s", funcname) + end + condvar("broadcast") +end + +local function sortfunc(a,b) + if ( a and b and a.name and b.name ) and ( a.name < b.name ) then + return true + end + return false +end + +action = function() + + local threads, results = {}, {} + local condvar = nmap.condvar( results ) + + -- Attempt to discover both devices and WCF web services + for _, f in ipairs( {"discoverDevices", "discoverWCFServices"} ) do + threads[stdnse.new_thread( discoverThread, f, results )] = true + end + + local done + -- wait for all threads to finish + while( not(done) ) do + condvar("wait") + done = true + for thread in pairs(threads) do + if (coroutine.status(thread) ~= "dead") then done = false end + end + end + + if ( results ) then + table.sort( results, sortfunc ) + return stdnse.format_output(true, results) + end +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 417bdb85f..bfd2f8ae2 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -8,6 +8,7 @@ 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 = "broadcast-wsdd-discover.nse", categories = { "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", } } @@ -156,4 +157,5 @@ Entry { filename = "vnc-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "vnc-info.nse", categories = { "discovery", "safe", } } Entry { filename = "wdb-version.nse", categories = { "default", "discovery", "version", } } Entry { filename = "whois.nse", categories = { "discovery", "external", "safe", } } +Entry { filename = "wsdd-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "x11-access.nse", categories = { "auth", "default", "safe", } } diff --git a/scripts/wsdd-discover.nse b/scripts/wsdd-discover.nse new file mode 100644 index 000000000..3a16991ee --- /dev/null +++ b/scripts/wsdd-discover.nse @@ -0,0 +1,90 @@ +description = [[ +Discovers devices supporting the Web Services Dynamic Discovery (WS-Discovery) +protocol. It also attempts to locate any published Windows Communication +Framework (WCF) web services (.NET 4.0 or later). +]] + +--- +-- @usage +-- sudo ./nmap --script broadcast-wsdd-discover +-- +-- @output +-- PORT STATE SERVICE +-- 3702/udp open|filtered unknown +-- | wsdd-discover: +-- | Devices +-- | Message id: 39a2b7f2-fdbd-690c-c7c9-deadbeefceb3 +-- | Address: http://10.0.200.116:50000 +-- |_ Type: Device wprt:PrintDeviceType +-- +-- + +-- +-- Version 0.1 +-- Created 10/31/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"safe", "discovery", "default"} + +require 'shortport' +require 'wsdd' + +portrule = shortport.portnumber(3702, "udp", {"open", "open|filtered"}) + +-- function used for running several discovery threads in parallell +-- +-- @param funcname string containing the name of the function to run +-- the name should be one of the discovery functions in wsdd.Helper +-- @param result table into which the results are stored +discoverThread = function( funcname, host, port, results ) + -- calculates a timeout based on the timing template (default: 5s) + local timeout = ( 20000 / ( nmap.timing_level() + 1 ) ) + local condvar = nmap.condvar( results ) + local helper = wsdd.Helper:new(host, port) + helper:setTimeout(timeout) + + local func = loadstring( "return helper:" .. funcname .. "()" ) + setfenv(func, setmetatable({ helper=helper; }, {__index = _G})) + + if ( func ) then + local status, result = func() + if ( status ) then table.insert(results, result) end + else + stdnse.print_debug("ERROR: Failed to call function: %s", funcname) + end + condvar("broadcast") +end + +local function sortfunc(a,b) + if ( a and b and a.name and b.name ) and ( a.name < b.name ) then + return true + end + return false +end + +action = function(host, port) + + local threads, results = {}, {} + local condvar = nmap.condvar( results ) + + -- Attempt to discover both devices and WCF web services + for _, f in ipairs( {"discoverDevices", "discoverWCFServices"} ) do + threads[stdnse.new_thread( discoverThread, f, host, port, results )] = true + end + + local done + -- wait for all threads to finish + while( not(done) ) do + condvar("wait") + done = true + for thread in pairs(threads) do + if (coroutine.status(thread) ~= "dead") then done = false end + end + end + + if ( results ) then + table.sort( results, sortfunc ) + return stdnse.format_output(true, results) + end +end \ No newline at end of file