metasploit-framework/lib/msf/core/option_container.rb

736 lines
13 KiB
Ruby

require 'resolv'
require 'msf/core'
require 'rex/socket'
module Msf
###
#
# The base class for all options.
#
###
class OptBase
#
# Initializes a named option with the supplied attribute array.
# The array is composed of three values.
#
# attrs[0] = required (boolean type)
# attrs[1] = description (string)
# attrs[2] = default value
# attrs[3] = possible enum values
#
def initialize(in_name, attrs = [])
self.name = in_name
self.advanced = false
self.evasion = false
self.required = attrs[0] || false
self.desc = attrs[1]
self.default = attrs[2]
self.enums = [ *(attrs[3]) ].map { |x| x.to_s }
end
#
# Returns true if this is a required option.
#
def required?
return required
end
#
# Returns true if this is an advanced option.
#
def advanced?
return advanced
end
#
# Returns true if this is an evasion option.
#
def evasion?
return evasion
end
#
# Returns true if the supplied type is equivalent to this option's type.
#
def type?(in_type)
return (type == in_type)
end
#
# If it's required and the value is nil or empty, then it's not valid.
#
def valid?(value)
return (required? and (value == nil or value.to_s.empty?)) ? false : true
end
#
# Returns true if the value supplied is nil and it's required to be
# a valid value
#
def empty_required_value?(value)
return (required? and value.nil?)
end
#
# Normalizes the supplied value to conform with the type that the option is
# conveying.
#
def normalize(value)
value
end
#
# The name of the option.
#
attr_reader :name
#
# Whether or not the option is required.
#
attr_reader :required
#
# The description of the option.
#
attr_reader :desc
#
# The default value of the option.
#
attr_reader :default
#
# Storing the name of the option.
#
attr_writer :name
#
# Whether or not this is an advanced option.
#
attr_accessor :advanced
#
# Whether or not this is an evasion option.
#
attr_accessor :evasion
#
# The module or entity that owns this option.
#
attr_accessor :owner
#
# The list of potential valid values
#
attr_accessor :enums
protected
attr_writer :required, :desc, :default # :nodoc:
end
###
#
# Core option types. The core supported option types are:
#
# OptString - Multi-byte character string
# OptRaw - Multi-byte raw string
# OptBool - Boolean true or false indication
# OptPort - TCP/UDP service port
# OptAddress - IP address or hostname
# OptPath - Path name on disk or an Object ID
# OptInt - An integer value
# OptEnum - Select from a set of valid values
# OptAddressRange - A subnet or range of addresses
# OptSession - A session identifier
#
###
###
#
# Mult-byte character string option.
#
###
class OptString < OptBase
def type
return 'string'
end
def normalize(value)
if (value =~ /^file:(.*)/)
path = $1
begin
value = File.read(path)
rescue ::Errno::ENOENT, ::Errno::EISDIR
value = nil
end
end
value
end
def valid?(value=self.value)
value = normalize(value)
return false if empty_required_value?(value)
return super
end
end
###
#
# Raw, arbitrary data option.
#
###
class OptRaw < OptBase
def type
return 'raw'
end
def normalize(value)
if (value =~ /^file:(.*)/)
path = $1
begin
value = File.read(path)
rescue ::Errno::ENOENT, ::Errno::EISDIR
value = nil
end
end
value
end
def valid?(value=self.value)
value = normalize(value)
return false if empty_required_value?(value)
return super
end
end
###
#
# Boolean option.
#
###
class OptBool < OptBase
TrueRegex = /^(y|yes|t|1|true)$/i
def type
return 'bool'
end
def valid?(value)
return false if empty_required_value?(value)
if ((value != nil and
(value.to_s.empty? == false) and
(value.to_s.match(/^(y|yes|n|no|t|f|0|1|true|false)$/i) == nil)))
return false
end
true
end
def normalize(value)
if(value.nil? or value.to_s.match(TrueRegex).nil?)
false
else
true
end
end
def is_true?(value)
return normalize(value)
end
def is_false?(value)
return !is_true?(value)
end
end
###
#
# Enum option.
#
###
class OptEnum < OptBase
def type
return 'enum'
end
def valid?(value=self.value)
return false if empty_required_value?(value)
(value and self.enums.include?(value.to_s))
end
def normalize(value=self.value)
return nil if not self.valid?(value)
return value.to_s
end
def desc=(value)
self.desc_string = value
self.desc
end
def desc
if self.enums
str = self.enums.join(', ')
end
"#{self.desc_string || ''} (accepted: #{str})"
end
protected
attr_accessor :desc_string # :nodoc:
end
###
#
# Network port option.
#
###
class OptPort < OptBase
def type
return 'port'
end
def valid?(value)
return false if empty_required_value?(value)
if ((value != nil and value.to_s.empty? == false) and
((value.to_s.match(/^\d+$/) == nil or value.to_i < 0 or value.to_i > 65535)))
return false
end
return super
end
end
###
#
# Network address option.
#
###
class OptAddress < OptBase
def type
return 'address'
end
def valid?(value)
return false if empty_required_value?(value)
if (value != nil and value.empty? == false)
begin
::Rex::Socket.getaddress(value)
rescue
return false
end
end
return super
end
end
###
#
# Network address range option.
#
###
class OptAddressRange < OptBase
def type
return 'addressrange'
end
def normalize(value)
if (value =~ /^file:(.*)/)
path = $1
return false if not File.exists?(path) or File.directory?(path)
return File.readlines(path).map{ |s| s.strip}.join(" ")
elsif (value =~ /^rand:(.*)/)
count = $1.to_i
return false if count < 1
ret = ''
count.times {
ret << " " if not ret.empty?
ip = "%u.%u.%u.%u" % [1+rand(0xfe),1+rand(0xfe),1+rand(0xfe),1+rand(0xfe)]
ret << ip
}
return ret
end
return value
end
def valid?(value)
return false if empty_required_value?(value)
if (value != nil and value.empty? == false)
walker = Rex::Socket::RangeWalker.new(normalize(value))
if (not walker or not walker.valid?)
return false
end
end
return super
end
end
###
#
# File system path option.
#
###
class OptPath < OptBase
def type
return 'path'
end
# Generally, 'value' should be a file that exists.
def valid?(value)
return false if empty_required_value?(value)
if value and !value.empty?
if value =~ /^memory:\s*([0-9]+)/i
return false unless check_memory_location($1)
else
unless File.exists?(value)
return false
end
end
end
return super
end
# The AuthBrute mixin can take a memory address as well --
# currently, no other OptFile can make use of these objects.
# TODO: Implement memory:xxx to be more generally useful so
# the validator on OptFile isn't lying for non-AuthBrute.
def check_memory_location(id)
return false unless self.class.const_defined?(:ObjectSpace)
obj = ObjectSpace._id2ref(id.to_i) rescue nil
return false unless obj.respond_to? :acts_as_file?
return false unless obj.acts_as_file? # redundant?
return !!obj
end
end
###
#
# Integer option.
#
###
class OptInt < OptBase
def type
return 'integer'
end
def normalize(value)
if (value.to_s.match(/^0x[a-fA-F\d]+$/))
value.to_i(16)
else
value.to_i
end
end
def valid?(value)
return false if empty_required_value?(value)
if value and not normalize(value).to_s.match(/^\d+$/)
return false
end
return super
end
end
###
#
# The options purpose in life is to associate named options
# with arbitrary values at the most simplistic level. Each
# module contains a OptionContainer that is used to hold the
# various options that the module depends on. Example of options
# that are stored in the OptionContainer are rhost and rport for
# payloads or exploits that need to connect to a host and
# port, for instance.
#
###
class OptionContainer < Hash
#
# Merges in the supplied options and converts them to a OptBase
# as necessary.
#
def initialize(opts = {})
self.sorted = []
add_options(opts)
end
#
# Return the value associated with the supplied name.
#
def [](name)
return get(name)
end
#
# Return the option associated with the supplied name.
#
def get(name)
begin
return fetch(name)
rescue
end
end
#
# Returns whether or not the container has any options,
# excluding advanced (and evasions).
#
def has_options?
each_option { |name, opt|
return true if (opt.advanced? == false)
}
return false
end
#
# Returns whether or not the container has any advanced
# options.
#
def has_advanced_options?
each_option { |name, opt|
return true if (opt.advanced? == true)
}
return false
end
#
# Returns whether or not the container has any evasion
# options.
#
def has_evasion_options?
each_option { |name, opt|
return true if (opt.evasion? == true)
}
return false
end
#
# Removes an option.
#
def remove_option(name)
delete(name)
sorted.each_with_index { |e, idx|
sorted[idx] = nil if (e[0] == name)
}
sorted.delete(nil)
end
#
# Adds one or more options.
#
def add_options(opts, owner = nil, advanced = false, evasion = false)
return false if (opts == nil)
if (opts.kind_of?(Array))
add_options_array(opts, owner, advanced, evasion)
else
add_options_hash(opts, owner, advanced, evasion)
end
end
#
# Add options from a hash of names.
#
def add_options_hash(opts, owner = nil, advanced = false, evasion = false)
opts.each_pair { |name, opt|
add_option(opt, name, owner, advanced, evasion)
}
end
#
# Add options from an array of option instances or arrays.
#
def add_options_array(opts, owner = nil, advanced = false, evasion = false)
opts.each { |opt|
add_option(opt, nil, owner, advanced, evasion)
}
end
#
# Adds an option.
#
def add_option(option, name = nil, owner = nil, advanced = false, evasion = false)
if (option.kind_of?(Array))
option = option.shift.new(name, option)
elsif (!option.kind_of?(OptBase))
raise ArgumentError,
"The option named #{name} did not come in a compatible format.",
caller
end
option.advanced = advanced
option.evasion = evasion
option.owner = owner
self.store(option.name, option)
# Re-calculate the sorted list
self.sorted = self.sort
end
#
# Alias to add advanced options that sets the proper state flag.
#
def add_advanced_options(opts, owner = nil)
return false if (opts == nil)
add_options(opts, owner, true)
end
#
# Alias to add evasion options that sets the proper state flag.
#
def add_evasion_options(opts, owner = nil)
return false if (opts == nil)
add_options(opts, owner, false, true)
end
#
# Make sures that each of the options has a value of a compatible
# format and that all the required options are set.
#
def validate(datastore)
errors = []
each_pair { |name, option|
if (!option.valid?(datastore[name]))
errors << name
# If the option is valid, normalize its format to the correct type.
elsif ((val = option.normalize(datastore[name])) != nil)
datastore.update_value(name, val)
end
}
if (errors.empty? == false)
raise OptionValidateError.new(errors),
"One or more options failed to validate", caller
end
return true
end
#
# Creates string of options that were used from the datastore in VAR=VAL
# format separated by commas.
#
def options_used_to_s(datastore)
used = ''
each_pair { |name, option|
next if (datastore[name] == nil)
used += ", " if (used.length > 0)
used += "#{name}=#{datastore[name]}"
}
return used
end
#
# Enumerates each option name
#
def each_option(&block)
each_pair(&block)
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
#
# Merges the options in this container with another option container and
# returns the sorted results.
#
def merge_sort(other_container)
result = self.dup
other_container.each { |name, opt|
if (result.get(name) == nil)
result[name] = opt
end
}
result.sort
end
#
# The sorted array of options.
#
attr_reader :sorted
protected
attr_writer :sorted # :nodoc:
end
#
# Builtin framework options with shortcut methods
#
module Opt
@@builtin_opts =
{
'RHOST' => [ OptAddress, 'nil', true, '"The target address"' ],
'RPORT' => [ OptPort, 'nil', true, '"The target port"' ],
'LHOST' => [ OptAddress, 'nil', true, '"The listen address"' ],
'LPORT' => [ OptPort, 'nil', true, '"The listen port"' ],
'CPORT' => [ OptPort, 'nil', false, '"The local client port"' ],
'CHOST' => [ OptAddress, 'nil', false, '"The local client address"' ],
'Proxies' => [ OptString, 'nil', 'false', '"Use a proxy chain"']
}
#
# Build the builtin_xyz methods on the fly using the type information for each
# of the builtin framework options, such as RHOST.
#
class <<self
@@builtin_opts.each_pair { |opt, info|
eval(
"
def builtin_#{opt.downcase}(default = #{info[1]}, required = #{info[2]}, desc = #{info[3]})
#{info[0]}.new('#{opt}', [ required, desc, default ])
end
alias #{opt} builtin_#{opt.downcase}
")
}
end
#
# Define the constant versions of the options which are merely redirections to
# the class methods.
#
@@builtin_opts.each_pair { |opt, info|
eval("#{opt} = Msf::Opt::builtin_#{opt.downcase}")
}
end
end