From 9181d9380739e54eafdee2bc1226fa89df55909f Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 30 Jan 2024 17:23:09 -0500 Subject: [PATCH 01/35] Search for a resolv.conf file --- lib/rex/proto/dns/resolver.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 67dacd2d808c..8928ab0245c5 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -14,7 +14,7 @@ module DNS class Resolver < Net::DNS::Resolver Defaults = { - :config_file => "/etc/resolv.conf", + :config_file => nil, :log_file => File::NULL, # formerly $stdout, should be tied in with our loggers :port => 53, :searchlist => [], @@ -40,11 +40,12 @@ class Resolver < Net::DNS::Resolver # # Provide override for initializer to use local Defaults constant # - # @param config [Hash] Configuration options as conusumed by parent class + # @param config [Hash] Configuration options as consumed 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 + @config[:config_file] ||= self.class.default_config_file @raw = false # New logger facility @logger = Logger.new(@config[:log_file]) @@ -400,6 +401,15 @@ def query(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) end + def self.default_config_file + %w[ + /etc/resolv.conf + /data/data/com.termux/files/usr/etc/resolv.conf + ].find do |path| + File.file?(path) && File.readable?(path) + end + end + private def supports_udp?(nameserver_results) From 6fdfd7147c389e9ce398e26cd13a0fe4f30b09c5 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 31 Jan 2024 10:13:40 -0500 Subject: [PATCH 02/35] Print the system nameservers too --- lib/msf/ui/console/command_dispatcher/dns.rb | 16 ++++++++++------ lib/rex/proto/dns/custom_nameserver_provider.rb | 5 ++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index ec538a7c14e9..356381938b45 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -265,14 +265,18 @@ def purge_dns # 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 + print_dns_set( + 'Default nameservers', + # name servers loaded from the system environment are appended to the end + results[1] + resolver.nameservers.map { |ns| { id: '[system]', dns_server: ns } } + ) + + if results[0].length + results[1].length + resolver.nameservers.length == 0 + print_error('No DNS nameserver entries configured') + end end private @@ -299,7 +303,7 @@ def prettify_comm(comm, dns_server) def print_dns_set(heading, result_set) return if result_set.length == 0 - if result_set[0][:wildcard_rules].any? + if result_set[0][:wildcard_rules]&.any? columns = ['ID', 'Rules(s)', 'DNS Server', 'Comm channel'] else columns = ['ID', 'DNS Server', 'Commm channel'] diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index bab8c55390dc..f987dd0c3b51 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -11,7 +11,7 @@ module CustomNameserverProvider # # 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 + # be used. This is used to prevent DNS leaks of saved DNS rules that # were attached to a specific channel. ## class CommSink @@ -223,7 +223,6 @@ 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) @@ -239,4 +238,4 @@ def matches(domain, pattern) end end end -end \ No newline at end of file +end From 20f73867cac9f754d2668f4c934f3611048831fd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 1 Feb 2024 14:13:48 -0500 Subject: [PATCH 03/35] Print the default domain and search list too --- lib/msf/ui/console/command_dispatcher/dns.rb | 25 +++++++++++++++++--- lib/net/dns/resolver.rb | 4 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 356381938b45..4777c4f09813 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -247,7 +247,7 @@ def remove_dns(*args) 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_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed") print_dns_set('Deleted entries', removed) end end @@ -264,7 +264,26 @@ def purge_dns # Display the user-configured DNS settings # def print_dns - results = driver.framework.dns_resolver.nameserver_entries + default_domain = 'N/A' + if resolver.defname? && resolver.domain.present? + default_domain = resolver.domain + end + print_line("Default search domain: #{default_domain}") + + searchlist = resolver.searchlist + case searchlist.length + when 0 + print_line('Default search list: N/A') + when 1 + print_line("Default search list: #{searchlist.first}") + else + print_line('Default search list:') + searchlist.each do |entry| + print_line(" * #{entry}") + end + end + + results = resolver.nameserver_entries print_dns_set('Custom nameserver rules', results[0]) # Default nameservers don't include a rule @@ -335,4 +354,4 @@ def resolver end end end -end \ No newline at end of file +end diff --git a/lib/net/dns/resolver.rb b/lib/net/dns/resolver.rb index b1c2a4859d54..454274ed8b3f 100644 --- a/lib/net/dns/resolver.rb +++ b/lib/net/dns/resolver.rb @@ -253,7 +253,7 @@ def initialize(config = {}) # #=> ["example.com","a.example.com","b.example.com"] # def searchlist - @config[:searchlist].inspect + @config[:searchlist].deep_dup end # Set the resolver searchlist. @@ -350,7 +350,7 @@ def nameservers=(arg) # Return a string with the default domain # def domain - @config[:domain].inspect + @config[:domain].dup end # Set the domain for the query From c780bfcb663c6f7d4f0b9575838f662e8bc66002 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 31 Jan 2024 09:42:47 -0500 Subject: [PATCH 04/35] Add a DNS query command for inspection --- lib/msf/ui/console/command_dispatcher/dns.rb | 72 +++++++++++++++++--- lib/rex/proto/dns/resolver.rb | 2 +- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 4777c4f09813..a1d06b3b6516 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -14,6 +14,8 @@ class DNS ['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)' ], ) + @@query_opts = Rex::Parser::Arguments.new([]) + @@remove_opts = Rex::Parser::Arguments.new( ['-i'] => [true, 'Index to remove'] ) @@ -98,16 +100,18 @@ def cmd_dns_help print_line "Manage Metasploit's DNS resolution behaviour" print_line print_line "Usage:" - print_line " dns [add] [--session ] [--rule ] ..." + print_line " dns [add] [--session ] [--rule ] ..." print_line " dns [remove/del] -i [-i ...]" print_line " dns [purge]" print_line " dns [print]" + print_line " dns [query] ..." 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 " 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 " purge - remove all DNS resolution entries" + print_line " print - show all active DNS resolution entries" + print_line " query - resolve a hostname" print_line print_line "Examples:" print_line " Display all current DNS nameserver entries" @@ -115,22 +119,25 @@ def cmd_dns_help 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 " dns 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 " dns 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 " dns 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 " dns 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 " dns 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 " dns add 8.8.8.8 8.8.4.4" + print_line + print_line " Resolve a hostname using the current configuration" + print_line " dns query www.metasploit.com " print_line end @@ -160,6 +167,8 @@ def cmd_dns(*args) print_dns when "help" cmd_dns_help + when "query" + query_dns(*args) else print_error("Invalid command. To view help: dns -h") end @@ -230,6 +239,47 @@ def add_dns(*args) print_good("#{servers.length} DNS #{servers.length > 1 ? 'entries' : 'entry'} added") end + # + # Query a hostname using the configuration. This is useful for debugging and + # inspecting the active settings. + # + def query_dns(*args) + names = [] + + @@query_opts.parse(args) do |opt, idx, val| + unless names.empty? || opt.nil? + raise ::ArgumentError.new("Invalid command near #{opt}") + end + case opt + when nil + names << val + else + raise ::ArgumentError.new("Unknown flag: #{opt}") + end + end + + tbl = Table.new( + Table::Style::Default, + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => %W[ Name Result ] + ) + names.each do |name| + begin + result = resolver.query(name) + rescue NoResponseError + tbl << [name, ''] + else + result.answer.select do |answer| + answer.type == Dnsruby::Types::A + end.map(&:address).map(&:to_s).each do |address| + tbl << [name, address] + end + end + end + print(tbl.to_s) + end + # # Remove all matching user-configured DNS entries # @@ -325,7 +375,7 @@ def print_dns_set(heading, result_set) if result_set[0][:wildcard_rules]&.any? columns = ['ID', 'Rules(s)', 'DNS Server', 'Comm channel'] else - columns = ['ID', 'DNS Server', 'Commm channel'] + columns = ['ID', 'DNS Server', 'Comm channel'] end tbl = Table.new( diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 8928ab0245c5..fa505ca99fb2 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -18,7 +18,7 @@ class Resolver < Net::DNS::Resolver :log_file => File::NULL, # formerly $stdout, should be tied in with our loggers :port => 53, :searchlist => [], - :nameservers => [IPAddr.new("127.0.0.1")], + :nameservers => [], :domain => "", :source_port => 0, :source_address => IPAddr.new("0.0.0.0"), From fd943f1401d4e8a55b8a7dbabcf6d90ab3ec72a9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 31 Jan 2024 14:59:25 -0500 Subject: [PATCH 05/35] Make the resolve subcommand more like Meterpreter Make the dns resolve subcommand more like the resolve command in Meterpreter. Also alphabetize things. --- lib/msf/ui/console/command_dispatcher/dns.rb | 95 +++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index a1d06b3b6516..4963ec4a8c74 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -14,12 +14,15 @@ class DNS ['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)' ], ) - @@query_opts = Rex::Parser::Arguments.new([]) - @@remove_opts = Rex::Parser::Arguments.new( ['-i'] => [true, 'Index to remove'] ) + @@resolve_opts = Rex::Parser::Arguments.new( + # same usage syntax as Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Stdapi + ['-f'] => [true, 'Address family - IPv4 or IPv6 (default IPv4)'] + ) + def initialize(driver) super end @@ -33,7 +36,7 @@ def commands if framework.features.enabled?(Msf::FeatureManager::DNS_FEATURE) commands = { - 'dns' => "Manage Metasploit's DNS resolving behaviour" + 'dns' => "Manage Metasploit's DNS resolving behaviour" } end commands @@ -49,15 +52,12 @@ def cmd_dns_tabs(str, words) return if driver.framework.dns_resolver.nil? if words.length == 1 - options = ['add','del','remove','purge','print'] + options = %w[ add delete print purge query remove resolve ] 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. @@ -86,13 +86,23 @@ def cmd_dns_tabs(str, words) 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' + when 'help','print','purge' + # These commands don't have any arguments + return + when 'remove','delete' 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 } + return ids.select { |id| id.start_with?(str) } else return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) } end + when 'resolve','query' + if words[-1] == '-f' + families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive + return families.select { |family| family.downcase.start_with?(str.downcase) } + else + @@resolve_opts.option_keys.select { |opt| opt.start_with?(str) } + end end end @@ -104,14 +114,14 @@ def cmd_dns_help print_line " dns [remove/del] -i [-i ...]" print_line " dns [purge]" print_line " dns [print]" - print_line " dns [query] ..." + print_line " dns [resolve] ..." 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 " query - resolve a hostname" + 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 " resolve - resolve a hostname" print_line print_line "Examples:" print_line " Display all current DNS nameserver entries" @@ -137,7 +147,7 @@ def cmd_dns_help print_line " dns add 8.8.8.8 8.8.4.4" print_line print_line " Resolve a hostname using the current configuration" - print_line " dns query www.metasploit.com " + print_line " dns resolve -f IPv6 www.metasploit.com" print_line end @@ -159,16 +169,16 @@ def cmd_dns(*args) 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 - when "query" - query_dns(*args) + when "print" + print_dns + when "purge" + purge_dns + when "remove", "rm", "delete", "del" + remove_dns(*args) + when "resolve", "query" + resolve_dns(*args) else print_error("Invalid command. To view help: dns -h") end @@ -243,14 +253,24 @@ def add_dns(*args) # Query a hostname using the configuration. This is useful for debugging and # inspecting the active settings. # - def query_dns(*args) + def resolve_dns(*args) names = [] + query_type = Dnsruby::Types::A - @@query_opts.parse(args) do |opt, idx, val| + @@resolve_opts.parse(args) do |opt, idx, val| unless names.empty? || opt.nil? raise ::ArgumentError.new("Invalid command near #{opt}") end case opt + when '-f' + case val.downcase + when 'ipv4' + query_type = Dnsruby::Types::A + when'ipv6' + query_type = Dnsruby::Types::AAAA + else + raise ::ArgumentError.new("Invalid family: #{val}") + end when nil names << val else @@ -258,22 +278,31 @@ def query_dns(*args) end end + if names.length < 1 + raise ::ArgumentError.new('You must specify at least one hostname to resolve') + end + tbl = Table.new( Table::Style::Default, + 'Header' => 'Host resolutions', 'Prefix' => "\n", 'Postfix' => "\n", - 'Columns' => %W[ Name Result ] + 'Columns' => ['Hostname', 'IP Address'] ) names.each do |name| begin - result = resolver.query(name) + result = resolver.query(name, query_type) rescue NoResponseError - tbl << [name, ''] + tbl << [name, '[Failed To Resolve]'] else - result.answer.select do |answer| - answer.type == Dnsruby::Types::A - end.map(&:address).map(&:to_s).each do |address| - tbl << [name, address] + if result.answer.empty? + tbl << [name, '[Failed To Resolve]'] + else + result.answer.select do |answer| + answer.type == query_type + end.map(&:address).map(&:to_s).each do |address| + tbl << [name, address] + end end end end From 319cff7d3a94b03e15288ce67ff31b55cb0b2f44 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 31 Jan 2024 15:35:08 -0500 Subject: [PATCH 06/35] Change the DNS timeout from 30 to 5 seconds Also, add the #to_i method for timeouts This makes it compatible with Rex Sockets later on --- lib/net/dns/resolver/timeouts.rb | 16 ++++++++++------ lib/rex/proto/dns/resolver.rb | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/net/dns/resolver/timeouts.rb b/lib/net/dns/resolver/timeouts.rb index e7523e478f76..e19fb1cb8666 100644 --- a/lib/net/dns/resolver/timeouts.rb +++ b/lib/net/dns/resolver/timeouts.rb @@ -21,27 +21,31 @@ def transform(secs) class DnsTimeout # :nodoc: all include SecondsHandle - + def initialize(seconds) if seconds.is_a? Numeric and seconds >= 0 @timeout = seconds else raise DnsTimeoutArgumentError, "Invalid value for tcp timeout" - end + end + end + + def to_i + @timeout end - + def to_s - if @timeout == 0 + if @timeout == 0 @output else @timeout.to_s end end - + def pretty_to_s transform(@timeout) end - + def timeout unless block_given? raise DnsTimeoutArgumentError, "Block required but missing" diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index fa505ca99fb2..d68b84d2fd8e 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -30,8 +30,8 @@ class Resolver < Net::DNS::Resolver :use_tcp => false, :ignore_truncated => false, :packet_size => 512, - :tcp_timeout => 30, - :udp_timeout => 30, + :tcp_timeout => TcpTimeout.new(5), + :udp_timeout => UdpTimeout.new(5), :context => {}, :comm => nil } From 282f97ba2d3ff1a27eade9994978d7c4323774a9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 31 Jan 2024 16:04:26 -0500 Subject: [PATCH 07/35] Add the flush-cache subcommand Also rename purge to flush-entries and update descriptions to clarify what exactly is being flushed. --- lib/msf/ui/console/command_dispatcher/dns.rb | 38 +++++++++++++------- lib/rex/proto/dns/cache.rb | 8 +++++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 4963ec4a8c74..d25045fce0bf 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -52,7 +52,7 @@ def cmd_dns_tabs(str, words) return if driver.framework.dns_resolver.nil? if words.length == 1 - options = %w[ add delete print purge query remove resolve ] + options = %w[ add delete flush-cache flush-entries print query remove resolve ] return options.select { |opt| opt.start_with?(str) } end @@ -86,7 +86,7 @@ def cmd_dns_tabs(str, words) 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 'help','print','purge' + when 'flush-cache','flush-entries','help','print' # These commands don't have any arguments return when 'remove','delete' @@ -112,16 +112,18 @@ def cmd_dns_help print_line "Usage:" print_line " dns [add] [--session ] [--rule ] ..." print_line " dns [remove/del] -i [-i ...]" - print_line " dns [purge]" + print_line " dns [flush-cache]" + print_line " dns [flush-entries]" print_line " dns [print]" print_line " dns [resolve] ..." 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 " resolve - resolve a hostname" + 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 " print - show all configured DNS resolution entries" + print_line " flush-entries - remove all configured DNS resolution entries" + print_line " flush-cache - remove all cached DNS answers" + print_line " resolve - resolve a hostname" print_line print_line "Examples:" print_line " Display all current DNS nameserver entries" @@ -169,12 +171,14 @@ def cmd_dns(*args) case action when "add" add_dns(*args) + when "flush-entries" + flush_entries_dns + when "flush-cache" + flush_cache_dns when "help" cmd_dns_help when "print" print_dns - when "purge" - purge_dns when "remove", "rm", "delete", "del" remove_dns(*args) when "resolve", "query" @@ -331,12 +335,20 @@ def remove_dns(*args) end end + # + # Delete all cached DNS answers + # + def flush_cache_dns + resolver.cache.flush + print_good('DNS cache flushed') + end + # # Delete all user-configured DNS settings # - def purge_dns - driver.framework.dns_resolver.purge - print_good('DNS entries purged') + def flush_entries_dns + resolver.purge + print_good('DNS entries flushed') end # diff --git a/lib/rex/proto/dns/cache.rb b/lib/rex/proto/dns/cache.rb index d5dffff25d4f..ffe86354cbda 100644 --- a/lib/rex/proto/dns/cache.rb +++ b/lib/rex/proto/dns/cache.rb @@ -75,6 +75,14 @@ def add_static(name, address, type = Dnsruby::Types::A, replace = false) end end + # + # Delete all cache entries, this is different from pruning because the + # record's expiration is ignored + # + def flush + self.records.each {|rec, _| delete(rec)} + end + # # Prune cache entries # From 7fe10d8613a5a788cdb0edb9797dfb9406e85bfb Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 1 Feb 2024 11:32:57 -0500 Subject: [PATCH 08/35] Switch to a more generic upstream resolvers model Upstream resolvers can be DNS servers or something else. --- lib/msf/ui/console/command_dispatcher/dns.rb | 6 +-- .../proto/dns/custom_nameserver_provider.rb | 50 ++++++++++++------- lib/rex/proto/dns/exceptions.rb | 14 ++++++ lib/rex/proto/dns/resolver.rb | 13 +++-- lib/rex/proto/dns/upstream_resolver.rb | 21 ++++++++ 5 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 lib/rex/proto/dns/exceptions.rb create mode 100644 lib/rex/proto/dns/upstream_resolver.rb diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index d25045fce0bf..35e6707c1b4f 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -381,7 +381,7 @@ def print_dns print_dns_set( 'Default nameservers', # name servers loaded from the system environment are appended to the end - results[1] + resolver.nameservers.map { |ns| { id: '[system]', dns_server: ns } } + results[1] + resolver.nameservers.map { |ns| { id: '', server: ns } } ) if results[0].length + results[1].length + resolver.nameservers.length == 0 @@ -428,9 +428,9 @@ def print_dns_set(heading, result_set) ) 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])] + tbl << [hash[:id], hash[:wildcard_rules].join(','), hash[:server], prettify_comm(hash[:comm], hash[:server])] else - tbl << [hash[:id], hash[:dns_server], prettify_comm(hash[:comm], hash[:dns_server])] + tbl << [hash[:id], hash[:server], prettify_comm(hash[:comm], hash[:server])] end end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index f987dd0c3b51..f65ad7b30ef6 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -1,3 +1,5 @@ +require 'rex/proto/dns/upstream_resolver' + module Rex module Proto module DNS @@ -45,7 +47,7 @@ def save_config entry_set.each do |entry| key = entry[:id].to_s val = [entry[:wildcard_rules].join(','), - entry[:dns_server], + entry[:server], (!entry[:comm].nil?).to_s ].join(';') new_config[key] = val @@ -70,14 +72,14 @@ def load_config 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)} + raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid DNS server') unless Rex::Socket.is_ip_addr?(dns_server) + raise Rex::Proto::DNS::Exceptions::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, + :server => dns_server, :comm => comm, :id => id } @@ -103,13 +105,15 @@ def load_config # @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, + :type => UpstreamResolver::TYPE_DNS_SERVER, + :server => dns_server, :comm => comm, :id => self.next_id } @@ -160,7 +164,7 @@ def purge # @raise [ResolveError] If the packet contains multiple questions, which would end up sending to a different set of nameservers # @return [Array] A list of nameservers, each with Rex::Socket options # - def nameservers_for_packet(packet) + def upstream_resolvers_for_packet(packet) unless feature_set.enabled?(Msf::FeatureManager::DNS_FEATURE) return super end @@ -171,33 +175,41 @@ def nameservers_for_packet(packet) results_from_all_questions = [] packet.question.each do |question| name = question.qname.to_s - dns_servers = [] + upstream_resolvers = [] 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 + next unless matches(name, rule) + + socket_options = {} + socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? + upstream_resolvers.append(UpstreamResolver.new( + UpstreamResolver::TYPE_DNS_SERVER, + destination: entry[:server], + socket_options: socket_options + )) + break end end # Only look at the rule-less entries if no rules were found (avoids DNS leaks) - if dns_servers.empty? + if upstream_resolvers.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]) + upstream_resolvers.append(UpstreamResolver.new( + UpstreamResolver::TYPE_DNS_SERVER, + destination: entry[:dns_server], + socket_options: socket_options + )) end end - if dns_servers.empty? + if upstream_resolvers.empty? # Fall back to default nameservers - dns_servers = super + upstream_resolvers = super end - results_from_all_questions << dns_servers.uniq + results_from_all_questions << upstream_resolvers.uniq end results_from_all_questions.uniq! if results_from_all_questions.size != 1 diff --git a/lib/rex/proto/dns/exceptions.rb b/lib/rex/proto/dns/exceptions.rb new file mode 100644 index 000000000000..e06a167a92cd --- /dev/null +++ b/lib/rex/proto/dns/exceptions.rb @@ -0,0 +1,14 @@ +# -*- coding: binary -*- + +module Rex +module Proto +module DNS +module Exceptions + + class ConfigError < Rex::RuntimeError + end + +end +end +end +end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index d68b84d2fd8e..37fc21996f41 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -116,8 +116,10 @@ def proxies=(prox, timeout_added = 250) # # @return [Array] A list of nameservers, each with Rex::Socket options # - def nameservers_for_packet(_dns_message) - @config[:nameservers].map {|ns| [ns.to_s, {}]} + def upstream_resolvers_for_packet(_dns_message) + @config[:nameservers].map do |ns| + UpstreamResolver.new(UpstreamResolver::TYPE_DNS_SERVER, destination: ns.to_s) + end end # @@ -142,7 +144,12 @@ def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) packet = Rex::Proto::DNS::Packet.encode_drb(net_packet) end - nameservers = nameservers_for_packet(packet) + upstream_resolvers = upstream_resolvers_for_packet(packet) + nameservers = upstream_resolvers.select do |us| + us.type == UpstreamResolver::TYPE_DNS_SERVER + end.map do |us| + [us.destination, us.socket_options] + end if nameservers.size == 0 raise ResolverError, "No nameservers specified!" end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb new file mode 100644 index 000000000000..53f5e9bd0fe4 --- /dev/null +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -0,0 +1,21 @@ +# -*- coding: binary -*- + +require 'rex/socket' + +module Rex +module Proto +module DNS + class UpstreamResolver + TYPE_SYSTEM = :system + TYPE_DNS_SERVER = :dns_server + + attr_reader :type, :destination, :socket_options + def initialize(type, destination: nil, socket_options: {}) + @type = type + @destination = destination + @socket_options = socket_options + end + end +end +end +end From 464d2eef73a2b1d5896b0b34ef8be59657146674 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 1 Feb 2024 13:35:34 -0500 Subject: [PATCH 09/35] Add a method for upstream resolvers from query args --- lib/rex/proto/dns/resolver.rb | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 37fc21996f41..94dd2190fe50 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -122,6 +122,12 @@ def upstream_resolvers_for_packet(_dns_message) end end + def upstream_resolvers_for_query(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) + name, type, cls = preprocess_query_arguments(name, type, cls) + packet = make_query_packet(name, type, cls) + upstream_resolvers_for_packet(packet) + end + # # Send DNS request over appropriate transport and process response # @@ -394,18 +400,9 @@ def search(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) # # @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 - + name, type, cls = preprocess_query_arguments(name, type, cls) @logger.debug "Query(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" - - return send(name,type,cls) - + send(name,type,cls) end def self.default_config_file @@ -419,6 +416,16 @@ def self.default_config_file private + def preprocess_query_arguments(name, type, cls) + return [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 + [name, type, cls] + end + 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) } From a5dc63617fbe44425f1e2e45d6c2983585b64fd3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 1 Feb 2024 16:39:08 -0500 Subject: [PATCH 10/35] Refactor resolver entries to unify them Now resolution will match a single rule, but that rule can have multiple servers. --- lib/msf/ui/console/command_dispatcher/dns.rb | 63 +++++---- .../proto/dns/custom_nameserver_provider.rb | 128 +++++++----------- 2 files changed, 78 insertions(+), 113 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 35e6707c1b4f..a8148862e932 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -91,7 +91,7 @@ def cmd_dns_tabs(str, words) return when 'remove','delete' if words[-1] == '-i' - ids = driver.framework.dns_resolver.nameserver_entries.flatten.map { |entry| entry[:id].to_s } + ids = driver.framework.dns_resolver.upstream_entries.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) } @@ -192,7 +192,8 @@ def cmd_dns(*args) end def add_dns(*args) - rules = [] + rules = ['*'] + first_rule = true comm = nil servers = [] @@add_opts.parse(args) do |opt, idx, val| @@ -203,6 +204,8 @@ def add_dns(*args) when '--rule', '-r' raise ::ArgumentError.new('No rule specified') if val.nil? + rules.clear if first_rule # if the user defines even one rule, clear the defaults + first_rule = false rules << val when '--session', '-s' if val.nil? @@ -244,13 +247,10 @@ def add_dns(*args) rules.each do |rule| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') + driver.framework.dns_resolver.add_nameserver(servers, comm: comm_obj, wildcard_rule: rule) 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") + print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") end # @@ -374,17 +374,10 @@ def print_dns end end - results = resolver.nameserver_entries - print_dns_set('Custom nameserver rules', results[0]) - - # Default nameservers don't include a rule - print_dns_set( - 'Default nameservers', - # name servers loaded from the system environment are appended to the end - results[1] + resolver.nameservers.map { |ns| { id: '', server: ns } } - ) + upstream_entries = resolver.upstream_entries + print_dns_set('Resolver rule entries', upstream_entries) - if results[0].length + results[1].length + resolver.nameservers.length == 0 + if upstream_entries.empty? print_error('No DNS nameserver entries configured') end end @@ -413,24 +406,30 @@ def prettify_comm(comm, dns_server) 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', 'Comm channel'] - end + columns = ['ID', 'Rule', 'Resolver', 'Comm channel'] tbl = Table.new( - Table::Style::Default, - 'Header' => heading, - 'Prefix' => "\n", - 'Postfix' => "\n", - 'Columns' => columns - ) + Table::Style::Default, + 'Header' => heading, + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => columns, + 'SortIndex' => -1, + 'WordWrap' => false, + ) result_set.each do |hash| - if columns.size == 4 - tbl << [hash[:id], hash[:wildcard_rules].join(','), hash[:server], prettify_comm(hash[:comm], hash[:server])] - else - tbl << [hash[:id], hash[:server], prettify_comm(hash[:comm], hash[:server])] + if hash[:servers].length == 1 + tbl << [hash[:id], hash[:wildcard_rule], hash[:servers].first, prettify_comm(hash[:comm], hash[:servers].first)] + elsif hash[:servers].length > 1 + # XXX: By default rex-text tables strip preceding whitespace: + # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 + # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement + indent = "\xc2\xa0\xc2\xa0\\_ " + + tbl << [hash[:id], hash[:wildcard_rule], '', ''] + hash[:servers].each do |server| + tbl << ['.', indent, server, prettify_comm(hash[:comm], server)] + end end end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index f65ad7b30ef6..2cad9a4f9c60 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -33,8 +33,7 @@ def sid end def init - self.entries_with_rules = [] - self.entries_without_rules = [] + @upstream_entries = [] self.next_id = 0 end @@ -43,15 +42,13 @@ def init # 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[:server], - (!entry[:comm].nil?).to_s - ].join(';') - new_config[key] = val - end + @upstream_entries.each do |entry| + key = entry[:id].to_s + val = [entry[:wildcard_rule], + entry[:servers].join(','), + (!entry[:comm].nil?).to_s + ].join(';') + new_config[key] = val end Msf::Config.save(CONFIG_KEY => new_config) @@ -64,65 +61,54 @@ 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(',') + wildcard_rule, dns_servers, uses_comm = value.split(';') + wildcard_rule = '*' if wildcard_rule.blank? + dns_servers = dns_servers.split(',') raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid DNS server') unless Rex::Socket.is_ip_addr?(dns_server) - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless wildcard_rules.all? {|rule| valid_rule?(rule)} + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid DNS server') unless dns_servers.all? {|dns_server| Rex::Socket.is_ip_addr?(dns_server)} + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless valid_rule?(wildcard_rule) comm = uses_comm == 'true' ? CommSink.new : nil - entry = { - :wildcard_rules => wildcard_rules, - :server => dns_server, + with_rules << { + :wildcard_rule => wildcard_rule, + :servers => dns_servers, :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 + @upstream_entries = with_rules self.next_id = next_id end # Add a custom nameserver entry to the custom provider - # @param wildcard_rules [Array] The wildcard rules to match a DNS request against - # @param dns_server [Array] The list of IP addresses that would be used for this custom rule + # @param dns_servers [Array] 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) + # @param wildcard_rule String The wildcard rule to match a DNS request against + def add_nameserver(dns_servers, comm: nil, wildcard_rule: '*') + dns_servers = [dns_servers] if dns_servers.is_a?(String) # coerce into an array of strings + if (dns_server = dns_servers.find {|dns_server| !Rex::Socket.is_ip_addr?(dns_server)}) + raise ::ArgumentError.new("Invalid DNS server: #{dns_server}") end - entry = { - :wildcard_rules => wildcard_rules, + raise ::ArgumentError.new("Invalid rule: #{wildcard_rule}") unless valid_rule?(wildcard_rule) + + @upstream_entries << { + :wildcard_rule => wildcard_rule, :type => UpstreamResolver::TYPE_DNS_SERVER, - :server => dns_server, + :servers => dns_servers, :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 # @@ -134,27 +120,14 @@ def add_nameserver(wildcard_rules, dns_server, comm) 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_with, remaining_with = @upstream_entries.partition {|entry| entry[:id] == id} + @upstream_entries.replace(remaining_with) removed.concat(removed_with) - removed.concat(removed_without) end removed end - # - # The custom nameserver entries that have been configured - # @return [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 @@ -177,32 +150,19 @@ def upstream_resolvers_for_packet(packet) name = question.qname.to_s upstream_resolvers = [] - self.entries_with_rules.each do |entry| - entry[:wildcard_rules].each do |rule| - next unless matches(name, rule) - - socket_options = {} - socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? - upstream_resolvers.append(UpstreamResolver.new( - UpstreamResolver::TYPE_DNS_SERVER, - destination: entry[:server], - socket_options: socket_options - )) - break - end - end + self.upstream_entries.each do |entry| + next unless matches(name, entry[:wildcard_rule]) - # Only look at the rule-less entries if no rules were found (avoids DNS leaks) - if upstream_resolvers.empty? - self.entries_without_rules.each do |entry| - socket_options = {} - socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? + socket_options = {} + socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? + entry[:servers].each do |server| upstream_resolvers.append(UpstreamResolver.new( UpstreamResolver::TYPE_DNS_SERVER, - destination: entry[:dns_server], + destination: server, socket_options: socket_options )) end + break end if upstream_resolvers.empty? @@ -227,24 +187,30 @@ def set_framework(framework) self.feature_set = framework.features end + def upstream_entries + entries = @upstream_entries.dup + entries << { id: '', wildcard_rule: '*', servers: self.nameservers } + entries + 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]+$/ + rule == '*' || rule =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ end def matches(domain, pattern) - if pattern.start_with?('*.') + if pattern == '*' + true + elsif 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 From 3445c1b58834aac854f55c03453b786ec2ff8bb4 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 2 Feb 2024 16:10:20 -0500 Subject: [PATCH 11/35] Add the system resolver for A and AAAA queries --- lib/msf/ui/console/command_dispatcher/dns.rb | 29 ++-- .../proto/dns/custom_nameserver_provider.rb | 46 +++--- lib/rex/proto/dns/resolver.rb | 133 ++++++++++++------ 3 files changed, 137 insertions(+), 71 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index a8148862e932..08c01b510487 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -11,7 +11,7 @@ class DNS @@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)' ], + ['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)' ] ) @@remove_opts = Rex::Parser::Arguments.new( @@ -225,13 +225,12 @@ def add_dns(*args) 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) + unless Rex::Socket.is_ip_addr?(host) || host.casecmp?('system') raise ::ArgumentError.new("Invalid DNS server: #{host}") end end @@ -243,11 +242,15 @@ def add_dns(*args) 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] + if servers.include?('system') + comm_obj = nil if servers.length == 1 + print_warning("The session argument will be ignored for the system resolver") + end end rules.each do |rule| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') - driver.framework.dns_resolver.add_nameserver(servers, comm: comm_obj, wildcard_rule: rule) + driver.framework.dns_resolver.add_upstream_entry(servers, comm: comm_obj, wildcard_rule: rule) end print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") @@ -387,9 +390,11 @@ def print_dns # # 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) + def prettify_comm(comm, resolver) + if resolver.casecmp?('system') + 'N/A' + elsif comm.nil? + channel = Rex::Socket::SwitchBoard.best_comm(resolver) if channel.nil? nil else @@ -418,17 +423,17 @@ def print_dns_set(heading, result_set) 'WordWrap' => false, ) result_set.each do |hash| - if hash[:servers].length == 1 - tbl << [hash[:id], hash[:wildcard_rule], hash[:servers].first, prettify_comm(hash[:comm], hash[:servers].first)] - elsif hash[:servers].length > 1 + if hash[:resolvers].length == 1 + tbl << [hash[:id], hash[:wildcard_rule], hash[:resolvers].first, prettify_comm(hash[:comm], hash[:resolvers].first)] + elsif hash[:resolvers].length > 1 # XXX: By default rex-text tables strip preceding whitespace: # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement indent = "\xc2\xa0\xc2\xa0\\_ " tbl << [hash[:id], hash[:wildcard_rule], '', ''] - hash[:servers].each do |server| - tbl << ['.', indent, server, prettify_comm(hash[:comm], server)] + hash[:resolvers].each do |resolver| + tbl << ['.', indent, resolver, prettify_comm(hash[:comm], resolver)] end end end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 2cad9a4f9c60..a27c2f4af945 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -45,7 +45,7 @@ def save_config @upstream_entries.each do |entry| key = entry[:id].to_s val = [entry[:wildcard_rule], - entry[:servers].join(','), + entry[:resolvers].join(','), (!entry[:comm].nil?).to_s ].join(';') new_config[key] = val @@ -65,18 +65,18 @@ def load_config dns_settings = config.fetch(CONFIG_KEY, {}).each do |name, value| id = name.to_i - wildcard_rule, dns_servers, uses_comm = value.split(';') + wildcard_rule, resolvers, uses_comm = value.split(';') wildcard_rule = '*' if wildcard_rule.blank? - dns_servers = dns_servers.split(',') + resolvers = resolvers.split(',') raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid DNS server') unless dns_servers.all? {|dns_server| Rex::Socket.is_ip_addr?(dns_server)} + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid upstream DNS resolver') unless resolvers.all? {|resolver| valid_resolver?(resolver) } raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless valid_rule?(wildcard_rule) comm = uses_comm == 'true' ? CommSink.new : nil with_rules << { :wildcard_rule => wildcard_rule, - :servers => dns_servers, + :resolvers => resolvers, :comm => comm, :id => id } @@ -90,13 +90,13 @@ def load_config end # Add a custom nameserver entry to the custom provider - # @param dns_servers [Array] The list of IP addresses that would be used for this custom rule + # @param resolvers [Array] The list of upstream resolvers that would be used for this custom rule # @param comm [Msf::Session::Comm] The communication channel to be used for these DNS requests # @param wildcard_rule String The wildcard rule to match a DNS request against - def add_nameserver(dns_servers, comm: nil, wildcard_rule: '*') - dns_servers = [dns_servers] if dns_servers.is_a?(String) # coerce into an array of strings - if (dns_server = dns_servers.find {|dns_server| !Rex::Socket.is_ip_addr?(dns_server)}) - raise ::ArgumentError.new("Invalid DNS server: #{dns_server}") + def add_upstream_entry(resolvers, comm: nil, wildcard_rule: '*') + resolvers = [resolvers] if resolvers.is_a?(String) # coerce into an array of strings + if (resolver = resolvers.find {|resolver| !valid_resolver?(resolver)}) + raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") end raise ::ArgumentError.new("Invalid rule: #{wildcard_rule}") unless valid_rule?(wildcard_rule) @@ -104,7 +104,7 @@ def add_nameserver(dns_servers, comm: nil, wildcard_rule: '*') @upstream_entries << { :wildcard_rule => wildcard_rule, :type => UpstreamResolver::TYPE_DNS_SERVER, - :servers => dns_servers, + :resolvers => resolvers, :comm => comm, :id => self.next_id } @@ -155,12 +155,18 @@ def upstream_resolvers_for_packet(packet) socket_options = {} socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? - entry[:servers].each do |server| - upstream_resolvers.append(UpstreamResolver.new( - UpstreamResolver::TYPE_DNS_SERVER, - destination: server, - socket_options: socket_options - )) + entry[:resolvers].each do |resolver| + if resolver.casecmp?('system') + upstream_resolvers.append(UpstreamResolver.new( + UpstreamResolver::TYPE_SYSTEM + )) + elsif Rex::Socket.is_ip_addr?(resolver) + upstream_resolvers.append(UpstreamResolver.new( + UpstreamResolver::TYPE_DNS_SERVER, + destination: resolver, + socket_options: socket_options + )) + end end break end @@ -189,7 +195,7 @@ def set_framework(framework) def upstream_entries entries = @upstream_entries.dup - entries << { id: '', wildcard_rule: '*', servers: self.nameservers } + entries << { id: '', wildcard_rule: '*', resolvers: self.nameservers } entries end @@ -201,6 +207,10 @@ def valid_rule?(rule) rule == '*' || rule =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ end + def valid_resolver?(resolver) + Rex::Socket.is_ip_addr?(resolver) || resolver.casecmp?('system') + end + def matches(domain, pattern) if pattern == '*' true diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 94dd2190fe50..a6640542bd39 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -137,8 +137,6 @@ def upstream_resolvers_for_query(name, type = Dnsruby::Types::A, cls = Dnsruby:: # @return [Dnsruby::Message] DNS response # def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) - method = self.use_tcp? ? :send_tcp : :send_udp - case argument when Dnsruby::Message packet = argument @@ -151,50 +149,27 @@ def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) end upstream_resolvers = upstream_resolvers_for_packet(packet) - nameservers = upstream_resolvers.select do |us| - us.type == UpstreamResolver::TYPE_DNS_SERVER - end.map do |us| - [us.destination, us.socket_options] - end - if nameservers.size == 0 - raise ResolverError, "No nameservers specified!" + if upstream_resolvers.empty? + raise ResolverError, "No upstream resolvers specified!" 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 - 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 + ans = nil + upstream_resolvers.each do |upstream_resolver| + case upstream_resolver.type + when UpstreamResolver::TYPE_DNS_SERVER + ans = send_dns_server(upstream_resolver, packet, type, cls) + when UpstreamResolver::TYPE_SYSTEM + ans = send_system(upstream_resolver, packet, type, cls) end - end - if type == Dnsruby::Types::AXFR - @logger.warn "AXFR query, switching to TCP" unless method == :send_tcp - method = :send_tcp + break if (ans and ans[0].length > 0) end - ans = self.__send__(method, packet, packet_data, nameservers) - unless (ans and ans[0].length > 0) - @logger.fatal "No response from nameservers list: aborting" + @logger.fatal "No response from upstream resolvers: aborting" raise NoResponseError 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]) @@ -426,12 +401,88 @@ def preprocess_query_arguments(name, type, cls) [name, type, cls] end - 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? + def send_dns_server(upstream_resolver, packet, type, _cls) + method = self.use_tcp? ? :send_tcp : :send_udp + + # 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 + elsif !supports_udp?(upstream_resolver) + @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 + end + end + + if type == Dnsruby::Types::AXFR + @logger.warn "AXFR query, switching to TCP" unless method == :send_tcp + method = :send_tcp + end + + nameserver = [upstream_resolver.destination, upstream_resolver.socket_options] + ans = self.__send__(method, packet, packet_data, [nameserver]) + + if (ans and ans[0].length > 0) + @logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}" + end + + ans + end + + def send_system(upstream_resolver, packet, type, cls) + # This system resolver will use host operating systems `getaddrinfo` (or equivalent function) to perform name + # resolution. This is primarily useful if that functionality is hooked or modified by an external application such + # as proxychains. This handler though can only process A and AAAA requests. + return nil unless cls == Dnsruby::Classes::IN + + # todo: make sure this will work if the packet has multiple questions, figure out how that's handled + name = packet.question.first.qname.to_s + case type + when Dnsruby::Types::A + family = ::Socket::AF_INET + when Dnsruby::Types::AAAA + family = ::Socket::AF_INET6 + else + return nil end + + begin + addrinfos = ::Addrinfo.getaddrinfo(name, 0, family, ::Socket::SOCK_STREAM) + rescue ::SocketError + return nil + end + + message = Dnsruby::Message.new + message.add_question(name, type, cls) + addrinfos.each do |addrinfo| + message.add_answer(Dnsruby::RR.new_from_hash( + name: name, + type: type, + ttl: 0, # since we're querying the system, rely on that cache + address: addrinfo.ip_address + )) + end + [message.encode] + end + + def supports_udp?(upstream_resolver) + return false unless upstream_resolver.type == UpstreamResolver::TYPE_DNS_SERVER + + comm = upstream_resolver.socket_options.fetch('Comm') { @config[:comm] || Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination) } + return false if comm && !comm.supports_udp? + true end end # Resolver From 3c716041bd9e460ff26c980a9c4a1a5c1c201f32 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 2 Feb 2024 16:27:49 -0500 Subject: [PATCH 12/35] Add the blackhole resolver --- lib/msf/ui/console/command_dispatcher/dns.rb | 28 +++++++++++-------- .../proto/dns/custom_nameserver_provider.rb | 15 ++++++++-- lib/rex/proto/dns/resolver.rb | 6 ++++ lib/rex/proto/dns/upstream_resolver.rb | 1 + 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 08c01b510487..4d4adf3ef8b8 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -195,9 +195,9 @@ def add_dns(*args) rules = ['*'] first_rule = true comm = nil - servers = [] + resolvers = [] @@add_opts.parse(args) do |opt, idx, val| - unless servers.empty? || opt.nil? + unless resolvers.empty? || opt.nil? raise ::ArgumentError.new("Invalid command near #{opt}") end case opt @@ -218,20 +218,20 @@ def add_dns(*args) comm = val when nil - servers << val + resolvers << 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") + if resolvers.length < 1 + raise ::ArgumentError.new('You must specify at least one upstream DNS resolver') end - servers.each do |host| - unless Rex::Socket.is_ip_addr?(host) || host.casecmp?('system') - raise ::ArgumentError.new("Invalid DNS server: #{host}") + resolvers.each do |host| + unless Rex::Socket.is_ip_addr?(host) || SPECIAL_RESOLVERS.include?(host.downcase) + raise ::ArgumentError.new("Invalid DNS resolver: #{host}") end end @@ -242,15 +242,14 @@ def add_dns(*args) 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] - if servers.include?('system') - comm_obj = nil if servers.length == 1 + if resolvers.any? { |resolver| SPECIAL_RESOLVERS.include?(resolver.downcase) } print_warning("The session argument will be ignored for the system resolver") end end rules.each do |rule| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') - driver.framework.dns_resolver.add_upstream_entry(servers, comm: comm_obj, wildcard_rule: rule) + driver.framework.dns_resolver.add_upstream_entry(resolvers, comm: comm_obj, wildcard_rule: rule) end print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") @@ -387,11 +386,16 @@ def print_dns private + SPECIAL_RESOLVERS = [ + Rex::Proto::DNS::UpstreamResolver::TYPE_BLACKHOLE.to_s.downcase, + Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM.to_s.downcase + ].freeze + # # Get user-friendly text for displaying the session that this entry would go through # def prettify_comm(comm, resolver) - if resolver.casecmp?('system') + if !Rex::Socket.is_ip_addr?(resolver) 'N/A' elsif comm.nil? channel = Rex::Socket::SwitchBoard.best_comm(resolver) diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index a27c2f4af945..45066982799f 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -98,6 +98,7 @@ def add_upstream_entry(resolvers, comm: nil, wildcard_rule: '*') if (resolver = resolvers.find {|resolver| !valid_resolver?(resolver)}) raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") end + resolvers = resolvers.map { |resolver| Rex::Socket.is_ip_addr?(resolver) ? resolver : resolver.downcase.to_sym } raise ::ArgumentError.new("Invalid rule: #{wildcard_rule}") unless valid_rule?(wildcard_rule) @@ -156,7 +157,11 @@ def upstream_resolvers_for_packet(packet) socket_options = {} socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? entry[:resolvers].each do |resolver| - if resolver.casecmp?('system') + if resolver == UpstreamResolver::TYPE_BLACKHOLE + upstream_resolvers.append(UpstreamResolver.new( + UpstreamResolver::TYPE_BLACKHOLE + )) + elsif resolver == UpstreamResolver::TYPE_SYSTEM upstream_resolvers.append(UpstreamResolver.new( UpstreamResolver::TYPE_SYSTEM )) @@ -208,7 +213,13 @@ def valid_rule?(rule) end def valid_resolver?(resolver) - Rex::Socket.is_ip_addr?(resolver) || resolver.casecmp?('system') + return true if Rex::Socket.is_ip_addr?(resolver) + + resolver = resolver.downcase.to_sym + [ + UpstreamResolver::TYPE_BLACKHOLE, + UpstreamResolver::TYPE_SYSTEM + ].include?(resolver) end def matches(domain, pattern) diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index a6640542bd39..f947d52e812d 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -441,6 +441,12 @@ def send_dns_server(upstream_resolver, packet, type, _cls) ans end + def send_blackhole(upstream_resolver, packet, type, cls) + # do not just return nil because that will cause the next resolver to be used + @logger.info "No response from upstream resolvers: blackholed" + raise NoResponseError + end + def send_system(upstream_resolver, packet, type, cls) # This system resolver will use host operating systems `getaddrinfo` (or equivalent function) to perform name # resolution. This is primarily useful if that functionality is hooked or modified by an external application such diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index 53f5e9bd0fe4..b555337e5ae4 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -8,6 +8,7 @@ module DNS class UpstreamResolver TYPE_SYSTEM = :system TYPE_DNS_SERVER = :dns_server + TYPE_BLACKHOLE = :blackhole attr_reader :type, :destination, :socket_options def initialize(type, destination: nil, socket_options: {}) From 470a28921ece530633eb8f95f6208e066a4c0b7e Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 2 Feb 2024 17:16:23 -0500 Subject: [PATCH 13/35] Add dedicated help menus for subcommands with args --- lib/msf/ui/console/command_dispatcher/dns.rb | 112 +++++++++++++------ 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 4d4adf3ef8b8..76dcbebe94c2 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -9,15 +9,18 @@ class DNS include Msf::Ui::Console::CommandDispatcher + ADD_USAGE = 'dns [add] [--session ] [--rule ] ..."'.freeze @@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_USAGE = 'dns [remove/del] -i [-i ...]"'.freeze @@remove_opts = Rex::Parser::Arguments.new( ['-i'] => [true, 'Index to remove'] ) + RESOLVE_USAGE = 'dns [resolve] [-f
] ..."'.freeze @@resolve_opts = Rex::Parser::Arguments.new( # same usage syntax as Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Stdapi ['-f'] => [true, 'Address family - IPv4 or IPv6 (default IPv4)'] @@ -106,50 +109,42 @@ def cmd_dns_tabs(str, words) end end - def cmd_dns_help + def cmd_dns_help(*args) + if args.first.present? + if respond_to?("#{args.first}_dns_help") + # if it is a valid command with dedicated help information + return send("#{args.first}_dns_help") + elsif respond_to?("#{args.first}_dns") + # if it is a valid command without dedicated help information + print_error("No help menu is available for #{args.first}") + return + else + print_error("Invalid subcommand: #{args.first}") + end + end + print_line "Manage Metasploit's DNS resolution behaviour" print_line - print_line "Usage:" - print_line " dns [add] [--session ] [--rule ] ..." - print_line " dns [remove/del] -i [-i ...]" + print_line "USAGE:" + print_line " #{ADD_USAGE}" + print_line " #{REMOVE_USAGE}" print_line " dns [flush-cache]" print_line " dns [flush-entries]" print_line " dns [print]" - print_line " dns [resolve] ..." + print_line " #{RESOLVE_USAGE}" + print_line " dns [help] [subcommand]" 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 "SUBCOMMANDS:" + print_line " add - add a DNS resolution entry to resolve certain domain names through a particular DNS resolver" print_line " remove - delete a DNS resolution entry; 'del' is an alias" print_line " print - show all configured DNS resolution entries" print_line " flush-entries - remove all configured DNS resolution entries" print_line " flush-cache - remove all cached DNS answers" print_line " resolve - resolve a hostname" 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 " dns add --rule *.metasploit.com 192.168.1.10" - print_line - print_line " Add multiple entries at once" - print_line " dns 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 " dns add --session 2 --rule *.metasploit.com 192.168.1.10" - print_line - print_line " Delete the DNS resolution rule with ID 3" - print_line " dns remove -i 3" - print_line - print_line " Delete multiple entries in one command" - print_line " dns 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 " dns add 8.8.8.8 8.8.4.4" - print_line - print_line " Resolve a hostname using the current configuration" - print_line " dns resolve -f IPv6 www.metasploit.com" + print_line "EXAMPLES:" + print_line " Display help information for the 'add' subcommand" + print_line " dns help add" print_line end @@ -162,7 +157,13 @@ def cmd_dns(*args) args << 'print' if args.length == 0 # Short-circuit help if args.delete("-h") || args.delete("--help") - cmd_dns_help + if respond_to?("#{args.first}_dns_help") + # if it is a valid command with dedicated help information + send("#{args.first}_dns_help") + else + # otherwise print the top-level help information + cmd_dns_help + end return end @@ -176,7 +177,7 @@ def cmd_dns(*args) when "flush-cache" flush_cache_dns when "help" - cmd_dns_help + cmd_dns_help(*args) when "print" print_dns when "remove", "rm", "delete", "del" @@ -255,6 +256,26 @@ def add_dns(*args) print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") end + def add_dns_help + print_line "USAGE:" + print_line " #{ADD_USAGE}" + print_line @@add_opts.usage + print_line "RESOLVERS:" + print_line " ipv4 / ipv6 address - The IP address of an upstream DNS server to resolve from" + print_line " system - Use the host operating systems DNS resolution functionality (only for A/AAAA records)" + print_line " blackhole - Drop all queries" + print_line + print_line "EXAMPLES:" + print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10" + print_line " dns add --rule *.metasploit.com 192.168.1.10" + print_line + print_line " Add multiple entries at once" + print_line " dns 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 " dns add --session 2 --rule *.metasploit.com 192.168.1.10" + end + # # Query a hostname using the configuration. This is useful for debugging and # inspecting the active settings. @@ -315,6 +336,16 @@ def resolve_dns(*args) print(tbl.to_s) end + def resolve_dns_help + print_line "USAGE:" + print_line " #{RESOLVE_USAGE}" + print_line @@resolve_opts.usage + print_line "EXAMPLES:" + print_line " Resolve a hostname to an IPv6 address using the current configuration" + print_line " dns resolve -f IPv6 www.metasploit.com" + print_line + end + # # Remove all matching user-configured DNS entries # @@ -337,6 +368,19 @@ def remove_dns(*args) end end + def remove_dns_help + print_line "USAGE:" + print_line " #{REMOVE_USAGE}" + print_line(@@remove_opts.usage) + print_line "EXAMPLES:" + print_line " Delete the DNS resolution rule with ID 3" + print_line " dns remove -i 3" + print_line + print_line " Delete multiple entries in one command" + print_line " dns remove -i 3 -i 4 -i 5" + print_line + end + # # Delete all cached DNS answers # From 43a7993215615cab1dbf8334d2195111695f8839 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 5 Feb 2024 14:51:58 -0500 Subject: [PATCH 14/35] Show the matching rule for DNS resolution --- lib/msf/ui/console/command_dispatcher/dns.rb | 57 +++++---- .../proto/dns/custom_nameserver_provider.rb | 121 +++++------------- lib/rex/proto/dns/resolver.rb | 4 +- lib/rex/proto/dns/upstream_resolver.rb | 13 +- lib/rex/proto/dns/upstream_rule.rb | 71 ++++++++++ 5 files changed, 151 insertions(+), 115 deletions(-) create mode 100644 lib/rex/proto/dns/upstream_rule.rb diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 76dcbebe94c2..12e596901fb8 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -250,7 +250,7 @@ def add_dns(*args) rules.each do |rule| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') - driver.framework.dns_resolver.add_upstream_entry(resolvers, comm: comm_obj, wildcard_rule: rule) + driver.framework.dns_resolver.add_upstream_entry(resolvers, comm: comm_obj, wildcard: rule) end print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") @@ -312,23 +312,26 @@ def resolve_dns(*args) tbl = Table.new( Table::Style::Default, 'Header' => 'Host resolutions', - 'Prefix' => "\n", - 'Postfix' => "\n", - 'Columns' => ['Hostname', 'IP Address'] + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => ['Hostname', 'IP Address', 'ID', 'Rule', 'Resolver', 'Comm channel'], + 'SortIndex' => -1, + 'WordWrap' => false ) names.each do |name| + upstream_entry = resolver.upstream_entries.find { |ue| ue.matches_name?(name) } begin result = resolver.query(name, query_type) rescue NoResponseError - tbl << [name, '[Failed To Resolve]'] + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]']) else if result.answer.empty? - tbl << [name, '[Failed To Resolve]'] + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]']) else result.answer.select do |answer| answer.type == query_type end.map(&:address).map(&:to_s).each do |address| - tbl << [name, address] + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, address]) end end end @@ -438,11 +441,11 @@ def print_dns # # Get user-friendly text for displaying the session that this entry would go through # - def prettify_comm(comm, resolver) - if !Rex::Socket.is_ip_addr?(resolver) + def prettify_comm(comm, upstream_resolver) + if SPECIAL_RESOLVERS.include?(upstream_resolver.type.to_s) 'N/A' elsif comm.nil? - channel = Rex::Socket::SwitchBoard.best_comm(resolver) + channel = Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination) if channel.nil? nil else @@ -470,25 +473,31 @@ def print_dns_set(heading, result_set) 'SortIndex' => -1, 'WordWrap' => false, ) - result_set.each do |hash| - if hash[:resolvers].length == 1 - tbl << [hash[:id], hash[:wildcard_rule], hash[:resolvers].first, prettify_comm(hash[:comm], hash[:resolvers].first)] - elsif hash[:resolvers].length > 1 - # XXX: By default rex-text tables strip preceding whitespace: - # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 - # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement - indent = "\xc2\xa0\xc2\xa0\\_ " - - tbl << [hash[:id], hash[:wildcard_rule], '', ''] - hash[:resolvers].each do |resolver| - tbl << ['.', indent, resolver, prettify_comm(hash[:comm], resolver)] - end - end + result_set.each do |entry| + tbl = append_resolver_cells!(tbl, entry) end print(tbl.to_s) if tbl.rows.length > 0 end + def append_resolver_cells!(tbl, entry, prefix: [], suffix: []) + alignment_prefix = prefix.empty? ? [] : (['.'] * prefix.length) + if entry.resolvers.length == 1 + tbl << prefix + [entry.id, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix + elsif entry.resolvers.length > 1 + # XXX: By default rex-text tables strip preceding whitespace: + # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 + # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement + indent = "\xc2\xa0\xc2\xa0\\_ " + + tbl << prefix + [entry.id, entry.wildcard, '', ''] + suffix + entry.resolvers.each do |resolver| + tbl << alignment_prefix + ['.', indent, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) + end + end + tbl + end + def resolver self.driver.framework.dns_resolver end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 45066982799f..15bfff705927 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -42,13 +42,15 @@ def init # def save_config new_config = {} - @upstream_entries.each do |entry| - key = entry[:id].to_s - val = [entry[:wildcard_rule], - entry[:resolvers].join(','), - (!entry[:comm].nil?).to_s - ].join(';') - new_config[key] = val + @upstream_entries.each_with_index do |entry, index| + val = [ + entry.wildcard, + entry.resolvers.map do |resolver| + resolver.type == Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER ? resolver.destination : resolver.type.to_s + end.join(','), + (!entry.comm.nil?).to_s + ].join(';') + new_config["##{index}"] = val end Msf::Config.save(CONFIG_KEY => new_config) @@ -65,21 +67,21 @@ def load_config dns_settings = config.fetch(CONFIG_KEY, {}).each do |name, value| id = name.to_i - wildcard_rule, resolvers, uses_comm = value.split(';') - wildcard_rule = '*' if wildcard_rule.blank? + wildcard, resolvers, uses_comm = value.split(';') + wildcard = '*' if wildcard.blank? resolvers = resolvers.split(',') + uses_comm.downcase! raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid upstream DNS resolver') unless resolvers.all? {|resolver| valid_resolver?(resolver) } - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless valid_rule?(wildcard_rule) + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid upstream DNS resolver') unless resolvers.all? {|resolver| UpstreamRule.valid_resolver?(resolver) } + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless UpstreamRule.valid_wildcard?(wildcard) comm = uses_comm == 'true' ? CommSink.new : nil - with_rules << { - :wildcard_rule => wildcard_rule, - :resolvers => resolvers, - :comm => comm, - :id => id - } + with_rules << UpstreamRule.new( + wildcard: wildcard, + resolvers: resolvers, + comm: comm + ) next_id = [id + 1, next_id].max end @@ -92,24 +94,16 @@ def load_config # Add a custom nameserver entry to the custom provider # @param resolvers [Array] The list of upstream resolvers that would be used for this custom rule # @param comm [Msf::Session::Comm] The communication channel to be used for these DNS requests - # @param wildcard_rule String The wildcard rule to match a DNS request against - def add_upstream_entry(resolvers, comm: nil, wildcard_rule: '*') + # @param wildcard String The wildcard rule to match a DNS request against + def add_upstream_entry(resolvers, comm: nil, wildcard: '*') resolvers = [resolvers] if resolvers.is_a?(String) # coerce into an array of strings - if (resolver = resolvers.find {|resolver| !valid_resolver?(resolver)}) - raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") - end - resolvers = resolvers.map { |resolver| Rex::Socket.is_ip_addr?(resolver) ? resolver : resolver.downcase.to_sym } - - raise ::ArgumentError.new("Invalid rule: #{wildcard_rule}") unless valid_rule?(wildcard_rule) - - @upstream_entries << { - :wildcard_rule => wildcard_rule, - :type => UpstreamResolver::TYPE_DNS_SERVER, - :resolvers => resolvers, - :comm => comm, - :id => self.next_id - } - self.next_id += 1 + + @upstream_entries << UpstreamRule.new( + id: self.next_id, + wildcard: wildcard, + resolvers: resolvers, + comm: comm + ) end # @@ -149,34 +143,11 @@ def upstream_resolvers_for_packet(packet) results_from_all_questions = [] packet.question.each do |question| name = question.qname.to_s - upstream_resolvers = [] - - self.upstream_entries.each do |entry| - next unless matches(name, entry[:wildcard_rule]) - - socket_options = {} - socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? - entry[:resolvers].each do |resolver| - if resolver == UpstreamResolver::TYPE_BLACKHOLE - upstream_resolvers.append(UpstreamResolver.new( - UpstreamResolver::TYPE_BLACKHOLE - )) - elsif resolver == UpstreamResolver::TYPE_SYSTEM - upstream_resolvers.append(UpstreamResolver.new( - UpstreamResolver::TYPE_SYSTEM - )) - elsif Rex::Socket.is_ip_addr?(resolver) - upstream_resolvers.append(UpstreamResolver.new( - UpstreamResolver::TYPE_DNS_SERVER, - destination: resolver, - socket_options: socket_options - )) - end - end - break - end + upstream_entry = self.upstream_entries.find { |ue| ue.matches_name?(name) } - if upstream_resolvers.empty? + if upstream_entry + upstream_resolvers = upstream_entry.resolvers + else # Fall back to default nameservers upstream_resolvers = super end @@ -200,37 +171,11 @@ def set_framework(framework) def upstream_entries entries = @upstream_entries.dup - entries << { id: '', wildcard_rule: '*', resolvers: self.nameservers } + entries << UpstreamRule.new(resolvers: self.nameservers) entries end private - # - # Is the given wildcard DNS entry valid? - # - def valid_rule?(rule) - rule == '*' || rule =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ - end - - def valid_resolver?(resolver) - return true if Rex::Socket.is_ip_addr?(resolver) - - resolver = resolver.downcase.to_sym - [ - UpstreamResolver::TYPE_BLACKHOLE, - UpstreamResolver::TYPE_SYSTEM - ].include?(resolver) - end - - def matches(domain, pattern) - if pattern == '*' - true - elsif pattern.start_with?('*.') - domain.downcase.end_with?(pattern[1..-1].downcase) - else - domain.casecmp?(pattern) - end - end attr_accessor :next_id # The next ID to have been allocated to an entry attr_accessor :feature_set diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index f947d52e812d..c04b508ec492 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -124,7 +124,9 @@ def upstream_resolvers_for_packet(_dns_message) def upstream_resolvers_for_query(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) name, type, cls = preprocess_query_arguments(name, type, cls) - packet = make_query_packet(name, type, cls) + net_packet = make_query_packet(name, type, cls) + # This returns a Net::DNS::Packet. Convert to Dnsruby::Message for consistency + packet = Rex::Proto::DNS::Packet.encode_drb(net_packet) upstream_resolvers_for_packet(packet) end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index b555337e5ae4..ad1170d389ff 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -1,7 +1,5 @@ # -*- coding: binary -*- -require 'rex/socket' - module Rex module Proto module DNS @@ -16,6 +14,17 @@ def initialize(type, destination: nil, socket_options: {}) @destination = destination @socket_options = socket_options end + + def to_s + case type + when TYPE_BLACKHOLE + 'blackhole' + when TYPE_SYSTEM + 'system' + else + destination.to_s + end + end end end end diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb new file mode 100644 index 000000000000..f9bc7a465013 --- /dev/null +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -0,0 +1,71 @@ +# -*- coding: binary -*- + +require 'json' +require 'rex/socket' + +module Rex +module Proto +module DNS + class UpstreamRule + + attr_reader :id, :wildcard, :resolvers, :comm + def initialize(id: nil, wildcard: '*', resolvers: [], comm: nil) + @id = id + ::ArgumentError.new("Invalid wildcard text: #{wildcard}") unless self.class.valid_wildcard?(wildcard) + @wildcard = wildcard + socket_options = {} + socket_options['Comm'] = comm unless comm.nil? + @resolvers = resolvers.map do |resolver| + if resolver.is_a?(String) && !Rex::Socket.is_ip_addr?(resolver) + resolver = resolver.downcase.to_sym + end + + case resolver + when UpstreamResolver + resolver + when UpstreamResolver::TYPE_BLACKHOLE + UpstreamResolver.new(UpstreamResolver::TYPE_BLACKHOLE) + when UpstreamResolver::TYPE_SYSTEM + UpstreamResolver.new(UpstreamResolver::TYPE_SYSTEM) + else + if Rex::Socket.is_ip_addr?(resolver) + UpstreamResolver.new( + UpstreamResolver::TYPE_DNS_SERVER, + destination: resolver, + socket_options: socket_options + ) + else + raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") + end + end + end + @comm = comm + end + + def self.valid_resolver?(resolver) + return true if Rex::Socket.is_ip_addr?(resolver) + + resolver = resolver.downcase.to_sym + [ + UpstreamResolver::TYPE_BLACKHOLE, + UpstreamResolver::TYPE_SYSTEM + ].include?(resolver) + end + + def self.valid_wildcard?(wildcard) + wildcard == '*' || wildcard =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ + end + + def matches_name?(name) + if wildcard == '*' + true + elsif wildcard.start_with?('*.') + name.downcase.end_with?(wildcard[1..-1].downcase) + else + name.casecmp?(wildcard) + end + end + end +end +end +end From 2cf706e91f96c5e40081b715da66399acd533d36 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 5 Feb 2024 15:57:32 -0500 Subject: [PATCH 15/35] Use the dns entry index instead of unique IDs --- lib/msf/ui/console/command_dispatcher/dns.rb | 52 ++++++++++++------- .../proto/dns/custom_nameserver_provider.rb | 25 +++------ lib/rex/proto/dns/upstream_resolver.rb | 10 ++++ lib/rex/proto/dns/upstream_rule.rb | 15 ++++-- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 12e596901fb8..18a60aa6ebed 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -11,13 +11,14 @@ class DNS ADD_USAGE = 'dns [add] [--session ] [--rule ] ..."'.freeze @@add_opts = Rex::Parser::Arguments.new( + ['-i', '--index'] => [true, 'Index to insert at'], ['-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_USAGE = 'dns [remove/del] -i [-i ...]"'.freeze @@remove_opts = Rex::Parser::Arguments.new( - ['-i'] => [true, 'Index to remove'] + ['-i', '--index'] => [true, 'Index to remove at'] ) RESOLVE_USAGE = 'dns [resolve] [-f
] ..."'.freeze @@ -75,12 +76,12 @@ def cmd_dns_tabs(str, words) 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 '-s', '--session' + session_ids = driver.framework.sessions.keys.map { |k| k.to_s } + return session_ids.select { |id| id.start_with?(str) } when /^-/ # Unknown tag return @@ -197,18 +198,23 @@ def add_dns(*args) first_rule = true comm = nil resolvers = [] + position = -1 @@add_opts.parse(args) do |opt, idx, val| unless resolvers.empty? || opt.nil? raise ::ArgumentError.new("Invalid command near #{opt}") end case opt - when '--rule', '-r' + when '-i', '--index' + raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0 + + position = val.to_i - 1 + when '-r', '--rule' raise ::ArgumentError.new('No rule specified') if val.nil? rules.clear if first_rule # if the user defines even one rule, clear the defaults first_rule = false rules << val - when '--session', '-s' + when '-s', '--session' if val.nil? raise ::ArgumentError.new('No session specified') end @@ -248,9 +254,14 @@ def add_dns(*args) end end - rules.each do |rule| + rules.each_with_index do |rule, rule_index| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') - driver.framework.dns_resolver.add_upstream_entry(resolvers, comm: comm_obj, wildcard: rule) + driver.framework.dns_resolver.add_upstream_entry( + resolvers, + comm: comm_obj, + wildcard: rule, + position: (position == -1 ? -1 : position + rule_index) + ) end print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added") @@ -314,7 +325,7 @@ def resolve_dns(*args) 'Header' => 'Host resolutions', 'Prefix' => "\n", 'Postfix' => "\n", - 'Columns' => ['Hostname', 'IP Address', 'ID', 'Rule', 'Resolver', 'Comm channel'], + 'Columns' => ['Hostname', 'IP Address', 'Rule #', 'Rule', 'Resolver', 'Comm channel'], 'SortIndex' => -1, 'WordWrap' => false ) @@ -356,15 +367,15 @@ 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 + when '-i', '--index' + raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0 + + remove_ids << val.to_i - 1 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? + removed = resolver.remove_ids(remove_ids) + print_warning('Some entries were not removed') unless removed.length == remove_ids.length if removed.length > 0 print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed") print_dns_set('Deleted entries', removed) @@ -376,7 +387,7 @@ def remove_dns_help print_line " #{REMOVE_USAGE}" print_line(@@remove_opts.usage) print_line "EXAMPLES:" - print_line " Delete the DNS resolution rule with ID 3" + print_line " Delete the DNS resolution rule #3" print_line " dns remove -i 3" print_line print_line " Delete multiple entries in one command" @@ -462,7 +473,7 @@ def prettify_comm(comm, upstream_resolver) def print_dns_set(heading, result_set) return if result_set.length == 0 - columns = ['ID', 'Rule', 'Resolver', 'Comm channel'] + columns = ['#', 'Rule', 'Resolver', 'Comm channel'] tbl = Table.new( Table::Style::Default, @@ -482,15 +493,18 @@ def print_dns_set(heading, result_set) def append_resolver_cells!(tbl, entry, prefix: [], suffix: []) alignment_prefix = prefix.empty? ? [] : (['.'] * prefix.length) + entry_index = resolver.upstream_entries.index(entry) + entry_index += 1 if entry_index + if entry.resolvers.length == 1 - tbl << prefix + [entry.id, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix + tbl << prefix + [entry_index, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix elsif entry.resolvers.length > 1 # XXX: By default rex-text tables strip preceding whitespace: # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement indent = "\xc2\xa0\xc2\xa0\\_ " - tbl << prefix + [entry.id, entry.wildcard, '', ''] + suffix + tbl << prefix + [entry_index, entry.wildcard, '', ''] + suffix entry.resolvers.each do |resolver| tbl << alignment_prefix + ['.', indent, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 15bfff705927..2eb523817670 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -34,7 +34,6 @@ def sid def init @upstream_entries = [] - self.next_id = 0 end # @@ -63,7 +62,6 @@ def load_config config = Msf::Config.load with_rules = [] - next_id = 0 dns_settings = config.fetch(CONFIG_KEY, {}).each do |name, value| id = name.to_i @@ -82,45 +80,39 @@ def load_config resolvers: resolvers, comm: comm ) - - next_id = [id + 1, next_id].max end # Now that config has successfully read, update the global values @upstream_entries = with_rules - self.next_id = next_id end # Add a custom nameserver entry to the custom provider # @param resolvers [Array] The list of upstream resolvers that would be used for this custom rule # @param comm [Msf::Session::Comm] The communication channel to be used for these DNS requests # @param wildcard String The wildcard rule to match a DNS request against - def add_upstream_entry(resolvers, comm: nil, wildcard: '*') + def add_upstream_entry(resolvers, comm: nil, wildcard: '*', position: -1) resolvers = [resolvers] if resolvers.is_a?(String) # coerce into an array of strings - @upstream_entries << UpstreamRule.new( - id: self.next_id, + @upstream_entries.insert(position, UpstreamRule.new( wildcard: wildcard, resolvers: resolvers, comm: comm - ) + )) end # # Remove entries with the given IDs # Ignore entries that are not found # @param ids [Array] The IDs to removed - # @return [Array] The removed entries + # @return [Array] The removed entries # def remove_ids(ids) - removed= [] - ids.each do |id| - removed_with, remaining_with = @upstream_entries.partition {|entry| entry[:id] == id} - @upstream_entries.replace(remaining_with) - removed.concat(removed_with) + removed = [] + ids.sort.reverse.each do |id| + removed << @upstream_entries.delete_at(id) end - removed + removed.reverse end def purge @@ -177,7 +169,6 @@ def upstream_entries private - attr_accessor :next_id # The next ID to have been allocated to an entry attr_accessor :feature_set end end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index ad1170d389ff..28e1b4393886 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -25,6 +25,16 @@ def to_s destination.to_s end end + + def eql?(other) + return false unless other.is_a?(self.class) + return false unless other.type == type + return false unless other.destination == destination + return false unless other.socket_options == socket_options + true + end + + alias == eql? end end end diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index f9bc7a465013..402a67082291 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -8,9 +8,8 @@ module Proto module DNS class UpstreamRule - attr_reader :id, :wildcard, :resolvers, :comm - def initialize(id: nil, wildcard: '*', resolvers: [], comm: nil) - @id = id + attr_reader :wildcard, :resolvers, :comm + def initialize(wildcard: '*', resolvers: [], comm: nil) ::ArgumentError.new("Invalid wildcard text: #{wildcard}") unless self.class.valid_wildcard?(wildcard) @wildcard = wildcard socket_options = {} @@ -65,6 +64,16 @@ def matches_name?(name) name.casecmp?(wildcard) end end + + def eql?(other) + return false unless other.is_a?(self.class) + return false unless other.wildcard == wildcard + return false unless other.resolvers == resolvers + return false unless other.comm == comm + true + end + + alias == eql? end end end From d940bfd3127ae11ca7677cb9f14e3481af9072ed Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 5 Feb 2024 16:10:44 -0500 Subject: [PATCH 16/35] Show the number of cached dns records --- lib/msf/ui/console/command_dispatcher/dns.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 18a60aa6ebed..2bd2a189b9c4 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -433,6 +433,7 @@ def print_dns print_line(" * #{entry}") end end + print_line("Current cache size: #{resolver.cache.records.length}") upstream_entries = resolver.upstream_entries print_dns_set('Resolver rule entries', upstream_entries) From fcd84a41aae5683dfa6f7498cacd527ab5a768f3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 Feb 2024 13:07:23 -0500 Subject: [PATCH 17/35] Add a DNS resolver for static entries Move support for static entries out of the cache so it can be controlled like the other resolvers. --- lib/msf/ui/console/command_dispatcher/dns.rb | 47 +++++++++----- lib/rex/proto/dns/cache.rb | 19 ------ lib/rex/proto/dns/cached_resolver.rb | 49 --------------- lib/rex/proto/dns/resolver.rb | 64 +++++++++++++++++--- lib/rex/proto/dns/upstream_resolver.rb | 14 ++--- lib/rex/proto/dns/upstream_rule.rb | 9 ++- 6 files changed, 102 insertions(+), 100 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 2bd2a189b9c4..77d031dbf408 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -136,12 +136,12 @@ def cmd_dns_help(*args) print_line " dns [help] [subcommand]" print_line print_line "SUBCOMMANDS:" - print_line " add - add a DNS resolution entry to resolve certain domain names through a particular DNS resolver" - print_line " remove - delete a DNS resolution entry; 'del' is an alias" - print_line " print - show all configured DNS resolution entries" - print_line " flush-entries - remove all configured DNS resolution entries" - print_line " flush-cache - remove all cached DNS answers" - print_line " resolve - resolve a hostname" + print_line " add - Add a DNS resolution entry to resolve certain domain names through a particular DNS resolver" + print_line " remove - Delete a DNS resolution entry; 'del' is an alias" + print_line " print - Show all configured DNS resolution entries" + print_line " flush-entries - Remove all configured DNS resolution entries" + print_line " flush-cache - Remove all cached DNS answers" + print_line " resolve - Resolve a hostname" print_line print_line "EXAMPLES:" print_line " Display help information for the 'add' subcommand" @@ -236,9 +236,9 @@ def add_dns(*args) raise ::ArgumentError.new('You must specify at least one upstream DNS resolver') end - resolvers.each do |host| - unless Rex::Socket.is_ip_addr?(host) || SPECIAL_RESOLVERS.include?(host.downcase) - raise ::ArgumentError.new("Invalid DNS resolver: #{host}") + resolvers.each do |resolver| + unless Rex::Proto::DNS::UpstreamRule.valid_resolver?(resolver) + raise ::ArgumentError.new("Invalid DNS resolver: #{resolver}") end end @@ -273,8 +273,9 @@ def add_dns_help print_line @@add_opts.usage print_line "RESOLVERS:" print_line " ipv4 / ipv6 address - The IP address of an upstream DNS server to resolve from" - print_line " system - Use the host operating systems DNS resolution functionality (only for A/AAAA records)" print_line " blackhole - Drop all queries" + print_line " static - Reply with statically configured addresses (only for A/AAAA records)" + print_line " system - Use the host operating systems DNS resolution functionality (only for A/AAAA records)" print_line print_line "EXAMPLES:" print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10" @@ -331,6 +332,7 @@ def resolve_dns(*args) ) names.each do |name| upstream_entry = resolver.upstream_entries.find { |ue| ue.matches_name?(name) } + begin result = resolver.query(name, query_type) rescue NoResponseError @@ -437,16 +439,33 @@ def print_dns upstream_entries = resolver.upstream_entries print_dns_set('Resolver rule entries', upstream_entries) - if upstream_entries.empty? print_error('No DNS nameserver entries configured') end + + static_hosts = resolver.static_hosts + tbl = Table.new( + Table::Style::Default, + 'Header' => 'Static hostnames', + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => ['Hostname', 'IPv4 Address', 'IPv6 Address'], + 'SortIndex' => -1, + 'WordWrap' => false + ) + static_hosts.each do |hostname, addresses| + tbl << [hostname, addresses[::Socket::AF_INET], addresses[::Socket::AF_INET6]] + end + print_line(tbl.to_s) + if static_hosts.empty? + print_line('No static hostname entries are configured') + end end private SPECIAL_RESOLVERS = [ - Rex::Proto::DNS::UpstreamResolver::TYPE_BLACKHOLE.to_s.downcase, + Rex::Proto::DNS::UpstreamResolver::TYPE_BLACK_HOLE.to_s.downcase, Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM.to_s.downcase ].freeze @@ -454,7 +473,7 @@ def print_dns # Get user-friendly text for displaying the session that this entry would go through # def prettify_comm(comm, upstream_resolver) - if SPECIAL_RESOLVERS.include?(upstream_resolver.type.to_s) + if !Rex::Socket.is_ip_addr?(upstream_resolver.destination) 'N/A' elsif comm.nil? channel = Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination) @@ -483,7 +502,7 @@ def print_dns_set(heading, result_set) 'Postfix' => "\n", 'Columns' => columns, 'SortIndex' => -1, - 'WordWrap' => false, + 'WordWrap' => false ) result_set.each do |entry| tbl = append_resolver_cells!(tbl, entry) diff --git a/lib/rex/proto/dns/cache.rb b/lib/rex/proto/dns/cache.rb index ffe86354cbda..7deb6a3d62ca 100644 --- a/lib/rex/proto/dns/cache.rb +++ b/lib/rex/proto/dns/cache.rb @@ -56,25 +56,6 @@ def cache_record(record) end end - # - # Add static record to cache - # - # @param name [String] Name of record - # @param address [String] Address of record - # @param type [Dnsruby::Types] Record type to add - # @param replace [TrueClass, FalseClass] Replace existing records - def add_static(name, address, type = Dnsruby::Types::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 - # # Delete all cache entries, this is different from pruning because the # record's expiration is ignored diff --git a/lib/rex/proto/dns/cached_resolver.rb b/lib/rex/proto/dns/cached_resolver.rb index 1adbf0f34fa1..5fd92ae31a53 100644 --- a/lib/rex/proto/dns/cached_resolver.rb +++ b/lib/rex/proto/dns/cached_resolver.rb @@ -24,55 +24,6 @@ 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 - hf = Rex::Compat.is_windows ? '%WINDIR%/system32/drivers/etc/hosts' : '/etc/hosts' - entries = begin - File.read(hf).lines.map(&:strip).select do |entry| - Rex::Socket.is_ip_addr?(entry.gsub(/\s+/,' ').split(' ').first) and - not entry.match(/::.*ip6-/) # Ignore Debian/Ubuntu-specific notation for IPv6 hosts - end.map do |entry| - entry.gsub(/\s+/,' ').split(' ') - end - rescue => e - @logger.error(e) - [] - end - entries.each do |ent| - next if ent.first =~ /^127\./ - # Deal with multiple hostnames per address - while ent.length > 2 - hostname = ent.pop - next unless MATCH_HOSTNAME.match hostname - begin - if Rex::Socket.is_ipv4?(ent.first) - self.cache.add_static(hostname, ent.first, Dnsruby::Types::A) - elsif Rex::Socket.is_ipv6?(ent.first) - self.cache.add_static(hostname, ent.first, Dnsruby::Types::AAAA) - else - raise "Unknown IP address format #{ent.first} in hosts file!" - end - rescue => e - # Deal with edge-cases in users' hostsfile - @logger.error(e) - end - end - hostname = ent.pop - begin - if MATCH_HOSTNAME.match hostname - if Rex::Socket.is_ipv4?(ent.first) - self.cache.add_static(hostname, ent.first, Dnsruby::Types::A) - elsif Rex::Socket.is_ipv6?(ent.first) - self.cache.add_static(hostname, ent.first, Dnsruby::Types::AAAA) - else - raise "Unknown IP address format #{ent.first} in hosts file!" - end - end - rescue => e - # Deal with edge-cases in users' hostsfile - @logger.error(e) - end - end - # TODO: inotify or similar on hostsfile for live updates? Easy-button functionality self.cache.start unless dns_cache_no_start return end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index c04b508ec492..bb8be87018cd 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -33,7 +33,8 @@ class Resolver < Net::DNS::Resolver :tcp_timeout => TcpTimeout.new(5), :udp_timeout => UdpTimeout.new(5), :context => {}, - :comm => nil + :comm => nil, + :static_hosts => {} } attr_accessor :context, :comm @@ -71,6 +72,11 @@ def initialize(config = {}) #------------------------------------------------------------ parse_environment_variables + #------------------------------------------------------------ + # Parsing ENV variables + #------------------------------------------------------------ + parse_static_hosts_file + #------------------------------------------------------------ # Parsing arguments #------------------------------------------------------------ @@ -150,6 +156,7 @@ def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) packet = Rex::Proto::DNS::Packet.encode_drb(net_packet) end + upstream_resolvers = upstream_resolvers_for_packet(packet) if upstream_resolvers.empty? raise ResolverError, "No upstream resolvers specified!" @@ -158,8 +165,12 @@ def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) ans = nil upstream_resolvers.each do |upstream_resolver| case upstream_resolver.type + when UpstreamResolver::TYPE_BLACK_HOLE + ans = send_blackhole(upstream_resolver, packet, type, cls) when UpstreamResolver::TYPE_DNS_SERVER ans = send_dns_server(upstream_resolver, packet, type, cls) + when UpstreamResolver::TYPE_STATIC + ans = send_static(upstream_resolver, packet, type, cls) when UpstreamResolver::TYPE_SYSTEM ans = send_system(upstream_resolver, packet, type, cls) end @@ -391,6 +402,10 @@ def self.default_config_file end end + def static_hosts + @config[:static_hosts].dup + end + private def preprocess_query_arguments(name, type, cls) @@ -449,10 +464,24 @@ def send_blackhole(upstream_resolver, packet, type, cls) raise NoResponseError end + def send_static(upstream_resolver, packet, type, cls) + simple_name_lookup(upstream_resolver, packet, type, cls) do |name, family| + ip_address = static_hosts.fetch(name, {}).fetch(family, nil) + ip_address ? [ip_address] : nil + end + end + def send_system(upstream_resolver, packet, type, cls) # This system resolver will use host operating systems `getaddrinfo` (or equivalent function) to perform name # resolution. This is primarily useful if that functionality is hooked or modified by an external application such # as proxychains. This handler though can only process A and AAAA requests. + simple_name_lookup(upstream_resolver, packet, type, cls) do |name, family| + addrinfos = ::Addrinfo.getaddrinfo(name, 0, family, ::Socket::SOCK_STREAM) + addrinfos.map(&:ip_address) + end + end + + def simple_name_lookup(upstream_resolver, packet, type, cls, &block) return nil unless cls == Dnsruby::Classes::IN # todo: make sure this will work if the packet has multiple questions, figure out how that's handled @@ -466,20 +495,22 @@ def send_system(upstream_resolver, packet, type, cls) return nil end + ip_addresses = nil begin - addrinfos = ::Addrinfo.getaddrinfo(name, 0, family, ::Socket::SOCK_STREAM) - rescue ::SocketError - return nil + ip_addresses = block.call(name, family) + rescue StandardError => e + @logger.error("The #{upstream_resolver.type} name lookup block failed for #{name}") end + return nil unless ip_addresses && !ip_addresses.empty? message = Dnsruby::Message.new message.add_question(name, type, cls) - addrinfos.each do |addrinfo| + ip_addresses.each do |ip_address| message.add_answer(Dnsruby::RR.new_from_hash( name: name, type: type, - ttl: 0, # since we're querying the system, rely on that cache - address: addrinfo.ip_address + ttl: 0, + address: ip_address.to_s )) end [message.encode] @@ -493,6 +524,25 @@ def supports_udp?(upstream_resolver) true end + + def parse_static_hosts_file + path = '/etc/hosts' + return unless File.file?(path) && File.readable?(path) + + static_hosts = {} + ::IO.foreach(path) do |line| + words = line.split + next unless words.length > 1 && Rex::Socket.is_ip_addr?(words.first) + + ip = IPAddr.new(words.shift) + words.each do |hostname| + this_host = static_hosts.fetch(hostname, {}) + this_host[ip.family] = ip unless this_host.key?(ip.family) # only honor the first definition + static_hosts[hostname] = this_host + end + end + @config[:static_hosts].merge!(static_hosts) + end end # Resolver end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index 28e1b4393886..5e880a797e63 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -4,9 +4,10 @@ module Rex module Proto module DNS class UpstreamResolver + TYPE_BLACK_HOLE = %s[black-hole] + TYPE_DNS_SERVER = %s[dns-server] + TYPE_STATIC = :static TYPE_SYSTEM = :system - TYPE_DNS_SERVER = :dns_server - TYPE_BLACKHOLE = :blackhole attr_reader :type, :destination, :socket_options def initialize(type, destination: nil, socket_options: {}) @@ -16,13 +17,10 @@ def initialize(type, destination: nil, socket_options: {}) end def to_s - case type - when TYPE_BLACKHOLE - 'blackhole' - when TYPE_SYSTEM - 'system' - else + if type == TYPE_DNS_SERVER destination.to_s + else + type.to_s end end diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index 402a67082291..6a96fed870fd 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -22,8 +22,10 @@ def initialize(wildcard: '*', resolvers: [], comm: nil) case resolver when UpstreamResolver resolver - when UpstreamResolver::TYPE_BLACKHOLE - UpstreamResolver.new(UpstreamResolver::TYPE_BLACKHOLE) + when UpstreamResolver::TYPE_BLACK_HOLE + UpstreamResolver.new(UpstreamResolver::TYPE_BLACK_HOLE) + when UpstreamResolver::TYPE_STATIC + UpstreamResolver.new(UpstreamResolver::TYPE_STATIC) when UpstreamResolver::TYPE_SYSTEM UpstreamResolver.new(UpstreamResolver::TYPE_SYSTEM) else @@ -46,7 +48,8 @@ def self.valid_resolver?(resolver) resolver = resolver.downcase.to_sym [ - UpstreamResolver::TYPE_BLACKHOLE, + UpstreamResolver::TYPE_BLACK_HOLE, + UpstreamResolver::TYPE_STATIC, UpstreamResolver::TYPE_SYSTEM ].include?(resolver) end From bd7d4f0099c3de66a1b7a18b083d699991f2de79 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 Feb 2024 16:28:56 -0500 Subject: [PATCH 18/35] Add commands to manage static hostname records --- lib/msf/ui/console/command_dispatcher/dns.rb | 135 +++++++++++++++++-- lib/rex/proto/dns/resolver.rb | 41 +----- lib/rex/proto/dns/static_hostnames.rb | 88 ++++++++++++ 3 files changed, 217 insertions(+), 47 deletions(-) create mode 100644 lib/rex/proto/dns/static_hostnames.rb diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 77d031dbf408..c33c8c1220c7 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -16,12 +16,19 @@ class DNS ['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)' ] ) - REMOVE_USAGE = 'dns [remove/del] -i [-i ...]"'.freeze + ADD_STATIC_USAGE = 'dns [add-static] '.freeze + + REMOVE_USAGE = 'dns [remove/del] -i [-i ...]'.freeze @@remove_opts = Rex::Parser::Arguments.new( ['-i', '--index'] => [true, 'Index to remove at'] ) - RESOLVE_USAGE = 'dns [resolve] [-f
] ..."'.freeze + REMOVE_STATIC_USAGE = 'dns [remove-static] [-f
] ...'.freeze + @@remove_static_opts = Rex::Parser::Arguments.new( + ['-f'] => [true, 'Address family - IPv4 or IPv6 (default both)'] + ) + + RESOLVE_USAGE = 'dns [resolve] [-f
] ...'.freeze @@resolve_opts = Rex::Parser::Arguments.new( # same usage syntax as Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Stdapi ['-f'] => [true, 'Address family - IPv4 or IPv6 (default IPv4)'] @@ -56,7 +63,7 @@ def cmd_dns_tabs(str, words) return if driver.framework.dns_resolver.nil? if words.length == 1 - options = %w[ add delete flush-cache flush-entries print query remove resolve ] + options = %w[ add add-static delete flush-cache flush-entries flush-static print query remove remove-static resolve ] return options.select { |opt| opt.start_with?(str) } end @@ -100,6 +107,13 @@ def cmd_dns_tabs(str, words) else return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) } end + when 'remove-static' + if words[-1] == '-f' + families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive + return families.select { |family| family.downcase.start_with?(str.downcase) } + else + @@resolve_opts.option_keys.select { |opt| opt.start_with?(str) } + end when 'resolve','query' if words[-1] == '-f' families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive @@ -128,19 +142,25 @@ def cmd_dns_help(*args) print_line print_line "USAGE:" print_line " #{ADD_USAGE}" + print_line " #{ADD_STATIC_USAGE}" print_line " #{REMOVE_USAGE}" + print_line " #{REMOVE_STATIC_USAGE}" print_line " dns [flush-cache]" print_line " dns [flush-entries]" + print_line " dns [flush-static]" print_line " dns [print]" print_line " #{RESOLVE_USAGE}" print_line " dns [help] [subcommand]" print_line print_line "SUBCOMMANDS:" print_line " add - Add a DNS resolution entry to resolve certain domain names through a particular DNS resolver" - print_line " remove - Delete a DNS resolution entry; 'del' is an alias" - print_line " print - Show all configured DNS resolution entries" - print_line " flush-entries - Remove all configured DNS resolution entries" + print_line " add-static - Add a statically defined hostname" print_line " flush-cache - Remove all cached DNS answers" + print_line " flush-entries - Remove all configured DNS resolution entries" + print_line " flush-static - Remove all statically defined hostnames" + print_line " print - Show all configured DNS resolution entries" + print_line " remove - Delete a DNS resolution entry" + print_line " remove-static - Delete a statically defined hostname" print_line " resolve - Resolve a hostname" print_line print_line "EXAMPLES:" @@ -158,9 +178,9 @@ def cmd_dns(*args) args << 'print' if args.length == 0 # Short-circuit help if args.delete("-h") || args.delete("--help") - if respond_to?("#{args.first}_dns_help") + if respond_to?("#{args.first.gsub('-', '_')}_dns_help") # if it is a valid command with dedicated help information - send("#{args.first}_dns_help") + send("#{args.first.gsub('-', '_')}_dns_help") else # otherwise print the top-level help information cmd_dns_help @@ -173,16 +193,22 @@ def cmd_dns(*args) case action when "add" add_dns(*args) + when "add-static" + add_static_dns(*args) when "flush-entries" flush_entries_dns when "flush-cache" flush_cache_dns + when "flush-static" + flush_static_dns when "help" cmd_dns_help(*args) when "print" print_dns when "remove", "rm", "delete", "del" remove_dns(*args) + when "remove-static" + remove_static_dns(*args) when "resolve", "query" resolve_dns(*args) else @@ -288,8 +314,33 @@ def add_dns_help print_line " dns add --session 2 --rule *.metasploit.com 192.168.1.10" end + def add_static_dns(*args) + if args.length < 2 + raise ::ArgumentError.new('A hostname and IP address must be provided') + elsif args.length > 2 + raise ::ArgumentError.new("Unknown argument: #{args[2]}") + end + + hostname, ip_address = args + if !Rex::Socket.is_ip_addr?(ip_address) + raise ::ArgumentError.new("Invalid IP address: #{ip_address}") + end + + resolver.static_hostnames.add(hostname, ip_address) + print_status("Added static hostname mapping #{hostname} to #{ip_address}") + end + + def add_static_dns_help + print_line "USAGE:" + print_line " #{ADD_STATIC_USAGE}" + print_line + print_line "EXAMPLES:" + print_line " Define a static entry mapping localhost6 to ::1" + print_line " dns add-static localhost6 ::1" + end + # - # Query a hostname using the configuration. This is useful for debugging and + # Query a hostname using the configuration. This is useful for debugging anddns # inspecting the active settings. # def resolve_dns(*args) @@ -397,6 +448,60 @@ def remove_dns_help print_line end + def remove_static_dns(*args) + names = [] + query_type = nil + + @@resolve_opts.parse(args) do |opt, idx, val| + unless names.empty? || opt.nil? + raise ::ArgumentError.new("Invalid command near #{opt}") + end + case opt + when '-f' + case val.downcase + when 'ipv4' + query_type = Dnsruby::Types::A + when'ipv6' + query_type = Dnsruby::Types::AAAA + else + raise ::ArgumentError.new("Invalid family: #{val}") + end + when nil + names << val + else + raise ::ArgumentError.new("Unknown flag: #{opt}") + end + end + + if names.length < 1 + raise ::ArgumentError.new('You must specify at least one hostname to remove') + end + + names.each do |name| + if query_type.nil? || query_type == Dnsruby::Types::A + resolver.static_hostnames.delete(name, Dnsruby::Types::A) + end + + if query_type.nil? || query_type == Dnsruby::Types::AAAA + resolver.static_hostnames.delete(name, Dnsruby::Types::AAAA) + end + end + print_good('DNS hostnames have been deleted') + end + + def remove_static_dns_help + print_line "USAGE:" + print_line " #{REMOVE_STATIC_USAGE}" + print_line @@remove_static_opts.usage + print_line "EXAMPLES:" + print_line " Remove IPv4 and IPv6 addresses for 'localhost'" + print_line " dns remove-static localhost" + print_line + print_line " Remove only IPv6 addresses for 'localhost6'" + print_line " dns remove-static -f IPv6 localhost6" + print_line + end + # # Delete all cached DNS answers # @@ -413,6 +518,11 @@ def flush_entries_dns print_good('DNS entries flushed') end + def flush_static_dns + resolver.static_hostnames.flush + print_good('DNS static hostnames flushed') + end + # # Display the user-configured DNS settings # @@ -443,7 +553,6 @@ def print_dns print_error('No DNS nameserver entries configured') end - static_hosts = resolver.static_hosts tbl = Table.new( Table::Style::Default, 'Header' => 'Static hostnames', @@ -453,11 +562,11 @@ def print_dns 'SortIndex' => -1, 'WordWrap' => false ) - static_hosts.each do |hostname, addresses| - tbl << [hostname, addresses[::Socket::AF_INET], addresses[::Socket::AF_INET6]] + resolver.static_hostnames.each do |hostname, addresses| + tbl << [hostname, addresses[Dnsruby::Types::A], addresses[Dnsruby::Types::AAAA]] end print_line(tbl.to_s) - if static_hosts.empty? + if resolver.static_hostnames.empty? print_line('No static hostname entries are configured') end end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index bb8be87018cd..665c26aeb220 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -37,7 +37,7 @@ class Resolver < Net::DNS::Resolver :static_hosts => {} } - attr_accessor :context, :comm + attr_accessor :context, :comm, :static_hostnames # # Provide override for initializer to use local Defaults constant # @@ -60,8 +60,6 @@ def initialize(config = {}) # 4) defaults (and /etc/resolv.conf for config) #------------------------------------------------------------ - - #------------------------------------------------------------ # Parsing config file #------------------------------------------------------------ @@ -72,16 +70,12 @@ def initialize(config = {}) #------------------------------------------------------------ parse_environment_variables - #------------------------------------------------------------ - # Parsing ENV variables - #------------------------------------------------------------ - parse_static_hosts_file - #------------------------------------------------------------ # Parsing arguments #------------------------------------------------------------ comm = config.delete(:comm) - context = context = config.delete(:context) + context = config.delete(:context) + static_hosts = config.delete(:static_hosts) config.each do |key,val| next if key == :log_file or key == :config_file begin @@ -90,6 +84,8 @@ def initialize(config = {}) raise ResolverArgumentError, "Option #{key} not valid" end end + self.static_hostnames = StaticHostnames.new(hostnames: static_hosts) + self.static_hostnames.parse_hosts_file end # # Provides current proxy setting if configured @@ -402,10 +398,6 @@ def self.default_config_file end end - def static_hosts - @config[:static_hosts].dup - end - private def preprocess_query_arguments(name, type, cls) @@ -465,8 +457,8 @@ def send_blackhole(upstream_resolver, packet, type, cls) end def send_static(upstream_resolver, packet, type, cls) - simple_name_lookup(upstream_resolver, packet, type, cls) do |name, family| - ip_address = static_hosts.fetch(name, {}).fetch(family, nil) + simple_name_lookup(upstream_resolver, packet, type, cls) do |name, _family| + ip_address = static_hostnames.get(name, type) ip_address ? [ip_address] : nil end end @@ -524,25 +516,6 @@ def supports_udp?(upstream_resolver) true end - - def parse_static_hosts_file - path = '/etc/hosts' - return unless File.file?(path) && File.readable?(path) - - static_hosts = {} - ::IO.foreach(path) do |line| - words = line.split - next unless words.length > 1 && Rex::Socket.is_ip_addr?(words.first) - - ip = IPAddr.new(words.shift) - words.each do |hostname| - this_host = static_hosts.fetch(hostname, {}) - this_host[ip.family] = ip unless this_host.key?(ip.family) # only honor the first definition - static_hosts[hostname] = this_host - end - end - @config[:static_hosts].merge!(static_hosts) - end end # Resolver end diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb new file mode 100644 index 000000000000..ef68a55991be --- /dev/null +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -0,0 +1,88 @@ +# -*- coding: binary -*- + +require 'rex/socket' +require 'forwardable' + +module Rex +module Proto +module DNS + class StaticHostnames + extend Forwardable + + def_delegators :@hostnames, :each, :length, :empty? + + def initialize(hostnames: nil) + @hostnames = {} + if hostnames + hostnames.each do |hostname, ip_address| + add(hostname, ip_address) + end + end + end + + def parse_hosts_file + path = '/etc/hosts' + return unless File.file?(path) && File.readable?(path) + + hostnames = {} + ::IO.foreach(path) do |line| + words = line.split + next unless words.length > 1 && Rex::Socket.is_ip_addr?(words.first) + + ip_address = IPAddr.new(words.shift) + words.each do |hostname| + hostname = hostname.downcase + this_host = hostnames.fetch(hostname, {}) + if ip_address.family == ::Socket::AF_INET + type = Dnsruby::Types::A + else + type = Dnsruby::Types::AAAA + end + next if this_host.key?(type) # only honor the first definition + + this_host[type] = ip_address + hostnames[hostname] = this_host + end + end + @hostnames.merge!(hostnames) + end + + def get(hostname, type = Dnsruby::Types::A) + hostname = hostname.downcase + @hostnames.fetch(hostname, {}).fetch(type, nil) + end + + def add(hostname, ip_address) + hostname = hostname.downcase + ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) + + addresses = @hostnames.fetch(hostname, {}) + if ip_address.family == ::Socket::AF_INET + addresses[Dnsruby::Types::A] = ip_address + elsif ip_address.family == ::Socket::AF_INET6 + addresses[Dnsruby::Types::AAAA] = ip_address + end + @hostnames[hostname] = addresses + nil + end + + def delete(hostname, type = Dnsruby::Types::A) + hostname = hostname.downcase + addresses = @hostnames.fetch(hostname, {}) + addresses.delete(type) + if addresses.length == 0 + @hostnames.delete(hostname) + else + @hostnames[hostname] = addresses + end + + nil + end + + def flush + @hostnames.clear + end + end +end +end +end From 648a7b394d842d796758a9e508c908f547ea7e7e Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 Feb 2024 17:40:22 -0500 Subject: [PATCH 19/35] Update configuration saving and loading --- lib/msf/ui/console/command_dispatcher/dns.rb | 25 ++-- lib/msf/ui/console/driver.rb | 2 + .../proto/dns/custom_nameserver_provider.rb | 116 ++++++++++++------ lib/rex/proto/dns/static_hostnames.rb | 2 +- 4 files changed, 92 insertions(+), 53 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index c33c8c1220c7..d7769b207f4b 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -383,19 +383,20 @@ def resolve_dns(*args) ) names.each do |name| upstream_entry = resolver.upstream_entries.find { |ue| ue.matches_name?(name) } + upstream_entry_id = resolver.upstream_entries.index(upstream_entry) + 1 begin result = resolver.query(name, query_type) rescue NoResponseError - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]']) + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]'], index: upstream_entry_id) else if result.answer.empty? - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]']) + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]'], index: upstream_entry_id) else result.answer.select do |answer| answer.type == query_type end.map(&:address).map(&:to_s).each do |address| - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, address]) + tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, address], index: upstream_entry_id) end end end @@ -431,7 +432,7 @@ def remove_dns(*args) print_warning('Some entries were not removed') unless removed.length == remove_ids.length if removed.length > 0 print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed") - print_dns_set('Deleted entries', removed) + print_dns_set('Deleted entries', removed, ids: [nil] * removed.length) end end @@ -548,7 +549,7 @@ def print_dns print_line("Current cache size: #{resolver.cache.records.length}") upstream_entries = resolver.upstream_entries - print_dns_set('Resolver rule entries', upstream_entries) + print_dns_set('Resolver rule entries', upstream_entries, ids: (1..upstream_entries.length).to_a) if upstream_entries.empty? print_error('No DNS nameserver entries configured') end @@ -600,7 +601,7 @@ def prettify_comm(comm, upstream_resolver) end end - def print_dns_set(heading, result_set) + def print_dns_set(heading, result_set, ids: []) return if result_set.length == 0 columns = ['#', 'Rule', 'Resolver', 'Comm channel'] @@ -613,27 +614,25 @@ def print_dns_set(heading, result_set) 'SortIndex' => -1, 'WordWrap' => false ) - result_set.each do |entry| - tbl = append_resolver_cells!(tbl, entry) + result_set.each_with_index do |entry, index| + tbl = append_resolver_cells!(tbl, entry, index: ids[index]) end print(tbl.to_s) if tbl.rows.length > 0 end - def append_resolver_cells!(tbl, entry, prefix: [], suffix: []) + def append_resolver_cells!(tbl, entry, prefix: [], suffix: [], index: nil) alignment_prefix = prefix.empty? ? [] : (['.'] * prefix.length) - entry_index = resolver.upstream_entries.index(entry) - entry_index += 1 if entry_index if entry.resolvers.length == 1 - tbl << prefix + [entry_index, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix + tbl << prefix + [index.to_s, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix elsif entry.resolvers.length > 1 # XXX: By default rex-text tables strip preceding whitespace: # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement indent = "\xc2\xa0\xc2\xa0\\_ " - tbl << prefix + [entry_index, entry.wildcard, '', ''] + suffix + tbl << prefix + [index.to_s, entry.wildcard, '', ''] + suffix entry.resolvers.each do |resolver| tbl << alignment_prefix + ['.', indent, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) end diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index 9085b7625abf..cabf9626ace7 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -85,6 +85,8 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = { if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DNS_FEATURE) dns_resolver = Rex::Proto::DNS::CachedResolver.new dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) + dns_resolver.purge + dns_resolver.static_hostnames.flush dns_resolver.load_config # Defer loading of modules until paths from opts can be added below diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 2eb523817670..eb421608e85e 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -9,7 +9,7 @@ module DNS # for different requests, based on the domain being queried. ## module CustomNameserverProvider - CONFIG_KEY = 'framework/dns' + CONFIG_KEY_BASE = 'framework/dns' # # A Comm implementation that always reports as dead, so should never @@ -40,50 +40,16 @@ def init # Save the custom settings to the MSF config file # def save_config - new_config = {} - @upstream_entries.each_with_index do |entry, index| - val = [ - entry.wildcard, - entry.resolvers.map do |resolver| - resolver.type == Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER ? resolver.destination : resolver.type.to_s - end.join(','), - (!entry.comm.nil?).to_s - ].join(';') - new_config["##{index}"] = val - end - - Msf::Config.save(CONFIG_KEY => new_config) + save_config_entries + save_config_static_hostnames end # # Load the custom settings from the MSF config file # def load_config - config = Msf::Config.load - - with_rules = [] - - dns_settings = config.fetch(CONFIG_KEY, {}).each do |name, value| - id = name.to_i - wildcard, resolvers, uses_comm = value.split(';') - wildcard = '*' if wildcard.blank? - resolvers = resolvers.split(',') - uses_comm.downcase! - - raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid upstream DNS resolver') unless resolvers.all? {|resolver| UpstreamRule.valid_resolver?(resolver) } - raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless UpstreamRule.valid_wildcard?(wildcard) - - comm = uses_comm == 'true' ? CommSink.new : nil - with_rules << UpstreamRule.new( - wildcard: wildcard, - resolvers: resolvers, - comm: comm - ) - end - - # Now that config has successfully read, update the global values - @upstream_entries = with_rules + load_config_entries + load_config_static_hostnames end # Add a custom nameserver entry to the custom provider @@ -169,6 +135,78 @@ def upstream_entries private + def load_config_entries + config = Msf::Config.load + + with_rules = [] + config.fetch("#{CONFIG_KEY_BASE}/entries", {}).each do |_name, value| + wildcard, resolvers, uses_comm = value.split(';') + wildcard = '*' if wildcard.blank? + resolvers = resolvers.split(',') + uses_comm.downcase! + + raise Rex::Proto::DNS::Exceptions::ConfigError.new('DNS parsing failed: Comm must be true or false') unless ['true','false'].include?(uses_comm) + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid upstream DNS resolver') unless resolvers.all? {|resolver| UpstreamRule.valid_resolver?(resolver) } + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid rule') unless UpstreamRule.valid_wildcard?(wildcard) + + comm = uses_comm == 'true' ? CommSink.new : nil + with_rules << UpstreamRule.new( + wildcard: wildcard, + resolvers: resolvers, + comm: comm + ) + end + + # Now that config has successfully read, update the global values + @upstream_entries = with_rules + end + + def load_config_static_hostnames + config = Msf::Config.load + + config.fetch("#{CONFIG_KEY_BASE}/static_hostnames", {}).each do |_name, value| + values = value.split(';') + hostname = values.shift + values.each do |ip_address| + next if ip_address.blank? + + unless Rex::Socket.is_ip_addr?(ip_address) + raise Rex::Proto::DNS::Exceptions::ConfigError.new('Invalid DNS config: Invalid IP address') + end + + static_hostnames.add(hostname, ip_address) + end + end + end + + def save_config_entries + new_config = {} + @upstream_entries.each_with_index do |entry, index| + val = [ + entry.wildcard, + entry.resolvers.map do |resolver| + resolver.type == Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER ? resolver.destination : resolver.type.to_s + end.join(','), + (!entry.comm.nil?).to_s + ].join(';') + new_config["##{index}"] = val + end + Msf::Config.save("#{CONFIG_KEY_BASE}/entries" => new_config) + end + + def save_config_static_hostnames + new_config = {} + static_hostnames.each_with_index do |(hostname, addresses), index| + val = [ + hostname, + addresses[Dnsruby::Types::A], + addresses[Dnsruby::Types::AAAA] + ].join(';') + new_config["##{index}"] = val + end + Msf::Config.save("#{CONFIG_KEY_BASE}/static_hostnames" => new_config) + end + attr_accessor :feature_set end end diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index ef68a55991be..32a1a9e8480c 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -9,7 +9,7 @@ module DNS class StaticHostnames extend Forwardable - def_delegators :@hostnames, :each, :length, :empty? + def_delegators :@hostnames, :each, :each_with_index, :length, :empty? def initialize(hostnames: nil) @hostnames = {} From 2653a180e47a6784818eca5a05b97c2e25ec48bc Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 Feb 2024 10:55:05 -0500 Subject: [PATCH 20/35] Update tests add new initialization methods --- lib/msf/ui/console/command_dispatcher/dns.rb | 2 +- lib/rex/proto/dns/resolver.rb | 2 +- lib/rex/proto/dns/upstream_resolver.rb | 20 +++ lib/rex/proto/dns/upstream_rule.rb | 12 +- .../dns/custom_nameserver_provider_spec.rb | 118 +++--------------- .../rex/proto/dns/upstream_resolver_spec.rb | 90 +++++++++++++ spec/lib/rex/proto/dns/upstream_rule_spec.rb | 72 +++++++++++ 7 files changed, 207 insertions(+), 109 deletions(-) create mode 100644 spec/lib/rex/proto/dns/upstream_resolver_spec.rb create mode 100644 spec/lib/rex/proto/dns/upstream_rule_spec.rb diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index d7769b207f4b..7f21797ea7bc 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -76,7 +76,7 @@ def cmd_dns_tabs(str, words) 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 + return end tag_is_expected = !tag_is_expected end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 665c26aeb220..2a81eaea1982 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -120,7 +120,7 @@ def proxies=(prox, timeout_added = 250) # def upstream_resolvers_for_packet(_dns_message) @config[:nameservers].map do |ns| - UpstreamResolver.new(UpstreamResolver::TYPE_DNS_SERVER, destination: ns.to_s) + UpstreamResolver.new_dns_server(ns.to_s) end end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index 5e880a797e63..49ab270a5b6b 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -16,6 +16,26 @@ def initialize(type, destination: nil, socket_options: {}) @socket_options = socket_options end + def self.new_black_hole + self.new(TYPE_BLACK_HOLE) + end + + def self.new_dns_server(destination, socket_options: {}) + self.new( + TYPE_DNS_SERVER, + destination: destination, + socket_options: socket_options + ) + end + + def self.new_static + self.new(TYPE_STATIC) + end + + def self.new_system + self.new(TYPE_SYSTEM) + end + def to_s if type == TYPE_DNS_SERVER destination.to_s diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index 6a96fed870fd..2059bab9b2a4 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -23,18 +23,14 @@ def initialize(wildcard: '*', resolvers: [], comm: nil) when UpstreamResolver resolver when UpstreamResolver::TYPE_BLACK_HOLE - UpstreamResolver.new(UpstreamResolver::TYPE_BLACK_HOLE) + UpstreamResolver.new_black_hole when UpstreamResolver::TYPE_STATIC - UpstreamResolver.new(UpstreamResolver::TYPE_STATIC) + UpstreamResolver.new_static when UpstreamResolver::TYPE_SYSTEM - UpstreamResolver.new(UpstreamResolver::TYPE_SYSTEM) + UpstreamResolver.new_system else if Rex::Socket.is_ip_addr?(resolver) - UpstreamResolver.new( - UpstreamResolver::TYPE_DNS_SERVER, - destination: resolver, - socket_options: socket_options - ) + UpstreamResolver.new_dns_server(resolver, socket_options: socket_options) else raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") end diff --git a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb index 89313095c35b..737820bb801f 100755 --- a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb +++ b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb @@ -9,24 +9,12 @@ def packet_for(name) Rex::Proto::DNS::Packet.encode_drb(packet) end - let(:base_nameserver) do - '1.2.3.4' + let(:default_nameserver) do + '192.0.2.10' 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' + let(:metasploit_nameserver) do + '192.0.2.20' end let (:config) do @@ -47,96 +35,28 @@ def f.enabled?(_name) 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 + subject(:dns_resolver) do dns_resolver = Rex::Proto::DNS::CachedResolver.new(config) dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) - dns_resolver.nameservers = [base_nameserver] + dns_resolver.nameservers = [default_nameserver] + dns_resolver.add_upstream_entry([metasploit_nameserver], wildcard: '*.metasploit.com') 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, {}]]) + context 'When a condition matches' do + it 'The correct resolver is returned' do + packet = packet_for('subdomain.metasploit.com') + ns = dns_resolver.upstream_resolvers_for_packet(packet) + expect(ns).to eq([Rex::Proto::DNS::UpstreamResolver.new_dns_server(metasploit_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 + context 'When no conditions match' do + it 'The default resolver is returned' do + packet = packet_for('subdomain.test.lan') + ns = dns_resolver.upstream_resolvers_for_packet(packet) + expect(ns).to eq([Rex::Proto::DNS::UpstreamResolver.new_dns_server(default_nameserver)]) + end end -end \ No newline at end of file +end diff --git a/spec/lib/rex/proto/dns/upstream_resolver_spec.rb b/spec/lib/rex/proto/dns/upstream_resolver_spec.rb new file mode 100644 index 000000000000..a2a7a335ca6c --- /dev/null +++ b/spec/lib/rex/proto/dns/upstream_resolver_spec.rb @@ -0,0 +1,90 @@ +# -*- coding:binary -*- +require 'spec_helper' + + +RSpec.describe Rex::Proto::DNS::UpstreamResolver do + context 'when type is black-hole' do + let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_BLACK_HOLE } + let(:resolver) { described_class.new_black_hole } + + describe '.new_black_hole' do + it 'is expected to set the type correctly' do + expect(resolver.type).to eq type + end + + it 'is expected to set the destination correctly' do + expect(resolver.destination).to be_nil + end + end + + describe '#to_s' do + it 'is expected to return the type as a string' do + expect(resolver.to_s).to eq type.to_s + end + end + end + + context 'when type is dns-server' do + let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER } + let(:destination) { '192.0.2.10' } + let(:resolver) { described_class.new_dns_server(destination) } + + describe '.new_dns_server' do + it 'is expected to set the type correctly' do + expect(resolver.type).to eq type + end + + it 'is expected to set the destination correctly' do + expect(resolver.destination).to eq destination + end + end + + describe '#to_s' do + it 'is expected to return the nameserver IP address as a string' do + expect(resolver.to_s).to eq destination + end + end + end + + context 'when type is static' do + let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_STATIC } + let(:resolver) { described_class.new_static } + + describe '.new_static' do + it 'is expected to set the type correctly' do + expect(resolver.type).to eq type + end + + it 'is expected to set the destination correctly' do + expect(resolver.destination).to be_nil + end + end + + describe '#to_s' do + it 'is expected to return the type as a string' do + expect(resolver.to_s).to eq type.to_s + end + end + end + + context 'when type is system' do + let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM } + let(:resolver) { described_class.new_system } + + describe '.new_system' do + it 'is expected to set the type correctly' do + expect(resolver.type).to eq type + end + + it 'is expected to set the destination correctly' do + expect(resolver.destination).to be_nil + end + end + + describe '#to_s' do + it 'is expected to return the type as a string' do + expect(resolver.to_s).to eq type.to_s + end + end + end +end diff --git a/spec/lib/rex/proto/dns/upstream_rule_spec.rb b/spec/lib/rex/proto/dns/upstream_rule_spec.rb new file mode 100644 index 000000000000..faf9f6f6a169 --- /dev/null +++ b/spec/lib/rex/proto/dns/upstream_rule_spec.rb @@ -0,0 +1,72 @@ +# -*- coding:binary -*- +require 'spec_helper' +require 'rex/text' + +RSpec.describe Rex::Proto::DNS::UpstreamRule do + describe '.valid_resolver?' do + it 'returns true for "black-hole"' do + expect(described_class.valid_resolver?('black-hole')).to be_truthy + expect(described_class.valid_resolver?('Black-Hole')).to be_truthy + expect(described_class.valid_resolver?(%s[black-hole])).to be_truthy + end + + it 'returns true for IPv4 addresses' do + address = Rex::Socket.addr_ntoa(Random.new.bytes(4)) + expect(described_class.valid_resolver?(address)).to be_truthy + end + + it 'returns true for IPv6 addresses' do + address = Rex::Socket.addr_ntoa(Random.new.bytes(16)) + expect(described_class.valid_resolver?(address)).to be_truthy + end + + it 'returns true for "static"' do + expect(described_class.valid_resolver?('static')).to be_truthy + expect(described_class.valid_resolver?('Static')).to be_truthy + expect(described_class.valid_resolver?(:static)).to be_truthy + end + + it 'returns true for "system"' do + expect(described_class.valid_resolver?('system')).to be_truthy + expect(described_class.valid_resolver?('System')).to be_truthy + expect(described_class.valid_resolver?(:system)).to be_truthy + end + + it 'raises returns false for invalid resolvers' do + expect(described_class.valid_resolver?('fake')).to be_falsey + end + end + + context 'when using a wildcard condition' do + let(:subject) { described_class.new(wildcard: '*.metasploit.com') } + + it 'returns true for subdomains' do + expect(subject.matches_name?('www.metasploit.com')).to be_truthy + end + + it 'returns true for subsubdomains' do + expect(subject.matches_name?('one.two.metasploit.com')).to be_truthy + end + + it 'returns false for the domain' do + expect(subject.matches_name?('metasploit.com')).to be_falsey + end + + + it 'returns false for other domains' do + expect(subject.matches_name?('notmetasploit.com')).to be_falsey + end + end + + context 'when not using a wildcard condition' do + let(:subject) { described_class.new } + + it 'defaults to *' do + expect(subject.wildcard).to eq '*' + end + + it 'returns true for everything' do + expect(subject.matches_name?("#{Rex::Text.rand_text_alphanumeric(10)}.#{Rex::Text.rand_text_alphanumeric(3)}")).to be_truthy + end + end +end From 5c9b454291a39118b4a132425d4ca054cd88da9f Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 Feb 2024 15:26:13 -0500 Subject: [PATCH 21/35] Change initialization to allow reseting Change how the resolver is initialized so the instance can be reset to sane default values loaded from the operating system. --- lib/msf/ui/console/command_dispatcher/dns.rb | 102 +++++++++++++++--- lib/msf/ui/console/driver.rb | 4 +- .../proto/dns/custom_nameserver_provider.rb | 43 ++++++-- lib/rex/proto/dns/upstream_rule.rb | 6 +- .../dns/custom_nameserver_provider_spec.rb | 13 ++- spec/lib/rex/proto/dns/upstream_rule_spec.rb | 46 +++++--- 6 files changed, 174 insertions(+), 40 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 7f21797ea7bc..af543e2655e8 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -12,8 +12,8 @@ class DNS ADD_USAGE = 'dns [add] [--session ] [--rule ] ..."'.freeze @@add_opts = Rex::Parser::Arguments.new( ['-i', '--index'] => [true, 'Index to insert at'], - ['-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)' ] + ['-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)'] ) ADD_STATIC_USAGE = 'dns [add-static] '.freeze @@ -28,6 +28,12 @@ class DNS ['-f'] => [true, 'Address family - IPv4 or IPv6 (default both)'] ) + RESET_CONFIG_USAGE = 'dns [reset-config] [-y/--yes] [--system]'.freeze + @@reset_config_opts = Rex::Parser::Arguments.new( + ['-y', '--yes'] => [false, 'Assume yes and do not prompt for confirmation before resetting'], + ['--system'] => [false, 'Include the system resolver'] + ) + RESOLVE_USAGE = 'dns [resolve] [-f
] ...'.freeze @@resolve_opts = Rex::Parser::Arguments.new( # same usage syntax as Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Stdapi @@ -62,9 +68,9 @@ def commands def cmd_dns_tabs(str, words) return if driver.framework.dns_resolver.nil? + subcommands = %w[ add add-static delete flush-cache flush-entries flush-static help print query remove remove-static reset-config resolve ] if words.length == 1 - options = %w[ add add-static delete flush-cache flush-entries flush-static print query remove remove-static resolve ] - return options.select { |opt| opt.start_with?(str) } + return subcommands.select { |opt| opt.start_with?(str) } end cmd = words[1] @@ -97,9 +103,9 @@ def cmd_dns_tabs(str, words) 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 'flush-cache','flush-entries','help','print' + when 'help' # These commands don't have any arguments - return + return subcommands.select { |sc| sc.start_with?(str) } when 'remove','delete' if words[-1] == '-i' ids = driver.framework.dns_resolver.upstream_entries.map { |entry| entry[:id].to_s } @@ -114,6 +120,8 @@ def cmd_dns_tabs(str, words) else @@resolve_opts.option_keys.select { |opt| opt.start_with?(str) } end + when 'reset-config' + @@reset_config_opts.option_keys.select { |opt| opt.start_with?(str) } when 'resolve','query' if words[-1] == '-f' families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive @@ -126,10 +134,11 @@ def cmd_dns_tabs(str, words) def cmd_dns_help(*args) if args.first.present? - if respond_to?("#{args.first}_dns_help") + handler = "#{args.first.gsub('-', '_')}_dns" + if respond_to?("#{handler}_help") # if it is a valid command with dedicated help information - return send("#{args.first}_dns_help") - elsif respond_to?("#{args.first}_dns") + return send("#{handler}_help") + elsif respond_to?(handler) # if it is a valid command without dedicated help information print_error("No help menu is available for #{args.first}") return @@ -149,6 +158,7 @@ def cmd_dns_help(*args) print_line " dns [flush-entries]" print_line " dns [flush-static]" print_line " dns [print]" + print_line " #{RESET_CONFIG_USAGE}" print_line " #{RESOLVE_USAGE}" print_line " dns [help] [subcommand]" print_line @@ -161,6 +171,7 @@ def cmd_dns_help(*args) print_line " print - Show all configured DNS resolution entries" print_line " remove - Delete a DNS resolution entry" print_line " remove-static - Delete a statically defined hostname" + print_line " reset-config - Reset the DNS configuration" print_line " resolve - Resolve a hostname" print_line print_line "EXAMPLES:" @@ -209,6 +220,8 @@ def cmd_dns(*args) remove_dns(*args) when "remove-static" remove_static_dns(*args) + when "reset-config" + reset_config_dns(*args) when "resolve", "query" resolve_dns(*args) else @@ -340,7 +353,7 @@ def add_static_dns_help end # - # Query a hostname using the configuration. This is useful for debugging anddns + # Query a hostname using the configuration. This is useful for debugging and # inspecting the active settings. # def resolve_dns(*args) @@ -383,6 +396,11 @@ def resolve_dns(*args) ) names.each do |name| upstream_entry = resolver.upstream_entries.find { |ue| ue.matches_name?(name) } + if upstream_entry.nil? + tbl << [name, '[Failed To Resolve]', '', '', '', ''] + next + end + upstream_entry_id = resolver.upstream_entries.index(upstream_entry) + 1 begin @@ -503,6 +521,64 @@ def remove_static_dns_help print_line end + def reset_config_dns(*args) + add_system_resolver = false + should_confirm = true + @@reset_config_opts.parse(args) do |opt, idx, val| + case opt + when '--system' + add_system_resolver = true + when '-y', '--yes' + should_confirm = false + end + end + + if should_confirm + print("Are you sure you want to reset the DNS configuration? [y/N]: ") + response = gets.downcase.chomp + unless response.present? && 'yes'.start_with?(response) + return + end + end + + resolver.reinit + print_status('The DNS configuration has been reset') + + if add_system_resolver + # if the user requested that we add the system resolver + system_resolver = Rex::Proto::DNS::UpstreamResolver.new_system + # first find the default, catch-all rule + default_rule = resolver.upstream_entries.find { |ue| ue.matches_all? } + if default_rule.nil? + resolver.add_upstream_entries([ system_resolver ]) + else + # if the first resolver is for static hostnames, insert after that one + if default_rule.resolvers.first&.type == Rex::Proto::DNS::UpstreamResolver::TYPE_STATIC + index = 1 + else + index = 0 + end + default_rule.resolvers.insert(index, system_resolver) + end + end + + print_dns + + if ENV['PROXYCHAINS_CONF_FILE'] && !add_system_resolver + print_warning('Detected proxychains but the system resolver was not added') + end + end + + def reset_config_dns_help + print_line "USAGE:" + print_line " #{RESET_CONFIG_USAGE}" + print_line @@reset_config_opts.usage + print_line "EXAMPLES:" + print_line " Reset the configuration without prompting to confirm" + print_line " dns reset-config --yes" + print_line + end + # # Delete all cached DNS answers # @@ -515,7 +591,7 @@ def flush_cache_dns # Delete all user-configured DNS settings # def flush_entries_dns - resolver.purge + resolver.flush print_good('DNS entries flushed') end @@ -551,6 +627,7 @@ def print_dns upstream_entries = resolver.upstream_entries print_dns_set('Resolver rule entries', upstream_entries, ids: (1..upstream_entries.length).to_a) if upstream_entries.empty? + print_line print_error('No DNS nameserver entries configured') end @@ -628,7 +705,8 @@ def append_resolver_cells!(tbl, entry, prefix: [], suffix: [], index: nil) tbl << prefix + [index.to_s, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix elsif entry.resolvers.length > 1 # XXX: By default rex-text tables strip preceding whitespace: - # https://github.com/rapid7/rex-text/blob/1a7b639ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 + # https://github.com/rapid7/rex-text/blob/1a7b63993 + # ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement indent = "\xc2\xa0\xc2\xa0\\_ " diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index cabf9626ace7..0d0218c9f252 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -85,9 +85,7 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = { if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DNS_FEATURE) dns_resolver = Rex::Proto::DNS::CachedResolver.new dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) - dns_resolver.purge - dns_resolver.static_hostnames.flush - dns_resolver.load_config + dns_resolver.load_config if dns_resolver.has_config? # Defer loading of modules until paths from opts can be added below framework_create_options = framework_create_options.merge({ 'CustomDnsResolver' => dns_resolver }) diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index eb421608e85e..b248ece9a12f 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -34,6 +34,38 @@ def sid def init @upstream_entries = [] + + resolvers = [UpstreamResolver.new_static] + if @config[:nameservers].empty? + # if no nameservers are specified, fallback to the system + resolvers << UpstreamResolver.new_system + else + # migrate the originally configured name servers + resolvers += @config[:nameservers].map(&:to_s) + @config[:nameservers].clear + end + + add_upstream_entry(resolvers) + + nil + end + + def reinit + parse_config_file + parse_environment_variables + + self.static_hostnames.flush + self.static_hostnames.parse_hosts_file + + init + + cache.flush if respond_to?(:cache) + + nil + end + + def has_config? + Msf::Config.load.keys.any? { |group| group == CONFIG_KEY_BASE || group.starts_with?("#{CONFIG_KEY_BASE}/") } end # @@ -67,7 +99,7 @@ def add_upstream_entry(resolvers, comm: nil, wildcard: '*', position: -1) end # - # Remove entries with the given IDs + # Remove entries with the given indexes # Ignore entries that are not found # @param ids [Array] The IDs to removed # @return [Array] The removed entries @@ -81,8 +113,8 @@ def remove_ids(ids) removed.reverse end - def purge - init + def flush + @upstream_entries.clear end # The nameservers that match the given packet @@ -128,9 +160,7 @@ def set_framework(framework) end def upstream_entries - entries = @upstream_entries.dup - entries << UpstreamRule.new(resolvers: self.nameservers) - entries + @upstream_entries.dup end private @@ -164,6 +194,7 @@ def load_config_entries def load_config_static_hostnames config = Msf::Config.load + static_hostnames.flush config.fetch("#{CONFIG_KEY_BASE}/static_hostnames", {}).each do |_name, value| values = value.split(';') hostname = values.shift diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index 2059bab9b2a4..87cc38736958 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -54,8 +54,12 @@ def self.valid_wildcard?(wildcard) wildcard == '*' || wildcard =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ end + def matches_all? + wildcard == '*' + end + def matches_name?(name) - if wildcard == '*' + if matches_all? true elsif wildcard.start_with?('*.') name.downcase.end_with?(wildcard[1..-1].downcase) diff --git a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb index 737820bb801f..987e1db8c806 100755 --- a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb +++ b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb @@ -37,9 +37,9 @@ def f.enabled?(_name) subject(:dns_resolver) do dns_resolver = Rex::Proto::DNS::CachedResolver.new(config) - dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) dns_resolver.nameservers = [default_nameserver] - dns_resolver.add_upstream_entry([metasploit_nameserver], wildcard: '*.metasploit.com') + dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) + dns_resolver.add_upstream_entry([metasploit_nameserver], wildcard: '*.metasploit.com', position: 0) dns_resolver.set_framework(framework_with_dns_enabled) dns_resolver end @@ -48,7 +48,9 @@ def f.enabled?(_name) it 'The correct resolver is returned' do packet = packet_for('subdomain.metasploit.com') ns = dns_resolver.upstream_resolvers_for_packet(packet) - expect(ns).to eq([Rex::Proto::DNS::UpstreamResolver.new_dns_server(metasploit_nameserver)]) + expect(ns).to eq([ + Rex::Proto::DNS::UpstreamResolver.new_dns_server(metasploit_nameserver) + ]) end end @@ -56,7 +58,10 @@ def f.enabled?(_name) it 'The default resolver is returned' do packet = packet_for('subdomain.test.lan') ns = dns_resolver.upstream_resolvers_for_packet(packet) - expect(ns).to eq([Rex::Proto::DNS::UpstreamResolver.new_dns_server(default_nameserver)]) + expect(ns).to eq([ + Rex::Proto::DNS::UpstreamResolver.new_static, + Rex::Proto::DNS::UpstreamResolver.new_dns_server(default_nameserver) + ]) end end end diff --git a/spec/lib/rex/proto/dns/upstream_rule_spec.rb b/spec/lib/rex/proto/dns/upstream_rule_spec.rb index faf9f6f6a169..48aec5bb4e75 100644 --- a/spec/lib/rex/proto/dns/upstream_rule_spec.rb +++ b/spec/lib/rex/proto/dns/upstream_rule_spec.rb @@ -40,33 +40,51 @@ context 'when using a wildcard condition' do let(:subject) { described_class.new(wildcard: '*.metasploit.com') } - it 'returns true for subdomains' do - expect(subject.matches_name?('www.metasploit.com')).to be_truthy + describe '#matches_all?' do + it 'does not return true for everything' do + expect(subject.matches_all?).to be_falsey + end end - it 'returns true for subsubdomains' do - expect(subject.matches_name?('one.two.metasploit.com')).to be_truthy - end + describe '#matches_name?' do + it 'returns true for subdomains' do + expect(subject.matches_name?('www.metasploit.com')).to be_truthy + end - it 'returns false for the domain' do - expect(subject.matches_name?('metasploit.com')).to be_falsey - end + it 'returns true for subsubdomains' do + expect(subject.matches_name?('one.two.metasploit.com')).to be_truthy + end + + it 'returns false for the domain' do + expect(subject.matches_name?('metasploit.com')).to be_falsey + end - it 'returns false for other domains' do - expect(subject.matches_name?('notmetasploit.com')).to be_falsey + it 'returns false for other domains' do + expect(subject.matches_name?('notmetasploit.com')).to be_falsey + end end end context 'when not using a wildcard condition' do let(:subject) { described_class.new } - it 'defaults to *' do - expect(subject.wildcard).to eq '*' + describe '#wildcard' do + it 'defaults to *' do + expect(subject.wildcard).to eq '*' + end + end + + describe '#matches_all?' do + it 'returns true for everything' do + expect(subject.matches_all?).to be_truthy + end end - it 'returns true for everything' do - expect(subject.matches_name?("#{Rex::Text.rand_text_alphanumeric(10)}.#{Rex::Text.rand_text_alphanumeric(3)}")).to be_truthy + describe '#matches_name?' do + it 'returns true for everything' do + expect(subject.matches_name?("#{Rex::Text.rand_text_alphanumeric(10)}.#{Rex::Text.rand_text_alphanumeric(3)}")).to be_truthy + end end end end From 11ca24e290812f1b909bee919c13c780f869f19e Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 Feb 2024 16:51:32 -0500 Subject: [PATCH 22/35] Specify the record type for PTR lookups --- lib/msf/core/exploit/remote/dns/enumeration.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/remote/dns/enumeration.rb b/lib/msf/core/exploit/remote/dns/enumeration.rb index 3ffe7e8d9837..f3d3ac934410 100644 --- a/lib/msf/core/exploit/remote/dns/enumeration.rb +++ b/lib/msf/core/exploit/remote/dns/enumeration.rb @@ -183,7 +183,7 @@ def dns_get_ns(domain) end def dns_get_ptr(ip) - resp = dns_query(ip, nil) + resp = dns_query(ip, 'PTR') return if resp.blank? || resp.answer.blank? records = [] @@ -227,7 +227,7 @@ def dns_get_srv(domain) srv_record_types.each do |srv_record_type| srv_protos.each do |srv_proto| srv_record = "_#{srv_record_type}._#{srv_proto}.#{domain}" - resp = dns_query(srv_record, Net::DNS::SRV) + resp = dns_query(srv_record, 'SRV') next if resp.blank? || resp.answer.blank? srv_record_data = [] resp.answer.each do |r| From c1a08b97d27d4badf6243fda56d555ade59d055c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 Feb 2024 17:16:42 -0500 Subject: [PATCH 23/35] Load the termux hosts file path too --- lib/rex/proto/dns/static_hostnames.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index 32a1a9e8480c..e1e9114f2941 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -21,9 +21,17 @@ def initialize(hostnames: nil) end def parse_hosts_file - path = '/etc/hosts' - return unless File.file?(path) && File.readable?(path) + path = %w[ + %WINDIR%\system32\drivers\etc\hosts + /etc/hosts + /data/data/com.termux/files/usr/etc/hosts + ].find do |path| + path = File.expand_path(path) + File.file?(path) && File.readable?(path) + end + return unless path + path = File.expand_path(path) hostnames = {} ::IO.foreach(path) do |line| words = line.split From 243ebcb3a62cfc6ecfb7e2f4f5667f84717295a4 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 09:58:09 -0500 Subject: [PATCH 24/35] Add some missing documentation --- .../proto/dns/custom_nameserver_provider.rb | 2 ++ lib/rex/proto/dns/static_hostnames.rb | 25 ++++++++++++++++++- lib/rex/proto/dns/upstream_resolver.rb | 15 +++++++++++ lib/rex/proto/dns/upstream_rule.rb | 21 ++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index b248ece9a12f..eb3c84e9d755 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -50,6 +50,7 @@ def init nil end + # Reinitialize the configuration to its original state. def reinit parse_config_file parse_environment_variables @@ -64,6 +65,7 @@ def reinit nil end + # Check whether or not there is configuration data in Metasploit's configuration file which is persisted on disk. def has_config? Msf::Config.load.keys.any? { |group| group == CONFIG_KEY_BASE || group.starts_with?("#{CONFIG_KEY_BASE}/") } end diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index e1e9114f2941..c880d92fbf02 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -6,11 +6,16 @@ module Rex module Proto module DNS + ## + # This class manages statically defined hostnames for DNS resolution where each name is a mapping to an IPv4 and or + # an IPv6 address. A single hostname can only map to one address of each family. + ## class StaticHostnames extend Forwardable def_delegators :@hostnames, :each, :each_with_index, :length, :empty? + # @param [Hash] hostnames The hostnames to IP address mappings to initialize with. def initialize(hostnames: nil) @hostnames = {} if hostnames @@ -20,6 +25,8 @@ def initialize(hostnames: nil) end end + # Locate and parse a hosts file on the system. Only the first hostname to IP address definition is used which + # replicates the behavior of /etc/hosts on Linux. Loaded definitions are merged with existing definitions. def parse_hosts_file path = %w[ %WINDIR%\system32\drivers\etc\hosts @@ -55,11 +62,22 @@ def parse_hosts_file @hostnames.merge!(hostnames) end + # Get an IP address of the specified type for the hostname. + # + # @param [String] hostname The hostname to retrieve an address for. + # @param [Integer] type The family of address to return represented as a DNS type (either A or AAAA). + # @return Returns the IP address if it was found, otherwise nil. + # @rtype [IPAddr, nil] def get(hostname, type = Dnsruby::Types::A) hostname = hostname.downcase @hostnames.fetch(hostname, {}).fetch(type, nil) end + # Add an IP address for the specified hostname. + # + # @param [String] hostname The hostname whose IP address is being defined. + # @param [IPAddr, String] ip_address The IP address that is being defined for the hostname. If this value is a + # string, it will be converted to an IPAddr instance. def add(hostname, ip_address) hostname = hostname.downcase ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) @@ -74,11 +92,15 @@ def add(hostname, ip_address) nil end + # Delete an IP address for the specified hostname. + # + # @param [String] hostname The hostname whose IP address is being undefined. + # @param [Integer] type The family of address to undefine represented as a DNS type (either A or AAAA). def delete(hostname, type = Dnsruby::Types::A) hostname = hostname.downcase addresses = @hostnames.fetch(hostname, {}) addresses.delete(type) - if addresses.length == 0 + if addresses.empty? @hostnames.delete(hostname) else @hostnames[hostname] = addresses @@ -87,6 +109,7 @@ def delete(hostname, type = Dnsruby::Types::A) nil end + # Delete all hostname to IP address definitions. def flush @hostnames.clear end diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index 49ab270a5b6b..8dd45fdc648a 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -3,6 +3,9 @@ module Rex module Proto module DNS + ## + # This represents a single upstream DNS resolver target of one of the predefined types. + ## class UpstreamResolver TYPE_BLACK_HOLE = %s[black-hole] TYPE_DNS_SERVER = %s[dns-server] @@ -10,16 +13,26 @@ class UpstreamResolver TYPE_SYSTEM = :system attr_reader :type, :destination, :socket_options + + # @param [Symbol] type The resolver type. + # @param [String] destination An optional destination, as used by some resolver types. + # @param [Hash] socket_options Options to use for sockets when connecting to the destination, as used by some + # resolver types. def initialize(type, destination: nil, socket_options: {}) @type = type @destination = destination @socket_options = socket_options end + # Initialize a new black-hole resolver. def self.new_black_hole self.new(TYPE_BLACK_HOLE) end + # Initialize a new dns-server resolver. + # + # @param [String] destination The IP address of the upstream DNS server. + # @param [Hash] socket_options Options to use when connecting to the upstream DNS server. def self.new_dns_server(destination, socket_options: {}) self.new( TYPE_DNS_SERVER, @@ -28,10 +41,12 @@ def self.new_dns_server(destination, socket_options: {}) ) end + # Initialize a new static resolver. def self.new_static self.new(TYPE_STATIC) end + # Initialize a new system resolver. def self.new_system self.new(TYPE_SYSTEM) end diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index 87cc38736958..447309bf899f 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -6,9 +6,16 @@ module Rex module Proto module DNS + ## + # This represents a configuration rule for how names should be resolved. It matches a single wildcard which acts as a + # matching condition and maps it to 0 or more resolvers to use for lookups. + ## class UpstreamRule attr_reader :wildcard, :resolvers, :comm + # @param [String] wildcard The wildcard pattern to use for conditionally matching hostnames. + # @param [Array] resolvers The resolvers to use when this rule is applied. + # @param [Msf::Session::Comm] comm The communication channel to use when creating network connections. def initialize(wildcard: '*', resolvers: [], comm: nil) ::ArgumentError.new("Invalid wildcard text: #{wildcard}") unless self.class.valid_wildcard?(wildcard) @wildcard = wildcard @@ -39,6 +46,10 @@ def initialize(wildcard: '*', resolvers: [], comm: nil) @comm = comm end + # Check whether or not the defined resolver is valid. + # + # @param [String] resolver The resolver string to check. + # @rtype Boolean def self.valid_resolver?(resolver) return true if Rex::Socket.is_ip_addr?(resolver) @@ -50,14 +61,24 @@ def self.valid_resolver?(resolver) ].include?(resolver) end + # Check whether or not the defined wildcard is a valid pattern. + # + # @param [String] wildcard The wildcard text to check. + # @rtype Boolean def self.valid_wildcard?(wildcard) wildcard == '*' || wildcard =~ /^(\*\.)?([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]+$/ end + # Check whether or not the currently configured wildcard pattern will match all names. + # + # @rtype Boolean def matches_all? wildcard == '*' end + # Check whether or not the specified name matches the currently configured wildcard pattern. + # + # @rtype Boolean def matches_name?(name) if matches_all? true From 62e960352f27e33e45b8419f67be6e180fe2b680 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 10:17:21 -0500 Subject: [PATCH 25/35] Refactor naming entries as rules --- lib/msf/ui/console/command_dispatcher/dns.rb | 35 ++++++++--------- .../proto/dns/custom_nameserver_provider.rb | 39 ++++++++++--------- .../dns/custom_nameserver_provider_spec.rb | 2 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index af543e2655e8..5cc33cf18af3 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -108,8 +108,7 @@ def cmd_dns_tabs(str, words) return subcommands.select { |sc| sc.start_with?(str) } when 'remove','delete' if words[-1] == '-i' - ids = driver.framework.dns_resolver.upstream_entries.map { |entry| entry[:id].to_s } - return ids.select { |id| id.start_with?(str) } + return else return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) } end @@ -237,7 +236,7 @@ def add_dns(*args) first_rule = true comm = nil resolvers = [] - position = -1 + index = -1 @@add_opts.parse(args) do |opt, idx, val| unless resolvers.empty? || opt.nil? raise ::ArgumentError.new("Invalid command near #{opt}") @@ -246,7 +245,7 @@ def add_dns(*args) when '-i', '--index' raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0 - position = val.to_i - 1 + index = val.to_i - 1 when '-r', '--rule' raise ::ArgumentError.new('No rule specified') if val.nil? @@ -293,13 +292,13 @@ def add_dns(*args) end end - rules.each_with_index do |rule, rule_index| + rules.each_with_index do |rule, offset| print_warning("DNS rule #{rule} does not contain wildcards, so will not match subdomains") unless rule.include?('*') - driver.framework.dns_resolver.add_upstream_entry( + driver.framework.dns_resolver.add_upstream_rule( resolvers, comm: comm_obj, wildcard: rule, - position: (position == -1 ? -1 : position + rule_index) + index: (index == -1 ? -1 : offset + index) ) end @@ -395,26 +394,26 @@ def resolve_dns(*args) 'WordWrap' => false ) names.each do |name| - upstream_entry = resolver.upstream_entries.find { |ue| ue.matches_name?(name) } - if upstream_entry.nil? + upstream_rule = resolver.upstream_rules.find { |ur| ur.matches_name?(name) } + if upstream_rule.nil? tbl << [name, '[Failed To Resolve]', '', '', '', ''] next end - upstream_entry_id = resolver.upstream_entries.index(upstream_entry) + 1 + upstream_rule_idx = resolver.upstream_rules.index(upstream_rule) + 1 begin result = resolver.query(name, query_type) rescue NoResponseError - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]'], index: upstream_entry_id) + tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, '[Failed To Resolve]'], index: upstream_rule_idx) else if result.answer.empty? - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, '[Failed To Resolve]'], index: upstream_entry_id) + tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, '[Failed To Resolve]'], index: upstream_rule_idx) else result.answer.select do |answer| answer.type == query_type end.map(&:address).map(&:to_s).each do |address| - tbl = append_resolver_cells!(tbl, upstream_entry, prefix: [name, address], index: upstream_entry_id) + tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, address], index: upstream_rule_idx) end end end @@ -548,9 +547,9 @@ def reset_config_dns(*args) # if the user requested that we add the system resolver system_resolver = Rex::Proto::DNS::UpstreamResolver.new_system # first find the default, catch-all rule - default_rule = resolver.upstream_entries.find { |ue| ue.matches_all? } + default_rule = resolver.upstream_rules.find { |ur| ur.matches_all? } if default_rule.nil? - resolver.add_upstream_entries([ system_resolver ]) + resolver.add_upstream_rule([ system_resolver ]) else # if the first resolver is for static hostnames, insert after that one if default_rule.resolvers.first&.type == Rex::Proto::DNS::UpstreamResolver::TYPE_STATIC @@ -624,9 +623,9 @@ def print_dns end print_line("Current cache size: #{resolver.cache.records.length}") - upstream_entries = resolver.upstream_entries - print_dns_set('Resolver rule entries', upstream_entries, ids: (1..upstream_entries.length).to_a) - if upstream_entries.empty? + upstream_rules = resolver.upstream_rules + print_dns_set('Resolver rule entries', upstream_rules, ids: (1..upstream_rules.length).to_a) + if upstream_rules.empty? print_line print_error('No DNS nameserver entries configured') end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index eb3c84e9d755..60597bfa5aaa 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -33,7 +33,7 @@ def sid end def init - @upstream_entries = [] + @upstream_rules = [] resolvers = [UpstreamResolver.new_static] if @config[:nameservers].empty? @@ -45,7 +45,7 @@ def init @config[:nameservers].clear end - add_upstream_entry(resolvers) + add_upstream_rule(resolvers) nil end @@ -86,14 +86,16 @@ def load_config load_config_static_hostnames end - # Add a custom nameserver entry to the custom provider - # @param resolvers [Array] The list of upstream resolvers that would be used for this custom rule - # @param comm [Msf::Session::Comm] The communication channel to be used for these DNS requests - # @param wildcard String The wildcard rule to match a DNS request against - def add_upstream_entry(resolvers, comm: nil, wildcard: '*', position: -1) + # Add a custom nameserver entry to the custom provider. + # + # @param [Array] resolvers The list of upstream resolvers that would be used for this custom rule. + # @param [Msf::Session::Comm] comm The communication channel to be used for these DNS requests. + # @param [String] wildcard The wildcard rule to match a DNS request against. + # @param [Integer] index The index at which to insert the rule, defaults to -1 to append it at the end. + def add_upstream_rule(resolvers, comm: nil, wildcard: '*', index: -1) resolvers = [resolvers] if resolvers.is_a?(String) # coerce into an array of strings - @upstream_entries.insert(position, UpstreamRule.new( + @upstream_rules.insert(index, UpstreamRule.new( wildcard: wildcard, resolvers: resolvers, comm: comm @@ -101,22 +103,21 @@ def add_upstream_entry(resolvers, comm: nil, wildcard: '*', position: -1) end # - # Remove entries with the given indexes + # Remove upstream rules with the given indexes # Ignore entries that are not found # @param ids [Array] The IDs to removed # @return [Array] The removed entries - # def remove_ids(ids) removed = [] ids.sort.reverse.each do |id| - removed << @upstream_entries.delete_at(id) + removed << @upstream_rules.delete_at(id) end removed.reverse end def flush - @upstream_entries.clear + @upstream_rules.clear end # The nameservers that match the given packet @@ -135,10 +136,10 @@ def upstream_resolvers_for_packet(packet) results_from_all_questions = [] packet.question.each do |question| name = question.qname.to_s - upstream_entry = self.upstream_entries.find { |ue| ue.matches_name?(name) } + upstream_rule = self.upstream_rules.find { |ur| ur.matches_name?(name) } - if upstream_entry - upstream_resolvers = upstream_entry.resolvers + if upstream_rule + upstream_resolvers = upstream_rule.resolvers else # Fall back to default nameservers upstream_resolvers = super @@ -161,8 +162,8 @@ def set_framework(framework) self.feature_set = framework.features end - def upstream_entries - @upstream_entries.dup + def upstream_rules + @upstream_rules.dup end private @@ -190,7 +191,7 @@ def load_config_entries end # Now that config has successfully read, update the global values - @upstream_entries = with_rules + @upstream_rules = with_rules end def load_config_static_hostnames @@ -214,7 +215,7 @@ def load_config_static_hostnames def save_config_entries new_config = {} - @upstream_entries.each_with_index do |entry, index| + @upstream_rules.each_with_index do |entry, index| val = [ entry.wildcard, entry.resolvers.map do |resolver| diff --git a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb index 987e1db8c806..2c78ea79d384 100755 --- a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb +++ b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb @@ -39,7 +39,7 @@ def f.enabled?(_name) dns_resolver = Rex::Proto::DNS::CachedResolver.new(config) dns_resolver.nameservers = [default_nameserver] dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) - dns_resolver.add_upstream_entry([metasploit_nameserver], wildcard: '*.metasploit.com', position: 0) + dns_resolver.add_upstream_rule([metasploit_nameserver], wildcard: '*.metasploit.com', index: 0) dns_resolver.set_framework(framework_with_dns_enabled) dns_resolver end From 1cab98f4c2a385efafb3d02a493c6b6c030cbcf5 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 16:05:54 -0500 Subject: [PATCH 26/35] Support multiple addresses for a static hostname --- lib/msf/ui/console/command_dispatcher/dns.rb | 110 ++++++++---------- .../proto/dns/custom_nameserver_provider.rb | 8 +- lib/rex/proto/dns/resolver.rb | 3 +- lib/rex/proto/dns/static_hostnames.rb | 70 ++++++----- .../rex/proto/dns/static_hostnames_spec.rb | 78 +++++++++++++ 5 files changed, 173 insertions(+), 96 deletions(-) create mode 100644 spec/lib/rex/proto/dns/static_hostnames_spec.rb diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 5cc33cf18af3..84e6238ae61e 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -9,24 +9,21 @@ class DNS include Msf::Ui::Console::CommandDispatcher - ADD_USAGE = 'dns [add] [--session ] [--rule ] ..."'.freeze + ADD_USAGE = 'dns [add] [--index ] [--rule ] [--session ] ...'.freeze @@add_opts = Rex::Parser::Arguments.new( ['-i', '--index'] => [true, 'Index to insert at'], ['-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)'] ) - ADD_STATIC_USAGE = 'dns [add-static] '.freeze + ADD_STATIC_USAGE = 'dns [add-static] ...'.freeze REMOVE_USAGE = 'dns [remove/del] -i [-i ...]'.freeze @@remove_opts = Rex::Parser::Arguments.new( ['-i', '--index'] => [true, 'Index to remove at'] ) - REMOVE_STATIC_USAGE = 'dns [remove-static] [-f
] ...'.freeze - @@remove_static_opts = Rex::Parser::Arguments.new( - ['-f'] => [true, 'Address family - IPv4 or IPv6 (default both)'] - ) + REMOVE_STATIC_USAGE = 'dns [remove-static] [ ...]'.freeze RESET_CONFIG_USAGE = 'dns [reset-config] [-y/--yes] [--system]'.freeze @@reset_config_opts = Rex::Parser::Arguments.new( @@ -103,6 +100,11 @@ def cmd_dns_tabs(str, words) 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 'add-static' + if words.length == 2 + # tab complete existing hostnames because they can have more than one IP address + return resolver.static_hostnames.each.select { |hostname,_| hostname.downcase.start_with?(str.downcase) }.map { |hostname,_| hostname } + end when 'help' # These commands don't have any arguments return subcommands.select { |sc| sc.start_with?(str) } @@ -113,11 +115,12 @@ def cmd_dns_tabs(str, words) return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) } end when 'remove-static' - if words[-1] == '-f' - families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive - return families.select { |family| family.downcase.start_with?(str.downcase) } - else - @@resolve_opts.option_keys.select { |opt| opt.start_with?(str) } + if words.length == 2 + return resolver.static_hostnames.each.select { |hostname,_| hostname.downcase.start_with?(str.downcase) }.map { |hostname,_| hostname } + elsif words.length > 2 + hostname = words[2] + ip_addresses = resolver.static_hostnames.get(hostname, Dnsruby::Types::A) + resolver.static_hostnames.get(hostname, Dnsruby::Types::AAAA) + return ip_addresses.map(&:to_s).select { |ip_address| ip_address.start_with?(str) } end when 'reset-config' @@reset_config_opts.option_keys.select { |opt| opt.start_with?(str) } @@ -329,17 +332,17 @@ def add_dns_help def add_static_dns(*args) if args.length < 2 raise ::ArgumentError.new('A hostname and IP address must be provided') - elsif args.length > 2 - raise ::ArgumentError.new("Unknown argument: #{args[2]}") end - hostname, ip_address = args - if !Rex::Socket.is_ip_addr?(ip_address) + hostname = args.shift + if (ip_address = args.find { |a| !Rex::Socket.is_ip_addr?(a) }) raise ::ArgumentError.new("Invalid IP address: #{ip_address}") end - resolver.static_hostnames.add(hostname, ip_address) - print_status("Added static hostname mapping #{hostname} to #{ip_address}") + args.each do |ip_address| + resolver.static_hostnames.add(hostname, ip_address) + print_status("Added static hostname mapping #{hostname} to #{ip_address}") + end end def add_static_dns_help @@ -467,57 +470,36 @@ def remove_dns_help end def remove_static_dns(*args) - names = [] - query_type = nil - - @@resolve_opts.parse(args) do |opt, idx, val| - unless names.empty? || opt.nil? - raise ::ArgumentError.new("Invalid command near #{opt}") - end - case opt - when '-f' - case val.downcase - when 'ipv4' - query_type = Dnsruby::Types::A - when'ipv6' - query_type = Dnsruby::Types::AAAA - else - raise ::ArgumentError.new("Invalid family: #{val}") - end - when nil - names << val - else - raise ::ArgumentError.new("Unknown flag: #{opt}") - end + if args.length < 1 + raise ::ArgumentError.new('A hostname must be provided') end - if names.length < 1 - raise ::ArgumentError.new('You must specify at least one hostname to remove') - end + hostname = args.shift + ip_addresses = args - names.each do |name| - if query_type.nil? || query_type == Dnsruby::Types::A - resolver.static_hostnames.delete(name, Dnsruby::Types::A) + if ip_addresses.empty? + ip_addresses = resolver.static_hostnames.get(hostname, Dnsruby::Types::A) + resolver.static_hostnames.get(hostname, Dnsruby::Types::AAAA) + if ip_addresses.empty? + print_status("There are no definitions for hostname: #{hostname}") end + elsif (ip_address = ip_addresses.find { |ip| !Rex::Socket.is_ip_addr?(ip) }) + raise ::ArgumentError.new("Invalid IP address: #{ip_address}") + end - if query_type.nil? || query_type == Dnsruby::Types::AAAA - resolver.static_hostnames.delete(name, Dnsruby::Types::AAAA) - end + ip_addresses.each do |ip_address| + resolver.static_hostnames.delete(hostname, ip_address) + print_status("Removed static hostname mapping #{hostname} to #{ip_address}") end - print_good('DNS hostnames have been deleted') end def remove_static_dns_help print_line "USAGE:" print_line " #{REMOVE_STATIC_USAGE}" - print_line @@remove_static_opts.usage + print_line print_line "EXAMPLES:" - print_line " Remove IPv4 and IPv6 addresses for 'localhost'" + print_line " Remove all IPv4 and IPv6 addresses for 'localhost'" print_line " dns remove-static localhost" print_line - print_line " Remove only IPv6 addresses for 'localhost6'" - print_line " dns remove-static -f IPv6 localhost6" - print_line end def reset_config_dns(*args) @@ -640,7 +622,11 @@ def print_dns 'WordWrap' => false ) resolver.static_hostnames.each do |hostname, addresses| - tbl << [hostname, addresses[Dnsruby::Types::A], addresses[Dnsruby::Types::AAAA]] + ipv4_addresses = addresses.fetch(Dnsruby::Types::A, []) + ipv6_addresses = addresses.fetch(Dnsruby::Types::AAAA, []) + 0.upto([ipv4_addresses.length, ipv6_addresses.length].max - 1) do |idx| + tbl << [idx == 0 ? hostname : TABLE_INDENT, ipv4_addresses[idx], ipv6_addresses[idx]] + end end print_line(tbl.to_s) if resolver.static_hostnames.empty? @@ -655,6 +641,12 @@ def print_dns Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM.to_s.downcase ].freeze + # XXX: By default rex-text tables strip preceding whitespace: + # https://github.com/rapid7/rex-text/blob/1a7b63993 + # ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 + # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement + TABLE_INDENT = "\xc2\xa0\xc2\xa0\\_ ".freeze + # # Get user-friendly text for displaying the session that this entry would go through # @@ -703,15 +695,9 @@ def append_resolver_cells!(tbl, entry, prefix: [], suffix: [], index: nil) if entry.resolvers.length == 1 tbl << prefix + [index.to_s, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix elsif entry.resolvers.length > 1 - # XXX: By default rex-text tables strip preceding whitespace: - # https://github.com/rapid7/rex-text/blob/1a7b63993 - # ca62fd9102665d6986f918ae42cae244e/lib/rex/text/table.rb#L221-L222 - # So use https://en.wikipedia.org/wiki/Non-breaking_space as a workaround for now. A change should exist in Rex-Text to support this requirement - indent = "\xc2\xa0\xc2\xa0\\_ " - tbl << prefix + [index.to_s, entry.wildcard, '', ''] + suffix entry.resolvers.each do |resolver| - tbl << alignment_prefix + ['.', indent, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) + tbl << alignment_prefix + ['.', TABLE_INDENT, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) end end tbl diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 60597bfa5aaa..6c542a547aea 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -199,9 +199,8 @@ def load_config_static_hostnames static_hostnames.flush config.fetch("#{CONFIG_KEY_BASE}/static_hostnames", {}).each do |_name, value| - values = value.split(';') - hostname = values.shift - values.each do |ip_address| + hostname, ip_addresses = value.split(';', 2) + ip_addresses.split(',').each do |ip_address| next if ip_address.blank? unless Rex::Socket.is_ip_addr?(ip_address) @@ -233,8 +232,7 @@ def save_config_static_hostnames static_hostnames.each_with_index do |(hostname, addresses), index| val = [ hostname, - addresses[Dnsruby::Types::A], - addresses[Dnsruby::Types::AAAA] + (addresses.fetch(Dnsruby::Types::A, []) + addresses.fetch(Dnsruby::Types::AAAA, [])).join(',') ].join(';') new_config["##{index}"] = val end diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 2a81eaea1982..32c5dcf97e63 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -458,8 +458,7 @@ def send_blackhole(upstream_resolver, packet, type, cls) def send_static(upstream_resolver, packet, type, cls) simple_name_lookup(upstream_resolver, packet, type, cls) do |name, _family| - ip_address = static_hostnames.get(name, type) - ip_address ? [ip_address] : nil + static_hostnames.get(name, type) end end diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index c880d92fbf02..a2c95b8aef31 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -39,38 +39,37 @@ def parse_hosts_file return unless path path = File.expand_path(path) - hostnames = {} ::IO.foreach(path) do |line| words = line.split next unless words.length > 1 && Rex::Socket.is_ip_addr?(words.first) ip_address = IPAddr.new(words.shift) words.each do |hostname| - hostname = hostname.downcase - this_host = hostnames.fetch(hostname, {}) - if ip_address.family == ::Socket::AF_INET - type = Dnsruby::Types::A - else - type = Dnsruby::Types::AAAA - end - next if this_host.key?(type) # only honor the first definition - - this_host[type] = ip_address - hostnames[hostname] = this_host + add(hostname, ip_address) end end - @hostnames.merge!(hostnames) end - # Get an IP address of the specified type for the hostname. + # Get an IP address of the specified type for the hostname. Only the first address is returned in cases where + # multiple addresses are defined. # # @param [String] hostname The hostname to retrieve an address for. # @param [Integer] type The family of address to return represented as a DNS type (either A or AAAA). # @return Returns the IP address if it was found, otherwise nil. # @rtype [IPAddr, nil] + def get1(hostname, type = Dnsruby::Types::A) + get(hostname, type).first + end + + # Get all IP addresses of the specified type for the hostname. + # + # @param [String] hostname The hostname to retrieve an address for. + # @param [Integer] type The family of address to return represented as a DNS type (either A or AAAA). + # @return Returns an array of IP addresses. + # @rtype [Array] def get(hostname, type = Dnsruby::Types::A) hostname = hostname.downcase - @hostnames.fetch(hostname, {}).fetch(type, nil) + @hostnames.fetch(hostname, {}).fetch(type, []) end # Add an IP address for the specified hostname. @@ -79,31 +78,48 @@ def get(hostname, type = Dnsruby::Types::A) # @param [IPAddr, String] ip_address The IP address that is being defined for the hostname. If this value is a # string, it will be converted to an IPAddr instance. def add(hostname, ip_address) - hostname = hostname.downcase ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) - addresses = @hostnames.fetch(hostname, {}) + hostname = hostname.downcase + this_host = @hostnames.fetch(hostname, {}) if ip_address.family == ::Socket::AF_INET - addresses[Dnsruby::Types::A] = ip_address - elsif ip_address.family == ::Socket::AF_INET6 - addresses[Dnsruby::Types::AAAA] = ip_address + type = Dnsruby::Types::A + else + type = Dnsruby::Types::AAAA end - @hostnames[hostname] = addresses + this_type = this_host.fetch(type, []) + this_type << ip_address unless this_type.include?(ip_address) + this_host[type] = this_type + @hostnames[hostname] = this_host nil end # Delete an IP address for the specified hostname. # # @param [String] hostname The hostname whose IP address is being undefined. - # @param [Integer] type The family of address to undefine represented as a DNS type (either A or AAAA). - def delete(hostname, type = Dnsruby::Types::A) + # @param [IPAddr, String] ip_address The IP address that is being undefined. If this value is a string, it will be + # converted to an IPAddr instance. + def delete(hostname, ip_address) + ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) + if ip_address.family == ::Socket::AF_INET + type = Dnsruby::Types::A + else + type = Dnsruby::Types::AAAA + end + hostname = hostname.downcase - addresses = @hostnames.fetch(hostname, {}) - addresses.delete(type) - if addresses.empty? + this_host = @hostnames.fetch(hostname, {}) + this_type = this_host.fetch(type, []) + this_type.delete(ip_address) + if this_type.empty? + this_host.delete(type) + else + this_host[type] = this_type + end + if this_host.empty? @hostnames.delete(hostname) else - @hostnames[hostname] = addresses + @hostnames[hostname] = this_host end nil diff --git a/spec/lib/rex/proto/dns/static_hostnames_spec.rb b/spec/lib/rex/proto/dns/static_hostnames_spec.rb new file mode 100644 index 000000000000..0ba089042aec --- /dev/null +++ b/spec/lib/rex/proto/dns/static_hostnames_spec.rb @@ -0,0 +1,78 @@ +# -*- coding:binary -*- +require 'dnsruby' +require 'spec_helper' + +RSpec.describe Rex::Proto::DNS::StaticHostnames do + describe '#parse_hosts_file' do + context 'when parsing a file' do + let(:subject) { described_class.new } + let(:hosts_file) { + <<~CONTENT + # this is a comment + + 127.0.0.1 localhost localhost4 + ::1 localhost localhost6 + 127.1.1.1 localhost + thisIsInvalid + CONTENT + } + + before(:each) do + expect(File).to receive(:file?).and_return(true) + expect(File).to receive(:readable?).and_return(true) + expect(::IO).to receive(:foreach) do |_, &block| + hosts_file.split("\n").each do |line| + block.call(line) + end + end + subject.parse_hosts_file + end + + it 'is not empty' do + expect(subject.empty?).to be_falsey + end + + context 'when no type is specified' do + it 'returns an IPv4 address' do + expect(subject.get('localhost')).to eq ['127.0.0.1', '127.1.1.1'] + end + end + + it 'defines an IPv4 address for localhost' do + expect(subject.get('localhost', Dnsruby::Types::A)).to eq ['127.0.0.1', '127.1.1.1'] + end + + it 'defines an IPv6 address for localhost' do + expect(subject.get('localhost', Dnsruby::Types::AAAA)).to eq ['::1'] + end + end + end + + context 'when no hosts are defined' do + let(:subject) { described_class.new } + + describe '#empty?' do + it 'is true' do + expect(subject.empty?).to be_truthy + end + end + + describe '#get' do + it 'returns an empty array' do + expect(subject.get('localhost')).to eq [] + end + end + + describe '#get1' do + it 'returns nil' do + expect(subject.get1('localhost')).to be_nil + end + end + + describe '#length' do + it 'is zero' do + expect(subject.length).to eq 0 + end + end + end +end From 630301a0df96f259f7ab07a8415c0f0530b3d74c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 16:39:24 -0500 Subject: [PATCH 27/35] Add versioning to the DNS configuration --- .../proto/dns/custom_nameserver_provider.rb | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 6c542a547aea..81ca5a3a597b 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -10,6 +10,7 @@ module DNS ## module CustomNameserverProvider CONFIG_KEY_BASE = 'framework/dns' + CONFIG_VERSION = Rex::Version.new('1.0') # # A Comm implementation that always reports as dead, so should never @@ -67,14 +68,38 @@ def reinit # Check whether or not there is configuration data in Metasploit's configuration file which is persisted on disk. def has_config? - Msf::Config.load.keys.any? { |group| group == CONFIG_KEY_BASE || group.starts_with?("#{CONFIG_KEY_BASE}/") } + config = Msf::Config.load + version = config.fetch(CONFIG_KEY_BASE, {}).fetch('configuration_version', nil) + if version.nil? + @logger.info 'DNS configuration can not be loaded because the version is missing' + return false + end + + their_version = Rex::Version.new(version) + if their_version > CONFIG_VERSION # if the config is newer, it's incompatible (we only guarantee backwards compat) + @logger.info "DNS configuration version #{their_version} can not be loaded because it is too new" + return false + end + + my_minimum_version = Rex::Version.new(CONFIG_VERSION.canonical_segments.first.to_s) + if their_version < my_minimum_version # can not be older than our major version + @logger.info "DNS configuration version #{their_version} can not be loaded because it is too old" + return false + end + + true end # # Save the custom settings to the MSF config file # def save_config - save_config_entries + new_config = { + 'configuration_version' => CONFIG_VERSION.to_s + } + Msf::Config.save(CONFIG_KEY_BASE => new_config) + + save_config_upstream_rules save_config_static_hostnames end @@ -82,6 +107,10 @@ def save_config # Load the custom settings from the MSF config file # def load_config + unless has_config? + raise ResolverError.new('There is no compatible configuration data to load') + end + load_config_entries load_config_static_hostnames end @@ -212,7 +241,7 @@ def load_config_static_hostnames end end - def save_config_entries + def save_config_upstream_rules new_config = {} @upstream_rules.each_with_index do |entry, index| val = [ @@ -224,7 +253,7 @@ def save_config_entries ].join(';') new_config["##{index}"] = val end - Msf::Config.save("#{CONFIG_KEY_BASE}/entries" => new_config) + Msf::Config.save("#{CONFIG_KEY_BASE}/upstream_rules" => new_config) end def save_config_static_hostnames From 934b10a626fa0cb9e1afe106f42348454e34820d Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 16:52:45 -0500 Subject: [PATCH 28/35] Fix a bug when `dns -h` is run --- lib/msf/ui/console/command_dispatcher/dns.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 84e6238ae61e..4c6b4bfd564c 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -191,9 +191,10 @@ def cmd_dns(*args) args << 'print' if args.length == 0 # Short-circuit help if args.delete("-h") || args.delete("--help") - if respond_to?("#{args.first.gsub('-', '_')}_dns_help") + subcommand = args.first + if subcommand && respond_to?("#{subcommand.gsub('-', '_')}_dns_help") # if it is a valid command with dedicated help information - send("#{args.first.gsub('-', '_')}_dns_help") + send("#{subcommand.gsub('-', '_')}_dns_help") else # otherwise print the top-level help information cmd_dns_help From 56d2dfa46a0e580d973b56b8c71b89ed41c95a73 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 17:00:40 -0500 Subject: [PATCH 29/35] Fix removing invalid DNS rule IDs --- lib/msf/ui/console/command_dispatcher/dns.rb | 6 +++++- lib/rex/proto/dns/custom_nameserver_provider.rb | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 4c6b4bfd564c..eb9467926af7 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -449,6 +449,10 @@ def remove_dns(*args) end end + if remove_ids.empty? + raise ::ArgumentError.new('At least one index to remove must be provided') + end + removed = resolver.remove_ids(remove_ids) print_warning('Some entries were not removed') unless removed.length == remove_ids.length if removed.length > 0 @@ -465,7 +469,7 @@ def remove_dns_help print_line " Delete the DNS resolution rule #3" print_line " dns remove -i 3" print_line - print_line " Delete multiple entries in one command" + print_line " Delete multiple rules in one command" print_line " dns remove -i 3 -i 4 -i 5" print_line end diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 81ca5a3a597b..27be7f4577d4 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -139,7 +139,8 @@ def add_upstream_rule(resolvers, comm: nil, wildcard: '*', index: -1) def remove_ids(ids) removed = [] ids.sort.reverse.each do |id| - removed << @upstream_rules.delete_at(id) + upstream_rule = @upstream_rules.delete_at(id) + removed << upstream_rule if upstream_rule end removed.reverse From 1b2a2af4d4c63496bfee9a3f7b834e4cf56a632c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 8 Feb 2024 17:10:49 -0500 Subject: [PATCH 30/35] Fix unit tests on Ruby 3.2 because IPAddr =~ fails --- lib/rex/proto/dns/static_hostnames.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index a2c95b8aef31..cce09e171e77 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -78,7 +78,7 @@ def get(hostname, type = Dnsruby::Types::A) # @param [IPAddr, String] ip_address The IP address that is being defined for the hostname. If this value is a # string, it will be converted to an IPAddr instance. def add(hostname, ip_address) - ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) + ip_address = IPAddr.new(ip_address) if ip_address.is_a?(String) && Rex::Socket.is_ip_addr?(ip_address) hostname = hostname.downcase this_host = @hostnames.fetch(hostname, {}) @@ -100,7 +100,7 @@ def add(hostname, ip_address) # @param [IPAddr, String] ip_address The IP address that is being undefined. If this value is a string, it will be # converted to an IPAddr instance. def delete(hostname, ip_address) - ip_address = IPAddr.new(ip_address) if Rex::Socket.is_ip_addr?(ip_address) + ip_address = IPAddr.new(ip_address) if ip_address.is_a?(String) && Rex::Socket.is_ip_addr?(ip_address) if ip_address.family == ::Socket::AF_INET type = Dnsruby::Types::A else From 99b2bfec1fd975fb5134ed1c405327ab248019d0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 Feb 2024 08:57:23 -0500 Subject: [PATCH 31/35] Support -1 in the --session argument --- lib/msf/ui/console/command_dispatcher/dns.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index eb9467926af7..bca88d4ee6c9 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -287,10 +287,12 @@ def add_dns(*args) 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] + raise ::ArgumentError.new("Not a valid session: #{comm}") unless comm =~ /\A-?[0-9]+\Z/ + + comm_obj = driver.framework.sessions.get(comm.to_i) + raise ::ArgumentError.new("Session does not exist: #{comm}") unless comm_obj + raise ::ArgumentError.new("Socket Comm (Session #{comm}) does not implement Rex::Socket::Comm") unless comm_obj.is_a? ::Rex::Socket::Comm + if resolvers.any? { |resolver| SPECIAL_RESOLVERS.include?(resolver.downcase) } print_warning("The session argument will be ignored for the system resolver") end From 5036d28b44cc69ed4abc127587638f3426be8e84 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 Feb 2024 14:22:45 -0500 Subject: [PATCH 32/35] Validate hostnames before storing them This proactively fixes a potential DoS condition where if a user were to add a hostname containing a ; and followed by data that is not an IP address that MSF may fail to start. Example: dns add-static 'foo;bar' 192.0.2.1 save --- Gemfile | 2 ++ Gemfile.lock | 11 +++++++++-- lib/msf/ui/console/command_dispatcher/dns.rb | 14 +++++++++++--- lib/rex/proto/dns/static_hostnames.rb | 18 +++++++++++++++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 83b7b2811fbd..768eb09b0144 100644 --- a/Gemfile +++ b/Gemfile @@ -53,3 +53,5 @@ group :test do gem 'timecop' end +# remove after https://github.com/rapid7/rex-socket/pull/65 is landed +gem 'rex-socket', git: 'https://github.com/zeroSteiner/rex-socket', branch: 'feat/util-is-name' diff --git a/Gemfile.lock b/Gemfile.lock index 748516bd4b07..a203ed83eac9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/zeroSteiner/rex-socket + revision: 764718bf3dd397c0a5ecc41ee0874d826e9c9144 + branch: feat/util-is-name + specs: + rex-socket (0.1.56) + rex-core + PATH remote: . specs: @@ -419,8 +427,6 @@ GEM metasm rex-core rex-text - rex-socket (0.1.55) - rex-core rex-sslscan (0.1.10) rex-core rex-socket @@ -562,6 +568,7 @@ DEPENDENCIES pry-byebug rake redcarpet + rex-socket! rspec-rails rspec-rerun rubocop diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index bca88d4ee6c9..89db88e7d584 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -338,11 +338,16 @@ def add_static_dns(*args) end hostname = args.shift - if (ip_address = args.find { |a| !Rex::Socket.is_ip_addr?(a) }) + if !Rex::Proto::DNS::StaticHostnames.is_valid_hostname?(hostname) + raise ::ArgumentError.new("Invalid hostname: #{hostname}") + end + + ip_addresses = args + if (ip_address = ip_addresses.find { |a| !Rex::Socket.is_ip_addr?(a) }) raise ::ArgumentError.new("Invalid IP address: #{ip_address}") end - args.each do |ip_address| + ip_addresses.each do |ip_address| resolver.static_hostnames.add(hostname, ip_address) print_status("Added static hostname mapping #{hostname} to #{ip_address}") end @@ -482,8 +487,11 @@ def remove_static_dns(*args) end hostname = args.shift - ip_addresses = args + if !Rex::Proto::DNS::StaticHostnames.is_valid_hostname?(hostname) + raise ::ArgumentError.new("Invalid hostname: #{hostname}") + end + ip_addresses = args if ip_addresses.empty? ip_addresses = resolver.static_hostnames.get(hostname, Dnsruby::Types::A) + resolver.static_hostnames.get(hostname, Dnsruby::Types::AAAA) if ip_addresses.empty? diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb index cce09e171e77..4b43771b616b 100644 --- a/lib/rex/proto/dns/static_hostnames.rb +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -69,7 +69,7 @@ def get1(hostname, type = Dnsruby::Types::A) # @rtype [Array] def get(hostname, type = Dnsruby::Types::A) hostname = hostname.downcase - @hostnames.fetch(hostname, {}).fetch(type, []) + @hostnames.fetch(hostname, {}).fetch(type, []).dup end # Add an IP address for the specified hostname. @@ -78,6 +78,12 @@ def get(hostname, type = Dnsruby::Types::A) # @param [IPAddr, String] ip_address The IP address that is being defined for the hostname. If this value is a # string, it will be converted to an IPAddr instance. def add(hostname, ip_address) + unless self.class.is_valid_hostname?(hostname) + # it is important to validate the hostname because assumptions about what characters it may contain are made + # when saving and loading it from the configuration + raise ::ArgumentError.new("Invalid hostname: #{hostname}") + end + ip_address = IPAddr.new(ip_address) if ip_address.is_a?(String) && Rex::Socket.is_ip_addr?(ip_address) hostname = hostname.downcase @@ -129,6 +135,16 @@ def delete(hostname, ip_address) def flush @hostnames.clear end + + def self.is_valid_hostname?(name) + # check if it appears to be a fully qualified domain name, e.g. www.metasploit.com + return true if Rex::Socket.is_name?(name) + + # check if it appears to at least be a valid hostname, e.g. localhost + return true if (name =~ /^([a-z0-9][a-z0-9\-]{0,61})?[a-z0-9]$/i) && (name =~ /\s/).nil? + + false + end end end end From df81cda304652daeaaafd5e4ab2816eb5013b3c0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Feb 2024 09:39:51 -0500 Subject: [PATCH 33/35] Bump rex-socket to pull in validation changes --- Gemfile | 2 -- Gemfile.lock | 11 ++--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 768eb09b0144..83b7b2811fbd 100644 --- a/Gemfile +++ b/Gemfile @@ -53,5 +53,3 @@ group :test do gem 'timecop' end -# remove after https://github.com/rapid7/rex-socket/pull/65 is landed -gem 'rex-socket', git: 'https://github.com/zeroSteiner/rex-socket', branch: 'feat/util-is-name' diff --git a/Gemfile.lock b/Gemfile.lock index a203ed83eac9..a6185a4307e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,3 @@ -GIT - remote: https://github.com/zeroSteiner/rex-socket - revision: 764718bf3dd397c0a5ecc41ee0874d826e9c9144 - branch: feat/util-is-name - specs: - rex-socket (0.1.56) - rex-core - PATH remote: . specs: @@ -427,6 +419,8 @@ GEM metasm rex-core rex-text + rex-socket (0.1.56) + rex-core rex-sslscan (0.1.10) rex-core rex-socket @@ -568,7 +562,6 @@ DEPENDENCIES pry-byebug rake redcarpet - rex-socket! rspec-rails rspec-rerun rubocop From 27ccb26de10eb318a151b454796dc3a33e874804 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Feb 2024 14:27:05 -0500 Subject: [PATCH 34/35] Adjust the confirmation logic before resetting --- lib/msf/ui/console/command_dispatcher/dns.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index 89db88e7d584..d320d380889a 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -532,9 +532,7 @@ def reset_config_dns(*args) if should_confirm print("Are you sure you want to reset the DNS configuration? [y/N]: ") response = gets.downcase.chomp - unless response.present? && 'yes'.start_with?(response) - return - end + return unless response =~ /^y/i end resolver.reinit From eca99e2c7716613c92aa1f8fbd147a05121e5c33 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Feb 2024 14:35:49 -0500 Subject: [PATCH 35/35] Refactor resolver types --- lib/msf/ui/console/command_dispatcher/dns.rb | 8 +++--- .../proto/dns/custom_nameserver_provider.rb | 6 ++-- lib/rex/proto/dns/resolver.rb | 28 +++++++++---------- lib/rex/proto/dns/upstream_resolver.rb | 28 ++++++++++--------- lib/rex/proto/dns/upstream_rule.rb | 20 ++++++------- .../dns/custom_nameserver_provider_spec.rb | 6 ++-- .../rex/proto/dns/upstream_resolver_spec.rb | 16 +++++------ 7 files changed, 57 insertions(+), 55 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index d320d380889a..96991b072e88 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -540,14 +540,14 @@ def reset_config_dns(*args) if add_system_resolver # if the user requested that we add the system resolver - system_resolver = Rex::Proto::DNS::UpstreamResolver.new_system + system_resolver = Rex::Proto::DNS::UpstreamResolver.create_system # first find the default, catch-all rule default_rule = resolver.upstream_rules.find { |ur| ur.matches_all? } if default_rule.nil? resolver.add_upstream_rule([ system_resolver ]) else # if the first resolver is for static hostnames, insert after that one - if default_rule.resolvers.first&.type == Rex::Proto::DNS::UpstreamResolver::TYPE_STATIC + if default_rule.resolvers.first&.type == Rex::Proto::DNS::UpstreamResolver::Type::STATIC index = 1 else index = 0 @@ -650,8 +650,8 @@ def print_dns private SPECIAL_RESOLVERS = [ - Rex::Proto::DNS::UpstreamResolver::TYPE_BLACK_HOLE.to_s.downcase, - Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM.to_s.downcase + Rex::Proto::DNS::UpstreamResolver::Type::BLACK_HOLE.to_s.downcase, + Rex::Proto::DNS::UpstreamResolver::Type::SYSTEM.to_s.downcase ].freeze # XXX: By default rex-text tables strip preceding whitespace: diff --git a/lib/rex/proto/dns/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index 27be7f4577d4..b8c3aeff1c10 100755 --- a/lib/rex/proto/dns/custom_nameserver_provider.rb +++ b/lib/rex/proto/dns/custom_nameserver_provider.rb @@ -36,10 +36,10 @@ def sid def init @upstream_rules = [] - resolvers = [UpstreamResolver.new_static] + resolvers = [UpstreamResolver.create_static] if @config[:nameservers].empty? # if no nameservers are specified, fallback to the system - resolvers << UpstreamResolver.new_system + resolvers << UpstreamResolver.create_system else # migrate the originally configured name servers resolvers += @config[:nameservers].map(&:to_s) @@ -248,7 +248,7 @@ def save_config_upstream_rules val = [ entry.wildcard, entry.resolvers.map do |resolver| - resolver.type == Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER ? resolver.destination : resolver.type.to_s + resolver.type == Rex::Proto::DNS::UpstreamResolver::Type::DNS_SERVER ? resolver.destination : resolver.type.to_s end.join(','), (!entry.comm.nil?).to_s ].join(';') diff --git a/lib/rex/proto/dns/resolver.rb b/lib/rex/proto/dns/resolver.rb index 32c5dcf97e63..3a3f0211cfe5 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -120,7 +120,7 @@ def proxies=(prox, timeout_added = 250) # def upstream_resolvers_for_packet(_dns_message) @config[:nameservers].map do |ns| - UpstreamResolver.new_dns_server(ns.to_s) + UpstreamResolver.create_dns_server(ns.to_s) end end @@ -161,14 +161,14 @@ def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN) ans = nil upstream_resolvers.each do |upstream_resolver| case upstream_resolver.type - when UpstreamResolver::TYPE_BLACK_HOLE - ans = send_blackhole(upstream_resolver, packet, type, cls) - when UpstreamResolver::TYPE_DNS_SERVER - ans = send_dns_server(upstream_resolver, packet, type, cls) - when UpstreamResolver::TYPE_STATIC - ans = send_static(upstream_resolver, packet, type, cls) - when UpstreamResolver::TYPE_SYSTEM - ans = send_system(upstream_resolver, packet, type, cls) + when UpstreamResolver::Type::BLACK_HOLE + ans = resolve_via_blackhole(upstream_resolver, packet, type, cls) + when UpstreamResolver::Type::DNS_SERVER + ans = resolve_via_dns_server(upstream_resolver, packet, type, cls) + when UpstreamResolver::Type::STATIC + ans = resolve_via_static(upstream_resolver, packet, type, cls) + when UpstreamResolver::Type::SYSTEM + ans = resolve_via_system(upstream_resolver, packet, type, cls) end break if (ans and ans[0].length > 0) @@ -410,7 +410,7 @@ def preprocess_query_arguments(name, type, cls) [name, type, cls] end - def send_dns_server(upstream_resolver, packet, type, _cls) + def resolve_via_dns_server(upstream_resolver, packet, type, _cls) method = self.use_tcp? ? :send_tcp : :send_udp # Store packet_data for performance improvements, @@ -450,19 +450,19 @@ def send_dns_server(upstream_resolver, packet, type, _cls) ans end - def send_blackhole(upstream_resolver, packet, type, cls) + def resolve_via_blackhole(upstream_resolver, packet, type, cls) # do not just return nil because that will cause the next resolver to be used @logger.info "No response from upstream resolvers: blackholed" raise NoResponseError end - def send_static(upstream_resolver, packet, type, cls) + def resolve_via_static(upstream_resolver, packet, type, cls) simple_name_lookup(upstream_resolver, packet, type, cls) do |name, _family| static_hostnames.get(name, type) end end - def send_system(upstream_resolver, packet, type, cls) + def resolve_via_system(upstream_resolver, packet, type, cls) # This system resolver will use host operating systems `getaddrinfo` (or equivalent function) to perform name # resolution. This is primarily useful if that functionality is hooked or modified by an external application such # as proxychains. This handler though can only process A and AAAA requests. @@ -508,7 +508,7 @@ def simple_name_lookup(upstream_resolver, packet, type, cls, &block) end def supports_udp?(upstream_resolver) - return false unless upstream_resolver.type == UpstreamResolver::TYPE_DNS_SERVER + return false unless upstream_resolver.type == UpstreamResolver::Type::DNS_SERVER comm = upstream_resolver.socket_options.fetch('Comm') { @config[:comm] || Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination) } return false if comm && !comm.supports_udp? diff --git a/lib/rex/proto/dns/upstream_resolver.rb b/lib/rex/proto/dns/upstream_resolver.rb index 8dd45fdc648a..c45cd3db8a6a 100644 --- a/lib/rex/proto/dns/upstream_resolver.rb +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -7,10 +7,12 @@ module DNS # This represents a single upstream DNS resolver target of one of the predefined types. ## class UpstreamResolver - TYPE_BLACK_HOLE = %s[black-hole] - TYPE_DNS_SERVER = %s[dns-server] - TYPE_STATIC = :static - TYPE_SYSTEM = :system + module Type + BLACK_HOLE = :"black-hole" + DNS_SERVER = :"dns-server" + STATIC = :static + SYSTEM = :system + end attr_reader :type, :destination, :socket_options @@ -25,34 +27,34 @@ def initialize(type, destination: nil, socket_options: {}) end # Initialize a new black-hole resolver. - def self.new_black_hole - self.new(TYPE_BLACK_HOLE) + def self.create_black_hole + self.new(Type::BLACK_HOLE) end # Initialize a new dns-server resolver. # # @param [String] destination The IP address of the upstream DNS server. # @param [Hash] socket_options Options to use when connecting to the upstream DNS server. - def self.new_dns_server(destination, socket_options: {}) + def self.create_dns_server(destination, socket_options: {}) self.new( - TYPE_DNS_SERVER, + Type::DNS_SERVER, destination: destination, socket_options: socket_options ) end # Initialize a new static resolver. - def self.new_static - self.new(TYPE_STATIC) + def self.create_static + self.new(Type::STATIC) end # Initialize a new system resolver. - def self.new_system - self.new(TYPE_SYSTEM) + def self.create_system + self.new(Type::SYSTEM) end def to_s - if type == TYPE_DNS_SERVER + if type == Type::DNS_SERVER destination.to_s else type.to_s diff --git a/lib/rex/proto/dns/upstream_rule.rb b/lib/rex/proto/dns/upstream_rule.rb index 447309bf899f..349ae3f6e76c 100644 --- a/lib/rex/proto/dns/upstream_rule.rb +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -29,15 +29,15 @@ def initialize(wildcard: '*', resolvers: [], comm: nil) case resolver when UpstreamResolver resolver - when UpstreamResolver::TYPE_BLACK_HOLE - UpstreamResolver.new_black_hole - when UpstreamResolver::TYPE_STATIC - UpstreamResolver.new_static - when UpstreamResolver::TYPE_SYSTEM - UpstreamResolver.new_system + when UpstreamResolver::Type::BLACK_HOLE + UpstreamResolver.create_black_hole + when UpstreamResolver::Type::STATIC + UpstreamResolver.create_static + when UpstreamResolver::Type::SYSTEM + UpstreamResolver.create_system else if Rex::Socket.is_ip_addr?(resolver) - UpstreamResolver.new_dns_server(resolver, socket_options: socket_options) + UpstreamResolver.create_dns_server(resolver, socket_options: socket_options) else raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") end @@ -55,9 +55,9 @@ def self.valid_resolver?(resolver) resolver = resolver.downcase.to_sym [ - UpstreamResolver::TYPE_BLACK_HOLE, - UpstreamResolver::TYPE_STATIC, - UpstreamResolver::TYPE_SYSTEM + UpstreamResolver::Type::BLACK_HOLE, + UpstreamResolver::Type::STATIC, + UpstreamResolver::Type::SYSTEM ].include?(resolver) end diff --git a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb index 2c78ea79d384..8935b519d2a5 100755 --- a/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb +++ b/spec/lib/rex/proto/dns/custom_nameserver_provider_spec.rb @@ -49,7 +49,7 @@ def f.enabled?(_name) packet = packet_for('subdomain.metasploit.com') ns = dns_resolver.upstream_resolvers_for_packet(packet) expect(ns).to eq([ - Rex::Proto::DNS::UpstreamResolver.new_dns_server(metasploit_nameserver) + Rex::Proto::DNS::UpstreamResolver.create_dns_server(metasploit_nameserver) ]) end end @@ -59,8 +59,8 @@ def f.enabled?(_name) packet = packet_for('subdomain.test.lan') ns = dns_resolver.upstream_resolvers_for_packet(packet) expect(ns).to eq([ - Rex::Proto::DNS::UpstreamResolver.new_static, - Rex::Proto::DNS::UpstreamResolver.new_dns_server(default_nameserver) + Rex::Proto::DNS::UpstreamResolver.create_static, + Rex::Proto::DNS::UpstreamResolver.create_dns_server(default_nameserver) ]) end end diff --git a/spec/lib/rex/proto/dns/upstream_resolver_spec.rb b/spec/lib/rex/proto/dns/upstream_resolver_spec.rb index a2a7a335ca6c..e313ce75babb 100644 --- a/spec/lib/rex/proto/dns/upstream_resolver_spec.rb +++ b/spec/lib/rex/proto/dns/upstream_resolver_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Rex::Proto::DNS::UpstreamResolver do context 'when type is black-hole' do - let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_BLACK_HOLE } - let(:resolver) { described_class.new_black_hole } + let(:type) { Rex::Proto::DNS::UpstreamResolver::Type::BLACK_HOLE } + let(:resolver) { described_class.create_black_hole } describe '.new_black_hole' do it 'is expected to set the type correctly' do @@ -25,9 +25,9 @@ end context 'when type is dns-server' do - let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_DNS_SERVER } + let(:type) { Rex::Proto::DNS::UpstreamResolver::Type::DNS_SERVER } let(:destination) { '192.0.2.10' } - let(:resolver) { described_class.new_dns_server(destination) } + let(:resolver) { described_class.create_dns_server(destination) } describe '.new_dns_server' do it 'is expected to set the type correctly' do @@ -47,8 +47,8 @@ end context 'when type is static' do - let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_STATIC } - let(:resolver) { described_class.new_static } + let(:type) { Rex::Proto::DNS::UpstreamResolver::Type::STATIC } + let(:resolver) { described_class.create_static } describe '.new_static' do it 'is expected to set the type correctly' do @@ -68,8 +68,8 @@ end context 'when type is system' do - let(:type) { Rex::Proto::DNS::UpstreamResolver::TYPE_SYSTEM } - let(:resolver) { described_class.new_system } + let(:type) { Rex::Proto::DNS::UpstreamResolver::Type::SYSTEM } + let(:resolver) { described_class.create_system } describe '.new_system' do it 'is expected to set the type correctly' do