# -*- coding: binary -*-


module Msf

###
#
# This module exposes methods for querying a remote DNS service
#
###
module Exploit::Remote::DNS
module Client

  include Common
  include Exploit::Remote::Udp
  include Exploit::Remote::Tcp

  #
  # Initializes an exploit module that interacts with a DNS server.
  #
  def initialize(info = {})
    super

    deregister_options('RHOST')
    register_options(
      [
        Opt::RPORT(53),
        Opt::Proxies,
        OptString.new('DOMAIN', [ false, "The target domain name"]),
        OptString.new('NS', [ false, "Specify the nameservers to use for queries, space separated" ]),
        OptString.new('SEARCHLIST', [ false, "DNS domain search list, comma separated"]),
        OptInt.new('THREADS', [true, "Number of threads to use in threaded queries", 1])
      ], Exploit::Remote::DNS::Client
    )

    register_advanced_options(
      [
        OptString.new('DnsClientDefaultNS', [ false, "Specify the default to use for queries, space separated", '8.8.8.8 8.8.4.4' ]),
        OptInt.new('DnsClientRetry', [ false, "Number of times to try to resolve a record if no response is received", 2]),
        OptInt.new('DnsClientRetryInterval', [ false, "Number of seconds to wait before doing a retry", 2]),
        OptBool.new('DnsClientReportARecords', [false, "Add hosts found via BRT and RVL to DB", true]),
        OptBool.new('DnsClientRVLExistingOnly', [false, "Only perform lookups on hosts in DB", true]),
        OptBool.new('DnsClientTcpDns', [false, "Run queries over TCP", false]),
        OptPath.new('DnsClientResolvconf', [true, "Resolvconf formatted configuration file to use for Resolver", "/dev/null"])
      ], Exploit::Remote::DNS::Client
    )

    register_autofilter_ports([ 53 ]) if respond_to?(:register_autofilter_ports)
    register_autofilter_services(%W{ dns }) if respond_to?(:register_autofilter_services)
  end


  #
  # Convenience wrapper around Resolver's query method - send DNS request
  #
  # @param domain [String] Domain for which to request a record
  # @param type [String] Type of record to request for domain
  #
  # @return [Dnsruby::RR] DNS response
  def query(domain = datastore['DOMAIN'], type = 'A')
    client.query(domain, type)
  end

  #
  # Performs a set of asynchronous lookups for an array of domain,type pairs
  #
  # @param queries [Array] Set of domain,type pairs to pass into #query
  # @param threadmax [Fixnum] Max number of running threads at a time
  # @param block [Proc] Code block to execute with the query result
  #
  # @return [Array] Resulting set of responses or responses processed by passed blocks
  def query_async(queries = [], threadmax = datastore['THREADS'], &block)
    running = []
    while !queries.empty?
      domain, type = queries.shift
      running << framework.threads.spawn("Module(#{self.refname})-#{domain} #{type}", false) do |qat|
        if block
          block.call(query(domain,type))
        else
          query(domain,type)
        end
      end
      while running.select(&:alive?).count >= threadmax
        Rex::ThreadSafe.sleep(1)
      end
    end
    return running.join
  end

  #
  # Switch DNS forwarders in resolver with thread safety
  #
  # @param ns [Array, String] List of (or single) nameservers to use
  def set_nameserver(ns = [])
    if ns.respond_to?(:split)
      ns = [ns]
    end
    @lock.synchronize do
      @dns_resolver.nameserver = ns.flatten
    end
  end

  #
  # Switch nameservers to use explicit NS or SOA for target
  #
  # @param domain [String] Domain for which to find SOA
  def switchdns(domain)
    if datastore['NS'].blank?
      resp_soa = client.query(target, "SOA")
      if (resp_soa)
        (resp_soa.answer.select { |i| i.is_a?(Dnsruby::RR::SOA)}).each do |rr|
          resp_1_soa = client.search(rr.mname)
          if (resp_1_soa and resp_1_soa.answer[0])
            set_nameserver(resp_1_soa.answer.map(&:address).compact.map(&:to_s))
            print_status("Set DNS Server to #{target} NS: #{client.nameserver.join(', ')}")
            break
          end
        end
      end
    else
      vprint_status("Using DNS Server: #{client.nameserver.join(', ')}")
      client.nameserver = process_nameservers
    end
  end

  #
  # Detect if target has wildcards enabled for a record type
  #
  # @param domain [String] Domain to test
  # @param type [String] Record type to test
  #
  # @return [String] Address which is returned for wildcard requests
  def wildcard(domain, type = "A")
    addr = false
    rendsub = rand(10000).to_s
    response = query("#{rendsub}.#{target}", type)
    if response.answer.length != 0
      vprint_status("This domain has wildcards enabled!!")
      response.answer.each do |rr|
        print_status("Wildcard IP for #{rendsub}.#{target} is: #{rr.address.to_s}") if rr.class != Dnsruby::RR::CNAME
        addr = rr.address.to_s
      end
    end
    return addr
  end

  #
  # Create and configure Resolver object
  #
  def setup_resolver
    options.validate(datastore) # This is a hack, DS values should not be Strings prior to this
    config = {
      :config_file => datastore['DnsClientResolvconf'],
      :nameservers => process_nameservers,
      :port => datastore['RPORT'],
      :retry_number => datastore['DnsClientRetry'].to_i,
      :retry_interval => datastore['DnsClientRetryInterval'].to_i,
      :use_tcp => datastore['DnsClientTcpDns'],
      :context => {'Msf' => framework, 'MsfExploit' => self}
    }
    if datastore['SEARCHLIST']
      if datastore['SEARCHLIST'].split(',').all? do |search|
        search.match(MATCH_HOSTNAME)
      end
        config[:search_list] = datastore['SEARCHLIST'].split(',')
      else
        raise 'Domain search list must consist of valid domains'
      end
    end
    if datastore['CHOST']
      config[:source_address] = IPAddr.new(datastore['CHOST'].to_s)
    end
    if datastore['CPORT']
      config[:source_port] = datastore['CPORT'] unless datastore['CPORT'] == 0
    end
    if datastore['Proxies']
      vprint_status("Using DNS/TCP resolution for proxy config")
      config[:use_tcp] = true
      config[:proxies] = datastore['Proxies']
    end
    @dns_resolver_lock = Mutex.new unless @dns_resolver_lock
    @dns_resolver = Rex::Proto::DNS::Resolver.new(config)
  end

  #
  # Convenience method for DNS resolver as client
  # Executes setup_resolver if none exists
  #
  def client
    setup_resolver unless @dns_resolver
    @dns_resolver
  end

  #
  # Sets the resolver's nameservers
  # Uses explicitly defined NS option if set
  # Uses RHOSTS if not explicitly defined
  def process_nameservers
    if datastore['NS'].blank?
      nameservers = datastore['DnsClientDefaultNS'].split(/\s|,/)
    else
      nameservers = datastore['NS'].split(/\s|,/)
    end

    invalid = nameservers.select { |ns| !Rex::Socket.dotted_ip?(ns) }
    if !invalid.empty?
      raise "Nameservers must be IP addresses. The following were invalid: #{invalid.join(", ")}"
    end

    nameservers
  end

end
end
end
