1--- A library that enables scripts to send Web Service Dynamic Discovery probes
2-- and perform some very basic decoding of responses. The library is in no way
3-- a full WSDD implementation it's rather the result of some packet captures
4-- and some creative coding.
5--
6-- The "general" probe was captured of the wire of a Windows 7 box while
7-- connecting to the network. The "wcf" probe was captured from a custom tool
8-- tool performing WCF discovery in .NET 4.0.
9--
10-- More information about the protocol can be found here:
11-- * http://docs.oasis-open.org/ws-dd/discovery/1.1/os/wsdd-discovery-1.1-spec-os.pdf
12-- * http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf
13--
14-- The library contains the following classes
15-- * <code>Comm</code>
16-- ** A class that handles most communication
17-- * <code>Helper</code>
18-- ** The helper class wraps the <code>Comm</code> class using functions with a more descriptive name.
19-- * <code>Util</code>
20-- ** The Util class contains a number of static functions mainly used to convert data.
21-- * <code>Decoders</code>
22-- ** The Decoders class contains static functions used for decoding probe matches
23--
24-- The following code snippet shows how the library can be used:
25-- <code>
26-- local helper = wsdd.Helper:new()
27-- helper:setMulticast(true)
28-- return stdnse.format_output( helper:discoverDevices() )
29-- </code>
30--
31-- @author Patrik Karlsson <patrik@cqure.net>
32-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
33--
34
35local nmap = require "nmap"
36local stdnse = require "stdnse"
37local table = require "table"
38local target = require "target"
39_ENV = stdnse.module("wsdd", stdnse.seeall)
40
41local HAVE_SSL, openssl = pcall(require,'openssl')
42
43-- The different probes
44local probes = {
45
46  -- Detects devices supporting the WSDD protocol
47  {
48    name  = 'general',
49    desc = 'Devices',
50    data = '<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" ' ..
51    'xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" ' ..
52    'xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery">' ..
53    '<env:Header>' ..
54    '<wsd:AppSequence InstanceId="1285624958737" MessageNumber="1" ' ..
55    'SequenceId="urn:uuid:#uuid#"/>' ..
56    '<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>' ..
57    '<wsa:Action>' ..
58    'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe' ..
59    '</wsa:Action><wsa:MessageID>urn:uuid:#uuid#</wsa:MessageID>' ..
60    '</env:Header><env:Body><wsd:Probe/></env:Body></env:Envelope>'
61  },
62
63  -- Detects Windows Communication Framework (WCF) web services
64  {
65    name = 'wcf',
66    desc = 'WCF Services',
67    data = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ' ..
68    'xmlns:a="http://www.w3.org/2005/08/addressing">' ..
69    '<s:Header>' ..
70    '<a:Action s:mustUnderstand="1">' ..
71    'http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe' ..
72    '</a:Action>' ..
73    '<a:MessageID>urn:uuid:#uuid#</a:MessageID>' ..
74    '<a:To s:mustUnderstand="1">' ..
75    'urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01' ..
76    '</a:To>' ..
77    '</s:Header>' ..
78    '<s:Body>' ..
79    '<Probe xmlns="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">' ..
80    '<Duration xmlns="http://schemas.microsoft.com/ws/2008/06/discovery">' ..
81    'PT20S' ..
82    '</Duration>' ..
83    '</Probe>' ..
84    '</s:Body>' ..
85    '</s:Envelope>',
86  }
87}
88
89-- A table that keeps track of received probe matches
90local probe_matches = {}
91
92Util = {
93
94  --- Creates a UUID
95  --
96  -- @return uuid string containing a uuid
97  generateUUID = function()
98    local rnd_bytes = stdnse.tohex(openssl.rand_bytes(16)):lower()
99
100    return ("%s-%s-%s-%s-%s"):format( rnd_bytes:sub(1, 8),
101    rnd_bytes:sub(9, 12), rnd_bytes:sub( 13, 16 ), rnd_bytes:sub( 17, 20 ),
102    rnd_bytes:sub(21, 32) )
103  end,
104
105  --- Retrieves a probe from the probes table by name
106  --
107  -- @param name string containing the name of the probe to retrieve
108  -- @return probe table containing the probe or nil if not found
109  getProbeByName = function( name )
110    for _, probe in ipairs(probes) do
111      if ( probe.name == name ) then
112        return probe
113      end
114    end
115    return
116  end,
117
118  getProbes = function() return probes end,
119
120  sha1sum = function(data) return openssl.sha1(data) end
121
122}
123
124Decoders = {
125
126  --- Decodes a wcf probe response
127  --
128  -- @param data string containing the response as received over the wire
129  -- @return status true on success, false on failure
130  -- @return response table containing the following fields
131  --         <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
132  --         err string containing the error message
133  ['wcf'] = function( data )
134    local response = {}
135
136    -- extracts the messagid, so we can check if we already got a response
137    response.msgid = data:match("<[^:]*:MessageID>urn:uuid:([^<]*)</[^:]*:MessageID>")
138
139    -- if unable to parse msgid return nil
140    if ( not(response.msgid) ) then
141      return false, "No message id was found"
142    end
143
144    response.xaddrs = data:match("<[^:]*:*XAddrs>(.*)</[^:]*:*XAddrs>")
145    response.types = data:match("<[^:]*:Types>[wsdp:]*(.*)</[^:]*:Types>")
146
147    return true, response
148  end,
149
150  --- Decodes a general probe response
151  --
152  -- @param data string containing the response as received over the wire
153  -- @return status true on success, false on failure
154  -- @return response table containing the following fields
155  --         <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
156  --         err string containing the error message
157  ['general'] = function( data )
158    return Decoders['wcf'](data)
159  end,
160
161  --- Decodes an error message received from the service
162  --
163  -- @param data string containing the response as received over the wire
164  -- @return status true on success, false on failure
165  -- @return err string containing the error message
166  ['error'] = function( data )
167    local err = data:match("<SOAP.-ENV:Reason><SOAP.-ENV:Text>(.-)<")
168    local response = "Failed to decode response from device: "
169    .. (err or "Unknown error")
170
171    return true, response
172  end,
173
174}
175
176
177Comm = {
178
179  --- Creates a new Comm instance
180  --
181  -- @param host string containing the host name or ip
182  -- @param port number containing the port to connect to
183  -- @return o a new instance of Comm
184  new = function( self, host, port, mcast )
185    local o = {}
186    setmetatable(o, self)
187    self.__index = self
188    o.host = host
189    o.port = port
190    o.mcast = mcast or false
191    o.sendcount = 2
192    o.timeout = 5000
193    return o
194  end,
195
196  --- Sets the timeout for socket reads
197  setTimeout = function( self, timeout ) self.timeout = timeout end,
198
199  --- Sends a probe over the wire
200  --
201  -- @return status true on success, false on failure
202  sendProbe = function( self )
203    local status, err
204
205    -- replace all instances of #uuid# in the probe
206    local probedata = self.probe.data:gsub("#uuid#", Util.generateUUID())
207
208    if ( self.mcast ) then
209      self.socket = nmap.new_socket("udp")
210      self.socket:set_timeout(self.timeout)
211    else
212      self.socket = nmap.new_socket()
213      self.socket:set_timeout(self.timeout)
214      status, err = self.socket:connect( self.host, self.port, "udp" )
215      if ( not(status) ) then return err end
216    end
217
218    for i=1, self.sendcount do
219      if ( self.mcast ) then
220        status, err = self.socket:sendto( self.host, self.port, probedata )
221      else
222        status, err = self.socket:send( probedata )
223      end
224      if ( not(status) ) then return err end
225    end
226    return true
227  end,
228
229  --- Sets a probe from the <code>probes</code> table to send
230  --
231  -- @param probe table containing a probe from <code>probes</code>
232  setProbe = function( self, probe )
233    self.probe = probe
234  end,
235
236  --- Receives one or more responses for a Probe
237  --
238  -- @return table containing decoded responses suitable for
239  --         <code>stdnse.format_output</code>
240  recvProbeMatches = function( self )
241    local responses = {}
242    repeat
243      local data
244
245      local status, data = self.socket:receive()
246      if ( not(status) ) then
247        if ( data == "TIMEOUT" ) then
248          break
249        else
250          return false, data
251        end
252      end
253
254      local _, ip
255      status, _, _, ip, _ = self.socket:get_info()
256      if( not(status) ) then
257        stdnse.debug3("wsdd.recvProbeMatches: ERROR: Failed to get socket info" )
258        return false, "ERROR: Failed to get socket info"
259      end
260
261      -- push the unparsed response to the response table
262      local status, response = Decoders[self.probe.name]( data )
263      local id, output
264      -- if we failed to decode the response indicate this
265      if ( status ) then
266        output = {}
267        table.insert(output, "Message id: " .. response.msgid)
268        if ( response.xaddrs ) then
269          table.insert(output, "Address: " .. response.xaddrs)
270        end
271        if ( response.types ) then
272          table.insert(output, "Type: " .. response.types)
273        end
274        id = response.msgid
275      else
276        status, response = Decoders["error"](data)
277        output = response
278        id = Util.sha1sum(data)
279      end
280
281      if ( self.mcast and not(probe_matches[id]) ) then
282        if target.ALLOW_NEW_TARGETS then target.add(ip) end
283        table.insert( responses, { name=ip, output } )
284      elseif ( not(probe_matches[id]) ) then
285        responses = output
286      end
287
288      -- avoid duplicates
289      probe_matches[id] = true
290    until( not(self.mcast) )
291
292    -- we're done with the socket
293    self.socket:close()
294
295    return true, responses
296  end
297
298}
299
300Helper = {
301
302  --- Creates a new helper instance
303  --
304  -- @param host string containing the host name or ip
305  -- @param port number containing the port to connect to
306  -- @return o a new instance of Helper
307  new = function( self, host, port )
308    local o = {}
309    setmetatable(o, self)
310    self.__index = self
311    o.host = host
312    o.port = port
313    o.mcast = false
314    o.timeout = 5000
315    return o
316  end,
317
318  --- Instructs the helper to use unconnected sockets supporting multicast
319  --
320  -- @param mcast boolean true if multicast is to be used, false otherwise
321  setMulticast = function( self, mcast )
322    assert( type(mcast)=="boolean", "mcast has to be either true or false")
323    local family = nmap.address_family()
324    self.mcast = mcast
325    self.host = (family=="inet6" and "FF02::C" or "239.255.255.250")
326    self.port = 3702
327  end,
328
329  --- Sets the timeout for socket reads
330  setTimeout = function( self, timeout ) self.timeout = timeout end,
331
332  --- Sends a probe, receives and decodes a probematch
333  --
334  -- @param probename string containing the name of the probe to send
335  --        check <code>probes</code> for available probes
336  -- @return status true on success, false on failure
337  -- @return matches table containing responses, suitable for printing using
338  --         the <code>stdnse.format_output</code> function
339  discoverServices = function( self, probename )
340    if ( not(HAVE_SSL) ) then return false, "The wsdd library requires OpenSSL" end
341
342    local comm = Comm:new(self.host, self.port, self.mcast)
343    local probe = Util.getProbeByName(probename)
344    comm:setProbe( probe )
345    comm:setTimeout( self.timeout )
346
347    local status = comm:sendProbe()
348    if ( not(status) ) then
349      return false, "ERROR: wcf.discoverServices failed"
350    end
351
352    local status, matches = comm:recvProbeMatches()
353    if ( not(status) ) then
354      return false, "ERROR: wcf.recvProbeMatches failed"
355    end
356
357    if ( #matches > 0 ) then matches.name = probe.desc end
358    return true, matches
359  end,
360
361  --- Sends a general probe to attempt to discover WSDD supporting devices
362  --
363  -- @return status true on success, false on failure
364  -- @return matches table containing responses, suitable for printing using
365  --         the <code>stdnse.format_output</code> function
366  discoverDevices = function( self )
367    return self:discoverServices('general')
368  end,
369
370
371  --- Sends a probe that attempts to discover WCF web services
372  --
373  -- @return status true on success, false on failure
374  -- @return matches table containing responses, suitable for printing using
375  --         the <code>stdnse.format_output</code> function
376  discoverWCFServices = function( self )
377    return self:discoverServices('wcf')
378  end,
379
380}
381
382return _ENV;
383