Path: blob/master/modules/auxiliary/fuzzers/dns/dns_fuzzer.rb
21545 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'bindata'67class MetasploitModule < Msf::Auxiliary8include Msf::Exploit::Remote::Udp9include Msf::Exploit::Remote::Tcp10include Msf::Auxiliary::Fuzzer11include Msf::Auxiliary::Scanner1213def initialize14super(15'Name' => 'DNS and DNSSEC Fuzzer',16'Description' => %q{17This module will connect to a DNS server and perform DNS and18DNSSEC protocol-level fuzzing. Note that this module may inadvertently19crash the target server.20},21'Author' => [ 'pello <fropert[at]packetfault.org>' ],22'License' => MSF_LICENSE,23'Notes' => {24'Stability' => [CRASH_SERVICE_DOWN],25'SideEffects' => [],26'Reliability' => []27}28)2930register_options([31Opt::RPORT(53),32OptInt.new('STARTSIZE', [ false, 'Fuzzing string startsize.', 0]),33OptInt.new('ENDSIZE', [ false, 'Max Fuzzing string size. (L2 Frame size)', 500]),34OptInt.new('STEPSIZE', [ false, 'Increment fuzzing string each attempt.', 100]),35OptInt.new('ERRORHDR', [ false, 'Introduces byte error in the DNS header.', 0]),36OptBool.new('CYCLIC', [ false, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),37OptInt.new('ITERATIONS', [true, 'Number of iterations to run by test case', 5]),38OptString.new('DOMAIN', [ false, 'Force DNS zone domain name.']),39OptString.new('IMPORTENUM', [ false, 'Import dns_enum database output and automatically use existing RR.']),40OptEnum.new('METHOD', [false, 'Underlayer protocol to use', 'UDP', ['UDP', 'TCP', 'AUTO']]),41OptBool.new('DNSSEC', [ false, 'Add DNSsec to each question (UDP payload size, EDNS0, ...)', false]),42OptBool.new('TRAILINGNUL', [ false, 'NUL byte terminate DNS names', true]),43OptBool.new('RAWPADDING', [ false, 'Generate totally random data from STARTSIZE to ENDSIZE', false]),44OptString.new('OPCODE', [ false, 'Comma separated list of opcodes to fuzz. Leave empty to fuzz all fields.', '' ]),45# OPCODE accepted values: QUERY,IQUERY,STATUS,UNASSIGNED,NOTIFY,UPDATE46OptString.new('CLASS', [ false, 'Comma separated list of classes to fuzz. Leave empty to fuzz all fields.', '' ]),47# CLASS accepted values: IN,CH,HS,NONE,ANY48OptString.new('RR', [ false, 'Comma separated list of requests to fuzz. Leave empty to fuzz all fields.', '' ])49# RR accepted values: A,CNAME,MX,PTR,TXT,AAAA,HINFO,SOA,NS,WKS,RRSIG,DNSKEY,DS,NSEC,NSEC3,NSEC3PARAM50# RR accepted values: AFSDB,ISDN,RP,RT,X25,PX,SRV,NAPTR,MD,MF,MB,MG,MR,NULL,MINFO,NSAP,NSAP-PTR,SIG51# RR accepted values: KEY,GPOS,LOC,NXT,EID,NIMLOC,ATMA,KX,CERT,A6,DNAME,SINK,OPT,APL,SSHFP,IPSECKEY52# RR accepted values: DHCID,HIP,NINFO,RKEY,TALINK,SPF,UINFO,UID,GID,UNSPEC,TKEY,TSIG,IXFR,AXFR,MAILB53# RR accepted values: MAIL,*,TA,DLV,RESERVED54])55end5657class DnsHeader < BinData::Record58endian :big59uint16 :txid, initial_value: rand(0xffff)60bit1 :qr61bit4 :opcode62bit1 :aa63bit1 :tc64bit1 :rd65bit1 :ra66bit3 :z67bit4 :rcode68uint16 :questions, initial_value: 169uint16 :answerRR70uint16 :authorityRR71uint16 :additionalRR72rest :payload73end7475class DnsAddRr < BinData::Record76endian :big77uint8 :name78uint16 :rr_type, initial_value: 0x002979uint16 :payloadsize, initial_value: 0x100080uint8 :highercode81uint8 :ednsversion82uint8 :zlow83uint8 :zhigh, initial_value: 0x8084uint16 :datalength85end8687def msg88"#{rhost}:#{rport} - DNS -"89end9091def check_response_construction(pkt)92# check if RCODE is not in the unassigned/reserved range93if pkt[4].to_i >= 0x17 || (pkt[4].to_i >= 0x0b && pkt[4].to_i <= 0x0f)94print_error("#{msg} Server replied incorrectly to the following request:\n#{@lastdata.unpack('H*')}")95return false96end9798return true99end100101def dns_alive(method)102connect_udp if method == 'UDP' || method == 'AUTO'103connect if method == 'TCP'104domain = ''105domain << Rex::Text.rand_text_alphanumeric(2..3)106domain << '.'107if @domain.nil?108domain << Rex::Text.rand_text_alphanumeric(3..8)109domain << '.'110domain << Rex::Text.rand_text_alphanumeric(2)111else112domain << @domain113end114115split_fqdn = domain.split('.')116payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }117pkt = DnsHeader.new118pkt.txid = rand(0xffff)119pkt.opcode = 0x0000120pkt.payload = payload + "\x00" + "\x00\x01" + "\x00\x01"121testing_pkt = pkt.to_binary_s122123if method == 'UDP'124udp_sock.put(testing_pkt)125res, = udp_sock.recvfrom(65535)126disconnect_udp127elsif method == 'TCP'128sock.put(testing_pkt)129res, = sock.get_once(-1, 20)130disconnect131end132133if res && res.empty?134print_error("#{msg} The remote server is not responding to DNS requests.")135return false136end137138return true139end140141def fuzz_padding(payload, size)142padding = size - payload.length143144return payload if padding <= 0145146if datastore['CYCLIC']147@fuzzdata = Rex::Text.rand_text_alphanumeric(padding)148else149@fuzzdata = 'A' * padding150end151152return payload.ljust(padding, @fuzzdata)153end154155def corrupt_header(pkt, nb)156len = pkt.length - 1157for _ in 0..nb - 1158select_byte = rand(len)159pkt[select_byte] = [rand(255).to_s].pack('H')160end161return pkt162end163164def random_payload(size)165pkt = Array.new166for i in 0..size - 1167pkt[i] = [rand(255).to_s].pack('H')168end169return pkt170end171172def setup_fqdn(domain, entry)173if domain.nil?174domain = ''175domain << Rex::Text.rand_text_alphanumeric(2..63)176domain << '.'177domain << Rex::Text.rand_text_alphanumeric(3..63)178domain << '.'179domain << Rex::Text.rand_text_alphanumeric(2..63)180elsif @dnsfile181domain = entry + '.' + domain182else183domain = Rex::Text.rand_text_alphanumeric(2..63) + '.' + domain184end185186return domain187end188189def import_enum_data(dnsfile)190enumdata = Array.new(File.foreach(dnsfile).inject(0) { |c, _line| c + 1 }, 0)191idx = 0192File.open(dnsfile, 'rb').each_line do |line|193line = line.split(',')194enumdata[idx] = Hash.new195enumdata[idx][:name] = line[0].strip196enumdata[idx][:rr] = line[1].strip197enumdata[idx][:class] = line[2].strip198idx += 1199end200return enumdata201end202203def setup_nsclass(nsclass)204classns = ''205for idx in nsclass206classns << {207'IN' => 0x0001, 'CH' => 0x0003, 'HS' => 0x0004,208'NONE' => 0x00fd, 'ANY' => 0x00ff209}.values_at(idx).pack('n')210end211return classns212end213214def setup_opcode(nsopcode)215opcode = ''216for idx in nsopcode217opcode << {218'QUERY' => 0x0000, 'IQUERY' => 0x0001, 'STATUS' => 0x0002,219'UNASSIGNED' => 0x0003, 'NOTIFY' => 0x0004, 'UPDATE' => 0x0005220}.values_at(idx).pack('n')221end222return opcode223end224225def setup_reqns(nsreq)226reqns = ''227for idx in nsreq228reqns << {229'A' => 0x0001, 'NS' => 0x0002, 'MD' => 0x0003, 'MF' => 0x0004,230'CNAME' => 0x0005, 'SOA' => 0x0006, 'MB' => 0x0007, 'MG' => 0x0008,231'MR' => 0x0009, 'NULL' => 0x000a, 'WKS' => 0x000b, 'PTR' => 0x000c,232'HINFO' => 0x000d, 'MINFO' => 0x000e, 'MX' => 0x000f, 'TXT' => 0x0010,233'RP' => 0x0011, 'AFSDB' => 0x0012, 'X25' => 0x0013, 'ISDN' => 0x0014,234'RT' => 0x0015, 'NSAP' => 0x0016, 'NSAP-PTR' => 0x0017, 'SIG' => 0x0018,235'KEY' => 0x0019, 'PX' => 0x001a, 'GPOS' => 0x001b, 'AAAA' => 0x001c,236'LOC' => 0x001d, 'NXT' => 0x001e, 'EID' => 0x001f, 'NIMLOC' => 0x0020,237'SRV' => 0x0021, 'ATMA' => 0x0022, 'NAPTR' => 0x0023, 'KX' => 0x0024,238'CERT' => 0x0025, 'A6' => 0x0026, 'DNAME' => 0x0027, 'SINK' => 0x0028,239'OPT' => 0x0029, 'APL' => 0x002a, 'DS' => 0x002b, 'SSHFP' => 0x002c,240'IPSECKEY' => 0x002d, 'RRSIG' => 0x002e, 'NSEC' => 0x002f, 'DNSKEY' => 0x0030,241'DHCID' => 0x0031, 'NSEC3' => 0x0032, 'NSEC3PARAM' => 0x0033, 'HIP' => 0x0037,242'NINFO' => 0x0038, 'RKEY' => 0x0039, 'TALINK' => 0x003a, 'SPF' => 0x0063,243'UINFO' => 0x0064, 'UID' => 0x0065, 'GID' => 0x0066, 'UNSPEC' => 0x0067,244'TKEY' => 0x00f9, 'TSIG' => 0x00fa, 'IXFR' => 0x00fb, 'AXFR' => 0x00fc,245'MAILA' => 0x00fd, 'MAILB' => 0x00fe, '*' => 0x00ff, 'TA' => 0x8000,246'DLV' => 0x8001, 'RESERVED' => 0xffff247}.values_at(idx).pack('n')248end249return reqns250end251252def build_packet(dns_opcode, dnssec, trailingnul, reqns, classns, payload)253pkt = DnsHeader.new254pkt.opcode = dns_opcode255if trailingnul256if @dnsfile257pkt.payload = payload + "\x00" + reqns + classns258else259pkt.payload = payload + "\x00" + [reqns].pack('n') + [classns].pack('n')260end261elsif @dnsfile262pkt.payload = payload + [rand(1..255).to_s].pack('H') + reqns + classns263else264pkt.payload = payload + [rand(1..255).to_s].pack('H') + [dns_req].pack('n') + [dns_class].pack('n')265end266267if dnssec268dnssecpkt = DnsAddRr.new269pkt.additionalRR = 1270pkt.payload = dnssecpkt.to_binary_s271end272273pkt.to_binary_s274end275276def dns_send(data, method)277method = 'UDP' if method == 'AUTO' && data.length < 512278method = 'TCP' if method == 'AUTO' && data.length >= 512279280connect_udp if method == 'UDP'281connect if method == 'TCP'282udp_sock.put(data) if method == 'UDP'283sock.put(data) if method == 'TCP'284285res, = udp_sock.recvfrom(65535, 1) if method == 'UDP'286res, = sock.get_once(-1, 1) if method == 'TCP'287288disconnect_udp if method == 'UDP'289disconnect if method == 'TCP'290291if res && res.empty?292@fail_count += 1293if @fail_count == 1294@probably_vuln = @lastdata if !@lastdata.nil?295elsif @fail_count >= 3296if dns_alive(method) == false297if @lastdata298print_error("#{msg} DNS is DOWN since the request:")299print_error(lastdata.unpack('H*'))300else301print_error("#{msg} DNS is DOWN")302end303return false304end305end306return true307elsif res && !res.empty?308@lastdata = data309if res[3].to_i >= 0x8000 # ignore server response as a query310@fail_count = 0311return true312end313314if @rawpadding315@fail_count = 0316return true317end318319if check_response_construction(res)320@fail_count = 0321return true322end323324return false325end326end327328def fix_variables329@fuzz_opcode = datastore['OPCODE'].blank? ? 'QUERY,IQUERY,STATUS,UNASSIGNED,NOTIFY,UPDATE' : datastore['OPCODE']330@fuzz_class = datastore['CLASS'].blank? ? 'IN,CH,HS,NONE,ANY' : datastore['CLASS']331fuzz_rr_queries = 'A,NS,MD,MF,CNAME,SOA,MB,MG,MR,NULL,WKS,PTR,' \332'HINFO,MINFO,MX,TXT,RP,AFSDB,X25,ISDN,RT,' \333'NSAP,NSAP-PTR,SIG,KEY,PX,GPOS,AAAA,LOC,NXT,' \334'EID,NIMLOC,SRV,ATMA,NAPTR,KX,CERT,A6,DNAME,' \335'SINK,OPT,APL,DS,SSHFP,IPSECKEY,RRSIG,NSEC,' \336'DNSKEY,DHCID,NSEC3,NSEC3PARAM,HIP,NINFO,RKEY,' \337'TALINK,SPF,UINFO,UID,GID,UNSPEC,TKEY,TSIG,' \338'IXFR,AXFR,MAILA,MAILB,*,TA,DLV,RESERVED'339@fuzz_rr = datastore['RR'].blank? ? fuzz_rr_queries : datastore['RR']340end341342def run_host(ip)343msg = "#{ip}:#{rhost} - DNS -"344@lastdata = nil345@probably_vuln = nil346@startsize = datastore['STARTSIZE']347@stepsize = datastore['STEPSIZE']348@endsize = datastore['ENDSIZE']349@underlayer_protocol = datastore['METHOD']350@fail_count = 0351@domain = datastore['DOMAIN']352@dnsfile = datastore['IMPORTENUM']353@rawpadding = datastore['RAWPADDING']354iter = datastore['ITERATIONS']355dnssec = datastore['DNSSEC']356errorhdr = datastore['ERRORHDR']357trailingnul = datastore['TRAILINGNUL']358359fix_variables360361return false if !dns_alive(@underlayer_protocol)362363print_status("#{msg} Fuzzing DNS server, this may take a while.")364365if @startsize < 12 && @startsize > 0366print_status("#{msg} STARTSIZE must be at least 12. STARTSIZE value has been modified.")367@startsize = 12368end369370if @rawpadding371if @domain.nil?372print_status('DNS Fuzzer: DOMAIN could be set for health check but not mandatory.')373end374nsopcode = @fuzz_opcode.split(',')375opcode = setup_opcode(nsopcode)376opcode.unpack('n*').each do |dns_opcode|3771.upto(iter) do378while @startsize <= @endsize379data = random_payload(@startsize).to_s380data[2] = 0x0381data[3] = dns_opcode382return false if !dns_send(data, @underlayer_protocol)383384@lastdata = data385@startsize += @stepsize386end387@startsize = datastore['STARTSIZE']388end389end390return391end392393if @dnsfile394if @domain.nil?395print_error('DNS Fuzzer: Domain variable must be set.')396return397end398399dnsenumdata = import_enum_data(@dnsfile)400nsreq = []401nsclass = []402nsentry = []403for req, _ in dnsenumdata404nsreq << req[:rr]405nsclass << req[:class]406nsentry << req[:name]407end408nsopcode = @fuzz_opcode.split(',')409else410nsreq = @fuzz_rr.split(',')411nsopcode = @fuzz_opcode.split(',')412nsclass = @fuzz_class.split(',')413begin414classns = setup_nsclass(nsclass)415raise ArgumentError, "Invalid CLASS: #{nsclass.inspect}" unless classns416417opcode = setup_opcode(nsopcode)418raise ArgumentError, "Invalid OPCODE: #{opcode.inspect}" unless nsopcode419420reqns = setup_reqns(nsreq)421raise ArgumentError, "Invalid RR: #{nsreq.inspect}" unless nsreq422rescue StandardError => e423print_error("DNS Fuzzer error, aborting: #{e}")424return425end426end427428for question in nsreq429case question430when 'RRSIG', 'DNSKEY', 'DS', 'NSEC', 'NSEC3', 'NSEC3PARAM'431dnssec = true432end433end434435if @dnsfile436classns = setup_nsclass(nsclass)437reqns = setup_reqns(nsreq)438opcode = setup_opcode(nsopcode)439opcode.unpack('n*').each do |dns_opcode|440for i in 0..nsentry.length - 1441reqns = setup_reqns(nsreq[i])442classns = setup_nsclass(nsclass[i])4431.upto(iter) do444nsdomain = setup_fqdn(@domain, nsentry[i])445split_fqdn = nsdomain.split('.')446payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }447pkt = build_packet(dns_opcode, dnssec, trailingnul, reqns, classns, payload)448pkt = corrupt_header(pkt, errorhdr) if errorhdr > 0449if @startsize == 0 && !dns_send(pkt, @underlayer_protocol)450break451end452453while @startsize <= @endsize454pkt = fuzz_padding(pkt, @startsize)455break if !dns_send(pkt, @underlayer_protocol)456457@startsize += @stepsize458end459@startsize = datastore['STARTSIZE']460end461end462end463else464classns.unpack('n*').each do |dns_class|465opcode.unpack('n*').each do |dns_opcode|466reqns.unpack('n*').each do |dns_req|4671.upto(iter) do468nsdomain = setup_fqdn(@domain, '')469split_fqdn = nsdomain.split('.')470payload = split_fqdn.inject('') { |a, x| a + [x.length, x].pack('CA*') }471pkt = build_packet(dns_opcode, dnssec, trailingnul, dns_req, dns_class, payload)472pkt = corrupt_header(pkt, errorhdr) if errorhdr > 0473if @startsize == 0 && !dns_send(pkt, @underlayer_protocol)474break475end476477while @startsize <= @endsize478pkt = fuzz_padding(pkt, @startsize)479break if !dns_send(pkt, @underlayer_protocol)480481@startsize += @stepsize482end483@startsize = datastore['STARTSIZE']484end485end486end487end488end489end490end491492493