Files
metasploit-gs/lib/msf/core/data_store.rb
T
Simon Janusz 8f00370370 Make datastore to_h sane (#19890)
* Bump metasploit_data_models gem

* Make datastore to_h sane
2025-02-18 15:54:53 +00:00

548 lines
14 KiB
Ruby

# -*- coding: binary -*-
module Msf
###
#
# The data store is just a bitbucket that holds keyed values. It is used
# by various classes to hold option values and other state information.
#
###
class DataStore
# The global framework datastore doesn't currently import options
# For now, store an ad-hoc list of keys that the shell handles
#
# This list could be removed if framework's bootup sequence registers
# these as datastore options
GLOBAL_KEYS = %w[
ConsoleLogging
LogLevel
MinimumRank
SessionLogging
TimestampOutput
Prompt
PromptChar
PromptTimeFormat
MeterpreterPrompt
SessionTlvLogging
]
#
# Initializes the data store's internal state.
#
def initialize
@options = Hash.new
@aliases = Hash.new
# default values which will be referenced when not defined by the user
@defaults = Hash.new
# values explicitly defined, which take precedence over default values
@user_defined = Hash.new
end
# @return [Hash{String => Msf::OptBase}] The options associated with this datastore. Used for validating values/defaults/etc
attr_accessor :options
#
# Returns a hash of user-defined datastore values. The returned hash does
# not include default option values.
#
# @return [Hash<String, Object>] values explicitly defined on the data store which will override any default datastore values
attr_accessor :user_defined
#
# Was this entry actually set or just using its default
#
# @return [TrueClass, FalseClass]
def default?(key)
search_for(key).default?
end
#
# Clears the imported flag for the supplied key since it's being set
# directly.
#
def []=(k, v)
k = find_key_case(k)
opt = @options[k]
unless opt.nil?
if opt.validate_on_assignment?
unless opt.valid?(v, check_empty: false)
raise Msf::OptionValidateError.new(["Value '#{v}' is not valid for option '#{k}'"])
end
v = opt.normalize(v)
end
end
@user_defined[k] = v
end
#
# Case-insensitive wrapper around hash lookup
#
def [](k)
search_result = search_for(k)
search_result.value
end
#
# Case-insensitive wrapper around store; Skips option validation entirely
#
def store(k,v)
@user_defined[find_key_case(k)] = v
end
#
# Updates a value in the datastore with the specified name, k, to the
# specified value, v. Skips option validation entirely.
#
def update_value(k, v)
store(k, v)
end
#
# unset the current key from the datastore
# @param [String] key The key to search for
def unset(key)
k = find_key_case(key)
search_result = search_for(k)
@user_defined.delete(k)
search_result.value
end
# @deprecated use #{unset} instead, or set the value explicitly to nil
# @param [String] key The key to search for
def delete(key)
unset(key)
end
#
# Removes an option and any associated value
#
# @param [String] name the option name
# @return [nil]
def remove_option(name)
k = find_key_case(name)
@user_defined.delete(k)
@aliases.delete_if { |_, v| v.casecmp?(k) }
@options.delete_if { |option_name, _v| option_name.casecmp?(k) || option_name.casecmp?(name) }
nil
end
#
# This method is a helper method that imports the default value for
# all of the supplied options
#
def import_options(options, imported_by = nil, overwrite = true)
options.each_option do |name, option|
if self.options[name].nil? || overwrite
key = name
option.aliases.each do |a|
@aliases[a.downcase] = key.downcase
end
@options[key] = option
end
end
end
#
# Imports option values from a whitespace separated string in
# VAR=VAL format.
#
def import_options_from_s(option_str, delim = nil)
hash = {}
# Figure out the delimiter, default to space.
if (delim.nil?)
delim = /\s/
if (option_str.split('=').length <= 2 or option_str.index(',') != nil)
delim = ','
end
end
# Split on the delimiter
option_str.split(delim).each { |opt|
var, val = opt.split('=', 2)
next if (var =~ /^\s+$/)
# Invalid parse? Raise an exception and let those bastards know.
if (var == nil or val == nil)
var = "unknown" if (!var)
raise Rex::ArgumentParseError, "Invalid option specified: #{var}",
caller
end
# Remove trailing whitespaces from the value
val.gsub!(/\s+$/, '')
# Store the value
hash[var] = val
}
merge!(hash)
end
#
# Imports values from a hash and stores them in the datastore.
#
# @deprecated use {#merge!} instead
# @return [nil]
def import_options_from_hash(option_hash, imported = true, imported_by = nil)
merge!(option_hash)
end
# Update defaults from a hash. These merged values are not validated by default.
#
# @param [Hash<String, Object>] hash The default values that should be used by the datastore
# @param [Object] imported_by Who imported the defaults, not currently used
# @return [nil]
def import_defaults_from_hash(hash, imported_by:)
@defaults.merge!(hash)
end
# TODO: Doesn't normalize data in the same vein as:
# https://github.com/rapid7/metasploit-framework/pull/6644
# @deprecated Use {#import_options}
def import_option(key, val, imported = true, imported_by = nil, option = nil)
store(key, val)
if option
option.aliases.each do |a|
@aliases[a.downcase] = key.downcase
end
end
@options[key] = option
end
# @return [Array<String>] The array of user defined datastore values, and registered option names
def keys
(@user_defined.keys + @options.keys).uniq(&:downcase)
end
# @return [Integer] The length of the registered keys
def length
keys.length
end
alias count length
alias size length
# @param [String] key
# @return [TrueClass, FalseClass] True if the key is present in the user defined values, or within registered options. False otherwise.
def key?(key)
matching_key = find_key_case(key)
keys.include?(matching_key)
end
alias has_key? key?
alias include? key?
alias member? key?
#
# Serializes the options in the datastore to a string.
#
def to_s(delim = ' ')
str = ''
keys.sort.each { |key|
str << "#{key}=#{self[key]}" + ((str.length) ? delim : '')
}
str
end
# Override Hash's to_h method so we can include the original case of each key
# (failing to do this breaks a number of places in framework and pro that use
# serialized datastores)
def to_h
datastore_hash = {}
self.keys.each do |k|
datastore_hash[k.to_s] = self[k]
end
datastore_hash
end
# Hack on a hack for the external modules
def to_external_message_h
datastore_hash = {}
array_nester = ->(arr) do
if arr.first.is_a? Array
arr.map &array_nester
else
arr.map { |item| item.to_s.dup.force_encoding('UTF-8') }
end
end
self.keys.each do |k|
# TODO arbitrary depth
if self[k].is_a? Array
datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = array_nester.call(self[k])
else
datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = self[k].to_s.dup.force_encoding('UTF-8')
end
end
datastore_hash
end
#
# Persists the contents of the data store to a file
#
def to_file(path, name = 'global')
ini = Rex::Parser::Ini.new(path)
ini.add_group(name)
# Save all user-defined options to the file.
@user_defined.each_pair { |k, v|
ini[name][k] = v
}
ini.to_file(path)
end
#
# Imports datastore values from the specified file path using the supplied
# name
#
def from_file(path, name = 'global')
begin
ini = Rex::Parser::Ini.from_file(path)
rescue
return
end
if ini.group?(name)
merge!(ini[name])
end
end
#
# Return a copy of this datastore. Only string values will be duplicated, other values
# will share the same reference
# @return [Msf::DataStore] a new datastore instance
def copy
new_instance = self.class.new
new_instance.copy_state(self)
new_instance
end
#
# Merge the other object into the current datastore's aliases and imported hashes
#
# @param [Msf::Datastore, Hash] other
def merge!(other)
if other.is_a?(DataStore)
self.aliases.merge!(other.aliases)
self.options.merge!(other.options)
self.defaults.merge!(other.defaults)
other.user_defined.each do |k, v|
@user_defined[find_key_case(k)] = v
end
else
other.each do |k, v|
self.store(k, v)
end
end
self
end
alias update merge!
#
# Reverse Merge the other object into the current datastore's aliases and imported hashes
# Equivalent to ActiveSupport's reverse_merge! functionality.
#
# @param [Msf::Datastore] other
def reverse_merge!(other)
raise ArgumentError, "invalid error type #{other.class}, expected ::Msf::DataStore" unless other.is_a?(Msf::DataStore)
copy_state(other.merge(self))
end
#
# Override merge to ensure we merge the aliases and imported hashes
#
# @param [Msf::Datastore,Hash] other
def merge(other)
ds = self.copy
ds.merge!(other)
end
#
# Completely clear all values in the data store
#
def clear
self.options.clear
self.aliases.clear
self.defaults.clear
self.user_defined.clear
self
end
#
# Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+
# "can't add a new key into hash during iteration"
#
def each(&block)
list = []
self.keys.sort.each do |sidx|
list << [sidx, self[sidx]]
end
list.each(&block)
end
alias each_pair each
def each_key(&block)
self.keys.each(&block)
end
#
# Case-insensitive key lookup
#
# @return [String]
def find_key_case(k)
# Scan each alias looking for a key
search_k = k.downcase
if self.aliases.has_key?(search_k)
search_k = self.aliases[search_k]
end
# Check to see if we have an exact key match - otherwise we'll have to search manually to check case sensitivity
if @user_defined.key?(search_k) || options.key?(search_k)
return search_k
end
# Scan each key looking for a match
each_key do |rk|
if rk.casecmp(search_k) == 0
return rk
end
end
# Fall through to the non-existent value
k
end
# Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc.
#
# @param [String] key The key to search for
# @return [DataStoreSearchResult]
def search_for(key)
k = find_key_case(key)
return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k)
option = @options.fetch(k) { @options.find { |option_name, _option| option_name.casecmp?(k) }&.last }
if option
# If the key isn't present - check any additional fallbacks that have been registered with the option.
# i.e. handling the scenario of SMBUser not being explicitly set, but the option has registered a more
# generic 'Username' fallback
option.fallbacks.each do |fallback|
fallback_search = search_for(fallback)
if fallback_search.found?
return search_result(:option_fallback, fallback_search.value, fallback_key: fallback)
end
end
end
# Checking for imported default values, ignoring case again
imported_default_match = @defaults.find { |default_key, _default_value| default_key.casecmp?(k) }
return search_result(:imported_default, imported_default_match.last) if imported_default_match
return search_result(:option_default, option.default) if option
search_result(:not_found, nil)
end
protected
# These defaults will be used if the user has not explicitly defined a specific datastore value.
# These will be checked as a priority to any options that also provide defaults.
#
# @return [Hash{String => Msf::OptBase}] The hash of default values
attr_accessor :defaults
# @return [Hash{String => String}] The key is the old option name, the value is the new option name
attr_accessor :aliases
#
# Copy the state from the other Msf::DataStore. The state will be coped in a shallow fashion, other than
# imported and user_defined strings.
#
# @param [Msf::DataStore] other The other datastore to copy state from
# @return [Msf::DataStore] the current datastore instance
def copy_state(other)
self.options = other.options.dup
self.aliases = other.aliases.dup
self.defaults = other.defaults.transform_values { |value| value.kind_of?(String) ? value.dup : value }
self.user_defined = other.user_defined.transform_values { |value| value.kind_of?(String) ? value.dup : value }
self
end
# Raised when the specified key is not found
# @param [string] key
def key_error_for(key)
::KeyError.new "key not found: #{key.inspect}"
end
#
# Simple dataclass for storing the result of a datastore search
#
class DataStoreSearchResult
# @return [String, nil] the key associated with the fallback value
attr_reader :fallback_key
# @return [object, nil] The value if found
attr_reader :value
def initialize(result, value, namespace: nil, fallback_key: nil)
@namespace = namespace
@result = result
@value = value
@fallback_key = fallback_key
end
def default?
result == :imported_default || result == :option_default || !found?
end
def found?
result != :not_found
end
def fallback?
result == :option_fallback
end
def global?
namespace == :global_data_store && found?
end
protected
# @return [Symbol] namespace Where the search result was found, i.e. a module datastore or global datastore
attr_reader :namespace
# @return [Symbol] result is one of `user_defined`, `not_found`, `option_fallback`, `option_default`, `imported_default`
attr_reader :result
end
def search_result(result, value, fallback_key: nil)
DataStoreSearchResult.new(result, value, namespace: :global_data_store, fallback_key: fallback_key)
end
end
end