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