summaryrefslogtreecommitdiffstats
path: root/nselib/wsdd.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/wsdd.lua')
-rw-r--r--nselib/wsdd.lua382
1 files changed, 382 insertions, 0 deletions
diff --git a/nselib/wsdd.lua b/nselib/wsdd.lua
new file mode 100644
index 0000000..4ad537d
--- /dev/null
+++ b/nselib/wsdd.lua
@@ -0,0 +1,382 @@
+--- 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
+-- * <code>Comm</code>
+-- ** A class that handles most communication
+-- * <code>Helper</code>
+-- ** The helper class wraps the <code>Comm</code> class using functions with a more descriptive name.
+-- * <code>Util</code>
+-- ** The Util class contains a number of static functions mainly used to convert data.
+-- * <code>Decoders</code>
+-- ** The Decoders class contains static functions used for decoding probe matches
+--
+-- The following code snippet shows how the library can be used:
+-- <code>
+-- local helper = wsdd.Helper:new()
+-- helper:setMulticast(true)
+-- return stdnse.format_output( helper:discoverDevices() )
+-- </code>
+--
+-- @author Patrik Karlsson <patrik@cqure.net>
+-- @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 = '<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" ' ..
+ 'xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" ' ..
+ 'xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery">' ..
+ '<env:Header>' ..
+ '<wsd:AppSequence InstanceId="1285624958737" MessageNumber="1" ' ..
+ 'SequenceId="urn:uuid:#uuid#"/>' ..
+ '<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>' ..
+ '<wsa:Action>' ..
+ 'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe' ..
+ '</wsa:Action><wsa:MessageID>urn:uuid:#uuid#</wsa:MessageID>' ..
+ '</env:Header><env:Body><wsd:Probe/></env:Body></env:Envelope>'
+ },
+
+ -- Detects Windows Communication Framework (WCF) web services
+ {
+ name = 'wcf',
+ desc = 'WCF Services',
+ data = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ' ..
+ 'xmlns:a="http://www.w3.org/2005/08/addressing">' ..
+ '<s:Header>' ..
+ '<a:Action s:mustUnderstand="1">' ..
+ 'http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe' ..
+ '</a:Action>' ..
+ '<a:MessageID>urn:uuid:#uuid#</a:MessageID>' ..
+ '<a:To s:mustUnderstand="1">' ..
+ 'urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01' ..
+ '</a:To>' ..
+ '</s:Header>' ..
+ '<s:Body>' ..
+ '<Probe xmlns="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">' ..
+ '<Duration xmlns="http://schemas.microsoft.com/ws/2008/06/discovery">' ..
+ 'PT20S' ..
+ '</Duration>' ..
+ '</Probe>' ..
+ '</s:Body>' ..
+ '</s:Envelope>',
+ }
+}
+
+-- 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
+ -- <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
+ -- 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
+ -- <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
+ -- 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("<SOAP.-ENV:Reason><SOAP.-ENV:Text>(.-)<")
+ 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 <code>probes</code> table to send
+ --
+ -- @param probe table containing a probe from <code>probes</code>
+ setProbe = function( self, probe )
+ self.probe = probe
+ end,
+
+ --- Receives one or more responses for a Probe
+ --
+ -- @return table containing decoded responses suitable for
+ -- <code>stdnse.format_output</code>
+ 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 <code>probes</code> for available probes
+ -- @return status true on success, false on failure
+ -- @return matches table containing responses, suitable for printing using
+ -- the <code>stdnse.format_output</code> 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 <code>stdnse.format_output</code> 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 <code>stdnse.format_output</code> function
+ discoverWCFServices = function( self )
+ return self:discoverServices('wcf')
+ end,
+
+}
+
+return _ENV;