diff --git a/lib/msf/core/exploit/remote/smb/client/remote_paths.rb b/lib/msf/core/exploit/remote/smb/client/remote_paths.rb index 59912adabf..53feebe462 100644 --- a/lib/msf/core/exploit/remote/smb/client/remote_paths.rb +++ b/lib/msf/core/exploit/remote/smb/client/remote_paths.rb @@ -13,9 +13,10 @@ module Exploit::Remote::SMB::Client::RemotePaths end def setup - unless (datastore['FILE_RPATHS'] && !datastore['RPATH']) || (!datastore['FILE_RPATHS'] && datastore['RPATH']) - fail_with(::Msf::Module::Failure::BadConfig, 'One and only one of FILE_RPATHS or RPATH must be specified') - end + # rpath can be set individually now for each RHOST value that's walked over. This is only really an issue for scanners. + # unless (datastore['FILE_RPATHS'] && !datastore['RPATH']) || (!datastore['FILE_RPATHS'] && datastore['RPATH']) + # fail_with(::Msf::Module::Failure::BadConfig, 'One and only one of FILE_RPATHS or RPATH must be specified') + # end end def remote_paths diff --git a/lib/msf/core/option_container.rb b/lib/msf/core/option_container.rb index dca0dc71d3..5bc1710643 100644 --- a/lib/msf/core/option_container.rb +++ b/lib/msf/core/option_container.rb @@ -192,32 +192,60 @@ module Msf # # Make sures that each of the options has a value of a compatible # format and that all the required options are set. - # + # TODO: Decide if this lives here, or if the scanner is the real smell here def validate(datastore) - errors = [] + if include?('RHOSTS') + Msf::RhostsWalker.new(self['RHOSTS'], datastore).each do |datastore| + errors = [] + each_pair { |name, option| + if (!option.valid?(datastore[name])) + errors << name + # If the option is valid, normalize its format to the correct type. + elsif ((val = option.normalize(datastore[name])) != nil) + # This *will* result in a module that previously used the + # global datastore to have its local datastore set, which + # means that changing the global datastore and re-running + # the same module will now use the newly-normalized local + # datastore value instead. This is mostly mitigated by + # forcing a clone through mod.replicant, but can break + # things in corner cases. + # TODO: Decide if this side effect lives here + datastore[name] = val + end + } - each_pair { |name, option| - if (!option.valid?(datastore[name])) - errors << name - # If the option is valid, normalize its format to the correct type. - elsif ((val = option.normalize(datastore[name])) != nil) - # This *will* result in a module that previously used the - # global datastore to have its local datastore set, which - # means that changing the global datastore and re-running - # the same module will now use the newly-normalized local - # datastore value instead. This is mostly mitigated by - # forcing a clone through mod.replicant, but can break - # things in corner cases. - datastore[name] = val + if errors.empty? == false + raise Msf::OptionValidateError.new(errors), + # TODO: It would be great to have the real value, i.e. 'Target http://www.example.com/' + "Target #{datastore['RHOSTS']} has one or more options failed to validate: #{errors.join(', ')}." + end end - } + else + errors = [] + each_pair { |name, option| + if (!option.valid?(datastore[name])) + errors << name + # If the option is valid, normalize its format to the correct type. + elsif ((val = option.normalize(datastore[name])) != nil) + # This *will* result in a module that previously used the + # global datastore to have its local datastore set, which + # means that changing the global datastore and re-running + # the same module will now use the newly-normalized local + # datastore value instead. This is mostly mitigated by + # forcing a clone through mod.replicant, but can break + # things in corner cases. + # TODO: Decide if this side effect lives here + datastore[name] = val + end + } - if (errors.empty? == false) - raise Msf::OptionValidateError.new(errors), - "One or more options failed to validate: #{errors.join(', ')}.", caller + if errors.empty? == false + raise Msf::OptionValidateError.new(errors), + "One or more options failed to validate: #{errors.join(', ')}." + end end - return true + true end # diff --git a/lib/msf/core/rhosts_walker.rb b/lib/msf/core/rhosts_walker.rb index 3b7f760e38..777323f417 100644 --- a/lib/msf/core/rhosts_walker.rb +++ b/lib/msf/core/rhosts_walker.rb @@ -83,6 +83,7 @@ module Msf # Parses the input rhosts string, and yields the possible combinations of datastore values. # # @param value [String] the http string + # @param datastore [Msf::Datastore] the datastore # @return [Enumerable] The calculated datastore values that can be iterated over for # enumerating the given rhosts, or the error that occurred when iterating over the input def parse(input, datastore) @@ -96,6 +97,11 @@ module Msf results << result end end + elsif value.start_with?('smb:') + smb_options = parse_smb_uri(value, datastore) + Rex::Socket::RangeWalker.new(smb_options['RHOSTS']).each_ip do |ip| + results << datastore.merge(smb_options.merge('RHOSTS' => ip)) + end elsif value.start_with?('http:') || value.start_with?('https:') http_options = parse_http_uri(value, datastore) Rex::Socket::RangeWalker.new(http_options['RHOSTS']).each_ip do |ip| @@ -125,6 +131,47 @@ module Msf end end + # Parses an smb string such as smb://domain;user:pass@domain/share_name/file.txt into a hash which can safely be + # merged with a [Msf::DataStore] datastore for setting smb options. + # + # @param value [String] the http string + # @return [Hash] A hash where keys match the required datastore options associated with + # the http uri value + def parse_smb_uri(value, datastore) + uri = URI.parse(value) + result = {} + + result['RHOSTS'] = uri.hostname + result['RPORT'] = (uri.port || 445) if datastore.options.include?('RPORT') + + # Handle users in the format: + # user + # domain;user + if uri.user && uri.user.include?(';') + domain, user = uri.user.split(';') + result['SMBDomain'] = domain + result['SMBUser'] = user + elsif uri.user + result['SMBUser'] = uri.user + end + if uri.password + result['SMBPass'] = uri.password + end + + # Handle paths of the format: + # / + # /share_name + # /share_name/file + # /share_name/dir/file + if uri.path + _preceding_slash, share, *rpath = uri.path.split('/') + result['SMBSHARE'] = share if datastore.options.include?('SMBSHARE') + result['RPATH'] = rpath.join('/') if datastore.options.include?('RPATH') + end + + result + end + # Parses an http string such as http://example.com into a hash which can safely be # merged with a [Msf::DataStore] datastore for setting http options. # @@ -146,11 +193,9 @@ module Msf result['TARGETURI'] = target_uri if datastore.options.include?('TARGETURI') result['URI'] = target_uri if datastore.options.include?('URI') - if uri.scheme && %(http https).include?(uri.scheme) - result['VHOST'] = uri.hostname unless Rex::Socket.is_ip_addr?(uri.hostname) - result['HttpUsername'] = uri.user.to_s - result['HttpPassword'] = uri.password.to_s - end + result['VHOST'] = uri.hostname unless Rex::Socket.is_ip_addr?(uri.hostname) + result['HttpUsername'] = uri.user if uri.user + result['HttpPassword'] = uri.password if uri.password result end diff --git a/spec/lib/msf/core/rhosts_walker_spec.rb b/spec/lib/msf/core/rhosts_walker_spec.rb index ed35010982..26be77784d 100644 --- a/spec/lib/msf/core/rhosts_walker_spec.rb +++ b/spec/lib/msf/core/rhosts_walker_spec.rb @@ -16,18 +16,12 @@ RSpec::Matchers.define :have_datastore_values do |expected| end def http_options_for(datastores) + http_keys = %w[RHOSTS RPORT VHOST SSL HttpUsername HttpPassword TARGETURI URI] + smb_keys = %w[RHOSTS RPORT SMBDomain SMBUser SMBPass SMBSHARE RPATH] + required_keys = http_keys + smb_keys datastores.map do |datastore| - # Slice the datastore options we care about, ignoring other values that just add noise such as VERBOSE/WORKSPACE/etc. - datastore.to_h.slice( - 'RHOSTS', - 'RPORT', - 'VHOST', - 'SSL', - 'HttpUsername', - 'HttpPassword', - 'TARGETURI', - 'URI' - ) + # Slice the datastore options that we care about, ignoring other values that just add noise such as VERBOSE/WORKSPACE/etc. + datastore.to_h.slice(*required_keys) end end end @@ -111,6 +105,65 @@ RSpec.describe Msf::RhostsWalker do mod end + let(:smb_scanner_mod) do + mod_klass = Class.new(Msf::Auxiliary) do + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB::Client + + # Scanner mixin should be near last + include Msf::Auxiliary::Scanner + include Msf::Auxiliary::Report + + def initialize + super( + 'Name' => 'mock smb module', + 'Description' => 'mock smb module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE + ) + + deregister_options('RPORT') + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(nil) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + mod + end + + let(:smb_share_mod) do + mod_klass = Class.new(Msf::Auxiliary) do + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB::Client + include Msf::Exploit::Remote::SMB::Client::RemotePaths + + def initialize + super( + 'Name' => 'mock smb share module', + 'Description' => 'mock smb share module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Msf::OptString.new('SMBShare', [true, 'Target share', '']), + ] + ) + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(nil) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + mod + end + def each_host_for(mod) described_class.new(mod.datastore['RHOSTS'], mod.datastore).to_enum end @@ -295,6 +348,27 @@ RSpec.describe Msf::RhostsWalker do expect(each_host_for(http_mod)).to have_datastore_values(expected) end + it 'enumerates http values with user/passwords' do + http_mod.datastore.import_options( + Msf::OptionContainer.new( + [ + Msf::OptString.new('HttpUsername', [true, 'The username to authenticate as', 'admin']), + Msf::OptString.new('HttpPassword', [true, 'The password for the specified username', 'admin']) + ] + ), + http_mod.class, + true + ) + http_mod.datastore['RHOSTS'] = 'http://example.com/ http://user@example.com/ http://user:password@example.com http://:@example.com' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'RPORT' => 80, 'VHOST' => 'example.com', 'SSL' => false, 'HttpUsername' => 'admin', 'HttpPassword' => 'admin', 'TARGETURI' => '/' }, + { 'RHOSTS' => '192.0.2.2', 'RPORT' => 80, 'VHOST' => 'example.com', 'SSL' => false, 'HttpUsername' => 'user', 'HttpPassword' => 'admin', 'TARGETURI' => '/' }, + { 'RHOSTS' => '192.0.2.2', 'RPORT' => 80, 'VHOST' => 'example.com', 'SSL' => false, 'HttpUsername' => 'user', 'HttpPassword' => 'password', 'TARGETURI' => '/' }, + { 'RHOSTS' => '192.0.2.2', 'RPORT' => 80, 'VHOST' => 'example.com', 'SSL' => false, 'HttpUsername' => '', 'HttpPassword' => '', 'TARGETURI' => '/' } + ] + expect(each_host_for(http_mod)).to have_datastore_values(expected) + end + it 'enumerates a cidr scheme with a single http value' do http_mod.datastore['RHOSTS'] = 'cidr:/30:http://127.0.0.1:3000/foo/bar' expected = [ @@ -407,8 +481,88 @@ RSpec.describe Msf::RhostsWalker do expect(each_host_for(http_mod)).to have_datastore_values(expected) end + context 'when using the smb scheme' do + it 'enumerates smb schemes for scanners when no user or password are specified' do + smb_scanner_mod.datastore['RHOSTS'] = 'smb://example.com/' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBUser' => '', 'SMBPass' => '' } + ] + expect(each_host_for(smb_scanner_mod)).to have_datastore_values(expected) + end + + it 'enumerates smb schemes for scanners when no user or password are specified and uses the default option values instead' do + smb_scanner_mod.datastore.import_options( + Msf::OptionContainer.new( + [ + Msf::OptString.new('SMBUser', [true, 'The username to authenticate as', 'db2admin']), + Msf::OptString.new('SMBPass', [true, 'The password for the specified username', 'db2admin']) + ] + ), + smb_scanner_mod.class, + true + ) + smb_scanner_mod.datastore['RHOSTS'] = 'smb://example.com/ smb://user@example.com/ smb://user:password@example.com smb://:@example.com' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBUser' => 'db2admin', 'SMBPass' => 'db2admin' }, + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBUser' => 'user', 'SMBPass' => 'db2admin' }, + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBUser' => 'user', 'SMBPass' => 'password' }, + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBUser' => '', 'SMBPass' => '' } + ] + expect(each_host_for(smb_scanner_mod)).to have_datastore_values(expected) + end + + # TODO: Scanners in general are awkward because: + # - the 'setup' method of scanner modules is called _before_ we've walked the RHOSTS to calculate the datastore options. Some setup methods validate options. + # Example: + # ``` + # use auxiliary/admin/smb/download_file + # set rhosts smb://alan:a@192.168.222.135/my_share/helloworld.txt + # run + # ``` + # - the OptionsContainer attempts validation _before_ we've walked the RHOSTS to calculate the datastore options. + # Example: + # ``` + # use scanner/smb/impacket/secretsdump + # set rhosts smb://alan:a@192.168.222.135 + # run + # [-] Auxiliary failed: Msf::OptionValidateError One or more options failed to validate: SMBPass, SMBUser. + # ^--> validate happens before 'run' is called, which actually invokes the rhosts walker for scanners + # ``` + # At the minute both master and this branch copy/pasta RHOSTS validation checks into the module dispatchers, and bypass datastore validation entirely. + it 'enumerates smb schemes for scanners when a user and password are specified' do + smb_scanner_mod.datastore['RHOSTS'] = 'smb://user:pass@example.com/' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => '.', 'SMBPass' => 'pass', 'SMBUser' => 'user' } + ] + expect(each_host_for(smb_scanner_mod)).to have_datastore_values(expected) + end + + it 'enumerates smb schemes for scanners when a domain, user and password are specified' do + smb_scanner_mod.datastore['RHOSTS'] = 'smb://domain;user:pass@example.com/' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => 'domain', 'SMBPass' => 'pass', 'SMBUser' => 'user' } + ] + expect(each_host_for(smb_scanner_mod)).to have_datastore_values(expected) + end + + it 'enumerates smb schemes for ' do + smb_scanner_mod.datastore['RHOSTS'] = 'smb://domain;user:pass@example.com/' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'SSL' => false, 'SMBDomain' => 'domain', 'SMBPass' => 'pass', 'SMBUser' => 'user' } + ] + expect(each_host_for(smb_scanner_mod)).to have_datastore_values(expected) + end + + it 'enumerates smb schemes for when the module has SMBSHARE and RPATHS available' do + smb_share_mod.datastore['RHOSTS'] = 'smb://user@example.com/share_name/path/to/file.txt' + expected = [ + { 'RHOSTS' => '192.0.2.2', 'RPORT' => 445, 'SSL' => false, 'SMBDomain' => '.', 'SMBPass' => '', 'SMBUser' => 'user', 'RPATH' => 'path/to/file.txt' } + ] + expect(each_host_for(smb_share_mod)).to have_datastore_values(expected) + end + end + # TODO: Discuss adding a test for the datastore containing an existing TARGETURI,and running with a HTTP url without a path. Should the TARGETURI be overridden to '/', '', or unaffected, and the default value is used instead? - # TODO: Discuss adding a test for the datastore containing an existing HttpUsername/HttpPassword value, and running with a HTTP url without a specified user/password. Is the user/password an empty string, or the default values? it 'enumerates a combination of all syntaxes' do temp_file_a = create_tempfile("\n192.0.2.0\n\n\n127.0.0.5\n\nhttp://user:pass@example.com:9000/foo\ncidr:/30:https://user:pass@multiple_ips.example.com:9000/foo")