# encoding: binary

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Capture
  include Msf::Auxiliary::UDPScanner
  include Msf::Auxiliary::DRDoS

  def initialize
    super(
      'Name' => 'DNS Amplification Scanner',
      'Description' => %q{
          This module can be used to discover DNS servers which expose recursive
          name lookups which can be used in an amplification attack against a
          third party.
      },
      'Author' => [ 'xistence <xistence[at]0x90.nl>'], # Original scanner module
      'License' => MSF_LICENSE,
      'References' => [
        ['CVE', '2006-0987'],
        ['CVE', '2006-0988'],
      ]
    )

    register_options([
      Opt::RPORT(53),
      OptString.new('DOMAINNAME', [true, 'Domain to use for the DNS request', 'isc.org' ]),
      OptString.new('QUERYTYPE', [true, 'Query type(A, NS, SOA, MX, TXT, AAAA, RRSIG, DNSKEY, ANY)', 'ANY' ]),
    ])
  end

  def rport
    datastore['RPORT']
  end

  def setup
    super

    # Check for DNS query types byte
    case datastore['QUERYTYPE']
    when 'A'
      querypacket = "\x01"
    when 'NS'
      querypacket = "\x02"
    when 'SOA'
      querypacket = "\x06"
    when 'MX'
      querypacket = "\x0f"
    when 'TXT'
      querypacket = "\x10"
    when 'AAAA'
      querypacket = "\x1c"
    when 'RRSIG'
      querypacket = "\x2e"
    when 'DNSKEY'
      querypacket = "\x30"
    when 'ANY'
      querypacket = "\xff"
    else
      print_error("Invalid query type!")
      return
    end

    targdomainpacket = []
    # Before every part of the domainname there should be the length of that part (instead of a ".")
    # So isc.org divided is 3isc3org
    datastore['DOMAINNAME'].split('.').each do |domainpart|
      # The length of the domain part in hex
      domainpartlength = "%02x" % domainpart.length
      # Convert the name part to a hex string
      domainpart = domainpart.each_byte.map { |b| b.to_s(16) }.join()
      # Combine the length of the name part and the name part
      targdomainpacket.push(domainpartlength + domainpart)
    end
    # Convert the targdomainpacket to a string
    targdomainpacket = targdomainpacket.join.to_s
    # Create a correct hex character string to be used in the packet
    targdomainpacket = targdomainpacket.scan(/../).map { |x| x.hex.chr }.join
    # DNS Packet including our target domain and query type
    @msearch_probe = "\x09\x8d\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" + targdomainpacket + "\x00\x00" + querypacket + "\x00\x01"
  end

  def scanner_prescan(batch)
    print_status("Sending DNS probes to #{batch[0]}->#{batch[-1]} (#{batch.length} hosts)")
    # Standard packet is 60 bytes. Add the domain size to this
    sendpacketsize = 60 + datastore['DOMAINNAME'].length
    print_status("Sending #{sendpacketsize} bytes to each host using the IN #{datastore['QUERYTYPE']} #{datastore['DOMAINNAME']} request")
    @results = {}
  end

  def scan_host(ip)
    if spoofed?
      datastore['ScannerRecvWindow'] = 0
      scanner_spoof_send(@msearch_probe, ip, datastore['RPORT'], datastore['SRCIP'], datastore['NUM_REQUESTS'])
    else
      scanner_send(@msearch_probe, ip, datastore['RPORT'])
    end
  end

  def scanner_process(data, shost, sport)
    # Check the response data for \x09\x8d and the next 2 bytes, which contain our DNS flags
    if data =~ /\x09\x8d(..)/
      flags = $1
      flags = flags.unpack('B*')[0].scan(/./)
      # Query Response
      qr = flags[0]
      # Recursion Available
      ra = flags[8]
      # Response Code
      rcode = flags[12] + flags[13] + flags[14] + flags[15]

      # If these flags are set, we get a valid response
      # don't test recursion available if correct answer received
      # at least the case with bind and "additional-from-cache no" or version < 9.5+
      if qr == "1" and rcode == "0000"
        sendlength = 60 + datastore['DOMAINNAME'].length
        receivelength = 42 + data.length
        amp = receivelength / sendlength.to_f
        print_good("#{shost}:#{datastore['RPORT']} - Response is #{receivelength} bytes [#{amp.round(2)}x Amplification]")
        report_service(:host => shost, :port => datastore['RPORT'], :proto => 'udp', :name => "dns")
        report_vuln(
          :host => shost,
          :port => datastore['RPORT'],
          :proto => 'udp',
          :name => "DNS",
          :info => "DNS amplification -  #{data.length} bytes [#{amp.round(2)}x Amplification]",
          :refs => self.references
        )
      end

      # If these flags are set, we get a valid response but recursion is not available
      if qr == "1" and ra == "0" and rcode == "0101"
        print_status("#{shost}:#{datastore['RPORT']} - Recursion not allowed")
        report_service(:host => shost, :port => datastore['RPORT'], :proto => 'udp', :name => "dns")
      end
    end
  end
end
