--- 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 snippet 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 https://nmap.org/book/man-legal.html -- local bin = require "bin" local nmap = require "nmap" local stdnse = require "stdnse" local table = require "table" local target = require "target" _ENV = stdnse.module("wsdd", stdnse.seeall) local HAVE_SSL, openssl = pcall(require,'openssl') -- 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 err = data:match("(.-)<") local response = "Failed to decode response from device: " .. (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.debug3("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") local family = nmap.address_family() self.mcast = mcast self.host = (family=="inet6" and "FF02::C" or "239.255.255.250") self.port = 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 ) if ( not(HAVE_SSL) ) then return false, "The wsdd library requires OpenSSL" end 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, } return _ENV;