From ff7a8e6351b10971eb28e720db498c29d5c62fc7 Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Fri, 12 Apr 2013 15:14:04 -0500 Subject: [PATCH 01/27] Msf::ModuleManager::ModulePaths shared example [#47979793] --- spec/lib/msf/core/module_manager_spec.rb | 74 +----------------- .../msf/module_manager/module_paths.rb | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 72 deletions(-) create mode 100644 spec/support/shared/examples/msf/module_manager/module_paths.rb diff --git a/spec/lib/msf/core/module_manager_spec.rb b/spec/lib/msf/core/module_manager_spec.rb index 714d545f64..bdc952fe89 100644 --- a/spec/lib/msf/core/module_manager_spec.rb +++ b/spec/lib/msf/core/module_manager_spec.rb @@ -32,81 +32,11 @@ describe Msf::ModuleManager do Msf::Framework.new end - subject do + subject(:module_manager) do described_class.new(framework) end - context '#add_module_path' do - it 'should strip trailing File::SEPARATOR from the path' do - Dir.mktmpdir do |path| - path_with_trailing_separator = path + File::SEPARATOR - subject.add_module_path(path_with_trailing_separator) - - subject.send(:module_paths).should_not include(path_with_trailing_separator) - subject.send(:module_paths).should include(path) - end - end - - context 'with Fastlib archive' do - it 'should raise an ArgumentError unless the File exists' do - file = Tempfile.new(archive_basename) - # unlink will clear path, so copy it to a variable - path = file.path - file.unlink - - File.exist?(path).should be_false - - expect { - subject.add_module_path(path) - }.to raise_error(ArgumentError, "The path supplied does not exist") - end - - it 'should add the path to #module_paths if the File exists' do - Tempfile.open(archive_basename) do |temporary_file| - path = temporary_file.path - - File.exist?(path).should be_true - - subject.add_module_path(path) - - subject.send(:module_paths).should include(path) - end - end - end - - context 'with directory' do - it 'should add path to #module_paths' do - Dir.mktmpdir do |path| - subject.add_module_path(path) - - subject.send(:module_paths).should include(path) - end - end - - context 'containing Fastlib archives' do - it 'should add each Fastlib archive to #module_paths' do - Dir.mktmpdir do |directory| - Tempfile.open(archive_basename, directory) do |file| - subject.add_module_path(directory) - - subject.send(:module_paths).should include(directory) - subject.send(:module_paths).should include(file.path) - end - end - end - end - end - - context 'with other file' do - it 'should raise ArgumentError' do - Tempfile.open(basename_prefix) do |file| - expect { - subject.add_module_path(file.path) - }.to raise_error(ArgumentError, 'The path supplied is not a valid directory.') - end - end - end - end + it_should_behave_like 'Msf::ModuleManager::ModulePaths' context '#file_changed?' do let(:module_basename) do diff --git a/spec/support/shared/examples/msf/module_manager/module_paths.rb b/spec/support/shared/examples/msf/module_manager/module_paths.rb new file mode 100644 index 0000000000..05fdc1f663 --- /dev/null +++ b/spec/support/shared/examples/msf/module_manager/module_paths.rb @@ -0,0 +1,77 @@ +shared_examples_for 'Msf::ModuleManager::ModulePaths' do + def module_paths + module_manager.send(:module_paths) + end + + context '#add_module_path' do + it 'should strip trailing File::SEPARATOR from the path' do + Dir.mktmpdir do |path| + path_with_trailing_separator = path + File::SEPARATOR + module_manager.add_module_path(path_with_trailing_separator) + + module_paths.should_not include(path_with_trailing_separator) + module_paths.should include(path) + end + end + + context 'with Fastlib archive' do + it 'should raise an ArgumentError unless the File exists' do + file = Tempfile.new(archive_basename) + # unlink will clear path, so copy it to a variable + path = file.path + file.unlink + + File.exist?(path).should be_false + + expect { + module_manager.add_module_path(path) + }.to raise_error(ArgumentError, "The path supplied does not exist") + end + + it 'should add the path to #module_paths if the File exists' do + Tempfile.open(archive_basename) do |temporary_file| + path = temporary_file.path + + File.exist?(path).should be_true + + module_manager.add_module_path(path) + + module_paths.should include(path) + end + end + end + + context 'with directory' do + it 'should add path to #module_paths' do + Dir.mktmpdir do |path| + module_manager.add_module_path(path) + + module_paths.should include(path) + end + end + + context 'containing Fastlib archives' do + it 'should add each Fastlib archive to #module_paths' do + Dir.mktmpdir do |directory| + Tempfile.open(archive_basename, directory) do |file| + module_manager.add_module_path(directory) + + module_paths.should include(directory) + module_paths.should include(file.path) + end + end + end + end + end + + context 'with other file' do + it 'should raise ArgumentError' do + Tempfile.open(basename_prefix) do |file| + expect { + subject.add_module_path(file.path) + }.to raise_error(ArgumentError, 'The path supplied is not a valid directory.') + end + end + end + end +end \ No newline at end of file From 070939557072ebd17051d4cab70913a30d56784c Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Fri, 12 Apr 2013 15:18:16 -0500 Subject: [PATCH 02/27] Msf::ModuleManager::Loading shared example [#47979793] --- spec/lib/msf/core/module_manager_spec.rb | 79 +------------------ .../examples/msf/module_manager/loading.rb | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 78 deletions(-) create mode 100644 spec/support/shared/examples/msf/module_manager/loading.rb diff --git a/spec/lib/msf/core/module_manager_spec.rb b/spec/lib/msf/core/module_manager_spec.rb index bdc952fe89..2bda423916 100644 --- a/spec/lib/msf/core/module_manager_spec.rb +++ b/spec/lib/msf/core/module_manager_spec.rb @@ -36,83 +36,6 @@ describe Msf::ModuleManager do described_class.new(framework) end + it_should_behave_like 'Msf::ModuleManager::Loading' it_should_behave_like 'Msf::ModuleManager::ModulePaths' - - context '#file_changed?' do - let(:module_basename) do - [basename_prefix, '.rb'] - end - - it 'should return true if module info is not cached' do - Tempfile.open(module_basename) do |tempfile| - module_path = tempfile.path - - subject.send(:module_info_by_path)[module_path].should be_nil - subject.file_changed?(module_path).should be_true - end - end - - it 'should return true if the cached type is Msf::MODULE_PAYLOAD' do - Tempfile.open(module_basename) do |tempfile| - module_path = tempfile.path - modification_time = File.mtime(module_path) - - subject.send(:module_info_by_path)[module_path] = { - # :modification_time must match so that it is the :type that is causing the `true` and not the - # :modification_time causing the `true`. - :modification_time => modification_time, - :type => Msf::MODULE_PAYLOAD - } - - subject.file_changed?(module_path).should be_true - end - end - - context 'with cache module info and not a payload module' do - it 'should return true if the file does not exist on the file system' do - tempfile = Tempfile.new(module_basename) - module_path = tempfile.path - modification_time = File.mtime(module_path).to_i - - subject.send(:module_info_by_path)[module_path] = { - :modification_time => modification_time - } - - tempfile.unlink - - File.exist?(module_path).should be_false - subject.file_changed?(module_path).should be_true - end - - it 'should return true if modification time does not match the cached modification time' do - Tempfile.open(module_basename) do |tempfile| - module_path = tempfile.path - modification_time = File.mtime(module_path).to_i - cached_modification_time = (modification_time * rand).to_i - - subject.send(:module_info_by_path)[module_path] = { - :modification_time => cached_modification_time - } - - cached_modification_time.should_not == modification_time - subject.file_changed?(module_path).should be_true - end - end - - it 'should return false if modification time does match the cached modification time' do - Tempfile.open(module_basename) do |tempfile| - module_path = tempfile.path - modification_time = File.mtime(module_path).to_i - cached_modification_time = modification_time - - subject.send(:module_info_by_path)[module_path] = { - :modification_time => cached_modification_time - } - - cached_modification_time.should == modification_time - subject.file_changed?(module_path).should be_false - end - end - end - end end \ No newline at end of file diff --git a/spec/support/shared/examples/msf/module_manager/loading.rb b/spec/support/shared/examples/msf/module_manager/loading.rb new file mode 100644 index 0000000000..5f51281ec2 --- /dev/null +++ b/spec/support/shared/examples/msf/module_manager/loading.rb @@ -0,0 +1,79 @@ +shared_examples_for 'Msf::ModuleManager::Loading' do + context '#file_changed?' do + let(:module_basename) do + [basename_prefix, '.rb'] + end + + it 'should return true if module info is not cached' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + + subject.send(:module_info_by_path)[module_path].should be_nil + subject.file_changed?(module_path).should be_true + end + end + + it 'should return true if the cached type is Msf::MODULE_PAYLOAD' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path) + + subject.send(:module_info_by_path)[module_path] = { + # :modification_time must match so that it is the :type that is causing the `true` and not the + # :modification_time causing the `true`. + :modification_time => modification_time, + :type => Msf::MODULE_PAYLOAD + } + + subject.file_changed?(module_path).should be_true + end + end + + context 'with cache module info and not a payload module' do + it 'should return true if the file does not exist on the file system' do + tempfile = Tempfile.new(module_basename) + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => modification_time + } + + tempfile.unlink + + File.exist?(module_path).should be_false + subject.file_changed?(module_path).should be_true + end + + it 'should return true if modification time does not match the cached modification time' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + cached_modification_time = (modification_time * rand).to_i + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => cached_modification_time + } + + cached_modification_time.should_not == modification_time + subject.file_changed?(module_path).should be_true + end + end + + it 'should return false if modification time does match the cached modification time' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + cached_modification_time = modification_time + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => cached_modification_time + } + + cached_modification_time.should == modification_time + subject.file_changed?(module_path).should be_false + end + end + end + end +end \ No newline at end of file From 2c681005c0f19055b6b70cbbe39ec8cfc6166f64 Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Mon, 15 Apr 2013 13:08:12 -0500 Subject: [PATCH 03/27] Msf::ModuleManager::Cache spec coverage [#47979793] --- Gemfile | 3 + Gemfile.lock | 9 + lib/msf/core/module_manager/cache.rb | 68 +-- spec/lib/msf/core/module_manager_spec.rb | 9 +- .../shared/contexts/database_cleaner.rb | 2 + .../examples/msf/module_manager/cache.rb | 403 ++++++++++++++++++ 6 files changed, 461 insertions(+), 33 deletions(-) create mode 100644 spec/support/shared/examples/msf/module_manager/cache.rb diff --git a/Gemfile b/Gemfile index 3f4ae66096..93d388b99d 100755 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,9 @@ group :test do gem 'database_cleaner' # testing framework gem 'rspec', '>= 2.12' + # add matchers from shoulda, such as query_the_database, which is useful for + # testing that the Msf::DBManager activation is respected. + gem 'shoulda-matchers' # code coverage for tests # any version newer than 0.5.4 gives an Encoding error when trying to read the source files. gem 'simplecov', '0.5.4', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 70f791c9d0..6fe9448399 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,8 @@ GEM i18n (= 0.6.1) multi_json (~> 1.0) arel (3.0.2) + bourne (1.4.0) + mocha (~> 0.13.2) builder (3.0.4) database_cleaner (0.9.1) diff-lcs (1.2.2) @@ -20,10 +22,13 @@ GEM activesupport (>= 3.0.0) i18n (0.6.1) json (1.7.7) + metaclass (0.0.1) metasploit_data_models (0.6.14) activerecord (>= 3.2.13) activesupport pg + mocha (0.13.3) + metaclass (~> 0.0.1) msgpack (0.5.4) multi_json (1.0.4) nokogiri (1.5.9) @@ -40,6 +45,9 @@ GEM rspec-expectations (2.13.0) diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.13.0) + shoulda-matchers (1.5.2) + activesupport (>= 3.0.0) + bourne (~> 1.3) simplecov (0.5.4) multi_json (~> 1.0.3) simplecov-html (~> 0.5.3) @@ -65,5 +73,6 @@ DEPENDENCIES redcarpet robots rspec (>= 2.12) + shoulda-matchers simplecov (= 0.5.4) yard diff --git a/lib/msf/core/module_manager/cache.rb b/lib/msf/core/module_manager/cache.rb index 0c6e281404..5832cc3acd 100644 --- a/lib/msf/core/module_manager/cache.rb +++ b/lib/msf/core/module_manager/cache.rb @@ -46,13 +46,19 @@ module Msf::ModuleManager::Cache loaded end - # Rebuild the cache for the module set + # @overload refresh_cache_from_module_files + # Rebuilds database and in-memory cache for all modules. # - # @return [void] - def refresh_cache_from_module_files(mod = nil) + # @return [void] + # @overload refresh_cache_from_module_files(module_class_or_instance) + # Rebuilds database and in-memory cache for given module_class_or_instance. + # + # @param (see Msf::DBManager#update_module_details) + # @return [void] + def refresh_cache_from_module_files(module_class_or_instance = nil) if framework_migrated? - if mod - framework.db.update_module_details(mod) + if module_class_or_instance + framework.db.update_module_details(module_class_or_instance) else framework.db.update_all_module_details end @@ -61,7 +67,7 @@ module Msf::ModuleManager::Cache end end - # Reset the module cache + # Refreshes the in-memory cache from the database cache. # # @return [void] def refresh_cache_from_database @@ -97,32 +103,38 @@ module Msf::ModuleManager::Cache self.module_info_by_path = {} if framework_migrated? - # TODO record module parent_path in {Mdm::ModuleDetail} so it does not need to be derived from file. - ::Mdm::ModuleDetail.find(:all).each do |module_detail| - path = module_detail.file - type = module_detail.mtype - reference_name = module_detail.refname + ActiveRecord::Base.connection_pool.with_connection do + # TODO record module parent_path in {Mdm::ModuleDetail} so it does not need to be derived from file. + # Use find_each so Mdm::ModuleDetails are returned in batches, which will + # handle the growing number of modules better than all.each. + Mdm::ModuleDetail.find_each do |module_detail| + path = module_detail.file + type = module_detail.mtype + reference_name = module_detail.refname - typed_path = Msf::Modules::Loader::Base.typed_path(type, reference_name) - escaped_typed_path = Regexp.escape(typed_path) - parent_path = path.gsub(/#{escaped_typed_path}$/, '') + typed_path = Msf::Modules::Loader::Base.typed_path(type, reference_name) + # join to '' so that typed_path_prefix starts with file separator + typed_path_suffix = File.join('', typed_path) + escaped_typed_path = Regexp.escape(typed_path_suffix) + parent_path = path.gsub(/#{escaped_typed_path}$/, '') - module_info_by_path[path] = { - :reference_name => reference_name, - :type => type, - :parent_path => parent_path, - :modification_time => module_detail.mtime - } + module_info_by_path[path] = { + :reference_name => reference_name, + :type => type, + :parent_path => parent_path, + :modification_time => module_detail.mtime + } - typed_module_set = module_set(type) + typed_module_set = module_set(type) - # Don't want to trigger as {Msf::ModuleSet#create} so check for - # key instead of using ||= which would call {Msf::ModuleSet#[]} - # which would potentially call {Msf::ModuleSet#create}. - unless typed_module_set.has_key? reference_name - typed_module_set[reference_name] = Msf::SymbolicModule - end - end + # Don't want to trigger as {Msf::ModuleSet#create} so check for + # key instead of using ||= which would call {Msf::ModuleSet#[]} + # which would potentially call {Msf::ModuleSet#create}. + unless typed_module_set.has_key? reference_name + typed_module_set[reference_name] = Msf::SymbolicModule + end + end + end end self.module_info_by_path diff --git a/spec/lib/msf/core/module_manager_spec.rb b/spec/lib/msf/core/module_manager_spec.rb index 2bda423916..5bab5a7104 100644 --- a/spec/lib/msf/core/module_manager_spec.rb +++ b/spec/lib/msf/core/module_manager_spec.rb @@ -16,6 +16,8 @@ require 'tmpdir' require 'msf/core' describe Msf::ModuleManager do + include_context 'Msf::Simple::Framework' + let(:archive_basename) do [basename_prefix, archive_extension] end @@ -28,14 +30,11 @@ describe Msf::ModuleManager do 'rspec' end - let(:framework) do - Msf::Framework.new - end - subject(:module_manager) do - described_class.new(framework) + framework.modules end + it_should_behave_like 'Msf::ModuleManager::Cache' it_should_behave_like 'Msf::ModuleManager::Loading' it_should_behave_like 'Msf::ModuleManager::ModulePaths' end \ No newline at end of file diff --git a/spec/support/shared/contexts/database_cleaner.rb b/spec/support/shared/contexts/database_cleaner.rb index 0c03ae6397..36ec8d86d3 100644 --- a/spec/support/shared/contexts/database_cleaner.rb +++ b/spec/support/shared/contexts/database_cleaner.rb @@ -1,3 +1,5 @@ +require 'metasploit/framework/database' + shared_context 'DatabaseCleaner' do def with_established_connection begin diff --git a/spec/support/shared/examples/msf/module_manager/cache.rb b/spec/support/shared/examples/msf/module_manager/cache.rb new file mode 100644 index 0000000000..026f519b3f --- /dev/null +++ b/spec/support/shared/examples/msf/module_manager/cache.rb @@ -0,0 +1,403 @@ +shared_examples_for 'Msf::ModuleManager::Cache' do + context '#cache_empty?' do + subject(:cache_empty?) do + module_manager.cache_empty? + end + + before(:each) do + module_manager.send(:module_info_by_path=, module_info_by_path) + end + + context 'with empty' do + let(:module_info_by_path) do + {} + end + + it { should be_true } + end + + context 'without empty' do + let(:module_info_by_path) do + { + 'path/to/module' => {} + } + end + + it { should be_false } + end + end + + context '#load_cached_module' do + let(:parent_path) do + Metasploit::Framework.root.join('modules').to_path + end + + let(:reference_name) do + 'windows/smb/ms08_067_netapi' + end + + let(:type) do + 'exploit' + end + + subject(:load_cached_module) do + module_manager.load_cached_module(type, reference_name) + end + + before(:each) do + module_manager.send(:module_info_by_path=, module_info_by_path) + end + + context 'with module info in cache' do + let(:module_info_by_path) do + { + 'path/to/module' => { + :parent_path => parent_path, + :reference_name => reference_name, + :type => type + } + } + end + + it 'should enumerate loaders until if it find the one where loadable?(parent_path) is true' do + module_manager.send(:loaders).each do |loader| + loader.should_receive(:loadable?).with(parent_path).and_call_original + end + + load_cached_module + end + + it 'should force load using #load_module on the loader' do + Msf::Modules::Loader::Directory.any_instance.should_receive( + :load_module + ).with( + parent_path, + type, + reference_name, + :force => true + ).and_call_original + + load_cached_module + end + + context 'return from load_module' do + before(:each) do + module_manager.send(:loaders).each do |loader| + loader.stub(:load_module => module_loaded) + end + end + + context 'with false' do + let(:module_loaded) do + false + end + + it { should be_false } + end + + context 'with true' do + let(:module_loaded) do + true + end + + it { should be_true } + end + end + end + + context 'without module info in cache' do + let(:module_info_by_path) do + {} + end + + it { should be_false } + end + end + + context '#refresh_cache_from_module_files' do + before(:each) do + module_manager.stub(:framework_migrated? => framework_migrated?) + end + + context 'with framework migrated' do + let(:framework_migrated?) do + true + end + + context 'with module argument' do + def refresh_cache_from_module_files + module_manager.refresh_cache_from_module_files(module_class_or_instance) + end + + let(:module_class_or_instance) do + Class.new(Msf::Module) + end + + it 'should update database and then update in-memory cache from the database for the given module_class_or_instance' do + framework.db.should_receive(:update_module_details).with(module_class_or_instance).ordered + module_manager.should_receive(:refresh_cache_from_database).ordered + + refresh_cache_from_module_files + end + end + + context 'without module argument' do + def refresh_cache_from_module_files + module_manager.refresh_cache_from_module_files + end + + it 'should update database and then update in-memory cache from the database for all modules' do + framework.db.should_receive(:update_all_module_details).ordered + module_manager.should_receive(:refresh_cache_from_database) + + refresh_cache_from_module_files + end + end + end + + context 'without framework migrated' do + def refresh_cache_from_module_files + module_manager.refresh_cache_from_module_files + end + + let(:framework_migrated?) do + false + end + + it 'should not call Msf::DBManager#update_module_details' do + framework.db.should_not_receive(:update_module_details) + + refresh_cache_from_module_files + end + + it 'should not call Msf::DBManager#update_all_module_details' do + framework.db.should_not_receive(:update_all_module_details) + + refresh_cache_from_module_files + end + + it 'should not call #refresh_cache_from_database' do + module_manager.should_not_receive(:refresh_cache_from_database) + + refresh_cache_from_module_files + end + end + end + + context '#refresh_cache_from_database' do + def refresh_cache_from_database + module_manager.refresh_cache_from_database + end + + it 'should call #module_info_by_path_from_database!' do + module_manager.should_receive(:module_info_by_path_from_database!) + + refresh_cache_from_database + end + end + + context '#framework_migrated?' do + subject(:framework_migrated?) do + module_manager.send(:framework_migrated?) + end + + context 'with framework database' do + before(:each) do + framework.db.stub(:migrated => migrated) + end + + context 'with migrated' do + let(:migrated) do + true + end + + it { should be_true } + end + + context 'without migrated' do + let(:migrated) do + false + end + + it { should be_false } + end + end + + context 'without framework database' do + before(:each) do + framework.stub(:db => nil) + end + + it { should be_false } + end + end + + context '#module_info_by_path' do + it { should respond_to(:module_info_by_path) } + end + + context '#module_info_by_path=' do + it { should respond_to(:module_info_by_path=) } + end + + context '#module_info_by_path_from_database!' do + def module_info_by_path + module_manager.send(:module_info_by_path) + end + + def module_info_by_path_from_database! + module_manager.send(:module_info_by_path_from_database!) + end + + before(:each) do + module_manager.stub(:framework_migrated? => framework_migrated?) + end + + context 'with framework migrated' do + include_context 'DatabaseCleaner' + + let(:framework_migrated?) do + true + end + + before(:each) do + configurations = Metasploit::Framework::Database.configurations + spec = configurations[Metasploit::Framework.env] + + # Need to connect or ActiveRecord::Base.connection_pool will raise an + # error. + framework.db.connect(spec) + end + + it 'should call ActiveRecord::Base.connection_pool.with_connection' do + # 1st is from with_established_connection + # 2nd is from module_info_by_path_from_database! + ActiveRecord::Base.connection_pool.should_receive(:with_connection).at_least(2).times + + module_info_by_path_from_database! + end + + it 'should use ActiveRecord::Batches#find_each to enumerate Mdm::ModuleDetails in batches' do + Mdm::ModuleDetail.should_receive(:find_each) + + module_info_by_path_from_database! + end + + context 'with database cache' do + let(:parent_path) do + parent_pathname.to_path + end + + let(:parent_pathname) do + Metasploit::Framework.root.join('modules') + end + + let(:path) do + pathname.to_path + end + + let(:pathname) do + parent_pathname.join( + 'exploits', + "#{reference_name}.rb" + ) + end + + let(:type) do + 'exploit' + end + + let(:reference_name) do + 'windows/smb/ms08_067_netapi' + end + + # + # Let!s (let + before(:each)) + # + + let!(:mdm_module_detail) do + FactoryGirl.create(:mdm_module_detail, + :file => path, + :mtype => type, + :mtime => pathname.mtime, + :refname => reference_name + ) + end + + it 'should create cache entry for path' do + module_info_by_path_from_database! + + module_info_by_path.should have_key(path) + end + + it 'should use Msf::Modules::Loader::Base.typed_path to derive parent_path' do + Msf::Modules::Loader::Base.should_receive(:typed_path).with(type, reference_name).and_call_original + + module_info_by_path_from_database! + end + + context 'cache entry' do + subject(:cache_entry) do + module_info_by_path[path] + end + + before(:each) do + module_info_by_path_from_database! + end + + its([:modification_time]) { should == pathname.mtime } + its([:parent_path]) { should == parent_path } + its([:reference_name]) { should == reference_name } + its([:type]) { should == type } + end + + context 'typed module set' do + let(:typed_module_set) do + module_manager.module_set(type) + end + + context 'with reference_name' do + before(:each) do + typed_module_set[reference_name] = mock('Msf::Module') + end + + it 'should not change reference_name value' do + expect { + module_info_by_path_from_database! + }.to_not change { + typed_module_set[reference_name] + } + end + end + + context 'without reference_name' do + it 'should set reference_name value to Msf::SymbolicModule' do + module_info_by_path_from_database! + + # have to use fetch because [] will trigger de-symbolization and + # instantiation. + typed_module_set.fetch(reference_name).should == Msf::SymbolicModule + end + end + end + end + end + + context 'without framework migrated' do + let(:framework_migrated?) do + false + end + + it { should_not query_the_database.when_calling(:module_info_by_path_from_database!) } + + it 'should reset #module_info_by_path' do + # pre-fill module_info_by_path so change can be detected + module_manager.send(:module_info_by_path=, mock('In-memory Cache')) + + module_info_by_path_from_database! + + module_info_by_path.should be_empty + end + end + end +end \ No newline at end of file From e5befb7094b25e8ebcdccac77f4a0c75c37185f1 Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Fri, 19 Apr 2013 10:11:33 -0500 Subject: [PATCH 04/27] Msf::DBManager#report_session specs [#47979793] --- Gemfile | 2 + Gemfile.lock | 2 + lib/msf/core/db.rb | 67 +++- spec/factories/mdm/routes.rb | 34 ++ spec/lib/msf/core/db_spec.rb | 661 +++++++++++++++++++++++++++++++++++ 5 files changed, 760 insertions(+), 6 deletions(-) create mode 100644 spec/factories/mdm/routes.rb diff --git a/Gemfile b/Gemfile index 93d388b99d..52b6993df7 100755 --- a/Gemfile +++ b/Gemfile @@ -48,4 +48,6 @@ group :test do # code coverage for tests # any version newer than 0.5.4 gives an Encoding error when trying to read the source files. gem 'simplecov', '0.5.4', :require => false + # Manipulate Time.now in specs + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index 6fe9448399..4ad79a6f7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,7 @@ GEM multi_json (~> 1.0.3) simplecov-html (~> 0.5.3) simplecov-html (0.5.3) + timecop (0.6.1) tzinfo (0.3.37) yard (0.8.5.2) @@ -75,4 +76,5 @@ DEPENDENCIES rspec (>= 2.12) shoulda-matchers simplecov (= 0.5.4) + timecop yard diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb index 6a4985be05..d48020c1c8 100644 --- a/lib/msf/core/db.rb +++ b/lib/msf/core/db.rb @@ -645,12 +645,69 @@ class DBManager } end - # Record a new session in the database + # @note The Mdm::Session#desc will be truncated to 255 characters. + # @todo https://www.pivotaltracker.com/story/show/48249739 # - # opts MUST contain either - # +:session+:: the Msf::Session object we are reporting - # +:host+:: the Host object we are reporting a session on. + # @overload report_session(opts) + # Creates an Mdm::Session from Msf::Session. If +via_exploit+ is set on the + # +session+, then an Mdm::Vuln and Mdm::ExploitAttempt is created for the + # session's host. The Mdm::Host for the +session_host+ is created using + # The session.session_host, +session.arch+ (if +session+ responds to arch), + # and the workspace derived from opts or the +session+. The Mdm::Session is + # assumed to be +last_seen+ and +opened_at+ at the time report_session is + # called. +session.exploit_datastore['ParentModule']+ is used for the + # Mdm::Session#via_exploit if +session.via_exploit+ is + # 'exploit/multi/handler'. # + # @param opts [Hash{Symbol => Object}] options + # @option opt [Msf::Session, #datastore, #platform, #type, #via_exploit, #via_payload] :session + # The in-memory session to persist to the database. + # @option opts [Mdm::Workspace] :workspace The workspace for in which the + # :session host is contained. Also used as the workspace for the + # Mdm::ExploitAttempt and Mdm::Vuln. Defaults to Mdm::Worksapce with + # Mdm::Workspace#name equal to +session.workspace+. + # @return [nil] if {Msf::DBManager#active} is +false+. + # @return [Mdm::Session] if session is saved + # @raise [ArgumentError] if :session is not an {Msf::Session}. + # @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be + # saved, in which case, the Mdm::ExploitAttempt and Mdm::Vuln will not be + # created, but the Mdm::Host will have been. (There is no transaction + # to rollback the Mdm::Host creation.) + # @see #find_or_create_host + # @see #normalize_host + # @see #report_exploit_success + # @see #report_vuln + # + # @overload report_session(opts) + # Creates an Mdm::Session from Mdm::Host. + # + # @param opts [Hash{Symbol => Object}] options + # @option opts [DateTime, Time] :closed_at The date and time the sesion was + # closed. + # @option opts [String] :close_reason Reason the session was closed. + # @option opts [Hash] :datastore {Msf::DataStore#to_h}. + # @option opts [String] :desc Session description. Will be truncated to 255 + # characters. + # @option opts [Mdm::Host] :host The host on which the session was opened. + # @option opts [DateTime, Time] :last_seen The last date and time the + # session was seen to be open. Defaults to :closed_at's value. + # @option opts [DateTime, Time] :opened_at The date and time that the + # session was opened. + # @option opts [String] :platform The platform of the host. + # @option opts [Array] :routes ([]) The routes through the session for + # pivoting. + # @option opts [String] :stype Session type. + # @option opts [String] :via_exploit The {Msf::Module#fullname} of the + # exploit that was used to open the session. + # @option option [String] :via_payload the {MSf::Module#fullname} of the + # payload sent to the host when the exploit was successful. + # @return [nil] if {Msf::DBManager#active} is +false+. + # @return [Mdm::Session] if session is saved. + # @raise [ArgumentError] if :host is not an Mdm::Host. + # @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be + # saved. + # + # @raise ArgumentError if :host and :session is +nil+ def report_session(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { @@ -719,8 +776,6 @@ class DBManager # If this is a live session, we know the host is vulnerable to something. if opts[:session] and session.via_exploit - return unless host - mod = framework.modules.create(session.via_exploit) if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] diff --git a/spec/factories/mdm/routes.rb b/spec/factories/mdm/routes.rb new file mode 100644 index 0000000000..a93ce603e9 --- /dev/null +++ b/spec/factories/mdm/routes.rb @@ -0,0 +1,34 @@ +FactoryGirl.define do + factory :mdm_route, :class => Mdm::Route do + netmask { generate :mdm_route_netmask } + subnet { generate :mdm_route_subnet } + + # + # Associations + # + association :session, :factory => :mdm_session + end + + sequence :mdm_route_netmask do |n| + bits = 32 + bitmask = n % bits + + [ (~((2 ** (bits - bitmask)) - 1)) & 0xffffffff ].pack('N').unpack('CCCC').join('.') + + bits = 32 + shift = n % bits + mask_range = 2 ** bits + full_mask = mask_range - 1 + + integer_netmask = (full_mask << shift) + formatted_netmask = [integer_netmask].pack('N').unpack('CCCC').join('.') + + formatted_netmask + end + + sequence :mdm_route_subnet do |n| + class_c_network = n % 255 + + "192.168.#{class_c_network}.0" + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/db_spec.rb b/spec/lib/msf/core/db_spec.rb index f73e129a18..1b1e220245 100644 --- a/spec/lib/msf/core/db_spec.rb +++ b/spec/lib/msf/core/db_spec.rb @@ -19,4 +19,665 @@ describe Msf::DBManager do end it_should_behave_like 'Msf::DBManager::ImportMsfXml' + + context '#report_session' do + include_context 'DatabaseCleaner' + + let(:options) do + {} + end + + subject(:report_session) do + db_manager.report_session(options) + end + + before(:each) do + configurations = Metasploit::Framework::Database.configurations + spec = configurations[Metasploit::Framework.env] + + # Need to connect or ActiveRecord::Base.connection_pool will raise an + # error. + db_manager.connect(spec) + + db_manager.stub(:active => active) + end + + context 'with active' do + let(:active) do + true + end + + it 'should create connection' do + # 1st time from with_established_connection + # 2nd time from report_session + ActiveRecord::Base.connection_pool.should_receive(:with_connection).exactly(2).times + + report_session + end + + context 'with :session' do + before(:each) do + options[:session] = session + end + + context 'with Msf::Session' do + let(:exploit_datastore) do + Msf::ModuleDataStore.new(module_instance).tap do |datastore| + datastore['ParentModule'] = parent_module_fullname + + remote_port = rand(2 ** 16 - 1) + datastore['RPORT'] = remote_port + end + end + + let(:host) do + FactoryGirl.create(:mdm_host, :workspace => session_workspace) + end + + let(:module_instance) do + name = 'multi/handler' + + mock( + 'Msf::Module', + :fullname => "exploit/#{name}", + :framework => framework, + :name => name + ) + end + + let(:options_workspace) do + FactoryGirl.create(:mdm_workspace) + end + + let(:parent_module_fullname) do + "exploit/#{parent_module_name}" + end + + let(:parent_module_name) do + 'windows/smb/ms08_067_netapi' + end + + let(:parent_path) do + Metasploit::Framework.root.join('modules').to_path + end + + let(:session) do + session_class.new.tap do |session| + session.exploit_datastore = exploit_datastore + session.info = 'Info' + session.platform = 'Platform' + session.session_host = host.address + session.sid = rand(100) + session.type = 'Session Type' + session.via_exploit = 'exploit/multi/handler' + session.via_payload = 'payload/single/windows/metsvc_bind_tcp' + session.workspace = session_workspace.name + end + end + + let(:session_class) do + Class.new do + include Msf::Session + + attr_accessor :datastore + attr_accessor :platform + attr_accessor :type + attr_accessor :via_exploit + attr_accessor :via_payload + end + end + + let(:session_workspace) do + FactoryGirl.create(:mdm_workspace) + end + + before(:each) do + reference_name = 'multi/handler' + path = File.join(parent_path, 'exploits', reference_name) + + # fake cache data for exploit/multi/handler so it can be loaded + framework.modules.send( + :module_info_by_path=, + { + path => + { + :parent_path => parent_path, + :reference_name => reference_name, + :type => 'exploit', + } + } + ) + + FactoryGirl.create( + :mdm_module_detail, + :fullname => parent_module_fullname, + :name => parent_module_name + ) + end + + context 'with :workspace' do + before(:each) do + options[:workspace] = options_workspace + end + + it 'should not find workspace from session' do + db_manager.should_not_receive(:find_workspace) + + report_session + end + end + + context 'without :workspace' do + it 'should find workspace from session' do + db_manager.should_receive(:find_workspace).with(session.workspace).and_call_original + + report_session + end + + it 'should pass session.workspace to #find_or_create_host' do + db_manager.should_receive(:find_or_create_host).with( + hash_including( + :workspace => session_workspace + ) + ).and_return(host) + + report_session + end + end + + context 'with workspace from either :workspace or session' do + it 'should pass normalized host from session as :host to #find_or_create_host' do + normalized_host = mock('Normalized Host') + db_manager.stub(:normalize_host).with(session).and_return(normalized_host) + # stub report_vuln so its use of find_or_create_host and normalize_host doesn't interfere. + db_manager.stub(:report_vuln) + + db_manager.should_receive(:find_or_create_host).with( + hash_including( + :host => normalized_host + ) + ).and_return(host) + + report_session + end + + context 'with session responds to arch' do + let(:arch) do + 'Arch' + end + + before(:each) do + session.stub(:arch => arch) + end + + it 'should pass :arch to #find_or_create_host' do + db_manager.should_receive(:find_or_create_host).with( + hash_including( + :arch => arch + ) + ).and_call_original + + report_session + end + end + + context 'without session responds to arch' do + it 'should not pass :arch to #find_or_create_host' do + db_manager.should_receive(:find_or_create_host).with( + hash_excluding( + :arch + ) + ).and_call_original + + report_session + end + end + + it 'should create an Mdm::Session' do + expect { + report_session + }.to change(Mdm::Session, :count).by(1) + end + + it { should be_an Mdm::Session } + + it 'should set session.db_record to created Mdm::Session' do + mdm_session = report_session + + session.db_record.should == mdm_session + end + + context 'with session.via_exploit' do + it 'should create session.via_exploit module' do + framework.modules.should_receive(:create).with(session.via_exploit).and_call_original + + report_session + end + + it 'should create Mdm::Vuln' do + expect { + report_session + }.to change(Mdm::Vuln, :count).by(1) + end + + context 'created Mdm::Vuln' do + let(:mdm_session) do + Mdm::Session.last + end + + let(:rport) do + nil + end + + before(:each) do + Timecop.freeze + + session.exploit_datastore['RPORT'] = rport + + report_session + end + + after(:each) do + Timecop.return + end + + subject(:vuln) do + Mdm::Vuln.last + end + + its(:host) { should == Mdm::Host.last } + its(:refs) { should == [] } + its(:exploited_at) { should == Time.now.utc } + + context "with session.via_exploit 'exploit/multi/handler'" do + context "with session.exploit_datastore['ParentModule']" do + its(:info) { should == "Exploited by #{parent_module_fullname} to create Session #{mdm_session.id}" } + its(:name) { should == parent_module_name } + end + end + + context "without session.via_exploit 'exploit/multi/handler'" do + let(:reference_name) do + 'windows/smb/ms08_067_netapi' + end + + before(:each) do + path = File.join( + parent_path, + 'exploits', + "#{reference_name}.rb" + ) + type = 'exploit' + + # fake cache data for ParentModule so it can be loaded + framework.modules.send( + :module_info_by_path=, + { + path => + { + :parent_path => parent_path, + :reference_name => reference_name, + :type => type, + } + } + ) + + session.via_exploit = "#{type}/#{reference_name}" + end + + its(:info) { should == "Exploited by #{session.via_exploit} to create Session #{mdm_session.id}"} + its(:name) { should == reference_name } + end + + context 'with RPORT' do + let(:rport) do + # use service.port instead of having service use rport so + # that service is forced to exist before call to + # report_service, which happens right after using rport in + # outer context's before(:each) + service.port + end + + let(:service) do + FactoryGirl.create( + :mdm_service, + :host => host + ) + end + + its(:service) { should == service } + end + + context 'without RPORT' do + its(:service) { should be_nil } + end + end + + context 'created Mdm::ExploitAttempt' do + let(:rport) do + nil + end + + before(:each) do + Timecop.freeze + + session.exploit_datastore['RPORT'] = rport + + report_session + end + + after(:each) do + Timecop.return + end + + subject(:exploit_attempt) do + Mdm::ExploitAttempt.last + end + + its(:attempted_at) { should == Time.now.utc } + # @todo https://www.pivotaltracker.com/story/show/48362615 + its(:session_id) { should == Mdm::Session.last.id } + its(:exploited) { should == true } + # @todo https://www.pivotaltracker.com/story/show/48362615 + its(:vuln_id) { should == Mdm::Vuln.last.id } + + context "with session.via_exploit 'exploit/multi/handler'" do + context "with session.datastore['ParentModule']" do + its(:module) { should == parent_module_fullname } + end + end + + context "without session.via_exploit 'exploit/multi/handler'" do + before(:each) do + session.via_exploit = parent_module_fullname + end + + its(:module) { should == session.via_exploit } + end + end + end + + context 'returned Mdm::Session' do + before(:each) do + Timecop.freeze + end + + after(:each) do + Timecop.return + end + + subject(:mdm_session) do + report_session + end + + # + # Ensure session has attributes present so its on mdm_session are + # not just comparing nils. + # + + it 'should have session.info present' do + session.info.should be_present + end + + it 'should have session.sid present' do + session.sid.should be_present + end + + it 'should have session.platform present' do + session.platform.should be_present + end + + it 'should have session.type present' do + session.type.should be_present + end + + it 'should have session.via_exploit present' do + session.via_exploit.should be_present + end + + it 'should have session.via_payload present' do + session.via_exploit.should be_present + end + + its(:datastore) { should == session.exploit_datastore.to_h } + its(:desc) { should == session.info } + its(:host_id) { should == Mdm::Host.last.id } + its(:last_seen) { should == Time.now.utc } + its(:local_id) { should == session.sid } + its(:opened_at) { should == Time.now.utc } + its(:platform) { should == session.platform } + its(:routes) { should == [] } + its(:stype) { should == session.type } + its(:via_payload) { should == session.via_payload } + + context "with session.via_exploit 'exploit/multi/handler'" do + it "should have session.via_exploit of 'exploit/multi/handler'" do + session.via_exploit.should == 'exploit/multi/handler' + end + + context "with session.exploit_datastore['ParentModule']" do + it "should have session.exploit_datastore['ParentModule']" do + session.exploit_datastore['ParentModule'].should_not be_nil + end + + its(:via_exploit) { should == parent_module_fullname } + end + end + + context "without session.via_exploit 'exploit/multi/handler'" do + before(:each) do + reference_name = 'windows/smb/ms08_067_netapi' + path = File.join( + parent_path, + 'exploits', + "#{reference_name}.rb" + ) + type = 'exploit' + + # fake cache data for ParentModule so it can be loaded + framework.modules.send( + :module_info_by_path=, + { + path => + { + :parent_path => parent_path, + :reference_name => reference_name, + :type => type, + } + } + ) + + session.via_exploit = "#{type}/#{reference_name}" + end + + it "should not have session.via_exploit of 'exploit/multi/handler'" do + session.via_exploit.should_not == 'exploit/multi/handler' + end + + its(:via_exploit) { should == session.via_exploit } + end + end + end + end + + context 'without Msf::Session' do + let(:session) do + mock('Not a Msf::Session') + end + + it 'should raise ArgumentError' do + expect { + report_session + }.to raise_error(ArgumentError, "Invalid :session, expected Msf::Session") + end + end + end + + context 'without :session' do + context 'with :host' do + before(:each) do + options[:host] = host + end + + context 'with Mdm::Host' do + let(:host) do + FactoryGirl.create(:mdm_host) + end + + context 'created Mdm::Session' do + let(:closed_at) do + nil + end + + let(:close_reason) do + 'Closed because...' + end + + let(:description) do + 'Session Description' + end + + let(:exploit_full_name) do + 'exploit/windows/smb/ms08_067_netapi' + end + + let(:last_seen) do + nil + end + + let(:opened_at) do + Time.now.utc - 5.minutes + end + + let(:payload_full_name) do + 'payload/singles/windows/metsvc_reverse_tcp' + end + + let(:platform) do + 'Host Platform' + end + + let(:routes) do + nil + end + + let(:session_type) do + 'Session Type' + end + + before(:each) do + options[:closed_at] = closed_at + options[:close_reason] = close_reason + options[:desc] = description + options[:last_seen] = last_seen + options[:opened_at] = opened_at + options[:platform] = platform + options[:routes] = routes + options[:stype] = session_type + options[:via_payload] = payload_full_name + options[:via_exploit] = exploit_full_name + end + + subject(:mdm_session) do + report_session + end + + its(:close_reason) { should == close_reason } + its(:desc) { should == description } + its(:host) { should == host } + its(:platform) { should == platform } + its(:stype) { should == session_type } + its(:via_exploit) { should == exploit_full_name } + its(:via_payload) { should == payload_full_name } + + context 'with :last_seen' do + let(:last_seen) do + opened_at + end + + its(:last_seen) { should == last_seen } + end + + context 'with :closed_at' do + let(:closed_at) do + opened_at + 1.minute + end + + its(:closed_at) { should == closed_at } + end + + context 'without :closed_at' do + its(:closed_at) { should == nil } + end + + context 'without :last_seen' do + context 'with :closed_at' do + let(:closed_at) do + opened_at + 1.minute + end + + its(:last_seen) { should == closed_at } + end + + context 'without :closed_at' do + its(:last_seen) { should be_nil } + end + end + + context 'with :routes' do + let(:routes) do + FactoryGirl.build_list( + :mdm_route, + 1, + :session => nil + ) + end + + its(:routes) { should == routes } + end + + context 'without :routes' do + its(:routes) { should == [] } + end + end + end + + context 'without Mdm::Host' do + let(:host) do + '192.168.0.1' + end + + it 'should raise ArgumentError' do + expect { + report_session + }.to raise_error(ArgumentError, "Invalid :host, expected Host object") + end + end + end + + context 'without :host' do + it 'should raise ArgumentError' do + expect { + report_session + }.to raise_error(ArgumentError) + end + end + end + end + + context 'without active' do + let(:active) do + false + end + + it { should be_nil } + + it 'should not create a connection' do + # 1st time for with_established_connection + ActiveRecord::Base.connection_pool.should_receive(:with_connection).once + + report_session + end + end + end end From 3bf3cfccc6fa483281a3ef6e80bd457871c11dcf Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Fri, 19 Apr 2013 12:07:12 -0500 Subject: [PATCH 05/27] Use be_within to loosen tolerance for Time comparisons [#47979793] [#48414569] Even though using Timecop locally on OS X makes the `should ==