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