# frozen_string_literal: true module WPScan module Model # WpItem (superclass of Plugin & Theme) class WpItem include Vulnerable include Finders::Finding include CMSScanner::Target::Platform::PHP include CMSScanner::Target::Server::Generic # Most common readme filenames, based on checking all public plugins and themes. READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data delegate :homepage_res, :error_404_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog # @param [ String ] slug The plugin/theme slug # @param [ Target ] blog The targeted blog # @param [ Hash ] opts # @option opts [ Symbol ] :mode The detection mode to use # @option opts [ Hash ] :version_detection The options to use when looking for the version # @option opts [ String ] :url The URL of the item def initialize(slug, blog, opts = {}) @slug = URI.decode(slug) @blog = blog @uri = Addressable::URI.parse(opts[:url]) if opts[:url] @detection_opts = { mode: opts[:mode] } @version_detection_opts = opts[:version_detection] || {} parse_finding_options(opts) end # @return [ Array ] def vulnerabilities return @vulnerabilities if @vulnerabilities @vulnerabilities = [] [*db_data['vulnerabilities']].each do |json_vuln| vulnerability = Vulnerability.load_from_json(json_vuln) @vulnerabilities << vulnerability if vulnerable_to?(vulnerability) end @vulnerabilities end # Checks if the wp_item is vulnerable to a specific vulnerability # # @param [ Vulnerability ] vuln Vulnerability to check the item against # # @return [ Boolean ] def vulnerable_to?(vuln) return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty? version < vuln.fixed_in end # @return [ String ] def latest_version @latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil end # Not used anywhere ATM # @return [ Boolean ] def popular? @popular ||= metadata['popular'] ? true : false end # @return [ String ] def last_updated @last_updated ||= metadata['last_updated'] end # @return [ Boolean ] def outdated? @outdated ||= if version && latest_version version < latest_version else false end end # URI.encode is preferered over Addressable::URI.encode as it will encode # leading # character: # URI.encode('#t#') => %23t%23 # Addressable::URI.encode('#t#') => #t%23 # # @param [ String ] path Optional path to merge with the uri # # @return [ String ] def url(path = nil) return unless @uri return @uri.to_s unless path @uri.join(URI.encode(path)).to_s end # @return [ Boolean ] def ==(other) self.class == other.class && slug == other.slug end def to_s slug end # @return [ Symbol ] The Class symbol associated to the item def classify @classify ||= classify_slug(slug) end # @return [ String, False ] The readme url if found, false otherwise def readme_url return if detection_opts[:mode] == :passive return @readme_url unless @readme_url.nil? potential_readme_filenames.each do |path| t_url = url(path) return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200 end @readme_url = false end def potential_readme_filenames @potential_readme_filenames ||= READMES end # @param [ String ] path # @param [ Hash ] params The request params # # @return [ Boolean ] def directory_listing?(path = nil, params = {}) return if detection_opts[:mode] == :passive super(path, params) end # @param [ String ] path # @param [ Hash ] params The request params # # @return [ Boolean ] def error_log?(path = 'error_log', params = {}) return if detection_opts[:mode] == :passive super(path, params) end # See CMSScanner::Target#head_and_get # # This is used by the error_log? above in the super() # to have the correct path (ie readme.txt checked from the plugin/theme location # and not from the blog root). Could also be used in finders # # @param [ String ] path # @param [ Array ] codes # @param [ Hash ] params The requests params # @option params [ Hash ] :head Request params for the HEAD # @option params [ hash ] :get Request params for the GET # # @return [ Typhoeus::Response ] def head_and_get(path, codes = [200], params = {}) final_path = +@path_from_blog final_path << URI.encode(path) unless path.nil? blog.head_and_get(final_path, codes, params) end end end end