Land #18526, Add a command to customise DNS resolution

This commit is contained in:
Christophe De La Fuente 2023-11-23 17:46:16 +01:00
commit 0d591a3136
No known key found for this signature in database
GPG Key ID: 9E350956EA00352A
13 changed files with 873 additions and 67 deletions

View File

@ -602,6 +602,10 @@ class Meterpreter < Rex::Post::Meterpreter::Client
sock
end
def supports_udp?
true
end
#
# Get a string representation of the current session platform
#

View File

@ -287,6 +287,10 @@ module Msf::Sessions
sock
end
def supports_udp?
false
end
def create_server_channel(params)
msf_channel = nil
mutex = Mutex.new

View File

@ -20,6 +20,7 @@ module Msf
MANAGER_COMMANDS = 'manager_commands'
METASPLOIT_PAYLOAD_WARNINGS = 'metasploit_payload_warnings'
DEFER_MODULE_LOADS = 'defer_module_loads'
DNS_FEATURE = 'dns_feature'
DEFAULTS = [
{
name: WRAPPED_TABLES,
@ -53,6 +54,12 @@ module Msf
description: 'When enabled will not eagerly load all modules',
requires_restart: true,
default_value: false
}.freeze,
{
name: DNS_FEATURE,
description: 'When enabled, allows configuration of DNS resolution behaviour in Metasploit',
requires_restart: false,
default_value: false
}.freeze
].freeze

View File

@ -82,6 +82,12 @@ class Framework
require 'msf/core/cert_provider'
Rex::Socket::Ssl.cert_provider = Msf::Ssl::CertProvider
if options.include?('CustomDnsResolver')
self.dns_resolver = options['CustomDnsResolver']
self.dns_resolver.set_framework(self)
Rex::Socket._install_global_resolver(self.dns_resolver)
end
subscriber = FrameworkEventSubscriber.new(self)
events.add_exploit_subscriber(subscriber)
events.add_session_subscriber(subscriber)
@ -147,6 +153,10 @@ class Framework
Version
end
#
# DNS resolver for the framework
#
attr_reader :dns_resolver
#
# Event management interface for registering event handler subscribers and
# for interacting with the correlation engine.
@ -278,6 +288,7 @@ protected
# @return [Hash]
attr_accessor :options
attr_writer :dns_resolver #:nodoc:
attr_writer :events # :nodoc:
attr_writer :modules # :nodoc:
attr_writer :datastore # :nodoc:

View File

@ -22,6 +22,13 @@ module Comm
def create(param)
raise NotImplementedError
end
#
# Does the Comm support sending UDP messages?
#
def supports_udp?
raise NotImplementedError
end
end
end

View File

@ -1350,6 +1350,7 @@ class Core
# Save the framework's datastore
begin
framework.save_config
driver.framework.dns_resolver.save_config
if active_module
active_module.save_config

View File

@ -0,0 +1,334 @@
# -*- coding: binary -*-
module Msf
module Ui
module Console
module CommandDispatcher
class DNS
include Msf::Ui::Console::CommandDispatcher
@@add_opts = Rex::Parser::Arguments.new(
['-r', '--rule'] => [true, 'Set a DNS wildcard entry to match against' ],
['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)' ],
)
@@remove_opts = Rex::Parser::Arguments.new(
['-i'] => [true, 'Index to remove']
)
def initialize(driver)
super
end
def name
'DNS'
end
def commands
commands = {}
if framework.features.enabled?(Msf::FeatureManager::DNS_FEATURE)
commands = {
'dns' => "Manage Metasploit's DNS resolving behaviour"
}
end
commands
end
#
# Tab completion for the dns command
#
# @param str [String] the string currently being typed before tab was hit
# @param words [Array<String>] the previously completed words on the command line. The array
# contains at least one entry when tab completion has reached this stage since the command itself has been completed
def cmd_dns_tabs(str, words)
return if driver.framework.dns_resolver.nil?
if words.length == 1
options = ['add','del','remove','purge','print']
return options.select { |opt| opt.start_with?(str) }
end
cmd = words[1]
case cmd
when 'purge','print'
# These commands don't have any arguments
return
when 'add'
# We expect a repeating pattern of tag (e.g. -r) and then a value (e.g. *.metasploit.com)
# Once this pattern is violated, we're just specifying DNS servers at that point.
tag_is_expected = true
if words.length > 2
words[2..-1].each do |word|
if tag_is_expected && !word.start_with?('-')
return # They're trying to specify a DNS server - we can't help them from here on out
end
tag_is_expected = !tag_is_expected
end
end
case words[-1]
when '-s', '--session'
session_ids = driver.framework.sessions.keys.map { |k| k.to_s }
return session_ids.select { |id| id.start_with?(str) }
when '-r', '--rule'
# Hard to auto-complete a rule with any meaningful value; just return
return
when /^-/
# Unknown tag
return
end
options = @@add_opts.option_keys.select { |opt| opt.start_with?(str) }
options << '' # Prevent tab-completion of a dash, given they could provide an IP address at this point
return options
when 'del','remove'
if words[-1] == '-i'
ids = driver.framework.dns_resolver.nameserver_entries.flatten.map { |entry| entry[:id].to_s }
return ids.select { |id| id.start_with? str }
else
return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) }
end
end
end
def cmd_dns_help
print_line "Manage Metasploit's DNS resolution behaviour"
print_line
print_line "Usage:"
print_line " dns [add] [--session <session_id>] [--rule <wildcard DNS entry>] <IP Address> <IP Address> ..."
print_line " dns [remove/del] -i <entry id> [-i <entry id> ...]"
print_line " dns [purge]"
print_line " dns [print]"
print_line
print_line "Subcommands:"
print_line " add - add a DNS resolution entry to resolve certain domain names through a particular DNS server"
print_line " remove - delete a DNS resolution entry; 'del' is an alias"
print_line " purge - remove all DNS resolution entries"
print_line " print - show all active DNS resolution entries"
print_line
print_line "Examples:"
print_line " Display all current DNS nameserver entries"
print_line " dns"
print_line " dns print"
print_line
print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10"
print_line " route add --rule *.metasploit.com 192.168.1.10"
print_line
print_line " Add multiple entries at once"
print_line " route add --rule *.metasploit.com --rule *.google.com 192.168.1.10 192.168.1.11"
print_line
print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10, but specifically to go through session 2"
print_line " route add --session 2 --rule *.metasploit.com 192.168.1.10"
print_line
print_line " Delete the DNS resolution rule with ID 3"
print_line " route remove -i 3"
print_line
print_line " Delete multiple entries in one command"
print_line " route remove -i 3 -i 4 -i 5"
print_line
print_line " Set the DNS server(s) to be used for all requests that match no rules"
print_line " route add 8.8.8.8 8.8.4.4"
print_line
end
#
# Manage Metasploit's DNS resolution rules
#
def cmd_dns(*args)
return if driver.framework.dns_resolver.nil?
args << 'print' if args.length == 0
# Short-circuit help
if args.delete("-h") || args.delete("--help")
cmd_dns_help
return
end
action = args.shift
begin
case action
when "add"
add_dns(*args)
when "remove", "del"
remove_dns(*args)
when "purge"
purge_dns
when "print"
print_dns
when "help"
cmd_dns_help
else
print_error("Invalid command. To view help: dns -h")
end
rescue ::ArgumentError => e
print_error(e.message)
end
end
def add_dns(*args)
rules = []
comm = nil
servers = []
@@add_opts.parse(args) do |opt, idx, val|
unless servers.empty? || opt.nil?
raise ::ArgumentError.new("Invalid command near #{opt}")
end
case opt
when '--rule', '-r'
raise ::ArgumentError.new('No rule specified') if val.nil?
rules << val
when '--session', '-s'
if val.nil?
raise ::ArgumentError.new('No session specified')
end
unless comm.nil?
raise ::ArgumentError.new('Only one session can be specified')
end
comm = val
when nil
servers << val
else
raise ::ArgumentError.new("Unknown flag: #{opt}")
end
end
# The remaining args should be the DNS servers
if servers.length < 1
raise ::ArgumentError.new("You must specify at least one DNS server")
end
servers.each do |host|
unless Rex::Socket.is_ip_addr?(host)
raise ::ArgumentError.new("Invalid DNS server: #{host}")
end
end
comm_obj = nil
unless comm.nil?
raise ::ArgumentError.new("Not a valid number: #{comm}") unless comm =~ /^\d+$/
comm_int = comm.to_i
raise ::ArgumentError.new("Session does not exist: #{comm}") unless driver.framework.sessions.include?(comm_int)
comm_obj = driver.framework.sessions[comm_int]
end
rules.each do |rule|
print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*')
end
# Split each DNS server entry up into a separate entry
servers.each do |server|
driver.framework.dns_resolver.add_nameserver(rules, server, comm_obj)
end
print_good("#{servers.length} DNS #{servers.length > 1 ? 'entries' : 'entry'} added")
end
#
# Remove all matching user-configured DNS entries
#
def remove_dns(*args)
remove_ids = []
@@remove_opts.parse(args) do |opt, idx, val|
case opt
when '-i'
raise ::ArgumentError.new("Not a valid number: #{val}") unless val =~ /^\d+$/
remove_ids << val.to_i
end
end
removed = driver.framework.dns_resolver.remove_ids(remove_ids)
difference = remove_ids.difference(removed.map { |entry| entry[:id] })
print_warning("Some entries were not removed: #{difference.join(', ')}") unless difference.empty?
if removed.length > 0
print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed")
print_dns_set('Deleted entries', removed)
end
end
#
# Delete all user-configured DNS settings
#
def purge_dns
driver.framework.dns_resolver.purge
print_good('DNS entries purged')
end
#
# Display the user-configured DNS settings
#
def print_dns
results = driver.framework.dns_resolver.nameserver_entries
columns = ['ID','Rule(s)', 'DNS Server', 'Comm channel']
print_dns_set('Custom nameserver rules', results[0])
# Default nameservers don't include a rule
columns = ['ID', 'DNS Server', 'Comm channel']
print_dns_set('Default nameservers', results[1])
print_line('No custom DNS nameserver entries configured') if results[0].length + results[1].length == 0
end
private
#
# Get user-friendly text for displaying the session that this entry would go through
#
def prettify_comm(comm, dns_server)
if comm.nil?
channel = Rex::Socket::SwitchBoard.best_comm(dns_server)
if channel.nil?
nil
else
"Session #{channel.sid} (route)"
end
else
if comm.alive?
"Session #{comm.sid}"
else
"Closed session (#{comm.sid})"
end
end
end
def print_dns_set(heading, result_set)
return if result_set.length == 0
if result_set[0][:wildcard_rules].any?
columns = ['ID', 'Rules(s)', 'DNS Server', 'Comm channel']
else
columns = ['ID', 'DNS Server', 'Commm channel']
end
tbl = Table.new(
Table::Style::Default,
'Header' => heading,
'Prefix' => "\n",
'Postfix' => "\n",
'Columns' => columns
)
result_set.each do |hash|
if columns.size == 4
tbl << [hash[:id], hash[:wildcard_rules].join(','), hash[:dns_server], prettify_comm(hash[:comm], hash[:dns_server])]
else
tbl << [hash[:id], hash[:dns_server], prettify_comm(hash[:comm], hash[:dns_server])]
end
end
print(tbl.to_s) if tbl.rows.length > 0
end
def resolver
self.driver.framework.dns_resolver
end
end
end
end
end
end

View File

@ -29,7 +29,8 @@ class Driver < Msf::Ui::Driver
CommandDispatcher::Resource,
CommandDispatcher::Db,
CommandDispatcher::Creds,
CommandDispatcher::Developer
CommandDispatcher::Developer,
CommandDispatcher::DNS
]
#
@ -79,8 +80,16 @@ class Driver < Msf::Ui::Driver
# Initialize attributes
dns_resolver = Rex::Proto::DNS::CachedResolver.new
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
dns_resolver.load_config
# Defer loading of modules until paths from opts can be added below
framework_create_options = opts.merge('DeferModuleLoads' => true)
framework_create_options = opts.merge({
'DeferModuleLoads' => true,
'CustomDnsResolver' => dns_resolver
}
)
self.framework = opts['Framework'] || Msf::Simple::Framework.create(framework_create_options)
if self.framework.datastore['Prompt']

View File

@ -975,7 +975,7 @@ module Net # :nodoc:
end
end
ans = self.old_send(method,packet,packet_data)
ans = self.old_send(method,packet,packet_data, nameservers.map {|ns| [ns, {}]})
unless ans
@logger.fatal "No response from nameservers list: aborting"
@ -1027,7 +1027,8 @@ module Net # :nodoc:
answers = []
soa = 0
self.old_send(method, packet, packet_data) do |ans|
nameservers_and_hash = nameservers.map {|ns| [ns, {}]}
self.old_send(method, packet, packet_data, nameservers_and_hash) do |ans|
@logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}"
begin
@ -1161,12 +1162,12 @@ module Net # :nodoc:
end
def send_tcp(packet,packet_data)
def send_tcp(packet,packet_data, nameservers)
ans = nil
length = [packet_data.size].pack("n")
@config[:nameservers].each do |ns|
nameservers.each do |ns, _unused|
begin
socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)
socket.bind(Socket.pack_sockaddr_in(@config[:source_port],@config[:source_address].to_s))
@ -1233,13 +1234,13 @@ module Net # :nodoc:
return nil
end
def send_udp(packet,packet_data)
def send_udp(packet, packet_data, nameservers)
socket = UDPSocket.new
socket.bind(@config[:source_address].to_s,@config[:source_port])
ans = nil
response = ""
@config[:nameservers].each do |ns|
nameservers.each do |ns, _unused|
begin
@config[:udp_timeout].timeout do
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"

View File

@ -21,6 +21,7 @@ module DNS
#
# @return [nil]
def initialize(config = {})
dns_cache_no_start = config.delete(:dns_cache_no_start)
super(config)
self.cache = Rex::Proto::DNS::Cache.new
# Read hostsfile into cache
@ -72,7 +73,7 @@ module DNS
end
end
# TODO: inotify or similar on hostsfile for live updates? Easy-button functionality
self.cache.start unless config[:dns_cache_no_start]
self.cache.start unless dns_cache_no_start
return
end

View File

@ -0,0 +1,242 @@
module Rex
module Proto
module DNS
##
# Provides a DNS resolver the ability to use different nameservers
# for different requests, based on the domain being queried.
##
module CustomNameserverProvider
CONFIG_KEY = 'framework/dns'
#
# A Comm implementation that always reports as dead, so should never
# be used. This is used to prevent DNS leaks of saved DNS rules that
# were attached to a specific channel.
##
class CommSink
include Msf::Session::Comm
def alive?
false
end
def supports_udp?
# It won't be used anyway, so let's just say we support it
true
end
def sid
'previous MSF session'
end
end
def init
self.entries_with_rules = []
self.entries_without_rules = []
self.next_id = 0
end
#
# Save the custom settings to the MSF config file
#
def save_config
new_config = {}
[self.entries_with_rules, self.entries_without_rules].each do |entry_set|
entry_set.each do |entry|
key = entry[:id].to_s
val = [entry[:wildcard_rules].join(','),
entry[:dns_server],
(!entry[:comm].nil?).to_s
].join(';')
new_config[key] = val
end
end
Msf::Config.save(CONFIG_KEY => new_config)
end
#
# Load the custom settings from the MSF config file
#
def load_config
config = Msf::Config.load
with_rules = []
without_rules = []
next_id = 0
dns_settings = config.fetch(CONFIG_KEY, {}).each do |name, value|
id = name.to_i
wildcard_rules, dns_server, uses_comm = value.split(';')
wildcard_rules = wildcard_rules.split(',')
raise Msf::Config::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm)
raise Msf::Config::ConfigError.new('Invalid DNS config: Invalid DNS server') unless Rex::Socket.is_ip_addr?(dns_server)
raise Msf::Config::ConfigError.new('Invalid DNS config: Invalid rule') unless wildcard_rules.all? {|rule| valid_rule?(rule)}
comm = uses_comm == 'true' ? CommSink.new : nil
entry = {
:wildcard_rules => wildcard_rules,
:dns_server => dns_server,
:comm => comm,
:id => id
}
if wildcard_rules.empty?
without_rules << entry
else
with_rules << entry
end
next_id = [id + 1, next_id].max
end
# Now that config has successfully read, update the global values
self.entries_with_rules = with_rules
self.entries_without_rules = without_rules
self.next_id = next_id
end
# Add a custom nameserver entry to the custom provider
# @param wildcard_rules [Array<String>] The wildcard rules to match a DNS request against
# @param dns_server [Array<String>] The list of IP addresses that would be used for this custom rule
# @param comm [Msf::Session::Comm] The communication channel to be used for these DNS requests
def add_nameserver(wildcard_rules, dns_server, comm)
raise ::ArgumentError.new("Invalid DNS server: #{dns_server}") unless Rex::Socket.is_ip_addr?(dns_server)
wildcard_rules.each do |rule|
raise ::ArgumentError.new("Invalid rule: #{rule}") unless valid_rule?(rule)
end
entry = {
:wildcard_rules => wildcard_rules,
:dns_server => dns_server,
:comm => comm,
:id => self.next_id
}
self.next_id += 1
if wildcard_rules.empty?
entries_without_rules << entry
else
entries_with_rules << entry
end
end
#
# Remove entries with the given IDs
# Ignore entries that are not found
# @param ids [Array<Integer>] The IDs to removed
# @return [Array<Hash>] The removed entries
#
def remove_ids(ids)
removed= []
ids.each do |id|
removed_with, remaining_with = self.entries_with_rules.partition {|entry| entry[:id] == id}
self.entries_with_rules.replace(remaining_with)
removed_without, remaining_without = self.entries_without_rules.partition {|entry| entry[:id] == id}
self.entries_without_rules.replace(remaining_without)
removed.concat(removed_with)
removed.concat(removed_without)
end
removed
end
#
# The custom nameserver entries that have been configured
# @return [Array<Array>] An array containing two elements: The entries with rules, and the entries without rules
#
def nameserver_entries
[entries_with_rules, entries_without_rules]
end
def purge
init
end
# The nameservers that match the given packet
# @param packet [Dnsruby::Message] The DNS packet to be sent
# @raise [ResolveError] If the packet contains multiple questions, which would end up sending to a different set of nameservers
# @return [Array<Array>] A list of nameservers, each with Rex::Socket options
#
def nameservers_for_packet(packet)
unless feature_set.enabled?(Msf::FeatureManager::DNS_FEATURE)
return super
end
# Leaky abstraction: a packet could have multiple question entries,
# and each of these could have different nameservers, or travel via
# different comm channels. We can't allow DNS leaks, so for now, we
# will throw an error here.
results_from_all_questions = []
packet.question.each do |question|
name = question.qname.to_s
dns_servers = []
self.entries_with_rules.each do |entry|
entry[:wildcard_rules].each do |rule|
if matches(name, rule)
socket_options = {}
socket_options['Comm'] = entry[:comm] unless entry[:comm].nil?
dns_servers.append([entry[:dns_server], socket_options])
break
end
end
end
# Only look at the rule-less entries if no rules were found (avoids DNS leaks)
if dns_servers.empty?
self.entries_without_rules.each do |entry|
socket_options = {}
socket_options['Comm'] = entry[:comm] unless entry[:comm].nil?
dns_servers.append([entry[:dns_server], socket_options])
end
end
if dns_servers.empty?
# Fall back to default nameservers
dns_servers = super
end
results_from_all_questions << dns_servers.uniq
end
results_from_all_questions.uniq!
if results_from_all_questions.size != 1
raise ResolverError.new('Inconsistent nameserver entries attempted to be sent in the one packet')
end
results_from_all_questions[0]
end
def self.extended(mod)
mod.init
end
def set_framework(framework)
self.feature_set = framework.features
end
private
#
# Is the given wildcard DNS entry valid?
#
def valid_rule?(rule)
rule =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/
end
def matches(domain, pattern)
if pattern.start_with?('*.')
domain.downcase.end_with?(pattern[1..-1].downcase)
else
domain.casecmp?(pattern)
end
end
attr_accessor :entries_with_rules # Set of custom nameserver entries that specify a rule
attr_accessor :entries_without_rules # Set of custom nameserver entries that do not include a rule
attr_accessor :next_id # The next ID to have been allocated to an entry
attr_accessor :feature_set
end
end
end
end

View File

@ -14,8 +14,8 @@ module DNS
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
:config_file => "/etc/resolv.conf",
:log_file => File::NULL, # formerly $stdout, should be tied in with our loggers
:port => 53,
:searchlist => [],
:nameservers => [IPAddr.new("127.0.0.1")],
@ -110,19 +110,25 @@ module DNS
end
end
#
# Find the nameservers to use for a given DNS request
# @param _dns_message [Dnsruby::Message] The DNS message to be sent
#
# @return [Array<Array>] A list of nameservers, each with Rex::Socket options
#
def nameservers_for_packet(_dns_message)
@config[:nameservers].map {|ns| [ns.to_s, {}]}
end
#
# Send DNS request over appropriate transport and process response
#
# @param argument [Object] An object holding the DNS message to be processed.
# @param type [Fixnum] Type of record to look up
# @param cls [Fixnum] Class of question to look up
#
# @return [Dnsruby::Message] DNS response
#
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
@ -136,6 +142,11 @@ module DNS
packet = Rex::Proto::DNS::Packet.encode_drb(net_packet)
end
nameservers = nameservers_for_packet(packet)
if nameservers.size == 0
raise ResolverError, "No nameservers specified!"
end
# Store packet_data for performance improvements,
# so methods don't keep on calling Packet#encode
packet_data = packet.encode
@ -149,6 +160,9 @@ module DNS
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
elsif !supports_udp?(nameservers)
@logger.info "Sending #{packet_size} bytes using TCP due to the presence of a non-UDP-compatible comm channel"
method = :send_tcp
else # Finally use UDP
@logger.info "Sending #{packet_size} bytes using UDP"
method = :send_udp unless method == :send_tcp
@ -160,7 +174,7 @@ module DNS
method = :send_tcp
end
ans = self.__send__(method, packet, packet_data)
ans = self.__send__(method, packet, packet_data, nameservers)
unless (ans and ans[0].length > 0)
@logger.fatal "No response from nameservers list: aborting"
@ -189,38 +203,47 @@ module DNS
#
# @param packet [Net::DNS::Packet] Packet associated with packet_data
# @param packet_data [String] Data segment of DNS request packet
# @param nameservers [Array<[String,Hash]>] List of nameservers to use for this request, and their associated socket options
# @param prox [String] Proxy configuration for TCP socket
#
# @return ans [String] Raw DNS reply
def send_tcp(packet,packet_data,prox = @config[:proxies])
def send_tcp(packet, packet_data, nameservers, prox = @config[:proxies])
ans = nil
length = [packet_data.size].pack("n")
@config[:nameservers].each do |ns|
nameservers.each do |ns, socket_options|
begin
socket = nil
config = {
'PeerHost' => ns.to_s,
'PeerPort' => @config[:port].to_i,
'Proxies' => prox,
'Context' => @config[:context],
'Comm' => @config[:comm]
}
config.update(socket_options)
unless config['Comm'].nil? || config['Comm'].alive?
@logger.warn("Session #{config['Comm'].sid} not active, and cannot be used to resolve DNS")
throw :next_ns
end
suffix = " over session #{@config['Comm'].sid}" unless @config['Comm'].nil?
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
@config[:tcp_timeout].timeout do
catch(:next_ns) do
suffix = ''
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]}"
@logger.warn "TCP Socket could not be established to #{ns}:#{@config[:port]} #{@config[:proxies]}#{suffix}"
throw :next_ns
end
next unless socket #
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}#{suffix}"
socket.write(length+packet_data)
got_something = false
loop do
@ -229,7 +252,7 @@ module DNS
begin
ans = socket.recv(2)
rescue Errno::ECONNRESET
@logger.warn "TCP Socket got Errno::ECONNRESET from #{ns}:#{@config[:port]} #{@config[:proxies]}"
@logger.warn "TCP Socket got Errno::ECONNRESET from #{ns}:#{@config[:port]} #{@config[:proxies]}#{suffix}"
attempts -= 1
retry if attempts > 0
end
@ -237,7 +260,7 @@ module DNS
if got_something
break #Proper exit from loop
else
@logger.warn "Connection reset to nameserver #{ns}, trying next."
@logger.warn "Connection reset to nameserver #{ns}#{suffix}, trying next."
throw :next_ns
end
end
@ -247,7 +270,7 @@ module DNS
@logger.info "Receiving #{len} bytes..."
if len.nil? or len == 0
@logger.warn "Receiving 0 length packet from nameserver #{ns}, trying next."
@logger.warn "Receiving 0 length packet from nameserver #{ns}#{suffix}, trying next."
throw :next_ns
end
@ -258,7 +281,7 @@ module DNS
end
unless buffer.size == len
@logger.warn "Malformed packet from nameserver #{ns}, trying next."
@logger.warn "Malformed packet from nameserver #{ns}#{suffix}, trying next."
throw :next_ns
end
if block_given?
@ -270,7 +293,7 @@ module DNS
end
end
rescue Timeout::Error
@logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
@logger.warn "Nameserver #{ns}#{suffix} not responding within TCP timeout, trying next one"
next
ensure
socket.close if socket
@ -284,41 +307,50 @@ module DNS
#
# @param packet [Net::DNS::Packet] Packet associated with packet_data
# @param packet_data [String] Data segment of DNS request packet
# @param nameservers [Array<[String,Hash]>] List of nameservers to use for this request, and their associated socket options
#
# @return ans [String] Raw DNS reply
def send_udp(packet,packet_data)
def send_udp(packet,packet_data, nameservers)
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]
nameservers.each do |ns, socket_options|
catch(:next_ns) do
begin
@config[:udp_timeout].timeout do
begin
config = {
'PeerHost' => ns.to_s,
'PeerPort' => @config[:port].to_i,
'Context' => @config[:context],
'Comm' => @config[:comm]
}
config.update(socket_options)
unless config['Comm'].nil? || config['Comm'].alive?
@logger.warn("Session #{config['Comm'].sid} not active, and cannot be used to resolve DNS")
throw :next_ns
end
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]}"
throw :next_ns
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
@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
@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])
break if ans
rescue Timeout::Error
@logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one"
throw :next_ds
end
break if ans
rescue Timeout::Error
@logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one"
next
end
end
return ans
@ -377,6 +409,17 @@ module DNS
return send(name,type,cls)
end
private
def supports_udp?(nameserver_results)
nameserver_results.each do |nameserver, socket_options|
comm = socket_options.fetch('Comm') { @config[:comm] || Rex::Socket::SwitchBoard.best_comm(nameserver) }
next if comm.nil?
return false unless comm.supports_udp?
end
true
end
end # Resolver
end

View File

@ -0,0 +1,142 @@
# -*- coding:binary -*-
require 'spec_helper'
require 'net/dns'
RSpec.describe Rex::Proto::DNS::CustomNameserverProvider do
def packet_for(name)
packet = Net::DNS::Packet.new(name, Net::DNS::A, Net::DNS::IN)
Rex::Proto::DNS::Packet.encode_drb(packet)
end
let(:base_nameserver) do
'1.2.3.4'
end
let(:ruleless_nameserver) do
'1.2.3.5'
end
let(:ruled_nameserver) do
'1.2.3.6'
end
let(:ruled_nameserver2) do
'1.2.3.7'
end
let(:ruled_nameserver3) do
'1.2.3.8'
end
let (:config) do
{:dns_cache_no_start => true}
end
let (:framework_with_dns_enabled) do
framework = Object.new
def framework.features
f = Object.new
def f.enabled?(_name)
true
end
f
end
framework
end
subject(:many_ruled_provider) do
dns_resolver = Rex::Proto::DNS::CachedResolver.new(config)
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
dns_resolver.nameservers = [base_nameserver]
dns_resolver.add_nameserver([], ruleless_nameserver, nil)
dns_resolver.add_nameserver(['*.metasploit.com'], ruled_nameserver, nil)
dns_resolver.add_nameserver(['*.metasploit.com'], ruled_nameserver2, nil)
dns_resolver.add_nameserver(['*.notmetasploit.com'], ruled_nameserver3, nil)
dns_resolver.set_framework(framework_with_dns_enabled)
dns_resolver
end
subject(:ruled_provider) do
dns_resolver = Rex::Proto::DNS::CachedResolver.new(config)
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
dns_resolver.nameservers = [base_nameserver]
dns_resolver.add_nameserver([], ruleless_nameserver, nil)
dns_resolver.add_nameserver(['*.metasploit.com'], ruled_nameserver, nil)
dns_resolver.set_framework(framework_with_dns_enabled)
dns_resolver
end
subject(:ruleless_provider) do
dns_resolver = Rex::Proto::DNS::CachedResolver.new(config)
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
dns_resolver.nameservers = [base_nameserver]
dns_resolver.add_nameserver([], ruleless_nameserver, nil)
dns_resolver.set_framework(framework_with_dns_enabled)
dns_resolver
end
subject(:empty_provider) do
dns_resolver = Rex::Proto::DNS::CachedResolver.new(config)
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
dns_resolver.nameservers = [base_nameserver]
dns_resolver.set_framework(framework_with_dns_enabled)
dns_resolver
end
context 'When no nameserver is configured' do
it 'The resolver base is returned' do
packet = packet_for('subdomain.metasploit.com')
ns = empty_provider.nameservers_for_packet(packet)
expect(ns).to eq([[base_nameserver, {}]])
end
end
context 'When a base nameserver is configured' do
it 'The base nameserver is returned' do
packet = packet_for('subdomain.metasploit.com')
ns = ruleless_provider.nameservers_for_packet(packet)
expect(ns).to eq([[ruleless_nameserver, {}]])
end
end
context 'When a nameserver rule is configured and a rule entry matches' do
it 'The correct nameserver is returned' do
packet = packet_for('subdomain.metasploit.com')
ns = ruled_provider.nameservers_for_packet(packet)
expect(ns).to eq([[ruled_nameserver, {}]])
end
end
context 'When a nameserver rule is configured and no rule entry is applicable' do
it 'The base nameserver is returned when no rule entry' do
packet = packet_for('subdomain.notmetasploit.com')
ns = ruled_provider.nameservers_for_packet(packet)
expect(ns).to eq([[ruleless_nameserver, {}]])
end
end
context 'When many rules are configured' do
it 'Returns multiple entries if multiple rules match' do
packet = packet_for('subdomain.metasploit.com')
ns = many_ruled_provider.nameservers_for_packet(packet)
expect(ns).to eq([[ruled_nameserver, {}], [ruled_nameserver2, {}]])
end
end
context 'When a packet contains multiple questions that have different nameserver results' do
it 'Throws an error' do
packet = packet_for('subdomain.metasploit.com')
q = Dnsruby::Question.new('subdomain.notmetasploit.com', Dnsruby::Types::A, Dnsruby::Classes::IN)
packet.question.append(q)
expect {many_ruled_provider.nameservers_for_packet(packet)}.to raise_error(ResolverError)
end
end
end