diff --git a/Gemfile.lock b/Gemfile.lock index 1e57120c537c..15a5626b25aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -419,7 +419,7 @@ GEM metasm rex-core rex-text - rex-socket (0.1.55) + rex-socket (0.1.56) rex-core rex-sslscan (0.1.10) rex-core 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| diff --git a/lib/msf/ui/console/command_dispatcher/dns.rb b/lib/msf/ui/console/command_dispatcher/dns.rb index ec538a7c14e9..96991b072e88 100755 --- a/lib/msf/ui/console/command_dispatcher/dns.rb +++ b/lib/msf/ui/console/command_dispatcher/dns.rb @@ -9,13 +9,32 @@ class DNS include Msf::Ui::Console::CommandDispatcher + ADD_USAGE = 'dns [add] [--index ] [--rule ] [--session ] ...'.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)' ], + ['-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 + + 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'] + ) + + REMOVE_STATIC_USAGE = 'dns [remove-static] [ ...]'.freeze + + 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 + ['-f'] => [true, 'Address family - IPv4 or IPv6 (default IPv4)'] ) def initialize(driver) @@ -31,7 +50,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 @@ -46,16 +65,13 @@ 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 = ['add','del','remove','purge','print'] - return options.select { |opt| opt.start_with?(str) } + return subcommands.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. @@ -63,19 +79,19 @@ 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 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 @@ -84,53 +100,85 @@ 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 '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) } + 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 else return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) } end + when 'remove-static' + 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) } + 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 - def cmd_dns_help + def cmd_dns_help(*args) + if args.first.present? + handler = "#{args.first.gsub('-', '_')}_dns" + if respond_to?("#{handler}_help") + # if it is a valid command with dedicated help information + 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 + 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 " dns [purge]" + 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 " #{RESET_CONFIG_USAGE}" + 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 " remove - delete a DNS resolution entry; 'del' is an alias" - print_line " purge - remove all DNS resolution entries" - print_line " print - show all active DNS resolution entries" - print_line - print_line "Examples:" - print_line " Display all current DNS nameserver entries" - print_line " dns" - print_line " dns print" - print_line - print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10" - print_line " route add --rule *.metasploit.com 192.168.1.10" - print_line - print_line " Add multiple entries at once" - print_line " route add --rule *.metasploit.com --rule *.google.com 192.168.1.10 192.168.1.11" - print_line - print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10, but specifically to go through session 2" - print_line " route add --session 2 --rule *.metasploit.com 192.168.1.10" - print_line - print_line " Delete the DNS resolution rule with ID 3" - print_line " route remove -i 3" - print_line - print_line " Delete multiple entries in one command" - print_line " route remove -i 3 -i 4 -i 5" + print_line "SUBCOMMANDS:" + print_line " add - Add a DNS resolution entry to resolve certain domain names through a particular DNS resolver" + 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 " reset-config - Reset the DNS configuration" + print_line " resolve - Resolve a hostname" 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 "EXAMPLES:" + print_line " Display help information for the 'add' subcommand" + print_line " dns help add" print_line end @@ -143,7 +191,14 @@ def cmd_dns(*args) args << 'print' if args.length == 0 # Short-circuit help if args.delete("-h") || args.delete("--help") - cmd_dns_help + subcommand = args.first + if subcommand && respond_to?("#{subcommand.gsub('-', '_')}_dns_help") + # if it is a valid command with dedicated help information + send("#{subcommand.gsub('-', '_')}_dns_help") + else + # otherwise print the top-level help information + cmd_dns_help + end return end @@ -152,14 +207,26 @@ def cmd_dns(*args) case action when "add" add_dns(*args) - when "remove", "del" - remove_dns(*args) - when "purge" - purge_dns + 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 "help" - cmd_dns_help + when "remove", "rm", "delete", "del" + remove_dns(*args) + when "remove-static" + remove_static_dns(*args) + when "reset-config" + reset_config_dns(*args) + when "resolve", "query" + resolve_dns(*args) else print_error("Invalid command. To view help: dns -h") end @@ -169,19 +236,27 @@ def cmd_dns(*args) end def add_dns(*args) - rules = [] + rules = ['*'] + first_rule = true comm = nil - servers = [] + resolvers = [] + index = -1 @@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 - when '--rule', '-r' + when '-i', '--index' + raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0 + + index = 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 @@ -192,42 +267,179 @@ 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) - raise ::ArgumentError.new("Invalid DNS server: #{host}") + resolvers.each do |resolver| + unless Rex::Proto::DNS::UpstreamRule.valid_resolver?(resolver) + raise ::ArgumentError.new("Invalid DNS resolver: #{resolver}") end end comm_obj = nil unless comm.nil? - raise ::ArgumentError.new("Not a valid number: #{comm}") unless comm =~ /^\d+$/ - comm_int = comm.to_i - raise ::ArgumentError.new("Session does not exist: #{comm}") unless driver.framework.sessions.include?(comm_int) - comm_obj = driver.framework.sessions[comm_int] + 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 end - rules.each do |rule| + 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_rule( + resolvers, + comm: comm_obj, + wildcard: rule, + index: (index == -1 ? -1 : offset + index) + ) + end + + 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 " 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" + 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 + + def add_static_dns(*args) + if args.length < 2 + raise ::ArgumentError.new('A hostname and IP address must be provided') + end + + hostname = args.shift + 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 + + ip_addresses.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 + 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 + # inspecting the active settings. + # + def resolve_dns(*args) + names = [] + query_type = Dnsruby::Types::A + + @@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 - # Split each DNS server entry up into a separate entry - servers.each do |server| - driver.framework.dns_resolver.add_nameserver(rules, server, comm_obj) + if names.length < 1 + raise ::ArgumentError.new('You must specify at least one hostname to resolve') end - print_good("#{servers.length} DNS #{servers.length > 1 ? 'entries' : 'entry'} added") + + tbl = Table.new( + Table::Style::Default, + 'Header' => 'Host resolutions', + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => ['Hostname', 'IP Address', 'Rule #', 'Rule', 'Resolver', 'Comm channel'], + 'SortIndex' => -1, + 'WordWrap' => false + ) + names.each do |name| + upstream_rule = resolver.upstream_rules.find { |ur| ur.matches_name?(name) } + if upstream_rule.nil? + tbl << [name, '[Failed To Resolve]', '', '', '', ''] + next + end + + 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_rule, prefix: [name, '[Failed To Resolve]'], index: upstream_rule_idx) + else + if result.answer.empty? + 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_rule, prefix: [name, address], index: upstream_rule_idx) + end + end + end + end + 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 # @@ -237,52 +449,225 @@ 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? + 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 - print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed") - print_dns_set('Deleted entries', removed) + print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed") + print_dns_set('Deleted entries', removed, ids: [nil] * removed.length) + 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 #3" + print_line " dns remove -i 3" + print_line + print_line " Delete multiple rules in one command" + print_line " dns remove -i 3 -i 4 -i 5" + print_line + end + + def remove_static_dns(*args) + if args.length < 1 + raise ::ArgumentError.new('A hostname must be provided') + end + + hostname = args.shift + 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? + 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 + + ip_addresses.each do |ip_address| + resolver.static_hostnames.delete(hostname, ip_address) + print_status("Removed static hostname mapping #{hostname} to #{ip_address}") + end + end + + def remove_static_dns_help + print_line "USAGE:" + print_line " #{REMOVE_STATIC_USAGE}" + print_line + print_line "EXAMPLES:" + print_line " Remove all IPv4 and IPv6 addresses for 'localhost'" + print_line " dns remove-static localhost" + 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 + return unless response =~ /^y/i + 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.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 + 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 + # + 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.flush + 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 # 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_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 + print_line("Current cache size: #{resolver.cache.records.length}") - # Default nameservers don't include a rule - columns = ['ID', 'DNS Server', 'Comm channel'] - print_dns_set('Default nameservers', results[1]) + 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 - print_line('No custom DNS nameserver entries configured') if results[0].length + results[1].length == 0 + tbl = Table.new( + Table::Style::Default, + 'Header' => 'Static hostnames', + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => ['Hostname', 'IPv4 Address', 'IPv6 Address'], + 'SortIndex' => -1, + 'WordWrap' => false + ) + resolver.static_hostnames.each do |hostname, addresses| + 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? + print_line('No static hostname entries are configured') + end end private + SPECIAL_RESOLVERS = [ + 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: + # 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 # - def prettify_comm(comm, dns_server) - if comm.nil? - channel = Rex::Socket::SwitchBoard.best_comm(dns_server) + def prettify_comm(comm, upstream_resolver) + if !Rex::Socket.is_ip_addr?(upstream_resolver.destination) + 'N/A' + elsif comm.nil? + channel = Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination) if channel.nil? nil else @@ -297,32 +682,40 @@ def prettify_comm(comm, dns_server) end end - def print_dns_set(heading, result_set) + def print_dns_set(heading, result_set, ids: []) return if result_set.length == 0 - if result_set[0][:wildcard_rules].any? - columns = ['ID', 'Rules(s)', 'DNS Server', 'Comm channel'] - else - columns = ['ID', 'DNS Server', 'Commm channel'] - end + columns = ['#', 'Rule', 'Resolver', 'Comm channel'] tbl = Table.new( - Table::Style::Default, - 'Header' => heading, - 'Prefix' => "\n", - 'Postfix' => "\n", - 'Columns' => columns - ) - result_set.each do |hash| - if columns.size == 4 - tbl << [hash[:id], hash[:wildcard_rules].join(','), hash[:dns_server], prettify_comm(hash[:comm], hash[:dns_server])] - else - tbl << [hash[:id], hash[:dns_server], prettify_comm(hash[:comm], hash[:dns_server])] - end + Table::Style::Default, + 'Header' => heading, + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => columns, + 'SortIndex' => -1, + 'WordWrap' => false + ) + 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: [], index: nil) + alignment_prefix = prefix.empty? ? [] : (['.'] * prefix.length) + + 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 + tbl << prefix + [index.to_s, entry.wildcard, '', ''] + suffix + entry.resolvers.each do |resolver| + tbl << alignment_prefix + ['.', TABLE_INDENT, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length) + end + end + tbl + end + def resolver self.driver.framework.dns_resolver end @@ -331,4 +724,4 @@ def resolver end end end -end \ No newline at end of file +end diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index 9085b7625abf..0d0218c9f252 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -85,7 +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.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/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 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/cache.rb b/lib/rex/proto/dns/cache.rb index d5dffff25d4f..7deb6a3d62ca 100644 --- a/lib/rex/proto/dns/cache.rb +++ b/lib/rex/proto/dns/cache.rb @@ -57,22 +57,11 @@ def cache_record(record) 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 + # 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 # 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/custom_nameserver_provider.rb b/lib/rex/proto/dns/custom_nameserver_provider.rb index bab8c55390dc..b8c3aeff1c10 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 @@ -7,11 +9,12 @@ module DNS # for different requests, based on the domain being queried. ## module CustomNameserverProvider - CONFIG_KEY = 'framework/dns' + CONFIG_KEY_BASE = 'framework/dns' + CONFIG_VERSION = Rex::Version.new('1.0') # # 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 @@ -31,128 +34,120 @@ def sid end def init - self.entries_with_rules = [] - self.entries_without_rules = [] - self.next_id = 0 - end + @upstream_rules = [] - # - # Save the custom settings to the MSF config file - # - def save_config - new_config = {} - [self.entries_with_rules, self.entries_without_rules].each do |entry_set| - entry_set.each do |entry| - key = entry[:id].to_s - val = [entry[:wildcard_rules].join(','), - entry[:dns_server], - (!entry[:comm].nil?).to_s - ].join(';') - new_config[key] = val - end + resolvers = [UpstreamResolver.create_static] + if @config[:nameservers].empty? + # if no nameservers are specified, fallback to the system + resolvers << UpstreamResolver.create_system + else + # migrate the originally configured name servers + resolvers += @config[:nameservers].map(&:to_s) + @config[:nameservers].clear end - Msf::Config.save(CONFIG_KEY => new_config) + add_upstream_rule(resolvers) + + nil end - # - # Load the custom settings from the MSF config file - # - def load_config - config = Msf::Config.load + # Reinitialize the configuration to its original state. + def reinit + parse_config_file + parse_environment_variables - with_rules = [] - without_rules = [] - next_id = 0 + self.static_hostnames.flush + self.static_hostnames.parse_hosts_file - 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(',') + init - 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)} + cache.flush if respond_to?(:cache) - comm = uses_comm == 'true' ? CommSink.new : nil - entry = { - :wildcard_rules => wildcard_rules, - :dns_server => dns_server, - :comm => comm, - :id => id - } - - if wildcard_rules.empty? - without_rules << entry - else - with_rules << entry - end + nil + end - next_id = [id + 1, next_id].max + # Check whether or not there is configuration data in Metasploit's configuration file which is persisted on disk. + def has_config? + 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 - # Now that config has successfully read, update the global values - self.entries_with_rules = with_rules - self.entries_without_rules = without_rules - self.next_id = next_id - end - - # Add a custom nameserver entry to the custom provider - # @param wildcard_rules [Array] 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 comm [Msf::Session::Comm] The communication channel to be used for these DNS requests - def add_nameserver(wildcard_rules, dns_server, comm) - raise ::ArgumentError.new("Invalid DNS server: #{dns_server}") unless Rex::Socket.is_ip_addr?(dns_server) - wildcard_rules.each do |rule| - raise ::ArgumentError.new("Invalid rule: #{rule}") unless valid_rule?(rule) - end - - entry = { - :wildcard_rules => wildcard_rules, - :dns_server => dns_server, - :comm => comm, - :id => self.next_id - } - self.next_id += 1 - if wildcard_rules.empty? - entries_without_rules << entry - else - entries_with_rules << entry + 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 # - # Remove entries with the given IDs - # Ignore entries that are not found - # @param ids [Array] The IDs to removed - # @return [Array] The removed entries + # Save the custom settings to the MSF config file # - 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) + def save_config + new_config = { + 'configuration_version' => CONFIG_VERSION.to_s + } + Msf::Config.save(CONFIG_KEY_BASE => new_config) - removed_without, remaining_without = self.entries_without_rules.partition {|entry| entry[:id] == id} - self.entries_without_rules.replace(remaining_without) + save_config_upstream_rules + save_config_static_hostnames + end - removed.concat(removed_with) - removed.concat(removed_without) + # + # 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 - removed + load_config_entries + load_config_static_hostnames end + # Add a custom nameserver entry to the custom provider. # - # The custom nameserver entries that have been configured - # @return [Array] An array containing two elements: The entries with rules, and the entries without rules + # @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_rules.insert(index, UpstreamRule.new( + wildcard: wildcard, + resolvers: resolvers, + comm: comm + )) + end + # - def nameserver_entries - [entries_with_rules, entries_without_rules] + # 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| + upstream_rule = @upstream_rules.delete_at(id) + removed << upstream_rule if upstream_rule + end + + removed.reverse end - def purge - init + def flush + @upstream_rules.clear end # The nameservers that match the given packet @@ -160,7 +155,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 +166,15 @@ def nameservers_for_packet(packet) results_from_all_questions = [] packet.question.each do |question| name = question.qname.to_s - dns_servers = [] - - self.entries_with_rules.each do |entry| - entry[:wildcard_rules].each do |rule| - if matches(name, rule) - socket_options = {} - socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? - dns_servers.append([entry[:dns_server], socket_options]) - break - end - end - end - - # Only look at the rule-less entries if no rules were found (avoids DNS leaks) - if dns_servers.empty? - self.entries_without_rules.each do |entry| - socket_options = {} - socket_options['Comm'] = entry[:comm] unless entry[:comm].nil? - dns_servers.append([entry[:dns_server], socket_options]) - end - end + upstream_rule = self.upstream_rules.find { |ur| ur.matches_name?(name) } - if dns_servers.empty? + if upstream_rule + upstream_resolvers = upstream_rule.resolvers + else # 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 @@ -215,28 +192,85 @@ def set_framework(framework) self.feature_set = framework.features end + def upstream_rules + @upstream_rules.dup + 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]+$/ + + 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_rules = with_rules end + def load_config_static_hostnames + config = Msf::Config.load - def matches(domain, pattern) - if pattern.start_with?('*.') - domain.downcase.end_with?(pattern[1..-1].downcase) - else - domain.casecmp?(pattern) + static_hostnames.flush + config.fetch("#{CONFIG_KEY_BASE}/static_hostnames", {}).each do |_name, value| + 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) + 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_upstream_rules + new_config = {} + @upstream_rules.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}/upstream_rules" => new_config) + end + + def save_config_static_hostnames + new_config = {} + static_hostnames.each_with_index do |(hostname, addresses), index| + val = [ + hostname, + (addresses.fetch(Dnsruby::Types::A, []) + addresses.fetch(Dnsruby::Types::AAAA, [])).join(',') + ].join(';') + new_config["##{index}"] = val + end + Msf::Config.save("#{CONFIG_KEY_BASE}/static_hostnames" => new_config) end - attr_accessor :entries_with_rules # Set of custom nameserver entries that specify a rule - attr_accessor :entries_without_rules # Set of custom nameserver entries that do not include a rule - attr_accessor :next_id # The next ID to have been allocated to an entry attr_accessor :feature_set end end end -end \ No newline at end of file +end 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 67dacd2d808c..3a3f0211cfe5 100644 --- a/lib/rex/proto/dns/resolver.rb +++ b/lib/rex/proto/dns/resolver.rb @@ -14,11 +14,11 @@ 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 => [], - :nameservers => [IPAddr.new("127.0.0.1")], + :nameservers => [], :domain => "", :source_port => 0, :source_address => IPAddr.new("0.0.0.0"), @@ -30,21 +30,23 @@ 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 + :comm => nil, + :static_hosts => {} } - attr_accessor :context, :comm + attr_accessor :context, :comm, :static_hostnames # # 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]) @@ -58,8 +60,6 @@ def initialize(config = {}) # 4) defaults (and /etc/resolv.conf for config) #------------------------------------------------------------ - - #------------------------------------------------------------ # Parsing config file #------------------------------------------------------------ @@ -74,7 +74,8 @@ def initialize(config = {}) # 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 @@ -83,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 @@ -115,8 +118,18 @@ 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.create_dns_server(ns.to_s) + 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) + 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 # @@ -128,8 +141,6 @@ def nameservers_for_packet(_dns_message) # @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 @@ -141,46 +152,33 @@ 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) - if nameservers.size == 0 - raise ResolverError, "No nameservers specified!" - end - # Store packet_data for performance improvements, - # so methods don't keep on calling Packet#encode - packet_data = packet.encode - packet_size = packet_data.size + upstream_resolvers = upstream_resolvers_for_packet(packet) + if upstream_resolvers.empty? + raise ResolverError, "No upstream resolvers specified!" + end - # 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::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 - 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]) @@ -386,28 +384,135 @@ 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) + name, type, cls = preprocess_query_arguments(name, type, cls) + @logger.debug "Query(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})" + send(name,type,cls) + 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 - return send(name,type,cls) if name.class == IPAddr + 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 - @logger.debug "Query(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(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, + # so methods don't keep on calling Packet#encode + packet_data = packet.encode + packet_size = packet_data.size - return send(name,type,cls) + # 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 - private + 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 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 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 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. + 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 + 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 + + ip_addresses = nil + begin + 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) + ip_addresses.each do |ip_address| + message.add_answer(Dnsruby::RR.new_from_hash( + name: name, + type: type, + ttl: 0, + address: ip_address.to_s + )) + 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 diff --git a/lib/rex/proto/dns/static_hostnames.rb b/lib/rex/proto/dns/static_hostnames.rb new file mode 100644 index 000000000000..4b43771b616b --- /dev/null +++ b/lib/rex/proto/dns/static_hostnames.rb @@ -0,0 +1,151 @@ +# -*- coding: binary -*- + +require 'rex/socket' +require 'forwardable' + +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 + hostnames.each do |hostname, ip_address| + add(hostname, ip_address) + end + 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 + /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) + ::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| + add(hostname, ip_address) + end + end + end + + # 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, []).dup + 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) + 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 + this_host = @hostnames.fetch(hostname, {}) + if ip_address.family == ::Socket::AF_INET + type = Dnsruby::Types::A + else + type = Dnsruby::Types::AAAA + end + 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 [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 ip_address.is_a?(String) && 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 + 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] = this_host + end + + nil + end + + # Delete all hostname to IP address definitions. + 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 +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..c45cd3db8a6a --- /dev/null +++ b/lib/rex/proto/dns/upstream_resolver.rb @@ -0,0 +1,76 @@ +# -*- coding: binary -*- + +module Rex +module Proto +module DNS + ## + # This represents a single upstream DNS resolver target of one of the predefined types. + ## + class UpstreamResolver + module Type + BLACK_HOLE = :"black-hole" + DNS_SERVER = :"dns-server" + STATIC = :static + SYSTEM = :system + end + + 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.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.create_dns_server(destination, socket_options: {}) + self.new( + Type::DNS_SERVER, + destination: destination, + socket_options: socket_options + ) + end + + # Initialize a new static resolver. + def self.create_static + self.new(Type::STATIC) + end + + # Initialize a new system resolver. + def self.create_system + self.new(Type::SYSTEM) + end + + def to_s + if type == Type::DNS_SERVER + destination.to_s + else + type.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 +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..349ae3f6e76c --- /dev/null +++ b/lib/rex/proto/dns/upstream_rule.rb @@ -0,0 +1,104 @@ +# -*- coding: binary -*- + +require 'json' +require 'rex/socket' + +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 + 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::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.create_dns_server(resolver, socket_options: socket_options) + else + raise ::ArgumentError.new("Invalid upstream DNS resolver: #{resolver}") + end + end + end + @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) + + resolver = resolver.downcase.to_sym + [ + UpstreamResolver::Type::BLACK_HOLE, + UpstreamResolver::Type::STATIC, + UpstreamResolver::Type::SYSTEM + ].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 + elsif wildcard.start_with?('*.') + name.downcase.end_with?(wildcard[1..-1].downcase) + else + 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 +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..8935b519d2a5 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,33 @@ 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.nameservers = [default_nameserver] dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider) - dns_resolver.nameservers = [base_nameserver] + dns_resolver.add_upstream_rule([metasploit_nameserver], wildcard: '*.metasploit.com', index: 0) 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.create_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.create_static, + Rex::Proto::DNS::UpstreamResolver.create_dns_server(default_nameserver) + ]) + end end -end \ No newline at end of file +end 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 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..e313ce75babb --- /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.create_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.create_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.create_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.create_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..48aec5bb4e75 --- /dev/null +++ b/spec/lib/rex/proto/dns/upstream_rule_spec.rb @@ -0,0 +1,90 @@ +# -*- 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') } + + describe '#matches_all?' do + it 'does not return true for everything' do + expect(subject.matches_all?).to be_falsey + end + end + + describe '#matches_name?' do + 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 + end + + context 'when not using a wildcard condition' do + let(:subject) { described_class.new } + + 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 + + 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