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