# -*- coding: binary -*- require 'addressable' module Msf ### # # Parses the RHOSTS datastore value, and yields the possible combinations of datastore values # that exist for each host # ### class RhostsWalker SUPPORTED_SCHEMAS = %w[ cidr file http https mysql postgres smb ssh tcp ].freeze private_constant :SUPPORTED_SCHEMAS ### # An error which additionally keeps track of a particular rhost substring which resulted in an error when enumerating # the provided rhost string ### class Error < StandardError attr_reader :value, :cause def initialize(value, msg = "Unexpected rhost value: #{value.inspect}", cause: nil) super(msg) @value = value @cause = cause set_backtrace(cause.backtrace) if cause end end class InvalidSchemaError < StandardError MESSAGE = 'Invalid schema' end class InvalidCIDRError < StandardError MESSAGE = 'Invalid CIDR' end class RhostResolveError < StandardError MESSAGE = 'Host resolution failed' end def initialize(value = '', datastore = Msf::ModuleDataStore.new(nil)) @value = value @datastore = datastore end # # Iterate over the valid rhosts datastores. This can be combined Calling `#valid?` beforehand to ensure # that there are no invalid configuration values, as they will be ignored by this method. # # @yield [Msf::DataStore] Yields only _valid_ rhost values. def each(&block) return unless @value return unless block_given? parse(@value, @datastore).each do |result| block.call(result) if result.is_a?(Msf::DataStore) || result.is_a?(Msf::DataStoreWithFallbacks) end nil end # Count the _valid_ datastore permutations for the current rhosts value. This count will # ignore any invalid values. # # @return [Integer] def count to_enum.count end # # Retrieve the list of errors associated with this rhosts walker # @yield [Msf::RhostsWalker::Error] Yields only invalid rhost values. def errors(&block) return unless @value return unless block_given? parse(@value, @datastore).each do |result| block.call(result) if result.is_a?(Msf::RhostsWalker::Error) end nil end # # Indicates that the rhosts value is valid and iterable # # @return [Boolean] True if all items are valid, and there are at least some items present to iterate over. False otherwise. def valid? parsed_values = parse(@value, @datastore) parsed_values.all? { |result| result.is_a?(Msf::DataStore) || result.is_a?(Msf::DataStoreWithFallbacks) } && parsed_values.count > 0 rescue StandardError => e elog('rhosts walker invalid', error: e) false end # # Parses the input rhosts string, and yields the possible combinations of datastore values. # # @param value [String] the rhost string # @param datastore [Msf::Datastore] the datastore # @return [Enumerable] The calculated datastore values that can be iterated over for # enumerating the given rhosts, or the error that occurred when iterating over the input def parse(value, datastore) Enumerator.new do |results| # extract the individual elements from the rhost string, ensuring that # whitespace, strings, escape characters, etc are handled correctly. values = Rex::Parser::Arguments.from_s(value) values.each do |value| if (value =~ %r{^file://(.*)}) || (value =~ /^file:(.*)/) file = Regexp.last_match(1) File.read(file).each_line(chomp: true) do |line| parse(line, datastore).each do |result| results << result end end elsif value =~ /^cidr:(.*)/ cidr, child_value = Regexp.last_match(1).split(':', 2) # Validate cidr syntax matches ipv6 '%scope_id/mask_part' or ipv4 '/mask_part' raise InvalidCIDRError unless cidr =~ %r{^(%\w+)?/\d{1,3}$} # Parse the values, then apply range walker over the result parse(child_value, datastore).each do |result| host_with_cidr = result['RHOSTS'] + cidr Rex::Socket::RangeWalker.new(host_with_cidr).each_ip do |rhost| results << result.merge('RHOSTS' => rhost, 'UNPARSED_RHOSTS' => value) end end elsif value =~ /^(?\w+):.*/ && SUPPORTED_SCHEMAS.include?(Regexp.last_match(:schema)) schema = Regexp.last_match(:schema) raise InvalidSchemaError unless SUPPORTED_SCHEMAS.include?(schema) found = false parse_method = "parse_#{schema}_uri" parsed_options = send(parse_method, value, datastore) Rex::Socket::RangeWalker.new(parsed_options['RHOSTS']).each_ip do |ip| results << datastore.merge( parsed_options.merge('RHOSTS' => ip, 'UNPARSED_RHOSTS' => value) ) found = true end unless found raise RhostResolveError.new(value) end else found = false Rex::Socket::RangeWalker.new(value).each_host do |rhost| overrides = {} overrides['UNPARSED_RHOSTS'] = value overrides['RHOSTS'] = rhost[:address] set_hostname(datastore, overrides, rhost[:hostname]) results << datastore.merge(overrides) found = true end unless found raise RhostResolveError.new(value) end end rescue ::Interrupt raise rescue StandardError => e results << Msf::RhostsWalker::Error.new(value, cause: e) end end end # Parses a string such as smb://domain;user:pass@domain/share_name/file.txt into a hash which can safely be # merged with a [Msf::DataStore] datastore for setting smb options. # # @param value [String] the http string # @return [Hash] A hash where keys match the required datastore options associated with # the smb uri value def parse_smb_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname result['RPORT'] = (uri.port || 445) if datastore.options.include?('RPORT') set_hostname(datastore, result, uri.hostname) # Handle users in the format: # user # domain;user if uri.user && uri.user.include?(';') domain, user = uri.user.split(';') result['SMBDomain'] = domain result['SMBUser'] = user set_username(datastore, result, user) elsif uri.user set_username(datastore, result, uri.user) end set_password(datastore, result, uri.password) if uri.password # Handle paths of the format: # / # /share_name # /share_name/file # /share_name/dir/file has_path_specified = !uri.path.blank? && uri.path != '/' if has_path_specified _preceding_slash, share, *rpath = uri.path.split('/') result['SMBSHARE'] = share if datastore.options.include?('SMBSHARE') result['RPATH'] = rpath.join('/') if datastore.options.include?('RPATH') end result end # Parses a string such as http://example.com into a hash which can safely be # merged with a [Msf::DataStore] datastore for setting http options. # # @param value [String] the http string # @return [Hash] A hash where keys match the required datastore options associated with # the uri value def parse_http_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname is_ssl = %w[ssl https].include?(uri.scheme) result['RPORT'] = uri.port || (is_ssl ? 443 : 80) result['SSL'] = is_ssl # Both `TARGETURI` and `URI` are used as datastore options to denote the path on a uri has_path_specified = !uri.path.blank? # && uri.path != '/' - Note HTTP path parsing differs to the other protocol's parsing if has_path_specified target_uri = uri.path.present? ? uri.path : '/' result['TARGETURI'] = target_uri if datastore.options.include?('TARGETURI') result['PATH'] = target_uri if datastore.options.include?('PATH') result['URI'] = target_uri if datastore.options.include?('URI') end result['HttpQueryString'] = uri.query if datastore.options.include?('HttpQueryString') set_hostname(datastore, result, uri.hostname) set_username(datastore, result, uri.user) if uri.user set_password(datastore, result, uri.password) if uri.password result end alias parse_https_uri parse_http_uri # Parses a uri string such as mysql://user:password@example.com into a hash # which can safely be merged with a [Msf::DataStore] datastore for setting mysql options. # # @param value [String] the uri string # @return [Hash] A hash where keys match the required datastore options associated with # the uri value def parse_mysql_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname result['RPORT'] = uri.port || 3306 has_database_specified = !uri.path.blank? && uri.path != '/' if datastore.options.include?('DATABASE') && has_database_specified result['DATABASE'] = uri.path[1..-1] end set_hostname(datastore, result, uri.hostname) set_username(datastore, result, uri.user) if uri.user set_password(datastore, result, uri.password) if uri.password result end # Parses a uri string such as postgres://user:password@example.com into a hash # which can safely be merged with a [Msf::DataStore] datastore for setting mysql options. # # @param value [String] the uri string # @return [Hash] A hash where keys match the required datastore options associated with # the uri value def parse_postgres_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname result['RPORT'] = uri.port || 5432 has_database_specified = !uri.path.blank? && uri.path != '/' if datastore.options.include?('DATABASE') && has_database_specified result['DATABASE'] = uri.path[1..-1] end set_hostname(datastore, result, uri.hostname) set_username(datastore, result, uri.user) if uri.user set_password(datastore, result, uri.password) if uri.password result end # Parses a uri string such as ssh://user:password@example.com into a hash # which can safely be merged with a [Msf::DataStore] datastore for setting mysql options. # # @param value [String] the uri string # @return [Hash] A hash where keys match the required datastore options associated with # the uri value def parse_ssh_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname result['RPORT'] = uri.port || 22 set_hostname(datastore, result, uri.hostname) set_username(datastore, result, uri.user) if uri.user set_password(datastore, result, uri.password) if uri.password result end # Parses a uri string such as tcp://user:password@example.com into a hash # which can safely be merged with a [Msf::DataStore] datastore for setting options. # # @param value [String] the uri string # @return [Hash] A hash where keys match the required datastore options associated with # the uri value def parse_tcp_uri(value, datastore) uri = ::Addressable::URI.parse(value) result = {} result['RHOSTS'] = uri.hostname if uri.port result['RPORT'] = uri.port end set_hostname(datastore, result, uri.hostname) set_username(datastore, result, uri.user) if uri.user set_password(datastore, result, uri.password) if uri.password result end protected def set_hostname(datastore, result, hostname) hostname = Rex::Socket.is_ip_addr?(hostname) ? nil : hostname result['RHOSTNAME'] = hostname if datastore['RHOSTNAME'].blank? result['VHOST'] = hostname if datastore.options.include?('VHOST') && datastore['VHOST'].blank? end def set_username(datastore, result, username) # Preference setting application specific values first username_set = false option_names = %w[SMBUser FtpUser Username user USER USERNAME username] option_names.each do |option_name| if datastore.options.include?(option_name) result[option_name] = username username_set = true end end # Only set basic auth HttpUsername as a fallback if !username_set && datastore.options.include?('HttpUsername') result['HttpUsername'] = username end result end def set_password(datastore, result, password) # Preference setting application specific values first password_set = false password_option_names = %w[SMBPass FtpPass Password pass PASSWORD password] password_option_names.each do |option_name| if datastore.options.include?(option_name) result[option_name] = password password_set = true end end # Only set basic auth HttpPassword as a fallback if !password_set && datastore.options.include?('HttpPassword') result['HttpPassword'] = password end result end end end