--- 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 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 = stdnse.tohex(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:([^<]*)[^:]*:MessageID>")
-- 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>(.*)[^:]*:*XAddrs>")
response.types = data:match("<[^:]*:Types>[wsdp:]*(.*)[^:]*:Types>")
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;