Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/fuzzers/dns/dns_fuzzer.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 'bindata'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Exploit::Remote::Udp
10
include Msf::Exploit::Remote::Tcp
11
include Msf::Auxiliary::Fuzzer
12
include Msf::Auxiliary::Scanner
13
14
def initialize
15
super(
16
'Name' => 'DNS and DNSSEC Fuzzer',
17
'Description' => %q{
18
This module will connect to a DNS server and perform DNS and
19
DNSSEC protocol-level fuzzing. Note that this module may inadvertently
20
crash the target server.
21
},
22
'Author' => [ 'pello <fropert[at]packetfault.org>' ],
23
'License' => MSF_LICENSE,
24
'Notes' => {
25
'Stability' => [CRASH_SERVICE_DOWN],
26
'SideEffects' => [],
27
'Reliability' => []
28
}
29
)
30
31
register_options([
32
Opt::RPORT(53),
33
OptInt.new('STARTSIZE', [ false, 'Fuzzing string startsize.', 0]),
34
OptInt.new('ENDSIZE', [ false, 'Max Fuzzing string size. (L2 Frame size)', 500]),
35
OptInt.new('STEPSIZE', [ false, 'Increment fuzzing string each attempt.', 100]),
36
OptInt.new('ERRORHDR', [ false, 'Introduces byte error in the DNS header.', 0]),
37
OptBool.new('CYCLIC', [ false, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),
38
OptInt.new('ITERATIONS', [true, 'Number of iterations to run by test case', 5]),
39
OptString.new('DOMAIN', [ false, 'Force DNS zone domain name.']),
40
OptString.new('IMPORTENUM', [ false, 'Import dns_enum database output and automatically use existing RR.']),
41
OptEnum.new('METHOD', [false, 'Underlayer protocol to use', 'UDP', ['UDP', 'TCP', 'AUTO']]),
42
OptBool.new('DNSSEC', [ false, 'Add DNSsec to each question (UDP payload size, EDNS0, ...)', false]),
43
OptBool.new('TRAILINGNUL', [ false, 'NUL byte terminate DNS names', true]),
44
OptBool.new('RAWPADDING', [ false, 'Generate totally random data from STARTSIZE to ENDSIZE', false]),
45
OptString.new('OPCODE', [ false, 'Comma separated list of opcodes to fuzz. Leave empty to fuzz all fields.', '' ]),
46
# OPCODE accepted values: QUERY,IQUERY,STATUS,UNASSIGNED,NOTIFY,UPDATE
47
OptString.new('CLASS', [ false, 'Comma separated list of classes to fuzz. Leave empty to fuzz all fields.', '' ]),
48
# CLASS accepted values: IN,CH,HS,NONE,ANY
49
OptString.new('RR', [ false, 'Comma separated list of requests to fuzz. Leave empty to fuzz all fields.', '' ])
50
# RR accepted values: A,CNAME,MX,PTR,TXT,AAAA,HINFO,SOA,NS,WKS,RRSIG,DNSKEY,DS,NSEC,NSEC3,NSEC3PARAM
51
# RR accepted values: AFSDB,ISDN,RP,RT,X25,PX,SRV,NAPTR,MD,MF,MB,MG,MR,NULL,MINFO,NSAP,NSAP-PTR,SIG
52
# RR accepted values: KEY,GPOS,LOC,NXT,EID,NIMLOC,ATMA,KX,CERT,A6,DNAME,SINK,OPT,APL,SSHFP,IPSECKEY
53
# RR accepted values: DHCID,HIP,NINFO,RKEY,TALINK,SPF,UINFO,UID,GID,UNSPEC,TKEY,TSIG,IXFR,AXFR,MAILB
54
# RR accepted values: MAIL,*,TA,DLV,RESERVED
55
])
56
end
57
58
class DnsHeader < BinData::Record
59
endian :big
60
uint16 :txid, initial_value: rand(0xffff)
61
bit1 :qr
62
bit4 :opcode
63
bit1 :aa
64
bit1 :tc
65
bit1 :rd
66
bit1 :ra
67
bit3 :z
68
bit4 :rcode
69
uint16 :questions, initial_value: 1
70
uint16 :answerRR
71
uint16 :authorityRR
72
uint16 :additionalRR
73
rest :payload
74
end
75
76
class DnsAddRr < BinData::Record
77
endian :big
78
uint8 :name
79
uint16 :rr_type, initial_value: 0x0029
80
uint16 :payloadsize, initial_value: 0x1000
81
uint8 :highercode
82
uint8 :ednsversion
83
uint8 :zlow
84
uint8 :zhigh, initial_value: 0x80
85
uint16 :datalength
86
end
87
88
def msg
89
"#{rhost}:#{rport} - DNS -"
90
end
91
92
def check_response_construction(pkt)
93
# check if RCODE is not in the unassigned/reserved range
94
if pkt[4].to_i >= 0x17 || (pkt[4].to_i >= 0x0b && pkt[4].to_i <= 0x0f)
95
print_error("#{msg} Server replied incorrectly to the following request:\n#{@lastdata.unpack('H*')}")
96
return false
97
end
98
99
return true
100
end
101
102
def dns_alive(method)
103
connect_udp if method == 'UDP' || method == 'AUTO'
104
connect if method == 'TCP'
105
domain = ''
106
domain << Rex::Text.rand_text_alphanumeric(2..3)
107
domain << '.'
108
if @domain.nil?
109
domain << Rex::Text.rand_text_alphanumeric(3..8)
110
domain << '.'
111
domain << Rex::Text.rand_text_alphanumeric(2)
112
else
113
domain << @domain
114
end
115
116
split_fqdn = domain.split('.')
117
payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }
118
pkt = DnsHeader.new
119
pkt.txid = rand(0xffff)
120
pkt.opcode = 0x0000
121
pkt.payload = payload + "\x00" + "\x00\x01" + "\x00\x01"
122
testing_pkt = pkt.to_binary_s
123
124
if method == 'UDP'
125
udp_sock.put(testing_pkt)
126
res, = udp_sock.recvfrom(65535)
127
disconnect_udp
128
elsif method == 'TCP'
129
sock.put(testing_pkt)
130
res, = sock.get_once(-1, 20)
131
disconnect
132
end
133
134
if res && res.empty?
135
print_error("#{msg} The remote server is not responding to DNS requests.")
136
return false
137
end
138
139
return true
140
end
141
142
def fuzz_padding(payload, size)
143
padding = size - payload.length
144
145
return payload if padding <= 0
146
147
if datastore['CYCLIC']
148
@fuzzdata = Rex::Text.rand_text_alphanumeric(padding)
149
else
150
@fuzzdata = 'A' * padding
151
end
152
153
return payload.ljust(padding, @fuzzdata)
154
end
155
156
def corrupt_header(pkt, nb)
157
len = pkt.length - 1
158
for _ in 0..nb - 1
159
select_byte = rand(len)
160
pkt[select_byte] = [rand(255).to_s].pack('H')
161
end
162
return pkt
163
end
164
165
def random_payload(size)
166
pkt = Array.new
167
for i in 0..size - 1
168
pkt[i] = [rand(255).to_s].pack('H')
169
end
170
return pkt
171
end
172
173
def setup_fqdn(domain, entry)
174
if domain.nil?
175
domain = ''
176
domain << Rex::Text.rand_text_alphanumeric(2..63)
177
domain << '.'
178
domain << Rex::Text.rand_text_alphanumeric(3..63)
179
domain << '.'
180
domain << Rex::Text.rand_text_alphanumeric(2..63)
181
elsif @dnsfile
182
domain = entry + '.' + domain
183
else
184
domain = Rex::Text.rand_text_alphanumeric(2..63) + '.' + domain
185
end
186
187
return domain
188
end
189
190
def import_enum_data(dnsfile)
191
enumdata = Array.new(File.foreach(dnsfile).inject(0) { |c, _line| c + 1 }, 0)
192
idx = 0
193
File.open(dnsfile, 'rb').each_line do |line|
194
line = line.split(',')
195
enumdata[idx] = Hash.new
196
enumdata[idx][:name] = line[0].strip
197
enumdata[idx][:rr] = line[1].strip
198
enumdata[idx][:class] = line[2].strip
199
idx += 1
200
end
201
return enumdata
202
end
203
204
def setup_nsclass(nsclass)
205
classns = ''
206
for idx in nsclass
207
classns << {
208
'IN' => 0x0001, 'CH' => 0x0003, 'HS' => 0x0004,
209
'NONE' => 0x00fd, 'ANY' => 0x00ff
210
}.values_at(idx).pack('n')
211
end
212
return classns
213
end
214
215
def setup_opcode(nsopcode)
216
opcode = ''
217
for idx in nsopcode
218
opcode << {
219
'QUERY' => 0x0000, 'IQUERY' => 0x0001, 'STATUS' => 0x0002,
220
'UNASSIGNED' => 0x0003, 'NOTIFY' => 0x0004, 'UPDATE' => 0x0005
221
}.values_at(idx).pack('n')
222
end
223
return opcode
224
end
225
226
def setup_reqns(nsreq)
227
reqns = ''
228
for idx in nsreq
229
reqns << {
230
'A' => 0x0001, 'NS' => 0x0002, 'MD' => 0x0003, 'MF' => 0x0004,
231
'CNAME' => 0x0005, 'SOA' => 0x0006, 'MB' => 0x0007, 'MG' => 0x0008,
232
'MR' => 0x0009, 'NULL' => 0x000a, 'WKS' => 0x000b, 'PTR' => 0x000c,
233
'HINFO' => 0x000d, 'MINFO' => 0x000e, 'MX' => 0x000f, 'TXT' => 0x0010,
234
'RP' => 0x0011, 'AFSDB' => 0x0012, 'X25' => 0x0013, 'ISDN' => 0x0014,
235
'RT' => 0x0015, 'NSAP' => 0x0016, 'NSAP-PTR' => 0x0017, 'SIG' => 0x0018,
236
'KEY' => 0x0019, 'PX' => 0x001a, 'GPOS' => 0x001b, 'AAAA' => 0x001c,
237
'LOC' => 0x001d, 'NXT' => 0x001e, 'EID' => 0x001f, 'NIMLOC' => 0x0020,
238
'SRV' => 0x0021, 'ATMA' => 0x0022, 'NAPTR' => 0x0023, 'KX' => 0x0024,
239
'CERT' => 0x0025, 'A6' => 0x0026, 'DNAME' => 0x0027, 'SINK' => 0x0028,
240
'OPT' => 0x0029, 'APL' => 0x002a, 'DS' => 0x002b, 'SSHFP' => 0x002c,
241
'IPSECKEY' => 0x002d, 'RRSIG' => 0x002e, 'NSEC' => 0x002f, 'DNSKEY' => 0x0030,
242
'DHCID' => 0x0031, 'NSEC3' => 0x0032, 'NSEC3PARAM' => 0x0033, 'HIP' => 0x0037,
243
'NINFO' => 0x0038, 'RKEY' => 0x0039, 'TALINK' => 0x003a, 'SPF' => 0x0063,
244
'UINFO' => 0x0064, 'UID' => 0x0065, 'GID' => 0x0066, 'UNSPEC' => 0x0067,
245
'TKEY' => 0x00f9, 'TSIG' => 0x00fa, 'IXFR' => 0x00fb, 'AXFR' => 0x00fc,
246
'MAILA' => 0x00fd, 'MAILB' => 0x00fe, '*' => 0x00ff, 'TA' => 0x8000,
247
'DLV' => 0x8001, 'RESERVED' => 0xffff
248
}.values_at(idx).pack('n')
249
end
250
return reqns
251
end
252
253
def build_packet(dns_opcode, dnssec, trailingnul, reqns, classns, payload)
254
pkt = DnsHeader.new
255
pkt.opcode = dns_opcode
256
if trailingnul
257
if @dnsfile
258
pkt.payload = payload + "\x00" + reqns + classns
259
else
260
pkt.payload = payload + "\x00" + [reqns].pack('n') + [classns].pack('n')
261
end
262
elsif @dnsfile
263
pkt.payload = payload + [rand(1..255).to_s].pack('H') + reqns + classns
264
else
265
pkt.payload = payload + [rand(1..255).to_s].pack('H') + [dns_req].pack('n') + [dns_class].pack('n')
266
end
267
268
if dnssec
269
dnssecpkt = DnsAddRr.new
270
pkt.additionalRR = 1
271
pkt.payload = dnssecpkt.to_binary_s
272
end
273
274
pkt.to_binary_s
275
end
276
277
def dns_send(data, method)
278
method = 'UDP' if method == 'AUTO' && data.length < 512
279
method = 'TCP' if method == 'AUTO' && data.length >= 512
280
281
connect_udp if method == 'UDP'
282
connect if method == 'TCP'
283
udp_sock.put(data) if method == 'UDP'
284
sock.put(data) if method == 'TCP'
285
286
res, = udp_sock.recvfrom(65535, 1) if method == 'UDP'
287
res, = sock.get_once(-1, 1) if method == 'TCP'
288
289
disconnect_udp if method == 'UDP'
290
disconnect if method == 'TCP'
291
292
if res && res.empty?
293
@fail_count += 1
294
if @fail_count == 1
295
@probably_vuln = @lastdata if !@lastdata.nil?
296
elsif @fail_count >= 3
297
if dns_alive(method) == false
298
if @lastdata
299
print_error("#{msg} DNS is DOWN since the request:")
300
print_error(lastdata.unpack('H*'))
301
else
302
print_error("#{msg} DNS is DOWN")
303
end
304
return false
305
end
306
end
307
return true
308
elsif res && !res.empty?
309
@lastdata = data
310
if res[3].to_i >= 0x8000 # ignore server response as a query
311
@fail_count = 0
312
return true
313
end
314
315
if @rawpadding
316
@fail_count = 0
317
return true
318
end
319
320
if check_response_construction(res)
321
@fail_count = 0
322
return true
323
end
324
325
return false
326
end
327
end
328
329
def fix_variables
330
@fuzz_opcode = datastore['OPCODE'].blank? ? 'QUERY,IQUERY,STATUS,UNASSIGNED,NOTIFY,UPDATE' : datastore['OPCODE']
331
@fuzz_class = datastore['CLASS'].blank? ? 'IN,CH,HS,NONE,ANY' : datastore['CLASS']
332
fuzz_rr_queries = 'A,NS,MD,MF,CNAME,SOA,MB,MG,MR,NULL,WKS,PTR,' \
333
'HINFO,MINFO,MX,TXT,RP,AFSDB,X25,ISDN,RT,' \
334
'NSAP,NSAP-PTR,SIG,KEY,PX,GPOS,AAAA,LOC,NXT,' \
335
'EID,NIMLOC,SRV,ATMA,NAPTR,KX,CERT,A6,DNAME,' \
336
'SINK,OPT,APL,DS,SSHFP,IPSECKEY,RRSIG,NSEC,' \
337
'DNSKEY,DHCID,NSEC3,NSEC3PARAM,HIP,NINFO,RKEY,' \
338
'TALINK,SPF,UINFO,UID,GID,UNSPEC,TKEY,TSIG,' \
339
'IXFR,AXFR,MAILA,MAILB,*,TA,DLV,RESERVED'
340
@fuzz_rr = datastore['RR'].blank? ? fuzz_rr_queries : datastore['RR']
341
end
342
343
def run_host(ip)
344
msg = "#{ip}:#{rhost} - DNS -"
345
@lastdata = nil
346
@probably_vuln = nil
347
@startsize = datastore['STARTSIZE']
348
@stepsize = datastore['STEPSIZE']
349
@endsize = datastore['ENDSIZE']
350
@underlayer_protocol = datastore['METHOD']
351
@fail_count = 0
352
@domain = datastore['DOMAIN']
353
@dnsfile = datastore['IMPORTENUM']
354
@rawpadding = datastore['RAWPADDING']
355
iter = datastore['ITERATIONS']
356
dnssec = datastore['DNSSEC']
357
errorhdr = datastore['ERRORHDR']
358
trailingnul = datastore['TRAILINGNUL']
359
360
fix_variables
361
362
return false if !dns_alive(@underlayer_protocol)
363
364
print_status("#{msg} Fuzzing DNS server, this may take a while.")
365
366
if @startsize < 12 && @startsize > 0
367
print_status("#{msg} STARTSIZE must be at least 12. STARTSIZE value has been modified.")
368
@startsize = 12
369
end
370
371
if @rawpadding
372
if @domain.nil?
373
print_status('DNS Fuzzer: DOMAIN could be set for health check but not mandatory.')
374
end
375
nsopcode = @fuzz_opcode.split(',')
376
opcode = setup_opcode(nsopcode)
377
opcode.unpack('n*').each do |dns_opcode|
378
1.upto(iter) do
379
while @startsize <= @endsize
380
data = random_payload(@startsize).to_s
381
data[2] = 0x0
382
data[3] = dns_opcode
383
return false if !dns_send(data, @underlayer_protocol)
384
385
@lastdata = data
386
@startsize += @stepsize
387
end
388
@startsize = datastore['STARTSIZE']
389
end
390
end
391
return
392
end
393
394
if @dnsfile
395
if @domain.nil?
396
print_error('DNS Fuzzer: Domain variable must be set.')
397
return
398
end
399
400
dnsenumdata = import_enum_data(@dnsfile)
401
nsreq = []
402
nsclass = []
403
nsentry = []
404
for req, _ in dnsenumdata
405
nsreq << req[:rr]
406
nsclass << req[:class]
407
nsentry << req[:name]
408
end
409
nsopcode = @fuzz_opcode.split(',')
410
else
411
nsreq = @fuzz_rr.split(',')
412
nsopcode = @fuzz_opcode.split(',')
413
nsclass = @fuzz_class.split(',')
414
begin
415
classns = setup_nsclass(nsclass)
416
raise ArgumentError, "Invalid CLASS: #{nsclass.inspect}" unless classns
417
418
opcode = setup_opcode(nsopcode)
419
raise ArgumentError, "Invalid OPCODE: #{opcode.inspect}" unless nsopcode
420
421
reqns = setup_reqns(nsreq)
422
raise ArgumentError, "Invalid RR: #{nsreq.inspect}" unless nsreq
423
rescue StandardError => e
424
print_error("DNS Fuzzer error, aborting: #{e}")
425
return
426
end
427
end
428
429
for question in nsreq
430
case question
431
when 'RRSIG', 'DNSKEY', 'DS', 'NSEC', 'NSEC3', 'NSEC3PARAM'
432
dnssec = true
433
end
434
end
435
436
if @dnsfile
437
classns = setup_nsclass(nsclass)
438
reqns = setup_reqns(nsreq)
439
opcode = setup_opcode(nsopcode)
440
opcode.unpack('n*').each do |dns_opcode|
441
for i in 0..nsentry.length - 1
442
reqns = setup_reqns(nsreq[i])
443
classns = setup_nsclass(nsclass[i])
444
1.upto(iter) do
445
nsdomain = setup_fqdn(@domain, nsentry[i])
446
split_fqdn = nsdomain.split('.')
447
payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }
448
pkt = build_packet(dns_opcode, dnssec, trailingnul, reqns, classns, payload)
449
pkt = corrupt_header(pkt, errorhdr) if errorhdr > 0
450
if @startsize == 0 && !dns_send(pkt, @underlayer_protocol)
451
break
452
end
453
454
while @startsize <= @endsize
455
pkt = fuzz_padding(pkt, @startsize)
456
break if !dns_send(pkt, @underlayer_protocol)
457
458
@startsize += @stepsize
459
end
460
@startsize = datastore['STARTSIZE']
461
end
462
end
463
end
464
else
465
classns.unpack('n*').each do |dns_class|
466
opcode.unpack('n*').each do |dns_opcode|
467
reqns.unpack('n*').each do |dns_req|
468
1.upto(iter) do
469
nsdomain = setup_fqdn(@domain, '')
470
split_fqdn = nsdomain.split('.')
471
payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }
472
pkt = build_packet(dns_opcode, dnssec, trailingnul, dns_req, dns_class, payload)
473
pkt = corrupt_header(pkt, errorhdr) if errorhdr > 0
474
if @startsize == 0 && !dns_send(pkt, @underlayer_protocol)
475
break
476
end
477
478
while @startsize <= @endsize
479
pkt = fuzz_padding(pkt, @startsize)
480
break if !dns_send(pkt, @underlayer_protocol)
481
482
@startsize += @stepsize
483
end
484
@startsize = datastore['STARTSIZE']
485
end
486
end
487
end
488
end
489
end
490
end
491
end
492
493