diff --git a/Gemfile b/Gemfile index b5731ee18f39..fb4042dbfd8d 100755 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,8 @@ source 'https://rubygems.org' gemspec name: 'metasploit-framework' gem 'sqlite3', '~>1.3.0' +# gem 'rex-text', path: '../rex-text' +gem 'rex-text', git: 'https://github.com/adfoster-r7/rex-text', branch: 'add-word-wrapping-to-rex-tables' # separate from test as simplecov is not run on travis-ci group :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index f85e113b5f0f..9ce906649c6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/adfoster-r7/rex-text + revision: f7990ab39b8a347b0a16d2db9c52ea227b332457 + branch: add-word-wrapping-to-rex-tables + specs: + rex-text (0.2.27) + PATH remote: . specs: @@ -347,7 +354,6 @@ GEM rex-socket rex-text rex-struct2 (0.1.2) - rex-text (0.2.26) rex-zip (0.1.3) rex-text rexml (3.2.4) @@ -448,6 +454,7 @@ DEPENDENCIES pry-byebug rake redcarpet + rex-text! rspec-rails rspec-rerun rubocop (= 0.86.0) diff --git a/lib/msf/core.rb b/lib/msf/core.rb index 9a07ef8d524f..92aa59055362 100644 --- a/lib/msf/core.rb +++ b/lib/msf/core.rb @@ -41,6 +41,7 @@ module Msf # Framework context and core classes require 'msf/core/framework' +require 'msf/core/feature_manager' require 'msf/core/db_manager' require 'msf/core/event_dispatcher' require 'msf/core/module_manager' diff --git a/lib/msf/core/data_store.rb b/lib/msf/core/data_store.rb index aae7a994dd4e..d4e5545073e4 100644 --- a/lib/msf/core/data_store.rb +++ b/lib/msf/core/data_store.rb @@ -43,14 +43,23 @@ def []=(k, v) end end - super(k,v) + if v.is_a? Hash + v.each { |key, value| self[key] = value } + else + super(k,v) + end end # # Case-insensitive wrapper around hash lookup # def [](k) - super(find_key_case(k)) + k = find_key_case(k) + if options[k].respond_to? :calculate_value + options[k].calculate_value(self) + else + super(k) + end end # diff --git a/lib/msf/core/exploit/http/client.rb b/lib/msf/core/exploit/http/client.rb index 0d9465471092..aed10846103c 100644 --- a/lib/msf/core/exploit/http/client.rb +++ b/lib/msf/core/exploit/http/client.rb @@ -30,6 +30,10 @@ def initialize(info = {}) ], self.class ) + if framework.features.enabled?("RHOST_HTTP_URL") + register_options([Opt::RHOST_HTTP_URL]) + end + register_advanced_options( [ OptString.new('UserAgent', [false, 'The User-Agent header to use for all requests', diff --git a/lib/msf/core/feature_manager.rb b/lib/msf/core/feature_manager.rb new file mode 100644 index 000000000000..b81f3b90824e --- /dev/null +++ b/lib/msf/core/feature_manager.rb @@ -0,0 +1,96 @@ +# -*- coding: binary -*- +# frozen_string_literal: true + +require 'msf/core/plugin' + +module Msf + ### + # + # The feature manager is responsible for managing feature flags that can change characteristics of framework. + # Each feature will have a default value. The user can choose to override this default value if they wish. + ### + class FeatureManager + + include Singleton + + CONFIG_KEY = 'framework/features' + WRAPPED_TABLES = 'wrapped_tables' + DEFAULTS = [ + { + name: 'wrapped_tables', + description: 'When enabled Metasploit will wordwrap all tables to fit into the available terminal width', + default_value: false + }.freeze, + { + name: 'RHOST_HTTP_URL', + description: 'When enabled in supported modules you can specify a URL as a target', + default_value: false + }.freeze + ].freeze + + # + # Initializes the feature manager. + # + def initialize + @flag_lookup = DEFAULTS.each_with_object({}) do |feature, acc| + key = feature[:name] + acc[key] = feature.dup + end + end + + def all + @flag_lookup.values.map do |feature| + feature.slice(:name, :description).merge(enabled: enabled?(feature[:name])) + end + end + + def enabled?(name) + return false unless @flag_lookup[name] + + feature = @flag_lookup[name] + feature.key?(:user_preference) ? feature[:user_preference] : feature[:default_value] + end + + def exists?(name) + @flag_lookup.key?(name) + end + + def names + all.map { |feature| feature[:name] } + end + + def set(name, value) + return false unless @flag_lookup[name] + + @flag_lookup[name][:user_preference] = value + + if name == WRAPPED_TABLES + if value + Rex::Text::Table.wrap_tables! + else + Rex::Text::Table.unwrap_tables! + end + end + end + + def load_config + conf = Msf::Config.load + conf.fetch(CONFIG_KEY, {}).each do |name, value| + set(name, value == 'true') + end + end + + def save_config + # Note, we intentionally omit features that have not explicitly been set by the user. + config = Msf::Config.load + old_config = config.fetch(CONFIG_KEY, {}) + new_config = @flag_lookup.values.each_with_object(old_config) do |feature, config| + next unless feature.key?(:user_preference) + + config.merge!(feature[:name] => feature[:user_preference].to_s) + end + + Msf::Config.save(CONFIG_KEY => new_config) + end + end +end diff --git a/lib/msf/core/framework.rb b/lib/msf/core/framework.rb index 4a4722e9c53e..be62b37e22d3 100644 --- a/lib/msf/core/framework.rb +++ b/lib/msf/core/framework.rb @@ -81,6 +81,7 @@ def initialize(options={}) self.analyze = Analyze.new(self) self.plugins = PluginManager.new(self) self.browser_profiles = Hash.new + self.features = FeatureManager.instance # Configure the thread factory Rex::ThreadFactory.provider = Metasploit::Framework::ThreadFactoryProvider.new(framework: self) @@ -195,6 +196,11 @@ def version # framework objects to offer related objects/actions available. # attr_reader :analyze + # + # The framework instance's feature manager. The feature manager is responsible + # for configuring feature flags that can change characteristics of framework. + # + attr_reader :features # # The framework instance's dependency @@ -277,6 +283,7 @@ def search(match, logger: nil) attr_writer :db # :nodoc: attr_writer :browser_profiles # :nodoc: attr_writer :analyze # :nodoc: + attr_writer :features # :nodoc: private diff --git a/lib/msf/core/opt.rb b/lib/msf/core/opt.rb index 20f8cc3a2f8c..5d557da4ed9c 100644 --- a/lib/msf/core/opt.rb +++ b/lib/msf/core/opt.rb @@ -14,88 +14,83 @@ module Msf # register_advanced_options([Opt::Proxies]) # module Opt - # @return [OptAddress] - def self.CHOST(default=nil, required=false, desc="The local client address") + def self.CHOST(default = nil, required = false, desc = 'The local client address') Msf::OptAddress.new(__method__.to_s, [ required, desc, default ]) end # @return [OptPort] - def self.CPORT(default=nil, required=false, desc="The local client port") + def self.CPORT(default = nil, required = false, desc = 'The local client port') Msf::OptPort.new(__method__.to_s, [ required, desc, default ]) end # @return [OptAddressLocal] - def self.LHOST(default=nil, required=true, desc="The listen address (an interface may be specified)") + def self.LHOST(default = nil, required = true, desc = 'The listen address (an interface may be specified)') Msf::OptAddressLocal.new(__method__.to_s, [ required, desc, default ]) end # @return [OptPort] - def self.LPORT(default=nil, required=true, desc="The listen port") + def self.LPORT(default = nil, required = true, desc = 'The listen port') Msf::OptPort.new(__method__.to_s, [ required, desc, default ]) end # @return [OptString] - def self.Proxies(default=nil, required=false, desc="A proxy chain of format type:host:port[,type:host:port][...]") + def self.Proxies(default = nil, required = false, desc = 'A proxy chain of format type:host:port[,type:host:port][...]') Msf::OptString.new(__method__.to_s, [ required, desc, default ]) end # @return [OptAddressRange] - def self.RHOSTS(default=nil, required=true, desc="The target host(s), range CIDR identifier, or hosts file with syntax 'file:'") + def self.RHOSTS(default = nil, required = true, desc = "The target host(s), range CIDR identifier, or hosts file with syntax 'file:'") Msf::OptAddressRange.new('RHOSTS', [ required, desc, default ]) end - def self.RHOST(default=nil, required=true, desc="The target host(s), range CIDR identifier, or hosts file with syntax 'file:'") + def self.RHOST(default = nil, required = true, desc = "The target host(s), range CIDR identifier, or hosts file with syntax 'file:'") Msf::OptAddressRange.new('RHOSTS', [ required, desc, default ], aliases: [ 'RHOST' ]) end # @return [OptPort] - def self.RPORT(default=nil, required=true, desc="The target port") + def self.RPORT(default = nil, required = true, desc = 'The target port') Msf::OptPort.new(__method__.to_s, [ required, desc, default ]) end # @return [OptEnum] def self.SSLVersion Msf::OptEnum.new('SSLVersion', - 'Specify the version of SSL/TLS to be used (Auto, TLS and SSL23 are auto-negotiate)', - enums: Rex::Socket::SslTcp.supported_ssl_methods - ) + 'Specify the version of SSL/TLS to be used (Auto, TLS and SSL23 are auto-negotiate)', + enums: Rex::Socket::SslTcp.supported_ssl_methods) + end + + def self.RHOST_HTTP_URL(default = nil, required = false, desc = 'The target URL, only applicable if there is a single URL') + Msf::OptHTTPRhostURL.new(__method__.to_s, [required, desc, default ]) end def self.stager_retry_options [ OptInt.new('StagerRetryCount', - 'The number of times the stager should retry if the first connect fails', - default: 10, - aliases: ['ReverseConnectRetries'] - ), + 'The number of times the stager should retry if the first connect fails', + default: 10, + aliases: ['ReverseConnectRetries']), OptInt.new('StagerRetryWait', - 'Number of seconds to wait for the stager between reconnect attempts', - default: 5 - ) + 'Number of seconds to wait for the stager between reconnect attempts', + default: 5) ] end def self.http_proxy_options [ OptString.new('HttpProxyHost', 'An optional proxy server IP address or hostname', - aliases: ['PayloadProxyHost'] - ), + aliases: ['PayloadProxyHost']), OptPort.new('HttpProxyPort', 'An optional proxy server port', - aliases: ['PayloadProxyPort'] - ), + aliases: ['PayloadProxyPort']), OptString.new('HttpProxyUser', 'An optional proxy server username', - aliases: ['PayloadProxyUser'], - max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1 - ), + aliases: ['PayloadProxyUser'], + max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1), OptString.new('HttpProxyPass', 'An optional proxy server password', - aliases: ['PayloadProxyPass'], - max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1 - ), + aliases: ['PayloadProxyPass'], + max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1), OptEnum.new('HttpProxyType', 'The type of HTTP proxy', - enums: ['HTTP', 'SOCKS'], - aliases: ['PayloadProxyType'] - ) + enums: ['HTTP', 'SOCKS'], + aliases: ['PayloadProxyType']) ] end @@ -114,6 +109,7 @@ def self.http_header_options Proxies = Proxies() RHOST = RHOST() RHOSTS = RHOSTS() + RHOST_HTTP_URL = RHOST_HTTP_URL() RPORT = RPORT() SSLVersion = SSLVersion() end diff --git a/lib/msf/core/opt_http_rhost_url.rb b/lib/msf/core/opt_http_rhost_url.rb new file mode 100644 index 000000000000..793fbc553515 --- /dev/null +++ b/lib/msf/core/opt_http_rhost_url.rb @@ -0,0 +1,91 @@ +# -*- coding: binary -*- + +module Msf + ### + # + # RHOST URL option. + # + ### + class OptHTTPRhostURL < OptBase + def type + 'rhost http url' + end + + def normalize(value) + return unless value + + uri = get_uri(value) + return unless uri + + option_hash = {} + # Blank this out since we don't know if this new value will have a `VHOST` to ensure we remove the old value + option_hash['VHOST'] = nil + + option_hash['RHOSTS'] = uri.hostname + option_hash['RPORT'] = uri.port + option_hash['SSL'] = %w[ssl https].include?(uri.scheme) + + # Both `TARGETURI` and `URI` are used as datastore options to denote the path on a uri + option_hash['TARGETURI'] = uri.path || '/' + option_hash['URI'] = uri.path || '/' + + if uri.scheme && %(http https).include?(uri.scheme) + option_hash['VHOST'] = uri.hostname unless Rex::Socket.is_ip_addr?(uri.hostname) + option_hash['HttpUsername'] = uri.user.to_s + option_hash['HttpPassword'] = uri.password.to_s + end + + option_hash + end + + def valid?(value, check_empty: false) + return true unless value || required + + uri = get_uri(value) + return false unless uri && !uri.host.nil? && !uri.port.nil? + + super + end + + def calculate_value(datastore) + return unless datastore['RHOSTS'] + begin + uri_type = datastore['SSL'] ? URI::HTTPS : URI::HTTP + uri = uri_type.build(host: datastore['RHOSTS']) + uri.port = datastore['RPORT'] + # The datastore uses both `TARGETURI` and `URI` to denote the path of a URL, we try both here and fall back to `/` + uri.path = (datastore['TARGETURI'] || datastore['URI'] || '/') + uri.user = datastore['HttpUsername'] + uri.password = datastore['HttpPassword'] if uri.user + uri + rescue URI::InvalidComponentError + nil + end + end + + protected + + def get_uri(value) + return unless value + return if check_for_range(value) + + value = 'http://' + value unless value.start_with?(%r{https?://}) + URI(value) + rescue URI::InvalidURIError + nil + end + + def check_for_range(value) + return false if value =~ /[^-0-9,.*\/]/ + walker = Rex::Socket::RangeWalker.new(value) + if walker&.valid? + # if there is only a single ip then it's not a range + return walker.length != 1 + end + rescue ::Exception + # couldn't create a range therefore it isn't one + return false + end + + end +end diff --git a/lib/msf/core/opt_raw.rb b/lib/msf/core/opt_raw.rb index b1443341651c..014296cb11da 100644 --- a/lib/msf/core/opt_raw.rb +++ b/lib/msf/core/opt_raw.rb @@ -28,7 +28,7 @@ def normalize(value) value end - def valid?(value=self.value) + def valid?(value=self.value, check_empty: true) value = normalize(value) return false if empty_required_value?(value) return super diff --git a/lib/msf/core/option_container.rb b/lib/msf/core/option_container.rb index c9b213e9c03d..fd07a7ddc295 100644 --- a/lib/msf/core/option_container.rb +++ b/lib/msf/core/option_container.rb @@ -18,6 +18,7 @@ module Msf autoload :OptRaw, 'msf/core/opt_raw' autoload :OptRegexp, 'msf/core/opt_regexp' autoload :OptString, 'msf/core/opt_string' + autoload :OptHTTPRhostURL, 'msf/core/opt_http_rhost_url' # # The options purpose in life is to associate named options with arbitrary diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index e23400f09f22..06658721f59e 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -113,6 +113,7 @@ def commands "color" => "Toggle color", "debug" => "Display information useful for debugging", "exit" => "Exit the console", + "features" => "Display the list of not yet released features that can be opted in to", "get" => "Gets the value of a context-specific variable", "getg" => "Gets the value of a global variable", "grep" => "Grep the output of another command", @@ -563,6 +564,120 @@ def cmd_exit(*args) alias cmd_quit cmd_exit + def cmd_features_help + print_line <<~CMD_FEATURE_HELP + Enable or disable unreleased features that Metasploit supports + + Usage: + features set feature_name [true/false] + features print + + Subcommands: + set - Enable or disable a given feature + print - show all available features and their current configuration + + Examples: + View available features: + features print + + Enable a feature: + features set new_feature true + + Disable a feature: + features set new_feature false + CMD_FEATURE_HELP + end + + # + # This method handles the features command which allows a user to opt into enabling + # features that are not yet released to everyone by default. + # + def cmd_features(*args) + args << 'print' if args.empty? + + action, *rest = args + case action + when 'set' + feature_name, value = rest + + unless framework.features.exists?(feature_name) + print_warning("Feature name '#{feature_name}' is not available. Either it has been removed, integrated by default, or does not exist in this version of Metasploit.") + print_warning("Currently supported features: #{framework.features.names.join(', ')}") if framework.features.all.any? + print_warning('There are currently no features to toggle.') if framework.features.all.empty? + return + end + + unless %w[true false].include?(value) + print_warning('Please specify true or false to configure this feature.') + return + end + + framework.features.set(feature_name, value == 'true') + print_line("#{feature_name} => #{value}") + when 'print' + if framework.features.all.empty? + print_line 'There are no features to enable at this time. Either the features have been removed, or integrated by default.' + return + end + + features_table = Table.new( + Table::Style::Default, + 'Header' => 'Features table', + 'Prefix' => "\n", + 'Postfix' => "\n", + 'Columns' => [ + '#', + 'Name', + 'Enabled', + 'Description', + ] + ) + + framework.features.all.each.with_index do |feature, index| + features_table << [ + index, + feature[:name], + feature[:enabled].to_s, + feature[:description] + ] + end + + print_line features_table.to_s + else + cmd_help + end + rescue StandardError => e + elog(e) + print_error(e.message) + end + + # + # Tab completion for the features command + # + # @param str [String] the string currently being typed before tab was hit + # @param words [Array] the previously completed words on the command line. words is always + # at least 1 when tab completion has reached this stage since the command itself has been completed + def cmd_features_tabs(_str, words) + if words.length == 1 + return %w[set print] + end + + _command_name, action, *rest = words + ret = [] + case action + when 'set' + feature_name, _value = rest + + if framework.features.exists?(feature_name) + ret += %w[true false] + else + ret += framework.features.names + end + end + + ret + end + def cmd_history(*args) length = Readline::HISTORY.length @@ -1127,6 +1242,12 @@ def cmd_save(*args) # Save the console config driver.save_config + begin + FeatureManager.instance.save_config + rescue StandardException => e + elog(e) + end + # Save the framework's datastore begin framework.save_config @@ -1599,15 +1720,11 @@ def cmd_set_help print_line end - # - # Sets a name to a value in a context aware environment. - # def cmd_set(*args) - # Figure out if these are global variables global = false - if (args[0] == '-g') + if args[0] == '-g' args.shift global = true end @@ -1615,37 +1732,29 @@ def cmd_set(*args) # Decide if this is an append operation append = false - if (args[0] == '-a') + if args[0] == '-a' args.shift append = true end - # Determine which data store we're operating on - if (active_module and global == false) - datastore = active_module.datastore - else - global = true - datastore = self.framework.datastore - end - # Dump the contents of the active datastore if no args were supplied - if (args.length == 0) + if args.length == 0 # If we aren't dumping the global data store, then go ahead and # dump it first - if (!global) + unless global print("\n" + - Msf::Serializer::ReadableText.dump_datastore( - "Global", framework.datastore)) + Msf::Serializer::ReadableText.dump_datastore( + "Global", framework.datastore)) end # Dump the active datastore print("\n" + - Msf::Serializer::ReadableText.dump_datastore( - (global) ? "Global" : "Module: #{active_module.refname}", - datastore) + "\n") + Msf::Serializer::ReadableText.dump_datastore( + (global) ? "Global" : "Module: #{active_module.refname}", + datastore) + "\n") return true - elsif (args.length == 1) - if (not datastore[args[0]].nil?) + elsif args.length == 1 + if not datastore[args[0]].nil? print_line("#{args[0]} => #{datastore[args[0]]}") return true else @@ -1659,6 +1768,22 @@ def cmd_set(*args) name = args[0] value = args[1, args.length-1].join(' ') + set_option(name, value, global: global, append: append) + end + + # + # Sets a name to a value in a context aware environment. + # + def set_option(name, value, global: false, append: false) + + # Determine which data store we're operating on + if active_module and !global + datastore = active_module.datastore + else + global = true + datastore = self.framework.datastore + end + # Set PAYLOAD if name.upcase == 'PAYLOAD' && active_module && (active_module.exploit? || active_module.evasion?) value = trim_path(value, 'payload') @@ -1673,7 +1798,7 @@ def cmd_set(*args) end # If the driver indicates that the value is not valid, bust out. - if (driver.on_variable_set(global, name, value) == false) + if driver.on_variable_set(global, name, value) == false print_error("The value specified for #{name} is not valid.") return false end diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index 13efc255ae19..e9a188fb06be 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -132,6 +132,12 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = { load_db_config(opts['Config']) + begin + FeatureManager.instance.load_config + rescue StandardException => e + elog(e) + end + if !framework.db || !framework.db.active if framework.db.error == "disabled" print_warning("Database support has been disabled") diff --git a/lib/msf/ui/console/table.rb b/lib/msf/ui/console/table.rb index d97afb114117..3d5b6b4a1c81 100644 --- a/lib/msf/ui/console/table.rb +++ b/lib/msf/ui/console/table.rb @@ -18,13 +18,10 @@ module Style Default = 0 end - # - # Initializes a wrappered table with the supplied style and options. - # - def initialize(style, opts = {}) - self.style = style + def self.new(*args, &block) + style, opts = args - if (self.style == Style::Default) + if style == Style::Default opts['Indent'] = 3 if (!opts['Prefix']) opts['Prefix'] = "\n" @@ -32,28 +29,26 @@ def initialize(style, opts = {}) if (!opts['Postfix']) opts['Postfix'] = "\n" end + end - super(opts) + instance = super(opts, &block) + if style == Style::Default + instance.extend(DefaultStyle) end + instance end - # - # Print nothing if there are no rows if the style is default. - # - def to_s - if (style == Style::Default) + module DefaultStyle + # + # Print nothing if there are no rows if the style is default. + # + def to_s return '' if (rows.length == 0) - end - super + super + end end - -protected - - attr_accessor :style # :nodoc: - end - end end end diff --git a/lib/msf/ui/debug.rb b/lib/msf/ui/debug.rb index 85067dfa60a7..fe37fb70201f 100644 --- a/lib/msf/ui/debug.rb +++ b/lib/msf/ui/debug.rb @@ -48,7 +48,7 @@ def self.datastore(framework, driver) # Delete all groups from the config ini that potentially have more up to date information ini.keys.each do |key| - unless key =~ %r{^framework/database} + unless key.start_with?("framework/database") || key.start_with?("framework/features") ini.delete(key) end end diff --git a/spec/lib/metasploit/framework/aws/client_spec.rb b/spec/lib/metasploit/framework/aws/client_spec.rb index cbc9cc0d508b..351324cee8cd 100644 --- a/spec/lib/metasploit/framework/aws/client_spec.rb +++ b/spec/lib/metasploit/framework/aws/client_spec.rb @@ -4,9 +4,12 @@ RSpec.describe Metasploit::Framework::Aws::Client do subject do - s = Class.new(Msf::Auxiliary) do + mod_klass = Class.new(Msf::Auxiliary) do include Metasploit::Framework::Aws::Client - end.new + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + s = mod_klass.new s.datastore['Region'] = 'us-east-1' s.datastore['RHOST'] = '127.0.0.1' s diff --git a/spec/lib/msf/core/exploit/http/jboss/base_spec.rb b/spec/lib/msf/core/exploit/http/jboss/base_spec.rb index 660bc8b2be7f..eaad39d044dd 100644 --- a/spec/lib/msf/core/exploit/http/jboss/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/jboss/base_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::Base do subject do - mod = ::Msf::Exploit.new - mod.extend Msf::Exploit::Remote::HTTP::JBoss - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include Msf::Exploit::Remote::HTTP::JBoss + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe "#deploy" do diff --git a/spec/lib/msf/core/exploit/http/jboss/bean_shell_scripts_spec.rb b/spec/lib/msf/core/exploit/http/jboss/bean_shell_scripts_spec.rb index ac10a2504fbc..c1b6901f816f 100644 --- a/spec/lib/msf/core/exploit/http/jboss/bean_shell_scripts_spec.rb +++ b/spec/lib/msf/core/exploit/http/jboss/bean_shell_scripts_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::BeanShellScripts do subject do - mod = ::Msf::Exploit.new - mod.extend Msf::Exploit::Remote::HTTP::JBoss - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include Msf::Exploit::Remote::HTTP::JBoss + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe "#generate_bsh" do diff --git a/spec/lib/msf/core/exploit/http/jboss/bean_shell_spec.rb b/spec/lib/msf/core/exploit/http/jboss/bean_shell_spec.rb index bc633e6dda96..502761f89f4c 100644 --- a/spec/lib/msf/core/exploit/http/jboss/bean_shell_spec.rb +++ b/spec/lib/msf/core/exploit/http/jboss/bean_shell_spec.rb @@ -7,10 +7,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::BeanShell do subject do - mod = ::Msf::Exploit.new - mod.extend Msf::Exploit::Remote::HTTP::JBoss - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include Msf::Exploit::Remote::HTTP::JBoss + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end before :example do diff --git a/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_scripts_spec.rb b/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_scripts_spec.rb index dca1a5778cd3..53b8107208fd 100644 --- a/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_scripts_spec.rb +++ b/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_scripts_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::DeploymentFileRepositoryScripts do subject do - mod = ::Msf::Exploit.new - mod.extend Msf::Exploit::Remote::HTTP::JBoss - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include Msf::Exploit::Remote::HTTP::JBoss + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe "#stager_jsp_with_payload" do diff --git a/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_spec.rb b/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_spec.rb index 5270a1a128b6..0ac5b616724b 100644 --- a/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_spec.rb +++ b/spec/lib/msf/core/exploit/http/jboss/deployment_file_repository_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::DeploymentFileRepository do subject do - mod = ::Msf::Exploit.new - mod.extend Msf::Exploit::Remote::HTTP::JBoss - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include Msf::Exploit::Remote::HTTP::JBoss + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end let (:base_name) do diff --git a/spec/lib/msf/core/exploit/http/joomla/base_spec.rb b/spec/lib/msf/core/exploit/http/joomla/base_spec.rb index 0edb4e107c8e..4374548f9ed7 100644 --- a/spec/lib/msf/core/exploit/http/joomla/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/joomla/base_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::Joomla::Base do subject do - mod = ::Msf::Exploit.new - mod.extend ::Msf::Exploit::Remote::HTTP::Joomla - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include ::Msf::Exploit::Remote::HTTP::Joomla + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end let(:joomla_body) do diff --git a/spec/lib/msf/core/exploit/http/joomla/version_spec.rb b/spec/lib/msf/core/exploit/http/joomla/version_spec.rb index 0ad5f7d2f784..3f8bd7a9aad9 100644 --- a/spec/lib/msf/core/exploit/http/joomla/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/joomla/version_spec.rb @@ -5,10 +5,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::Joomla::Version do subject do - mod = ::Msf::Exploit.new - mod.extend ::Msf::Exploit::Remote::HTTP::Joomla - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include ::Msf::Exploit::Remote::HTTP::Joomla + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end # From /joomla/language/en-GB/en-GB.xml diff --git a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb index d1e622e519a7..107f92a9fb48 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb @@ -8,10 +8,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Base do subject do - mod = ::Msf::Exploit.new - mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include ::Msf::Exploit::Remote::HTTP::Wordpress + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe '#wordpress_and_online?' do diff --git a/spec/lib/msf/core/exploit/http/wordpress/login_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/login_spec.rb index 48c2ee63bfe5..4256ae3fe203 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/login_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/login_spec.rb @@ -8,10 +8,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Login do subject do - mod = ::Msf::Exploit.new - mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include ::Msf::Exploit::Remote::HTTP::Wordpress + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe '#wordpress_login' do diff --git a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb index 328883b4985f..706ed05caa4c 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb @@ -8,10 +8,12 @@ RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Version do subject do - mod = ::Msf::Exploit.new - mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress - mod.send(:initialize) - mod + mod_klass = Class.new(::Msf::Exploit) do + include ::Msf::Exploit::Remote::HTTP::Wordpress + end + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end describe '#wordpress_version' do diff --git a/spec/lib/msf/core/feature_manager_spec.rb b/spec/lib/msf/core/feature_manager_spec.rb new file mode 100644 index 000000000000..7187260fb081 --- /dev/null +++ b/spec/lib/msf/core/feature_manager_spec.rb @@ -0,0 +1,222 @@ +# -*- coding:binary -*- + +require 'spec_helper' +require 'msf/core/feature_manager' + +RSpec.describe Msf::FeatureManager do + let(:mock_features) do + [ + { + name: 'filtered_options', + description: 'Add option filtering functionality to Metasploit', + default_value: false + }, + { + name: 'new_search_capabilities', + description: 'Add new search capabilities to Metasploit', + default_value: true + } + ] + end + let(:subject) { described_class.send(:new) } + + before(:each) do + stub_const('Msf::FeatureManager::DEFAULTS', mock_features) + end + + describe '#all' do + let(:expected_features) do + [ + { + name: 'filtered_options', + description: 'Add option filtering functionality to Metasploit', + enabled: false + }, + { + name: 'new_search_capabilities', + description: 'Add new search capabilities to Metasploit', + enabled: true + } + ] + end + it { expect(subject.all).to eql expected_features } + end + + describe '#enabled?' do + it { expect(subject.enabled?('missing_option')).to be false } + it { expect(subject.enabled?('filtered_options')).to be false } + it { expect(subject.enabled?('new_search_capabilities')).to be true } + end + + describe '#exists?' do + it { expect(subject.exists?('missing_option')).to be false } + it { expect(subject.exists?('filtered_options')).to be true } + it { expect(subject.exists?('new_search_capabilities')).to be true } + end + + describe 'names' do + it { expect(subject.names).to eq ['filtered_options', 'new_search_capabilities'] } + end + + describe '#set' do + context 'when a flag is enabled' do + before(:each) do + subject.set('filtered_options', true) + end + + it { expect(subject.enabled?('missing_option')).to be false } + it { expect(subject.enabled?('filtered_options')).to be true } + it { expect(subject.enabled?('new_search_capabilities')).to be true } + end + + context 'when a flag is disabled' do + before(:each) do + subject.set('new_search_capabilities', false) + end + + it { expect(subject.enabled?('missing_option')).to be false } + it { expect(subject.enabled?('filtered_options')).to be false } + it { expect(subject.enabled?('new_search_capabilities')).to be false } + end + + context 'when the flag does not exist' do + before(:each) do + subject.set('missing_option', false) + end + + it { expect(subject.enabled?('missing_option')).to be false } + it { expect(subject.enabled?('filtered_options')).to be false } + it { expect(subject.enabled?('new_search_capabilities')).to be true } + end + end + + describe "#load_config" do + before(:each) do + allow(Msf::Config).to receive(:load).and_return(Rex::Parser::Ini.from_s(config)) + subject.load_config + end + + context 'when the config file is empty' do + let(:config) do + <<~CONFIG + + CONFIG + end + + let(:expected_features) do + [ + { + name: 'filtered_options', + description: 'Add option filtering functionality to Metasploit', + enabled: false + }, + { + name: 'new_search_capabilities', + description: 'Add new search capabilities to Metasploit', + enabled: true + } + ] + end + + it { expect(subject.all).to eql expected_features } + end + + context 'when there are valid and invalid flags' do + let(:config) do + <<~CONFIG + [framework/features] + new_search_capabilities=false + missing_feature=true + CONFIG + end + + let(:expected_features) do + [ + { + name: 'filtered_options', + description: 'Add option filtering functionality to Metasploit', + enabled: false + }, + { + name: 'new_search_capabilities', + description: 'Add new search capabilities to Metasploit', + enabled: false + } + ] + end + + it { expect(subject.all).to eql expected_features } + end + end + + describe '#save_config' do + before(:each) do + allow(Msf::Config).to receive(:load).and_return(Rex::Parser::Ini.from_s(config)) + allow(Msf::Config).to receive(:save) + end + + context 'when there is no existing configuration' do + before(:each) do + subject.save_config + end + + let(:config) do + <<~CONFIG + [framework/features] + CONFIG + end + + let(:expected_config) do + { + "framework/features" => {} + } + end + + it { expect(Msf::Config).to have_received(:save).with(expected_config) } + end + + context 'when there is only a missing feature' do + before(:each) do + subject.save_config + end + + let(:config) do + <<~CONFIG + [framework/features] + missing_feature=true + CONFIG + end + + let(:expected_config) do + { + "framework/features" => { "missing_feature" => "true" } + } + end + + it { expect(Msf::Config).to have_received(:save).with(expected_config) } + end + + context 'when there are user preferences set' do + before(:each) do + subject.set('new_search_capabilities', true) + subject.save_config + end + + let(:config) do + <<~CONFIG + [framework/features] + new_search_capabilities=false + missing_feature=true + CONFIG + end + + let(:expected_config) do + { + "framework/features" => { "missing_feature" => "true", "new_search_capabilities"=>"true" } + } + end + + it { expect(Msf::Config).to have_received(:save).with(expected_config) } + end + end +end diff --git a/spec/lib/msf/core/opt_http_rhost_url_spec.rb b/spec/lib/msf/core/opt_http_rhost_url_spec.rb new file mode 100644 index 000000000000..158769fd7f79 --- /dev/null +++ b/spec/lib/msf/core/opt_http_rhost_url_spec.rb @@ -0,0 +1,61 @@ +require 'msf/core/opt_http_rhost_url' + +RSpec.describe Msf::OptHTTPRhostURL do + subject(:opt) { described_class.new('RHOST_HTTP_URL') } + + valid_values = [ + { value: 'http://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'https://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 443, 'SSL' => true, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'http://user:pass@example.com:1234/somePath', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 1234, 'SSL' => false, 'TARGETURI' => '/somePath', 'URI' => '/somePath', 'VHOST' => 'example.com', 'HttpUsername' => 'user', 'HttpPassword' => 'pass' } }, + { value: 'http://127.0.0.1', normalized: { 'RHOSTS' => '127.0.0.1', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => nil, 'HttpUsername' => '', 'HttpPassword' => '' } } + ] + + invalid_values = [ + { value: '192.0.2.0/24' }, + { value: '192.0.2.0-255' }, + { value: '192.0.2.0,1-255' }, + { value: '192.0.2.*' }, + { value: '192.0.2.0-192.0.2.255' } + ] + + it_behaves_like 'an option', valid_values, invalid_values, 'rhost http url' + + + calculate_values = [ + { value: 'http://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'https://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 443, 'SSL' => true, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'http://user:pass@example.com:1234/somePath', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 1234, 'SSL' => false, 'TARGETURI' => '/somePath', 'URI' => '/somePath', 'VHOST' => 'example.com', 'HttpUsername' => 'user', 'HttpPassword' => 'pass' } }, + { value: 'http://127.0.0.1', normalized: { 'RHOSTS' => '127.0.0.1', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => nil, 'HttpUsername' => '', 'HttpPassword' => '' } } + ] + + calculate_values_no_schema = [ + { value: 'example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'example.com:443', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 443, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } }, + { value: 'user:pass@example.com:1234/somePath', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 1234, 'SSL' => false, 'TARGETURI' => '/somePath', 'URI' => '/somePath', 'VHOST' => 'example.com', 'HttpUsername' => 'user', 'HttpPassword' => 'pass' } }, + { value: '127.0.0.1', normalized: { 'RHOSTS' => '127.0.0.1', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => nil, 'HttpUsername' => '', 'HttpPassword' => '' } } + ] + + describe '#calculate_value' do + calculate_values.each do | url | + + context url[:normalized].to_s do + it "should return #{url[:value]}" do + expect(subject.calculate_value(url[:normalized])).to eq(URI(url[:value])) + end + end + end + end + + describe '#calculate_value missing scheme' do + calculate_values_no_schema.each do | url | + + context url[:normalized].to_s do + it "should return #{url[:value]}" do + expect(subject.calculate_value(url[:normalized])).to eq(URI('http://' + url[:value])) + end + end + end + end + +end diff --git a/spec/tools/md5_lookup_spec.rb b/spec/tools/md5_lookup_spec.rb index 889038b53160..79117ad7c268 100644 --- a/spec/tools/md5_lookup_spec.rb +++ b/spec/tools/md5_lookup_spec.rb @@ -55,7 +55,10 @@ end subject do - Md5LookupUtility::Md5Lookup.new + mod_klass = Md5LookupUtility::Md5Lookup + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new end def set_expected_response(body) @@ -253,7 +256,11 @@ def stub_load(with_setting=true) describe '#get_hash_results' do context 'when a hash is found' do it 'yields a result' do - search_engine = Md5LookupUtility::Md5Lookup.new + mod_klass = Md5LookupUtility::Md5Lookup + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + search_engine = mod_klass.new + allow(search_engine).to receive(:lookup).and_return(good_result) allow(Md5LookupUtility::Md5Lookup).to receive(:new).and_return(search_engine) diff --git a/spec/tools/virustotal_spec.rb b/spec/tools/virustotal_spec.rb index 09803e540611..04d936226106 100644 --- a/spec/tools/virustotal_spec.rb +++ b/spec/tools/virustotal_spec.rb @@ -87,7 +87,11 @@ let(:vt) do file = double(File, read: malware_data) allow(File).to receive(:open).with(filename, 'rb') {|&block| block.yield file} - VirusTotalUtility::VirusTotal.new({'api_key'=>api_key, 'sample'=>filename}) + + mod_klass = VirusTotalUtility::VirusTotal + features = instance_double(Msf::FeatureManager, enabled?: false) + mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {}) + mod_klass.new({'api_key'=>api_key, 'sample'=>filename}) end context ".Initializer" do