diff --git a/lib/msf/core/post/dns.rb b/lib/msf/core/post/dns.rb new file mode 100644 index 000000000000..3034a9f84761 --- /dev/null +++ b/lib/msf/core/post/dns.rb @@ -0,0 +1,4 @@ +# -*- coding: binary -*- + +module Msf::Post::DNS +end diff --git a/lib/msf/core/post/dns/resolve_host.rb b/lib/msf/core/post/dns/resolve_host.rb new file mode 100644 index 000000000000..d2cbe6b1a601 --- /dev/null +++ b/lib/msf/core/post/dns/resolve_host.rb @@ -0,0 +1,72 @@ +# -*- coding: binary -*- + +require 'rex/post/meterpreter/extensions/stdapi/constants' + +module Msf + class Post + module DNS + ### + # + # This module resolves session DNS + # + ### + module ResolveHost + # Takes the host name and resolves the IP + # + # @param [String] host + # @param [Integer] family AF_INET for IPV4 and AF_INET6 for IPV6 + # @return [Hash] The resolved IPs + def resolve_host(host, family) + if client.respond_to?(:net) && client.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_NET_RESOLVE_HOST) + resolved_host = client.net.resolve.resolve_host(host, family) + resolved_host.reject { |k, _v| k == :ip } + else + ips = [] + data = cmd_exec("nslookup #{host}") + if data =~ /Name/ + # Remove unnecessary data and get the section with the addresses + returned_data = data.split(/Name:/)[1..] + # check each element of the array to see if they are IP + returned_data.each do |entry| + ip_list = entry.gsub(/\r\n\t |\r\n|Aliases:|Addresses:|Address:/, ' ').split(' ') - [host] + filtered_ips = filter_ips(ip_list, family) + ips = filtered_ips unless filtered_ips.empty? + end + # If nslookup responds with "no answer", fall back to resolving via host command + elsif data =~ /No answer/ + data = cmd_exec("host #{host}") + if data =~ /has address/ + # Remove unnecessary data and get the section with the addresses + returned_data = data.split("\n")[...-1] + # check each element of the array to see if they are IP + ip_list = returned_data.map { |entry| entry.split(' ').last } + filtered_ips = filter_ips(ip_list, family) + ips = filtered_ips unless filtered_ips.empty? + end + end + { hostname: host, ips: ips } + end + end + + # Takes the host and family and returns the IP address if it matches the appropriate family + # Needed to handle request that fallback to nslookup or host, as they return both IPV4 and IPV6. + # + # @param [Array] ips + # @param [Integer] family + # @return [Array] ips + def filter_ips(ips, family) + filtered_ips = [] + ips.each do |ip| + if family == AF_INET + filtered_ips << ip if !!(ip =~ Resolv::IPv4::Regex) + elsif family == AF_INET6 + filtered_ips << ip if !!(ip =~ Resolv::IPv6::Regex) + end + end + + filtered_ips + end + end + end + end +end diff --git a/modules/post/windows/gather/enum_computers.rb b/modules/post/windows/gather/enum_computers.rb index 936cbdb840b4..96dde66b3d07 100644 --- a/modules/post/windows/gather/enum_computers.rb +++ b/modules/post/windows/gather/enum_computers.rb @@ -3,10 +3,13 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +require 'rex/post/meterpreter/extensions/stdapi/constants' + class MetasploitModule < Msf::Post include Msf::Post::File include Msf::Post::Windows::Accounts include Msf::Post::Windows::Registry + include Msf::Post::DNS::ResolveHost def initialize(info = {}) super( @@ -58,36 +61,19 @@ def run # Takes the host name and makes use of nslookup to resolve the IP # - # @param [String] host Hostname + # @param [Object] hostname + # @param [Object] family # @return [String] ip The resolved IP - def resolve_host(host) - vprint_status("Looking up IP for #{host}") - return host if Rex::Socket.dotted_ip?(host) - - ip = [] - data = cmd_exec("nslookup #{host}") - if data =~ /Name/ - # Remove unnecessary data and get the section with the addresses - returned_data = data.split(/Name:/)[1] - # check each element of the array to see if they are IP - returned_data.gsub(/\r\n\t |\r\n|Aliases:|Addresses:|Address:/, ' ').split(' ').each do |e| - if Rex::Socket.dotted_ip?(e) - ip << e - end - end - end - - if ip.blank? - 'Not resolvable' - else - ip.join(', ') - end + def gethost(hostname, family) + ## get IP for host + vprint_status("Looking up IP for #{hostname}") + resolve_host(hostname, family) end def get_domain_computers computer_list = [] divisor = "-------------------------------------------------------------------------------\r\n" - net_view_response = cmd_exec('net view') + net_view_response = cmd_exec("cmd.exe", "/c net view") unless net_view_response.include?(divisor) print_error("The net view command failed with: #{net_view_response}") return [] @@ -104,6 +90,7 @@ def get_domain_computers end def list_computers(domain, hosts) + meterpreter_dns_resolving_errors = [] tbl = Rex::Text::Table.new( 'Header' => 'List of identified Hosts.', 'Indent' => 1, @@ -115,12 +102,28 @@ def list_computers(domain, hosts) ] ) hosts.each do |hostname| - hostip = resolve_host(hostname) - tbl << [domain, hostname, hostip] + hostipv4 = gethost(hostname, AF_INET) + hostipv6 = gethost(hostname, AF_INET6) + + if hostipv4[:ips].empty? + meterpreter_dns_resolving_errors << "IPV4: #{hostname} could not be resolved" + else + tbl << [domain, hostname, hostipv4[:ips].join(',')] + end + + if hostipv6[:ips].empty? + meterpreter_dns_resolving_errors << "IPV6: #{hostname} could not be resolved" if hostipv6[:ips].empty? + else + tbl << [domain, hostname, hostipv6[:ips].join(',')] unless hostipv6[:ips].nil? + end end print_line("\n#{tbl}\n") + meterpreter_dns_resolving_errors.each do | error | + print_warning(error) + end + report_note( host: session, type: 'domain.hosts', diff --git a/spec/support/acceptance/command_shell/cmd.rb b/spec/support/acceptance/command_shell/cmd.rb index 8f9fca7f68cf..84bda40b88c7 100644 --- a/spec/support/acceptance/command_shell/cmd.rb +++ b/spec/support/acceptance/command_shell/cmd.rb @@ -17,6 +17,38 @@ module Acceptance::Session } ], module_tests: [ + { + name: 'post/test/resolve_host', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: 'post/test/cmd_exec', platforms: [ diff --git a/spec/support/acceptance/command_shell/linux.rb b/spec/support/acceptance/command_shell/linux.rb index 3eb40744ec55..8a5cc2dafc14 100644 --- a/spec/support/acceptance/command_shell/linux.rb +++ b/spec/support/acceptance/command_shell/linux.rb @@ -49,6 +49,32 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Payload not compiled for platform" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: "post/test/cmd_exec", platforms: [ diff --git a/spec/support/acceptance/command_shell/powershell.rb b/spec/support/acceptance/command_shell/powershell.rb index f941057eccd4..493edb7aa75a 100644 --- a/spec/support/acceptance/command_shell/powershell.rb +++ b/spec/support/acceptance/command_shell/powershell.rb @@ -17,6 +17,38 @@ module Acceptance::Session } ], module_tests: [ + { + name: 'post/test/resolve_host', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: 'post/test/cmd_exec', platforms: [ diff --git a/spec/support/acceptance/session/java.rb b/spec/support/acceptance/session/java.rb index 029ce6e672f0..c92c1f1dd7b7 100644 --- a/spec/support/acceptance/session/java.rb +++ b/spec/support/acceptance/session/java.rb @@ -73,6 +73,22 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [:linux, :osx, :windows], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: "post/test/cmd_exec", platforms: [:linux, :osx, :windows], diff --git a/spec/support/acceptance/session/mettle.rb b/spec/support/acceptance/session/mettle.rb index 182a0d370610..2012d15e0466 100644 --- a/spec/support/acceptance/session/mettle.rb +++ b/spec/support/acceptance/session/mettle.rb @@ -1,3 +1,4 @@ + module Acceptance::Session METTLE_METERPRETER = { payloads: [ @@ -69,6 +70,32 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Payload not compiled for platform" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: "post/test/cmd_exec", platforms: [ diff --git a/spec/support/acceptance/session/php.rb b/spec/support/acceptance/session/php.rb index 0cc3fb666858..a608da234b80 100644 --- a/spec/support/acceptance/session/php.rb +++ b/spec/support/acceptance/session/php.rb @@ -60,6 +60,23 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [:linux, :osx, :windows], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [ + ] + } + } + }, { name: "post/test/cmd_exec", platforms: [:linux, :osx, :windows], diff --git a/spec/support/acceptance/session/python.rb b/spec/support/acceptance/session/python.rb index eb2870c0eb7c..c93fc4bb37d7 100644 --- a/spec/support/acceptance/session/python.rb +++ b/spec/support/acceptance/session/python.rb @@ -72,6 +72,22 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [:linux, :osx, :windows], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: "post/test/cmd_exec", platforms: [:linux, :osx, :windows], diff --git a/spec/support/acceptance/session/windows_meterpreter.rb b/spec/support/acceptance/session/windows_meterpreter.rb index 7784ab66d2fa..a28d2c8bd74a 100644 --- a/spec/support/acceptance/session/windows_meterpreter.rb +++ b/spec/support/acceptance/session/windows_meterpreter.rb @@ -53,6 +53,38 @@ module Acceptance::Session } } }, + { + name: "post/test/resolve_host", + platforms: [ + [ + :linux, + { + skip: true, + reason: "Payload not compiled for platform" + } + ], + [ + :osx, + { + skip: true, + reason: "Payload not compiled for platform" + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, { name: "post/test/cmd_exec", platforms: [ diff --git a/test/modules/post/test/resolve_host.rb b/test/modules/post/test/resolve_host.rb new file mode 100644 index 000000000000..544399587627 --- /dev/null +++ b/test/modules/post/test/resolve_host.rb @@ -0,0 +1,90 @@ +require 'rex' +require 'rex/post/meterpreter/extensions/stdapi/constants' + +lib = File.join(Msf::Config.install_root, 'test', 'lib') +$LOAD_PATH.push(lib) unless $LOAD_PATH.include?(lib) +require 'module_test' + +class MetasploitModule < Msf::Post + include Msf::ModuleTest::PostTest + include Msf::Post::DNS::ResolveHost + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Meterpreter resolve_host test', + 'Description' => %q{ This module will test the meterpreter resolve_host API }, + 'License' => MSF_LICENSE, + 'Platform' => [ 'windows', 'linux', 'unix', 'java', 'osx' ], + 'SessionTypes' => ['meterpreter', 'shell', 'powershell'] + ) + ) + end + + def test_resolve_host + vprint_status('Starting resolve_host tests') + + it 'should return a Hash' do + hostname = 'localhost' + family = AF_INET6 + + resolved_host = resolve_host(hostname, family) + resolved_host.is_a?(Hash) + end + + it 'should return a valid IPV4 host' do + hostname = 'localhost' + family = AF_INET + + resolved_host = resolve_host(hostname, family) + if resolved_host[:ips].empty? + false + else + matches = resolved_host[:ips].map do |ip| + !!(ip =~ Resolv::IPv4::Regex) + end + + matches.all?(true) + end + end + + it 'should return a valid IPV6 host' do + hostname = 'localhost' + family = AF_INET6 + + resolved_host = resolve_host(hostname, family) + if resolved_host[:ips].empty? + false + else + matches = resolved_host[:ips].map do |ip| + !!(ip =~ Resolv::IPv6::Regex) + end + + matches.all?(true) + end + end + + it 'should handle an invalid IPV4 host' do + hostname = 'foo.bar' + family = AF_INET + + begin + resolve_host(hostname, family) + rescue Rex::Post::Meterpreter::RequestError => e + e.instance_of?(Rex::Post::Meterpreter::RequestError) + end + end + + it 'should handle an invalid IPV6 host' do + hostname = 'foo.bar' + family = AF_INET6 + + begin + resolve_host(hostname, family) + rescue Rex::Post::Meterpreter::RequestError => e + e.instance_of?(Rex::Post::Meterpreter::RequestError) + end + end + end +end