Land #18526, Add a command to customise DNS resolution
This commit is contained in:
commit
0d591a3136
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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']
|
||||
|
|
|
@ -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]}"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue