From d6beb94c59159d0d1f879bcc8ab4fa6f794105c3 Mon Sep 17 00:00:00 2001 From: Brent Cook Date: Mon, 22 Jan 2018 23:54:11 -0600 Subject: [PATCH] Land #6611, add native DNS to Rex, MSF mixin, sample modules --- lib/msf/core/exploit/dns.rb | 20 + lib/msf/core/exploit/dns/client.rb | 217 ++++++++++ lib/msf/core/exploit/dns/common.rb | 22 + lib/msf/core/exploit/dns/server.rb | 163 ++++++++ lib/msf/core/exploit/socket_server.rb | 172 ++++++++ lib/msf/core/exploit/tcp_server.rb | 131 +----- lib/net/dns/packet.rb | 1 + lib/rex/io/gram_server.rb | 94 +++++ lib/rex/post/meterpreter/channel.rb | 7 + lib/rex/proto.rb | 1 + lib/rex/proto/dns.rb | 17 + lib/rex/proto/dns/packet.rb | 315 +++++++++++++++ lib/rex/proto/dns/resolver.rb | 378 ++++++++++++++++++ lib/rex/proto/dns/server.rb | 377 +++++++++++++++++ modules/auxiliary/server/dns/native_server.rb | 107 +++++ modules/auxiliary/spoof/dns/native_spoofer.rb | 162 ++++++++ 16 files changed, 2061 insertions(+), 123 deletions(-) create mode 100644 lib/msf/core/exploit/dns.rb create mode 100644 lib/msf/core/exploit/dns/client.rb create mode 100644 lib/msf/core/exploit/dns/common.rb create mode 100644 lib/msf/core/exploit/dns/server.rb create mode 100644 lib/msf/core/exploit/socket_server.rb create mode 100644 lib/rex/io/gram_server.rb create mode 100644 lib/rex/proto/dns.rb create mode 100644 lib/rex/proto/dns/packet.rb create mode 100644 lib/rex/proto/dns/resolver.rb create mode 100644 lib/rex/proto/dns/server.rb create mode 100644 modules/auxiliary/server/dns/native_server.rb create mode 100644 modules/auxiliary/spoof/dns/native_spoofer.rb diff --git a/lib/msf/core/exploit/dns.rb b/lib/msf/core/exploit/dns.rb new file mode 100644 index 0000000000..5d7457de16 --- /dev/null +++ b/lib/msf/core/exploit/dns.rb @@ -0,0 +1,20 @@ +# -*- coding: binary -*- +require 'msf/core' +require 'rex/proto/dns' + + +module Msf + +### +# +# This namespace exposes methods for interacting with and providing services +# +### +module Exploit::Remote::DNS + +end +end + +require 'msf/core/exploit/dns/common' +require 'msf/core/exploit/dns/client' +require 'msf/core/exploit/dns/server' diff --git a/lib/msf/core/exploit/dns/client.rb b/lib/msf/core/exploit/dns/client.rb new file mode 100644 index 0000000000..687565eed3 --- /dev/null +++ b/lib/msf/core/exploit/dns/client.rb @@ -0,0 +1,217 @@ +# -*- coding: binary -*- +require 'msf/core' +require 'rex/proto/dns' + + +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 target [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 diff --git a/lib/msf/core/exploit/dns/common.rb b/lib/msf/core/exploit/dns/common.rb new file mode 100644 index 0000000000..810172bdf2 --- /dev/null +++ b/lib/msf/core/exploit/dns/common.rb @@ -0,0 +1,22 @@ +# -*- coding: binary -*- +require 'msf/core' +require 'rex/proto/dns' + + +module Msf + +### +# +# This module exposes methods for querying a remote DNS service +# +### +module Exploit::Remote::DNS +module Common + + MATCH_HOSTNAME = Rex::Proto::DNS::Constants::MATCH_HOSTNAME + + Packet = Rex::Proto::DNS::Packet + +end +end +end diff --git a/lib/msf/core/exploit/dns/server.rb b/lib/msf/core/exploit/dns/server.rb new file mode 100644 index 0000000000..6a272ffdda --- /dev/null +++ b/lib/msf/core/exploit/dns/server.rb @@ -0,0 +1,163 @@ +# -*- coding: binary -*- +require 'msf/core' +require 'rex/proto/dns' +require 'msf/core/exploit/dns/common' + +module Msf + +### +# +# This module exposes methods for querying a remote DNS service +# +### +module Exploit::Remote::DNS +module Server + include Exploit::Remote::DNS::Common + include Exploit::Remote::SocketServer + + # + # Initializes an exploit module that serves DNS requests + # + def initialize(info = {}) + super + + register_options( + [ + OptPort.new('SRVPORT', [true, 'The local port to listen on.', 53]), + OptString.new('STATIC_ENTRIES', [ false, "DNS domain search list (hosts file or space/semicolon separate entries)"]), + OptBool.new('DISABLE_RESOLVER', [ false, "Disable DNS request forwarding", false]), + OptBool.new('DISABLE_NS_CACHE', [ false, "Disable DNS response caching", false]) + ], Exploit::Remote::DNS::Server + ) + + register_advanced_options( + [ + OptBool.new('DnsServerUdp', [true, "Serve UDP DNS requests", true]), + OptBool.new('DnsServerTcp', [true, "Serve TCP DNS requests", false]) + ], Exploit::Remote::DNS::Server + ) + end + + attr_accessor :service # :nodoc: + + # + # Process static entries + # + # @param entries [String] Filename or String containing static entries + # @param type [String] Type of record for which to add static entries + # + # @return [Array] List of static entries in the cache + def add_static_hosts(entries = datastore['STATIC_ENTRIES'], type = 'A') + return if entries.nil? or entries.empty? + if File.file?(File.expand_path(entries)) + data = File.read(File.expand_path(entries)).split("\n") + else + data = entries.split(';') + end + data.each do |entry| + next if entry.gsub(/\s/,'').empty? + addr, names = entry.split(' ', 2) + names.split.each do |name| + name << '.' unless name[-1] == '.' or name == '*' + service.cache.add_static(name, addr, type) + end + end + service.cache.records.select {|r,e| e == 0} + end + + # + # Flush all static entries + # + def flush_static_hosts + data.cache.records.select {|r,e| e == 0}.each do |flush| + data.cache.delete(flush) + end + end + + # + # Flush cache entries + # @param static [TrueClass, FalseClass] flush static hosts + def flush_cache(static = false) + self.service.cache.stop(true) + flush_static_hosts if static + self.service.cache.start + end + + # + # Handle incoming requests + # Override this method in modules to take flow control + # + def on_dispatch_request(cli, data) + service.default_dispatch_request(cli,data) + end + + # + # Handle incoming requests + # Override this method in modules to take flow control + # + def on_send_response(cli, data) + cli.write(data) + end + + # + # Starts the server + # + def start_service + begin + + comm = _determine_server_comm + self.service = Rex::ServiceManager.start( + Rex::Proto::DNS::Server, + datastore['SRVHOST'], + datastore['SRVPORT'], + datastore['DnsServerUdp'], + datastore['DnsServerTcp'], + (use_resolver? ? setup_resolver : false), + comm, + {'Msf' => framework, 'MsfExploit' => self} + ) + + self.service.dispatch_request_proc = Proc.new do |cli, data| + on_dispatch_request(cli,data) + end + self.service.send_response_proc = Proc.new do |cli, data| + on_send_response(cli,data) + end + + add_static_hosts + self.service.start(!datastore['DISABLE_NS_CACHE']) + + rescue ::Errno::EACCES => e + raise Rex::BindFailed.new(e.message) + end + end + + # + # Stops the server + # @param destroy [TrueClass,FalseClass] Dereference the server object + def stop_service(destroy = false) + Rex::ServiceManager.stop_service(self.service) if self.service + if destroy + @dns_resolver = nil if @dns_resolver + self.service = nil if self.service + end + end + + # + # Resets the DNS server + # + def reset_service + stop_service(true) + start_service + end + + # + # Determines if resolver is available and configured for use + # + def use_resolver? + !datastore['DISABLE_RESOLVER'] and self.respond_to?(:setup_resolver) + end + +end +end +end diff --git a/lib/msf/core/exploit/socket_server.rb b/lib/msf/core/exploit/socket_server.rb new file mode 100644 index 0000000000..c0c474de73 --- /dev/null +++ b/lib/msf/core/exploit/socket_server.rb @@ -0,0 +1,172 @@ +# -*- coding: binary -*- + +module Msf + +### +# +# This mixin provides a generic interface for running a socket server of some +# sort that is designed to exploit clients. Exploits that include this mixin +# automatically take a passive stance. +# +### + +module Exploit::Remote::SocketServer + + def initialize(info = {}) + super(update_info(info, + 'Stance' => Msf::Exploit::Stance::Passive)) + + register_options( + [ + OptAddress.new('SRVHOST', [ true, "The local host to listen on. This must be an address on the local machine or 0.0.0.0", '0.0.0.0' ]), + OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8080 ]), + + ], Msf::Exploit::Remote::SocketServer + ) + + register_advanced_options( + [ + OptString.new('ListenerComm', [ false, 'The specific communication channel to use for this service']) + ], Msf::Exploit::Remote::SocketServer + ) + end + + # + # This mixin overrides the exploit method so that it can initiate the + # service that corresponds with what the client has requested. + # + def exploit + + start_service() + print_status("Server started.") + + # Call the exploit primer + primer + + # Wait on the service to stop + self.service.wait + end + + # + # Primer method to call after starting service but before handling connections + # + def primer + end + + # + # Stops the service, if one was created. + # + def cleanup + super + if(service) + stop_service() + print_status("Server stopped.") + end + end + + # + # Called when a client has data available for reading. + # + def on_client_data(client) + end + + # + # Starts the service. Override this method in consumers + # + def start_service(*args) + end + + # + # Stops the service. + # + def stop_service + if (service) + begin + self.service.deref if self.service.kind_of?(Rex::Service) + if self.service.kind_of?(Rex::Socket) + self.service.close + self.service.stop + end + + self.service = nil + rescue ::Exception + end + end + end + + # + # Returns the local host that is being listened on. + # + def srvhost + datastore['SRVHOST'] + end + + # + # Returns the local port that is being listened on. + # + def srvport + datastore['SRVPORT'] + end + + # + # Re-generates the payload, substituting the current RHOST and RPORT with + # the supplied client host and port from the socket. + # + def regenerate_payload(cli, arch = nil, platform = nil, target = nil) + + ohost = datastore['RHOST'] + oport = datastore['RPORT'] + p = nil + + begin + # Update the datastore with the supplied client peerhost/peerport + datastore['RHOST'] = cli.peerhost + datastore['RPORT'] = cli.peerport + + if ((p = super(arch, platform, target)) == nil) + print_error("Failed to generate payload") + return nil + end + + # Allow the payload to start a new handler + add_handler({ + 'RHOST' => datastore['RHOST'], + 'RPORT' => datastore['RPORT'] + }) + + ensure + datastore['RHOST'] = ohost + datastore['RPORT'] = oport + end + + p + end + +protected + + # + # Determines appropriate listener comm + # + def _determine_server_comm(srv_comm = datastore['ListenerComm'].to_s) + case srv_comm + when 'local' + comm = ::Rex::Socket::Comm::Local + when /\A[0-9]+\Z/ + comm = framework.sessions[srv_comm.to_i] + raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not exist") unless comm + raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not implement Rex::Socket::Comm") unless comm.is_a? ::Rex::Socket::Comm + when nil, '' + comm = nil + else + raise(RuntimeError, "SocketServer Comm '#{srv_comm}' is invalid") + end + + comm + end + + attr_accessor :service # :nodoc: + +end + +end + diff --git a/lib/msf/core/exploit/tcp_server.rb b/lib/msf/core/exploit/tcp_server.rb index 24eab777fb..949e742729 100644 --- a/lib/msf/core/exploit/tcp_server.rb +++ b/lib/msf/core/exploit/tcp_server.rb @@ -1,5 +1,7 @@ # -*- coding: binary -*- +require 'msf/core/exploit/socket_server' + module Msf ### @@ -10,20 +12,18 @@ module Msf # ### module Exploit::Remote::TcpServer + include Exploit::Remote::SocketServer def initialize(info = {}) - super(update_info(info, - 'Stance' => Msf::Exploit::Stance::Passive)) + super register_options( [ OptBool.new('SSL', [ false, 'Negotiate SSL for incoming connections', false]), # SSLVersion is currently unsupported for TCP servers (only supported by clients at the moment) - OptPath.new('SSLCert', [ false, 'Path to a custom SSL certificate (default is randomly generated)']), - OptAddress.new('SRVHOST', [ true, "The local host to listen on. This must be an address on the local machine or 0.0.0.0", '0.0.0.0' ]), - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8080 ]), - - ], Msf::Exploit::Remote::TcpServer) + OptPath.new('SSLCert', [ false, 'Path to a custom SSL certificate (default is randomly generated)']) + ], Msf::Exploit::Remote::TcpServer + ) register_advanced_options( [ @@ -40,51 +40,11 @@ module Exploit::Remote::TcpServer ) end - # - # This mixin overrides the exploit method so that it can initiate the - # service that corresponds with what the client has requested. - # - def exploit - - start_service() - print_status("Server started.") - - # Call the exploit primer - primer - - # Wait on the service to stop - self.service.wait - end - - # - # Primer method to call after starting service but before handling connections - # - def primer - end - - # - # Stops the service, if one was created. - # - def cleanup - super - if(service) - stop_service() - print_status("Server stopped.") - end - end - - # # Called when a client connects. # def on_client_connect(client) end - # - # Called when a client has data available for reading. - # - def on_client_data(client) - end - # # Called when a client has disconnected. # @@ -97,12 +57,7 @@ module Exploit::Remote::TcpServer def start_service(*args) begin - comm = datastore['ListenerComm'] - if comm == "local" - comm = ::Rex::Socket::Comm::Local - else - comm = nil - end + comm = _determine_server_comm self.service = Rex::Socket::TcpServer.create( 'LocalHost' => srvhost, @@ -151,38 +106,6 @@ module Exploit::Remote::TcpServer end end - # - # Stops the service. - # - def stop_service - if (service) - begin - self.service.deref if self.service.kind_of?(Rex::Service) - if self.service.kind_of?(Rex::Socket) - self.service.close - self.service.stop - end - - self.service = nil - rescue ::Exception - end - end - end - - # - # Returns the local host that is being listened on. - # - def srvhost - datastore['SRVHOST'] - end - - # - # Returns the local port that is being listened on. - # - def srvport - datastore['SRVPORT'] - end - # # Returns the SSL option # @@ -209,44 +132,6 @@ module Exploit::Remote::TcpServer datastore['SSLCompression'] end - # - # Re-generates the payload, substituting the current RHOST and RPORT with - # the supplied client host and port from the socket. - # - def regenerate_payload(cli, arch = nil, platform = nil, target = nil) - - ohost = datastore['RHOST'] - oport = datastore['RPORT'] - p = nil - - begin - # Update the datastore with the supplied client peerhost/peerport - datastore['RHOST'] = cli.peerhost - datastore['RPORT'] = cli.peerport - - if ((p = super(arch, platform, target)) == nil) - print_error("Failed to generate payload") - return nil - end - - # Allow the payload to start a new handler - add_handler({ - 'RHOST' => datastore['RHOST'], - 'RPORT' => datastore['RPORT'] - }) - - ensure - datastore['RHOST'] = ohost - datastore['RPORT'] = oport - end - - p - end - -protected - - attr_accessor :service # :nodoc: - end end diff --git a/lib/net/dns/packet.rb b/lib/net/dns/packet.rb index 5938d252d4..3fdb8a4831 100644 --- a/lib/net/dns/packet.rb +++ b/lib/net/dns/packet.rb @@ -184,6 +184,7 @@ module Net # :nodoc: nscount += 1 end @additional.each do |rr| + next if rr.nil? data += rr.data#(data.length) arcount += 1 end diff --git a/lib/rex/io/gram_server.rb b/lib/rex/io/gram_server.rb new file mode 100644 index 0000000000..3c004f2113 --- /dev/null +++ b/lib/rex/io/gram_server.rb @@ -0,0 +1,94 @@ +# -*- coding: binary -*- +require 'thread' + +module Rex +module IO + +### +# +# This mixin provides the framework and interface for implementing a datagram +# server that can handle incoming datagrams. Datagram servers include this mixin +# +### +module GramServer + + ## + # + # Abstract methods + # + ## + + ## + # + # Default server monitoring and client management implementation follows + # below. + # + ## + + + # + # This callback is notified when a client connection has data that needs to + # be processed. + # + def dispatch_request(client, data) + if (dispatch_request_proc) + dispatch_request_proc.call(client, data) + end + end + + # + # This callback is notified when data must be returned to the client + # @param client [Socket] Client/Socket to receive data + # @param data [String] Data to be sent to client/socket + def send_response(client, data) + if (send_response_proc) + send_response_proc.call(client, data) + else + client.write(data) + end + end + + # + # Start monitoring the listener socket for connections and keep track of + # all client connections. + # + def start + self.listener_thread = Rex::ThreadFactory.spawn("GramServerListener", false) { + monitor_listener + } + end + + # + # Terminates the listener monitoring threads and closes all active clients. + # + def stop + self.listener_thread.kill + end + + # + # This method waits on the server listener thread + # + def wait + self.listener_thread.join if self.listener_thread + end + + ## + # + # Callback procedures. + # + ## + + # + # This callback procedure can be set and will be called when clients + # have data to be processed. + # + attr_accessor :dispatch_request_proc, :send_response_proc + + attr_accessor :listener_thread# :nodoc: + + +end + +end +end + diff --git a/lib/rex/post/meterpreter/channel.rb b/lib/rex/post/meterpreter/channel.rb index dbb89d34ff..10dc519467 100644 --- a/lib/rex/post/meterpreter/channel.rb +++ b/lib/rex/post/meterpreter/channel.rb @@ -250,6 +250,13 @@ class Channel written.nil? ? 0 : written.value end + # + # Wrapper around check for self.cid + # + def closed? + self.cid.nil? + end + # # Wrapper around the low-level close. # diff --git a/lib/rex/proto.rb b/lib/rex/proto.rb index b190bed866..a4352a47d1 100644 --- a/lib/rex/proto.rb +++ b/lib/rex/proto.rb @@ -9,6 +9,7 @@ require 'rex/proto/kerberos' require 'rex/proto/rmi' require 'rex/proto/sms' require 'rex/proto/mms' +require 'rex/proto/dns' module Rex module Proto diff --git a/lib/rex/proto/dns.rb b/lib/rex/proto/dns.rb new file mode 100644 index 0000000000..96c5419c5e --- /dev/null +++ b/lib/rex/proto/dns.rb @@ -0,0 +1,17 @@ +# -*- coding: binary -*- + +module Rex +module Proto +module DNS + + module Constants + MATCH_HOSTNAME=/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]\.*)$/ + end + +end +end +end + +require 'rex/proto/dns/packet' +require 'rex/proto/dns/resolver' +require 'rex/proto/dns/server' diff --git a/lib/rex/proto/dns/packet.rb b/lib/rex/proto/dns/packet.rb new file mode 100644 index 0000000000..5958cdeb08 --- /dev/null +++ b/lib/rex/proto/dns/packet.rb @@ -0,0 +1,315 @@ +# -*- coding: binary -*- + +require 'net/dns' +require 'resolv' +require 'dnsruby' + +module Rex +module Proto +module DNS + +module Packet + + # + # Checks string to ensure it can be used as a valid hostname + # + # @param subject [String] Subject name to check + # + # @return [TrueClass,FalseClass] Disposition on name match + def self.valid_hostname?(subject = '') + !subject.match(Rex::Proto::DNS::Constants::MATCH_HOSTNAME).nil? + end + + # + # Reconstructs a packet with both standard DNS libraries + # Ensures that headers match the payload + # + # @param packet [String, Net::DNS::Packet, Dnsruby::Message] Data to be validated + # + # @return [Dnsruby::Message] + def self.validate(packet) + self.encode_drb(self.encode_net(self.encode_res(packet))) + end + + # + # Sets header values to match packet content + # + # @param packet [String] Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] + # + # @return [Dnsruby::Message] + def self.recalc_headers(packet) + packet = self.encode_drb(packet) + { + :qdcount= => :question, + :ancount= => :answer, + :nscount= => :authority, + :arcount= => :additional + }.each do |header,body| + packet.header.send(header,packet.send(body).count) + end + + return packet + end + + # + # Reads a packet into the Net::DNS::Packet format + # + # @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data + # + # @return [Net::DNS::Packet] + def self.encode_net(packet) + return packet if packet.is_a?(Net::DNS::Packet) + Net::DNS::Packet.parse( + self.encode_raw(packet) + ) + end + + # Reads a packet into the Resolv::DNS::Message format + # + # @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data + # + # @return [Resolv::DNS::Message] + def self.encode_res(packet) + return packet if packet.is_a?(Resolv::DNS::Message) + Resolv::DNS::Message.decode( + self.encode_raw(packet) + ) + end + + # Reads a packet into the Dnsruby::Message format + # + # @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data + # + # @return [Dnsruby::Message] + def self.encode_drb(packet) + return packet if packet.is_a?(Dnsruby::Message) + Dnsruby::Message.decode( + self.encode_raw(packet) + ) + end + + # Reads a packet into the raw String format + # + # @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data + # + # @return [String] + def self.encode_raw(packet) + return packet unless packet.respond_to?(:encode) or packet.respond_to?(:data) + (packet.respond_to?(:data) ? packet.data : packet.encode).force_encoding('binary') + end + + # + # Generates a request packet, taken from Net::DNS::Resolver + # + # @param subject [String] Subject name of question section + # @param type [Fixnum] Type of DNS record to query + # @param cls [Fixnum] Class of dns record to query + # @param recurse [Fixnum] Recursive query or not + # + # @return [Dnsruby::Message] request packet + def self.generate_request(subject, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN, recurse = 1) + case subject + when IPAddr + name = subject.reverse + type = Dnsruby::Types::PTR + when /\d/ # Contains a number, try to see if it's an IP or IPv6 address + begin + name = IPAddr.new(subject).reverse + type = Dnsruby::Types::PTR + rescue ArgumentError + name = subject if self.valid_hostname?(subject) + end + else + name = subject if self.valid_hostname?(subject) + end + + # Create the packet + packet = Dnsruby::Message.new(name, type, cls) + + if packet.header.opcode == Dnsruby::OpCode::Query + packet.header.recursive = recurse + end + + # DNSSEC and TSIG stuff to be inserted here + + return packet + end + + # + # Generates a response packet for an existing request + # + # @param request [String] Net::DNS::Packet, Resolv::DNS::Message] Original request + # @param answer [Array] Set of answers to provide in the response + # @param authority [Array] Set of authority records to provide in the response + # @param additional [Array] Set of additional records to provide in the response + # + # @return [Dnsruby::Message] Response packet + def self.generate_response(request, answer = nil, authority = nil, additional = nil) + packet = self.encode_drb(request) + packet.answer = answer if answer + packet.authority = authority if authority + packet.additional = additional if additional + packet = self.recalc_headers(packet) + + # Set error code for NXDomain or unset it if reprocessing a response + if packet.header.ancount < 1 + packet.header.rcode = Dnsruby::RCode::NXDOMAIN + else + if packet.header.qr and packet.header.get_header_rcode.to_i == 3 + packet.header.rcode = Dnsruby::RCode::NOERROR + end + end + # Set response bit last to allow reprocessing of responses + packet.header.qr = true + # Set recursion available bit if recursion desired + packet.header.ra = true if packet.header.rd + return packet + end + + module Raw + + # + # Convert data to little endian unsigned short + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_short_le(data) + [data].flatten.pack('S*') + end + + # + # Convert data from little endian unsigned short + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_short_le(data) + data.unpack('S*') + end + + # + # Convert data to little endian unsigned int + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_int_le(data) + [data].flatten.pack('I*') + end + + # + # Convert data from little endian unsigned int + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_int_le(data) + data.unpack('I*') + end + + # + # Convert data to little endian unsigned long + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_long_le(data) + [data].flatten.pack('L*') + end + + # + # Convert data from little endian unsigned long + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_long_le(data) + data.unpack('L*') + end + + # + # Convert data to big endian unsigned short + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_short_be(data) + [data].flatten.pack('S>*') + end + + # + # Convert data from big endian unsigned short + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_short_be(data) + data.unpack('S>*') + end + + # + # Convert data to big endian unsigned int + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_int_be(data) + [data].flatten.pack('I>*') + end + + # + # Convert data from big endian unsigned int + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_int_be(data) + data.unpack('I>*') + end + + # + # Convert data to big endian unsigned long + # + # @param data [Fixnum, Float, Array] Input for conversion + # + # @return [String] Raw output + def self.to_long_be(data) + [data].flatten.pack('L>*') + end + + # + # Convert data from big endian unsigned long + # + # @param data [String] Input for conversion + # + # @return [Array] Integer array output + def self.from_long_be(data) + data.unpack('L>*') + end + + # + # Returns request ID from raw packet skipping parsing + # + # @param data [String] Request data + # + # @return [Fixnum] Request ID + def self.request_id(data) + self.from_short_be(data[0..1])[0] + end + + # + # Returns request length from raw packet skipping parsing + # + # @param data [String] Request data + # + # @return [Fixnum] Request Length + def self.request_length(data) + self.from_short_le(data[0..2])[0] + end + end +end + +end +end +end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb new file mode 100644 index 0000000000..3780293c5b --- /dev/null +++ b/lib/rex/proto/dns/resolver.rb @@ -0,0 +1,378 @@ +# -*- coding: binary -*- + +require 'net/dns/resolver' + +module Rex +module Proto +module DNS + + ## + # Provides Rex::Sockets compatible version of Net::DNS::Resolver + # Modified to work with Dnsruby::Messages, their resolvers are too heavy + ## + class Resolver < Net::DNS::Resolver + + Defaults = { + :config_file => "/dev/null", # default can lead to info leaks + :log_file => "/dev/null", # formerly $stdout, should be tied in with our loggers + :port => 53, + :searchlist => [], + :nameservers => [IPAddr.new("127.0.0.1")], + :domain => "", + :source_port => 0, + :source_address => IPAddr.new("0.0.0.0"), + :retry_interval => 5, + :retry_number => 4, + :recursive => true, + :defname => true, + :dns_search => true, + :use_tcp => false, + :ignore_truncated => false, + :packet_size => 512, + :tcp_timeout => TcpTimeout.new(30), + :udp_timeout => UdpTimeout.new(30), + :context => {}, + :comm => nil + } + + attr_accessor :context, :comm + # + # Provide override for initializer to use local Defaults constant + # + # @param config [Hash] Configuration options as conusumed by parent class + def initialize(config = {}) + raise ResolverArgumentError, "Argument has to be Hash" unless config.kind_of? Hash + # config.key_downcase! + @config = Defaults.merge config + @raw = false + + # New logger facility + @logger = Logger.new(@config[:log_file]) + @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN + + #------------------------------------------------------------ + # Resolver configuration will be set in order from: + # 1) initialize arguments + # 2) ENV variables + # 3) config file + # 4) defaults (and /etc/resolv.conf for config) + #------------------------------------------------------------ + + + + #------------------------------------------------------------ + # Parsing config file + #------------------------------------------------------------ + parse_config_file + + #------------------------------------------------------------ + # Parsing ENV variables + #------------------------------------------------------------ + parse_environment_variables + + #------------------------------------------------------------ + # Parsing arguments + #------------------------------------------------------------ + comm = config.delete(:comm) + context = context = config.delete(:context) + config.each do |key,val| + next if key == :log_file or key == :config_file + begin + eval "self.#{key.to_s} = val" + rescue NoMethodError + raise ResolverArgumentError, "Option #{key} not valid" + end + end + end + # + # Provides current proxy setting if configured + # + # @return [String] Current proxy configuration + def proxies + @config[:proxies].inspect if @config[:proxies] + end + + # + # Configure proxy setting and additional timeout + # + # @param prox [String] SOCKS proxy connection string + # @param timeout_added [Fixnum] Added TCP timeout to account for proxy + def proxies=(prox, timeout_added = 250) + return if prox.nil? + if prox.is_a?(String) and prox.strip =~ /^socks/i + @config[:proxies] = prox.strip + @config[:use_tcp] = true + self.tcp_timeout = self.tcp_timeout.to_s.to_i + timeout_added + @logger.info "SOCKS proxy set, using TCP, increasing timeout" + else + raise ResolverError, "Only socks proxies supported" + end + end + + # + # Send DNS request over appropriate transport and process response + # + # @param argument + # @param type [Fixnum] Type of record to look up + # @param cls [Fixnum] Class of question to look up + def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) + if @config[:nameservers].size == 0 + raise ResolverError, "No nameservers specified!" + end + + method = self.use_tcp? ? :send_tcp : :send_udp + + case argument + when Dnsruby::Message + packet = argument + when Net::DNS::Packet, Resolv::DNS::Message + packet = Rex::Proto::DNS::Packet.encode_drb(argument) + else + packet = make_query_packet(argument,type,cls) + end + + # Store packet_data for performance improvements, + # so methods don't keep on calling Packet#encode + packet_data = packet.encode + packet_size = packet_data.size + + # Choose whether use TCP, UDP + if packet_size > @config[:packet_size] # Must use TCP + @logger.info "Sending #{packet_size} bytes using TCP due to size" + method = :send_tcp + else # Packet size is inside the boundaries + if use_tcp? or !(proxies.nil? or proxies.empty?) # User requested TCP + @logger.info "Sending #{packet_size} bytes using TCP due to tcp flag" + method = :send_tcp + else # Finally use UDP + @logger.info "Sending #{packet_size} bytes using UDP" + method = :send_udp unless method == :send_tcp + end + end + + if type == Dnsruby::Types::AXFR + @logger.warn "AXFR query, switching to TCP" unless method == :send_tcp + method = :send_tcp + end + + ans = self.__send__(method,packet_data) + + unless (ans and ans[0].length > 0) + @logger.fatal "No response from nameservers list: aborting" + raise NoResponseError + return nil + end + + @logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}" + # response = Net::DNS::Packet.parse(ans[0],ans[1]) + response = Dnsruby::Message.decode(ans[0]) + + if response.header.tc and not ignore_truncated? + @logger.warn "Packet truncated, retrying using TCP" + self.use_tcp = true + begin + return send(argument,type,cls) + ensure + self.use_tcp = false + end + end + + return response + end + + # + # Send request over TCP + # + # @param packet_data [String] Data segment of DNS request packet + # @param prox [String] Proxy configuration for TCP socket + # + # @return ans [String] Raw DNS reply + def send_tcp(packet_data,prox = @config[:proxies]) + ans = nil + length = [packet_data.size].pack("n") + @config[:nameservers].each do |ns| + begin + socket = nil + @config[:tcp_timeout].timeout do + catch(:next_ns) do + begin + config = { + 'PeerHost' => ns.to_s, + 'PeerPort' => @config[:port].to_i, + 'Proxies' => prox, + 'Context' => @config[:context], + 'Comm' => @config[:comm] + } + if @config[:source_port] > 0 + config['LocalPort'] = @config[:source_port] + end + if @config[:source_host].to_s != '0.0.0.0' + config['LocalHost'] = @config[:source_host] unless @config[:source_host].nil? + end + socket = Rex::Socket::Tcp.create(config) + rescue + @logger.warn "TCP Socket could not be established to #{ns}:#{@config[:port]} #{@config[:proxies]}" + throw :next_ns + end + next unless socket # + @logger.info "Contacting nameserver #{ns} port #{@config[:port]}" + socket.write(length+packet_data) + got_something = false + loop do + buffer = "" + ans = socket.recv(2) + if ans.size == 0 + if got_something + break #Proper exit from loop + else + @logger.warn "Connection reset to nameserver #{ns}, trying next." + throw :next_ns + end + end + got_something = true + len = ans.unpack("n")[0] + + @logger.info "Receiving #{len} bytes..." + + if len.nil? or len == 0 + @logger.warn "Receiving 0 length packet from nameserver #{ns}, trying next." + throw :next_ns + end + + while (buffer.size < len) + left = len - buffer.size + temp,from = socket.recvfrom(left) + buffer += temp + end + + unless buffer.size == len + @logger.warn "Malformed packet from nameserver #{ns}, trying next." + throw :next_ns + end + if block_given? + yield [buffer,["",@config[:port],ns.to_s,ns.to_s]] + else + return [buffer,["",@config[:port],ns.to_s,ns.to_s]] + end + end + end + end + rescue Timeout::Error + @logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one" + next + ensure + socket.close if socket + end + end + return nil + end + + # + # Send request over UDP + # + # @param packet_data [String] Data segment of DNS request packet + # + # @return ans [String] Raw DNS reply + def send_udp(packet_data) + ans = nil + response = "" + @config[:nameservers].each do |ns| + begin + @config[:udp_timeout].timeout do + begin + config = { + 'PeerHost' => ns.to_s, + 'PeerPort' => @config[:port].to_i, + 'Context' => @config[:context], + 'Comm' => @config[:comm] + } + if @config[:source_port] > 0 + config['LocalPort'] = @config[:source_port] + end + if @config[:source_host] != IPAddr.new('0.0.0.0') + config['LocalHost'] = @config[:source_host] unless @config[:source_host].nil? + end + socket = Rex::Socket::Udp.create(config) + rescue + @logger.warn "UDP Socket could not be established to #{ns}:#{@config[:port]}" + return nil + end + @logger.info "Contacting nameserver #{ns} port #{@config[:port]}" + #socket.sendto(packet_data, ns.to_s, @config[:port].to_i, 0) + socket.write(packet_data) + ans = socket.recvfrom(@config[:packet_size]) + end + break if ans + rescue Timeout::Error + @logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one" + next + end + end + return ans + end + + + # + # Perform search using the configured searchlist and resolvers + # + # @param name + # @param type [Fixnum] Type of record to look up + # @param cls [Fixnum] Class of question to look up + # + # @return ans [Dnsruby::Message] DNS Response + def search(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) + + return query(name,type,cls) if name.class == IPAddr + + # If the name contains at least one dot then try it as is first. + if name.include? "." + @logger.debug "Search(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" + ans = query(name,type,cls) + return ans if ans.header.ancount > 0 + end + + # If the name doesn't end in a dot then apply the search list. + if name !~ /\.$/ and @config[:dns_search] + @config[:searchlist].each do |domain| + newname = name + "." + domain + @logger.debug "Search(#{newname},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" + ans = query(newname,type,cls) + return ans if ans.header.ancount > 0 + end + end + + # Finally, if the name has no dots then try it as is. + @logger.debug "Search(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" + return query(name+".",type,cls) + + end + + end + + # + # Perform query with default domain validation + # + # @param name + # @param type [Fixnum] Type of record to look up + # @param cls [Fixnum] Class of question to look up + # + # @return ans [Dnsruby::Message] DNS Response + def query(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) + + return send(name,type,cls) if name.class == IPAddr + + # If the name doesn't contain any dots then append the default domain. + if name !~ /\./ and name !~ /:/ and @config[:defname] + name += "." + @config[:domain] + end + + @logger.debug "Query(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" + + return send(name,type,cls) + + end + + +end +end +end diff --git a/lib/rex/proto/dns/server.rb b/lib/rex/proto/dns/server.rb new file mode 100644 index 0000000000..ed04162371 --- /dev/null +++ b/lib/rex/proto/dns/server.rb @@ -0,0 +1,377 @@ +# -*- coding: binary -*- + +require 'rex/io/gram_server' +require 'rex/socket' +require 'rex/proto/dns' + +module Rex +module Proto +module DNS + +class Server + + class Cache + attr_reader :records, :lock, :monitor_thread + include Rex::Proto::DNS::Constants + # class DNSRecordError < ::Exception + # + # Create DNS Server cache + # + def initialize + @records = {} + @lock = Mutex.new + end + + # + # Find entries in cache, substituting names for '*' in return + # + # @param search [String] Name or address to search for + # @param type [String] Record type to search for + # + # @return [Array] Records found + def find(search, type = 'A') + self.records.select do |record,expire| + record.type == type and (expire < 1 or expire > Time.now.to_i) and + ( + record.name == '*' or + record.name == search or record.name[0..-2] == search or + ( record.respond_to?(:address) and record.address.to_s == search ) + ) + end.keys.map do |record| + if search.to_s.match(MATCH_HOSTNAME) and record.name == '*' + record = Dnsruby::RR.create(name: name, type: type, address: address) + else + record + end + end + end + + # + # Add record to cache, only when "running" + # + # @param record [Dnsruby::RR] Record to cache + def cache_record(record) + return unless @monitor_thread + if record.is_a?(Dnsruby::RR) and + (!record.respond_to?(:address) or Rex::Socket.is_ip_addr?(record.address.to_s)) and + record.name.to_s.match(MATCH_HOSTNAME) + add(record, Time.now.to_i + record.ttl) + else + raise "Invalid record for cache entry - #{record.inspect}" + end + end + + # + # Add static record to cache + # + # @param name [String] Name of record + # @param address [String] Address of record + # @param type [String] Record type to add + def add_static(name, address, type = 'A', replace = false) + if Rex::Socket.is_ip_addr?(address.to_s) and + ( name.to_s.match(MATCH_HOSTNAME) or name == '*') + find(name, type).each do |found| + delete(found) + end if replace + add(Dnsruby::RR.create(name: name, type: type, address: address),0) + else + raise "Invalid parameters for static entry - #{name}, #{address}, #{type}" + end + end + + # + # Prune cache entries + # + # @param before [Fixnum] Time in seconds before which records are evicted + def prune(before = Time.now.to_i) + self.records.select do |rec, expire| + expire > 0 and expire < before + end.each {|rec, exp| delete(rec)} + end + + # + # Start the cache monitor + # + def start + @monitor_thread = Rex::ThreadFactory.spawn("DNSServerCacheMonitor", false) { + while true + prune + Rex::ThreadSafe.sleep(0.5) + end + } unless @monitor_thread + end + + # + # Stop the cache monitor + # + # @param flush [TrueClass,FalseClass] Remove non-static entries + def stop(flush = false) + self.monitor_thread.kill unless @monitor_thread.nil? + @monitor_thread = nil + if flush + self.records.select do |rec, expire| + rec.ttl > 0 + end.each {|rec| delete(rec)} + end + end + + protected + + # + # Add a record to the cache with thread safety + # + # @param record [Dnsruby::RR] Record to add + # @param expire [Fixnum] Time in seconds when record becomes stale + def add(record, expire = 0) + self.lock.synchronize do + self.records[record] = expire + end + end + + # + # Delete a record from the cache with thread safety + # + # @param record [Dnsruby::RR] Record to delete + def delete(record) + self.lock.synchronize do + self.records.delete(record) + end + end + end # Cache + + class MockDnsClient + attr_reader :peerhost, :peerport, :srvsock + + # + # Create mock DNS client + # + # @param host [String] PeerHost IP address + # @param port [Fixnum] PeerPort integer + def initialize(host, port, sock) + @peerhost = host + @peerport = port + @srvsock = sock + end + + # + # Test method to prevent GC/ObjectSpace abuse via class lookups + # + def mock_dns_client? + true + end + + def write(data) + srvsock.sendto(data, peerhost, peerport) + end + end + + include Rex::IO::GramServer + + Packet = Rex::Proto::DNS::Packet + # + # Create DNS Server + # + # @param lhost [String] Listener address + # @param lport [Fixnum] Listener port + # @param udp [TrueClass, FalseClass] Listen on UDP socket + # @param tcp [TrueClass, FalseClass] Listen on TCP socket + # @param res [Rex::Proto::DNS::Resolver] Resolver to use, nil to create a fresh one + # @param ctx [Hash] Framework context for sockets + # @param dblock [Proc] Handler for :dispatch_request flow control interception + # @param sblock [Proc] Handler for :send_response flow control interception + # + # @return [Rex::Proto::DNS::Server] DNS Server object + attr_accessor :serve_tcp, :serve_udp, :fwd_res, :cache + attr_reader :serve_udp, :serve_tcp, :sock_options, :lock, :udp_sock, :tcp_sock + def initialize(lhost = '0.0.0.0', lport = 53, udp = true, tcp = false, res = nil, comm = nil, ctx = {}, dblock = nil, sblock = nil) + + @serve_udp = udp + @serve_tcp = tcp + @sock_options = { + 'LocalHost' => lhost, + 'LocalPort' => lport, + 'Context' => ctx, + 'Comm' => comm + } + self.fwd_res = res.nil? ? Rex::Proto::DNS::Resolver.new(:comm => comm, :context => ctx) : res + self.listener_thread = nil + self.dispatch_request_proc = dblock + self.send_response_proc = sblock + self.cache = Cache.new + @lock = Mutex.new + end + + # + # Switch DNS forwarders in resolver with thread safety + # + # @param ns [Array, String] List of (or single) nameservers to use + def switchns(ns = []) + if ns.respond_to?(:split) + ns = [ns] + end + self.lock.synchronize do + self.fwd_res.nameserver = ns + end + end + + # + # Check if server is running + # + def running? + self.listener_thread and self.listener_thread.alive? + end + + # + # Start the DNS server and cache + # @param start_cache [TrueClass, FalseClass] stop the cache + def start(start_cache = true) + + if self.serve_udp + @udp_sock = Rex::Socket::Udp.create(self.sock_options) + self.listener_thread = Rex::ThreadFactory.spawn("UDPDNSServerListener", false) { + monitor_listener + } + end + + if self.serve_tcp + @tcp_sock = Rex::Socket::TcpServer.create(self.sock_options) + self.tcp_sock.on_client_data_proc = Proc.new { |cli| + on_client_data(cli) + } + self.tcp_sock.start + if !self.serve_udp + self.listener_thread = tcp_sock.listener_thread + end + end + + self.cache.start if start_cache + end + + # + # Stop the DNS server and cache + # + # @param flush_cache [TrueClass,FalseClass] Flush eDNS cache on stop + def stop(flush_cache = false) + ensure_close = [self.udp_sock, self.tcp_sock].compact + begin + self.listener_thread.kill if self.listener_thread.respond_to?(:kill) + self.listener_thread = nil + ensure + while csock = ensure_close.shift + csock.stop if csock.respond_to?(:stop) + csock.close unless csock.respond_to?(:close) and csock.closed? + end + end + self.cache.stop(flush_cache) + end + + # + # Process client request, handled with dispatch_request_proc if set + # + # @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request + # @param data [String] raw DNS request data + def dispatch_request(cli, data) + if self.dispatch_request_proc + self.dispatch_request_proc.call(cli,data) + else + default_dispatch_request(cli,data) + end + end + + # + # Default DNS request dispatcher, attempts to find + # response records in cache or forwards request upstream + # + # @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request + # @param data [String] raw DNS request data + def default_dispatch_request(cli,data) + return if data.strip.empty? + req = Packet.encode_drb(data) + forward = req.dup + # Find cached items, remove request from forwarded packet + req.question.each do |ques| + cached = self.cache.find(ques.qname, ques.qtype.to_s) + if cached.empty? + next + else + req.answer = req.answer + cached + forward.question.delete(ques) + end + end + # Forward remaining requests, cache responses + if forward.question.count > 0 and @fwd_res + forwarded = self.fwd_res.send(validate_packet(forward)) + req.answer = req.answer + forwarded.answer + forwarded.answer.each do |ans| + self.cache.cache_record(ans) + end + req.header.ra = true # Set recursion bit + end + # Finalize answers in response + # Check for empty response prior to sending + if req.answer.size < 1 + req.header.rCode = Dnsruby::RCode::NOERROR + end + req.header.qr = true # Set response bit + send_response(cli, validate_packet(req).data) + end + + # + # Returns the hardcore alias for the DNS service + # + def self.hardcore_alias(*args) + "#{(args[0] || '')}#{(args[1] || '')}" + end + + # + # DNS server. + # + def alias + "DNS Server" + end + + +protected + # + # This method monitors the listener socket for new connections and calls + # the +on_client_connect+ callback routine. + # + def monitor_listener + while true + rds = [self.udp_sock] + wds = [] + eds = [self.udp_sock] + + r,_,_ = ::IO.select(rds,wds,eds,1) + + if (r != nil and r[0] == self.udp_sock) + buf,host,port = self.udp_sock.recvfrom(65535) + # Mock up a client object for sending back data + cli = MockDnsClient.new(host, port, r[0]) + dispatch_request(cli, buf) + end + end + end + + # + # Processes request coming from client + # + # @param cli [Rex::Socket::Tcp] Client sending request + def on_client_data(cli) + begin + data = cli.read(65535) + + raise ::EOFError if not data + raise ::EOFError if data.empty? + dispatch_request(cli, data) + rescue EOFError => e + self.tcp_socket.close_client(cli) if cli + raise e + end + end + +end + +end +end +end diff --git a/modules/auxiliary/server/dns/native_server.rb b/modules/auxiliary/server/dns/native_server.rb new file mode 100644 index 0000000000..60646a4451 --- /dev/null +++ b/modules/auxiliary/server/dns/native_server.rb @@ -0,0 +1,107 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core/exploit/dns' + +class MetasploitModule < Msf::Auxiliary + + include Msf::Exploit::Remote::DNS::Client + include Msf::Exploit::Remote::DNS::Server + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Native DNS Server (Example)', + 'Description' => %q{ + This module provides a Rex based DNS service which can store static entries, + resolve names over pivots, and serve DNS requests across routed session comms. + DNS tunnels can operate across the the Rex switchboard, and DNS other modules + can use this as a template. Setting static records via hostfile allows for DNS + spoofing attacks without direct traffic manipulation at the handlers. handlers + for requests and responses provided here mimic the internal Rex functionality, + but utilize methods within this module's namespace to output content processed + in the Proc contexts via vprint_status. + }, + 'Author' => 'RageLtMan ', + 'License' => MSF_LICENSE, + 'References' => [] + )) + end + + # + # Wrapper for service execution and cleanup + # + def run + begin + start_service + service.wait + rescue Rex::BindFailed => e + print_error "Failed to bind to port #{datastore['RPORT']}: #{e.message}" + ensure + stop_service(true) + end + end + + # + # Creates Proc to handle incoming requests + # + def on_dispatch_request(cli,data) + return if data.strip.empty? + req = Packet.encode_drb(data) + peer = "#{cli.peerhost}:#{cli.peerport}" + asked = req.question.map(&:qname).map(&:to_s).join(', ') + vprint_status("Received request for #{asked} from #{peer}") + answered = [] + # Find cached items, remove request from forwarded packet + req.question.each do |ques| + cached = service.cache.find(ques.qname, ques.qtype.to_s) + if cached.empty? + next + else + req.instance_variable_set(:@answer, (req.answer + cached).uniq) + answered << ques + cached.map do |hit| + if hit.respond_to?(:address) + hit.name.to_s + ':' + hit.address.to_s + ' ' + hit.type.to_s + else + hit.name.to_s + ' ' + hit.type.to_s + end + end.each {|h| vprint_status("Cache hit for #{h}")} + end + end unless service.cache.nil? + # Forward remaining requests, cache responses + if answered.count < req.question.count and service.fwd_res + if !req.header.rd + vprint_status("Recursion forbidden in query for #{req.question.first.name} from #{peer}") + else + forward = req.dup + # forward.question = req.question - answered + forward.instance_variable_set(:@question, req.question - answered) + forwarded = service.fwd_res.send(Packet.validate(forward)) + forwarded.answer.each do |ans| + rstring = ans.respond_to?(:address) ? "#{ans.name}:#{ans.address}" : ans.name + vprint_status("Caching response #{rstring} #{ans.type}") + service.cache.cache_record(ans) + end unless service.cache.nil? + # Merge the answers and use the upstream response + forward.instance_variable_set(:@question, (req.answer + forwarded.answer).uniq) + req = forwarded + end + end + service.send_response(cli, Packet.validate(Packet.generate_response(req)).encode) + end + + # + # Creates Proc to handle outbound responses + # + def on_send_response(cli,data) + res = Packet.encode_drb(data) + peer = "#{cli.peerhost}:#{cli.peerport}" + asked = res.question.map(&:qname).map(&:to_s).join(', ') + vprint_status("Sending response for #{asked} to #{peer}") + cli.write(data) + end + + +end diff --git a/modules/auxiliary/spoof/dns/native_spoofer.rb b/modules/auxiliary/spoof/dns/native_spoofer.rb new file mode 100644 index 0000000000..23ea530050 --- /dev/null +++ b/modules/auxiliary/spoof/dns/native_spoofer.rb @@ -0,0 +1,162 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core/exploit/dns' + +class MetasploitModule < Msf::Auxiliary + + include Msf::Exploit::Capture + include Msf::Exploit::Remote::DNS::Client + include Msf::Exploit::Remote::DNS::Server + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Native DNS Spoofer (Example)', + 'Description' => %q{ + This module provides a Rex based DNS service to resolve queries intercepted + via the capture mixin. Configure STATIC_ENTRIES to contain host-name mappings + desired for spoofing using a hostsfile or space/semicolon separated entries. + In default configuration, the service operates as a normal native DNS server + with the exception of consuming from and writing to the wire as opposed to a + listening socket. Best when compromising routers or spoofing L2 in order to + prevent return of the real reply which causes a race condition. The method + by which replies are filtered is up to the user (though iptables works fine). + }, + 'Author' => 'RageLtMan ', + 'License' => MSF_LICENSE, + 'References' => [] + )) + + register_options( + [ + OptString.new('FILTER', [false, 'The filter string for capturing traffic', 'dst port 53']), + OptAddress.new('SRVHOST', [true, 'The local host to listen on for DNS services.', '127.0.2.2']) + ]) + + deregister_options('PCAPFILE') + end + + # + # Wrapper for service execution and cleanup + # + def run + begin + start_service + capture_traffic + service.wait + rescue Rex::BindFailed => e + print_error "Failed to bind to port #{datastore['RPORT']}: #{e.message}" + ensure + @capture_thread.kill if @capture_thread + close_pcap + stop_service(true) + end + end + + # + # Generates reply with src and dst reversed + # Maintains original packet structure, proto, etc, changes ip_id + # + def reply_packet(pack) + rep = pack.dup + rep.eth_dst, rep.eth_src = rep.eth_src, rep.eth_dst + rep.ip_dst, rep.ip_src = rep.ip_src, rep.ip_dst + if pack.is_udp? + rep.udp_dst, rep.udp_src = rep.udp_src, rep.udp_dst + else + rep.tcp_dst, rep.tcp_src = rep.tcp_src, rep.tcp_dst + end + rep.ip_id = StructFu::Int16.new(rand(2**16)) + return rep + end + + # + # Configures capture and handoff + # + def capture_traffic + check_pcaprub_loaded() + ::Socket.do_not_reverse_lookup = true # Mac OS X workaround + open_pcap({'FILTER' => datastore['FILTER']}) + @capture_thread = Rex::ThreadFactory.spawn("DNSSpoofer", false) do + each_packet do |pack| + begin + parsed = PacketFu::Packet.parse(pack) + reply = reply_packet(parsed) + service.dispatch_request(reply, parsed.payload) + rescue => e + vprint_status("PacketFu could not parse captured packet") + dlog(e.backtrace) + end + end + end + end + + # + # Creates Proc to handle incoming requests + # + def on_dispatch_request(cli,data) + peer = "#{cli.ip_daddr}:" << (cli.is_udp? ? "#{cli.udp_dst}" : "#{cli.tcp_dst}") + # Deal with non DNS traffic + begin + req = Packet.encode_net(data) + rescue => e + print_error("Could not decode payload segment of packet from #{peer}, check log") + dlog e.backtrace + return + end + answered = [] + # Find cached items, remove request from forwarded packet + req.question.each do |ques| + cached = service.cache.find(ques.qName, ques.qType.to_s) + if cached.empty? + next + else + req.answer = (req.answer + cached).uniq + answered << ques + end + end + if answered.count < req.question.count and service.fwd_res + if !req.header.recursive? + vprint_status("Recursion forbidden in query for #{req.question.first.name} from #{peer}") + else + forward = req.dup + forward.question = req.question - answered + forwarded = service.fwd_res.send(Packet.validate(forward)) + forwarded.answer.each do |ans| + rstring = ans.respond_to?(:address) ? "#{ans.name}:#{ans.address}" : ans.name + vprint_status("Caching response #{rstring} #{ans.type}") + service.cache.cache_record(ans) + end unless service.cache.nil? + # Merge the answers and use the upstream response + forwarded.answer = (req.answer + forwarded.answer).uniq + req = forwarded + end + end + service.send_response(cli, Packet.validate(Packet.generate_response(req)).data) + end + + # + # Creates Proc to handle outbound responses + # + def on_send_response(cli,data) + cli.payload = data + cli.recalc + inject cli.to_s + sent_info(cli,data) if datastore['VERBOSE'] + end + + # + # Prints information about spoofed packet after injection to reduce latency of operation + # Shown to improve response time by >50% from ~1ms -> 0.3-0.4ms + # + def sent_info(cli,data) + net = Packet.encode_net(data) + peer = "#{cli.ip_daddr}:" << (cli.is_udp? ? "#{cli.udp_dst}" : "#{cli.tcp_dst}") + asked = net.question.map(&:qName).join(', ') + vprint_good("Sent packet with header:\n#{cli.inspect}") + vprint_good("Spoofed records for #{asked} to #{peer}") + end + +end