Spike smb uri support

This commit is contained in:
Alan Foster
2021-06-01 10:52:26 +01:00
committed by adfoster-r7
parent 4899884a33
commit f96dc59cd4
4 changed files with 268 additions and 40 deletions
@@ -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
+48 -20
View File
@@ -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
#
+50 -5
View File
@@ -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<Msf::DataStore|StandardError>] 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
+166 -12
View File
@@ -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")