|
| 1 | +## |
| 2 | +# This module requires Metasploit: https://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +class MetasploitModule < Msf::Auxiliary |
| 7 | + include Msf::Auxiliary::Report |
| 8 | + include Msf::Auxiliary::UDPScanner |
| 9 | + |
| 10 | + def initialize |
| 11 | + super( |
| 12 | + 'Name' => 'WS-Discovery Information Discovery', |
| 13 | + 'Description' => %q{ |
| 14 | + Discover information from Web Services Dynamic Discovery (WS-Discovery) |
| 15 | + enabled systems. |
| 16 | + }, |
| 17 | + 'Author' => 'Brendan Coles <bcoles[at]gmail.com>', |
| 18 | + 'License' => MSF_LICENSE, |
| 19 | + 'References' => |
| 20 | + [ |
| 21 | + ['URL', 'https://msdn.microsoft.com/en-us/library/windows/desktop/bb513684(v=vs.85).aspx'], |
| 22 | + ['URL', 'http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pd'], |
| 23 | + ['URL', 'https://en.wikipedia.org/wiki/Web_Services_for_Devices'], |
| 24 | + ['URL', 'https://en.wikipedia.org/wiki/WS-Discovery'], |
| 25 | + ['URL', 'https://en.wikipedia.org/wiki/Zero-configuration_networking#WS-Discovery'] |
| 26 | + ] |
| 27 | + ) |
| 28 | + register_options [ |
| 29 | + Opt::RPORT(3702), |
| 30 | + OptAddressRange.new('RHOSTS', [true, 'The multicast address or CIDR range of targets to query', '239.255.255.250']) |
| 31 | + ] |
| 32 | + end |
| 33 | + |
| 34 | + def rport |
| 35 | + datastore['RPORT'] |
| 36 | + end |
| 37 | + |
| 38 | + def wsdd_probe |
| 39 | + probe = '<?xml version="1.0" encoding="utf-8" ?>' |
| 40 | + probe << '<soap:Envelope' |
| 41 | + probe << ' xmlns:soap="http://www.w3.org/2003/05/soap-envelope"' |
| 42 | + probe << ' xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"' |
| 43 | + probe << ' xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery"' |
| 44 | + probe << ' xmlns:wsdp="http://schemas.xmlsoap.org/ws/2006/02/devprof">' |
| 45 | + |
| 46 | + probe << '<soap:Header>' |
| 47 | + # WS-Discovery |
| 48 | + probe << '<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>' |
| 49 | + # Action (Probe) |
| 50 | + probe << "<wsa:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>" |
| 51 | + # Message identifier (unique GUID) |
| 52 | + probe << "<wsa:MessageID>urn:uuid:#{SecureRandom.uuid}</wsa:MessageID>" |
| 53 | + probe << '</soap:Header>' |
| 54 | + |
| 55 | + probe << '<soap:Body>' |
| 56 | + probe << '<wsd:Probe/>' # WS-Discovery type (blank) |
| 57 | + probe << '</soap:Body>' |
| 58 | + probe << '</env:Envelope>' |
| 59 | + |
| 60 | + probe |
| 61 | + end |
| 62 | + |
| 63 | + def scanner_prescan(batch) |
| 64 | + print_status "Sending WS-Discovery probe to #{batch.length} hosts" |
| 65 | + @results = {} |
| 66 | + end |
| 67 | + |
| 68 | + def scan_host(ip) |
| 69 | + vprint_status "#{ip}:#{rport} - Sending WS-Discovery probe" |
| 70 | + scanner_send wsdd_probe, ip, datastore['RPORT'] |
| 71 | + end |
| 72 | + |
| 73 | + def scanner_postscan(_batch) |
| 74 | + if @results.empty? |
| 75 | + print_status 'No WS-Discovery endpoints found.' |
| 76 | + return |
| 77 | + end |
| 78 | + |
| 79 | + found = {} |
| 80 | + @results.each_pair do |ip, responses| |
| 81 | + responses.uniq.each do |res| |
| 82 | + found[ip] ||= {} |
| 83 | + next if found[ip][res] |
| 84 | + |
| 85 | + response_info = parse_wsdd_response res |
| 86 | + |
| 87 | + if response_info.nil? |
| 88 | + print_error "#{ip} responded with malformed data" |
| 89 | + next |
| 90 | + end |
| 91 | + |
| 92 | + msg = [] |
| 93 | + msg << "Address: #{response_info['Address']}" |
| 94 | + msg << "Types: #{response_info['Types'].to_s.split(/\s+/).join(', ')}" |
| 95 | + msg << "Vendor Extensions: #{response_info['VendorExtension']}" unless response_info['VendorExtension'].nil? |
| 96 | + |
| 97 | + print_good "#{ip} responded with:\n#{msg.join("\n")}" |
| 98 | + |
| 99 | + report_service(host: ip, port: rport, proto: 'udp', name: 'wsdd', info: response_info) |
| 100 | + found[ip][res] = true |
| 101 | + end |
| 102 | + end |
| 103 | + end |
| 104 | + |
| 105 | + def parse_wsdd_response(wsdd_res) |
| 106 | + info = {} |
| 107 | + |
| 108 | + # Validate ProbeMatches SOAP response contains a ProbeMatch |
| 109 | + begin |
| 110 | + soap = ::Nokogiri::XML wsdd_res |
| 111 | + return nil if soap.xpath('//soap:Body//wsd:ProbeMatches//wsd:ProbeMatch').empty? |
| 112 | + rescue |
| 113 | + return nil |
| 114 | + end |
| 115 | + |
| 116 | + # Convert SOAP response to Hash |
| 117 | + begin |
| 118 | + res = Hash.from_xml wsdd_res |
| 119 | + rescue REXML::ParseException |
| 120 | + return nil |
| 121 | + end |
| 122 | + |
| 123 | + # Use the first ProbeMatch |
| 124 | + probe_match = res['Envelope']['Body']['ProbeMatches'].first |
| 125 | + return nil unless probe_match[0].eql? 'ProbeMatch' |
| 126 | + return nil if probe_match[1].nil? || probe_match[1].empty? |
| 127 | + match = probe_match[1] |
| 128 | + |
| 129 | + # Device Address |
| 130 | + info['Address'] = match['XAddrs'] || '' |
| 131 | + |
| 132 | + # Device Types |
| 133 | + info['Types'] = match['Types'] || '' |
| 134 | + |
| 135 | + # Optional vendor extensions |
| 136 | + unless match['VendorExtension'].nil? || match['VendorExtension'].empty? |
| 137 | + info['VendorExtension'] = match['VendorExtension'] |
| 138 | + end |
| 139 | + |
| 140 | + info |
| 141 | + end |
| 142 | +end |
0 commit comments