HELLO v3!!!
This commit is contained in:
48
lib/wpscan.rb
Normal file
48
lib/wpscan.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# Gems
|
||||
# Believe it or not, active_support MUST be the first one,
|
||||
# otherwise encoding issues can happen when using JSON format.
|
||||
# Not kidding.
|
||||
require 'active_support/all'
|
||||
require 'cms_scanner'
|
||||
require 'yajl/json_gem'
|
||||
require 'addressable/uri'
|
||||
# Standard Lib
|
||||
require 'uri'
|
||||
require 'time'
|
||||
require 'readline'
|
||||
require 'securerandom'
|
||||
|
||||
# Custom Libs
|
||||
require 'wpscan/helper'
|
||||
require 'wpscan/db'
|
||||
require 'wpscan/version'
|
||||
require 'wpscan/errors/wordpress'
|
||||
require 'wpscan/errors/http'
|
||||
require 'wpscan/errors/update'
|
||||
require 'wpscan/browser'
|
||||
require 'wpscan/target'
|
||||
require 'wpscan/finders'
|
||||
require 'wpscan/controller'
|
||||
require 'wpscan/controllers'
|
||||
require 'wpscan/references'
|
||||
require 'wpscan/vulnerable'
|
||||
require 'wpscan/vulnerability'
|
||||
|
||||
Encoding.default_external = Encoding::UTF_8
|
||||
|
||||
# WPScan
|
||||
module WPScan
|
||||
include CMSScanner
|
||||
|
||||
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
|
||||
DB_DIR = File.join(Dir.home, '.wpscan', 'db')
|
||||
|
||||
# Override, otherwise it would be returned as 'wp_scan'
|
||||
#
|
||||
# @return [ String ]
|
||||
def self.app_name
|
||||
'wpscan'
|
||||
end
|
||||
end
|
||||
|
||||
require "#{WPScan::APP_DIR}/app"
|
||||
16
lib/wpscan/browser.rb
Normal file
16
lib/wpscan/browser.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module WPScan
|
||||
# Custom Browser
|
||||
class Browser < CMSScanner::Browser
|
||||
extend Actions
|
||||
|
||||
# @return [ String ] The path to the user agents list
|
||||
def user_agents_list
|
||||
@user_agents_list ||= File.join(DB_DIR, 'user-agents.txt')
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def default_user_agent
|
||||
"WPScan v#{VERSION} (https://wpscan.org/)"
|
||||
end
|
||||
end
|
||||
end
|
||||
8
lib/wpscan/controller.rb
Normal file
8
lib/wpscan/controller.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
module WPScan
|
||||
# Needed to load at least the Core controller
|
||||
# Otherwise, the following error will be raised:
|
||||
# `initialize': uninitialized constant WPScan::Controller::Core (NameError)
|
||||
module Controller
|
||||
include CMSScanner::Controller
|
||||
end
|
||||
end
|
||||
8
lib/wpscan/controllers.rb
Normal file
8
lib/wpscan/controllers.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
module WPScan
|
||||
# Override to set the OptParser's summary width to 45 (instead of 40 from the CMSScanner)
|
||||
class Controllers < CMSScanner::Controllers
|
||||
def initialize(option_parser = OptParseValidator::OptParser.new(nil, 45))
|
||||
super(option_parser)
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/wpscan/db.rb
Normal file
14
lib/wpscan/db.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require_relative 'db/wp_item'
|
||||
require_relative 'db/updater'
|
||||
require_relative 'db/wp_items'
|
||||
require_relative 'db/plugins'
|
||||
require_relative 'db/themes'
|
||||
require_relative 'db/plugin'
|
||||
require_relative 'db/theme'
|
||||
require_relative 'db/wp_version'
|
||||
require_relative 'db/fingerprints'
|
||||
|
||||
require_relative 'db/dynamic_finders/base'
|
||||
require_relative 'db/dynamic_finders/plugin'
|
||||
require_relative 'db/dynamic_finders/theme'
|
||||
require_relative 'db/dynamic_finders/wordpress'
|
||||
41
lib/wpscan/db/dynamic_finders/base.rb
Normal file
41
lib/wpscan/db/dynamic_finders/base.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
module WPScan
|
||||
module DB
|
||||
module DynamicFinders
|
||||
class Base
|
||||
# @return [ String ]
|
||||
def self.db_file
|
||||
@db_file ||= File.join(DB_DIR, 'dynamic_finders.yml')
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.db_data
|
||||
# true allows aliases to be loaded
|
||||
@db_data ||= YAML.safe_load(File.read(db_file), [Regexp], [], true)
|
||||
end
|
||||
|
||||
# @return [ Array<Symbol> ]
|
||||
def self.allowed_classes
|
||||
@allowed_classes ||= %i[Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter ConfigParser]
|
||||
end
|
||||
|
||||
# @param [ Symbol ] sym
|
||||
def self.method_missing(sym)
|
||||
super unless sym =~ /\A(passive|aggressive)_(.*)_finder_configs\z/i
|
||||
|
||||
finder_class = Regexp.last_match[2].camelize.to_sym
|
||||
|
||||
raise "#{finder_class} is not allowed as a Dynamic Finder" unless allowed_classes.include?(finder_class)
|
||||
|
||||
finder_configs(
|
||||
finder_class,
|
||||
Regexp.last_match[1] == 'aggressive'
|
||||
)
|
||||
end
|
||||
|
||||
def self.respond_to_missing?(sym, *_args)
|
||||
sym =~ /\A(passive|aggressive)_(.*)_finder_configs\z/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
111
lib/wpscan/db/dynamic_finders/plugin.rb
Normal file
111
lib/wpscan/db/dynamic_finders/plugin.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
module WPScan
|
||||
module DB
|
||||
module DynamicFinders
|
||||
class Plugin < Base
|
||||
# @return [ Hash ]
|
||||
def self.db_data
|
||||
@db_data ||= super['plugins'] || {}
|
||||
end
|
||||
|
||||
def self.version_finder_module
|
||||
Finders::PluginVersion
|
||||
end
|
||||
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
db_data.each do |slug, finders|
|
||||
# Quite sure better can be done with some kind of logic statement in the select
|
||||
fs = if aggressive
|
||||
finders.reject { |_f, c| c['path'].nil? }
|
||||
else
|
||||
finders.select { |_f, c| c['path'].nil? }
|
||||
end
|
||||
|
||||
fs.each do |finder_name, config|
|
||||
klass = config['class'] || finder_name
|
||||
|
||||
next unless klass.to_sym == finder_class
|
||||
|
||||
configs[slug] ||= {}
|
||||
configs[slug][finder_name] = config
|
||||
end
|
||||
end
|
||||
|
||||
configs
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.versions_finders_configs
|
||||
return @versions_finders_configs if @versions_finders_configs
|
||||
|
||||
@versions_finders_configs = {}
|
||||
|
||||
db_data.each do |slug, finders|
|
||||
finders.each do |finder_name, config|
|
||||
next unless config.key?('version')
|
||||
|
||||
@versions_finders_configs[slug] ||= {}
|
||||
@versions_finders_configs[slug][finder_name] = config
|
||||
end
|
||||
end
|
||||
|
||||
@versions_finders_configs
|
||||
end
|
||||
|
||||
# @param [ String ] slug
|
||||
# @return [ Constant ]
|
||||
def self.maybe_create_modudle(slug)
|
||||
# What about slugs such as js_composer which will be done as JsComposer, just like js-composer
|
||||
constant_name = classify_slug(slug)
|
||||
|
||||
unless version_finder_module.constants.include?(constant_name)
|
||||
version_finder_module.const_set(constant_name, Module.new)
|
||||
end
|
||||
|
||||
version_finder_module.const_get(constant_name)
|
||||
end
|
||||
|
||||
def self.create_versions_finders
|
||||
versions_finders_configs.each do |slug, finders|
|
||||
# Kind of an issue here, module is created even if there is no valid classes
|
||||
# Could put the #maybe_ directly in the #send() BUT it would be checked everytime,
|
||||
# which is kind of a waste
|
||||
mod = maybe_create_modudle(slug)
|
||||
|
||||
finders.each do |finder_class, config|
|
||||
klass = config['class'] || finder_class
|
||||
|
||||
# Instead of raising exceptions, skip unallowed/already defined finders
|
||||
# So that, when new DF configs are put in the .yml
|
||||
# users with old version of WPScan will still be able to scan blogs
|
||||
# when updating the DB but not the tool
|
||||
next if mod.constants.include?(finder_class.to_sym) ||
|
||||
!allowed_classes.include?(klass.to_sym)
|
||||
|
||||
version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The idea here would be to check if the class exist in
|
||||
# the Finders::DynamicFinders::Plugins/Themes::klass or WpItemVersion::klass
|
||||
# and return the related constant when one has been found.
|
||||
#
|
||||
# So far, the Finders::DynamicFinders::WPItemVersion is enought
|
||||
# as nothing else is used
|
||||
#
|
||||
# @param [ String, Symbol ] klass
|
||||
# @return [ Constant ]
|
||||
def self.version_finder_super_class(klass)
|
||||
"WPScan::Finders::DynamicFinder::WpItemVersion::#{klass}".constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/wpscan/db/dynamic_finders/theme.rb
Normal file
16
lib/wpscan/db/dynamic_finders/theme.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module WPScan
|
||||
module DB
|
||||
module DynamicFinders
|
||||
class Theme < Plugin
|
||||
# @return [ Hash ]
|
||||
def self.db_data
|
||||
@db_data ||= super['themes'] || {}
|
||||
end
|
||||
|
||||
def self.version_finder_module
|
||||
Finders::ThemeVersion
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
75
lib/wpscan/db/dynamic_finders/wordpress.rb
Normal file
75
lib/wpscan/db/dynamic_finders/wordpress.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
module WPScan
|
||||
module DB
|
||||
module DynamicFinders
|
||||
class Wordpress < Base
|
||||
# @return [ Hash ]
|
||||
def self.db_data
|
||||
@db_data ||= super['wordpress'] || {}
|
||||
end
|
||||
|
||||
# @return [ Constant ]
|
||||
def self.version_finder_module
|
||||
Finders::WpVersion
|
||||
end
|
||||
|
||||
# @return [ Array<Symbol> ]
|
||||
def self.allowed_classes
|
||||
@allowed_classes ||= %i[
|
||||
Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter WpItemQueryParameter
|
||||
]
|
||||
end
|
||||
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
finders = if aggressive
|
||||
db_data.reject { |_f, c| c['path'].nil? }
|
||||
else
|
||||
db_data.select { |_f, c| c['path'].nil? }
|
||||
end
|
||||
|
||||
finders.each do |finder_name, config|
|
||||
klass = config['class'] || finder_name
|
||||
|
||||
next unless klass.to_sym == finder_class
|
||||
|
||||
configs[finder_name] = config
|
||||
end
|
||||
|
||||
configs
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.versions_finders_configs
|
||||
@versions_finders_configs ||= db_data.select { |_finder_name, config| config.key?('version') }
|
||||
end
|
||||
|
||||
def self.create_versions_finders
|
||||
versions_finders_configs.each do |finder_class, config|
|
||||
klass = config['class'] || finder_class
|
||||
|
||||
# Instead of raising exceptions, skip unallowed/already defined finders
|
||||
# So that, when new DF configs are put in the .yml
|
||||
# users with old version of WPScan will still be able to scan blogs
|
||||
# when updating the DB but not the tool
|
||||
next if version_finder_module.constants.include?(finder_class.to_sym) ||
|
||||
!allowed_classes.include?(klass.to_sym)
|
||||
|
||||
version_finder_super_class(klass).create_child_class(version_finder_module, finder_class.to_sym, config)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String, Symbol ] klass
|
||||
# @return [ Constant ]
|
||||
def self.version_finder_super_class(klass)
|
||||
"WPScan::Finders::DynamicFinder::WpVersion::#{klass}".constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
50
lib/wpscan/db/fingerprints.rb
Normal file
50
lib/wpscan/db/fingerprints.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# Fingerprints class
|
||||
class Fingerprints
|
||||
# @param [ Hash ] data
|
||||
#
|
||||
# @return [ Hash ] the unique fingerprints in the data argument given
|
||||
# Format returned:
|
||||
# {
|
||||
# file_path_1: {
|
||||
# md5_hash_1: version_1,
|
||||
# md5_hash_2: version_2
|
||||
# },
|
||||
# file_path_2: {
|
||||
# md5_hash_3: version_1,
|
||||
# md5_hash_4: version_3
|
||||
# }
|
||||
# }
|
||||
def self.unique_fingerprints(data)
|
||||
unique_fingerprints = {}
|
||||
|
||||
data.each do |file_path, fingerprints|
|
||||
fingerprints.each do |md5sum, versions|
|
||||
next unless versions.size == 1
|
||||
|
||||
unique_fingerprints[file_path] ||= {}
|
||||
unique_fingerprints[file_path][md5sum] = versions.first
|
||||
end
|
||||
end
|
||||
|
||||
unique_fingerprints
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def self.wp_fingerprints_path
|
||||
@wp_fingerprints_path ||= File.join(DB_DIR, 'wp_fingerprints.json')
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.wp_fingerprints
|
||||
@wp_fingerprints ||= read_json_file(wp_fingerprints_path)
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.wp_unique_fingerprints
|
||||
@wp_unique_fingerprints ||= unique_fingerprints(wp_fingerprints)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/wpscan/db/plugin.rb
Normal file
11
lib/wpscan/db/plugin.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# Plugin DB
|
||||
class Plugin < WpItem
|
||||
# @return [ String ]
|
||||
def self.db_file
|
||||
@db_file ||= File.join(DB_DIR, 'plugins.json')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/wpscan/db/plugins.rb
Normal file
11
lib/wpscan/db/plugins.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# WP Plugins
|
||||
class Plugins < WpItems
|
||||
# @return [ JSON ]
|
||||
def self.db
|
||||
Plugin.db
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/wpscan/db/theme.rb
Normal file
11
lib/wpscan/db/theme.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# Theme DB
|
||||
class Theme < WpItem
|
||||
# @return [ String ]
|
||||
def self.db_file
|
||||
@db_file ||= File.join(DB_DIR, 'themes.json')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/wpscan/db/themes.rb
Normal file
11
lib/wpscan/db/themes.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# WP Themes
|
||||
class Themes < WpItems
|
||||
# @return [ JSON ]
|
||||
def self.db
|
||||
Theme.db
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
162
lib/wpscan/db/updater.rb
Normal file
162
lib/wpscan/db/updater.rb
Normal file
@@ -0,0 +1,162 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# Class used to perform DB updates
|
||||
# :nocov:
|
||||
class Updater
|
||||
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
|
||||
FILES = %w[
|
||||
plugins.json themes.json wordpresses.json
|
||||
timthumbs-v3.txt user-agents.txt config_backups.txt
|
||||
db_exports.txt dynamic_finders.yml wp_fingerprints.json LICENSE
|
||||
].freeze
|
||||
|
||||
OLD_FILES = %w[wordpress.db dynamic_finders_01.yml].freeze
|
||||
|
||||
attr_reader :repo_directory
|
||||
|
||||
def initialize(repo_directory)
|
||||
@repo_directory = repo_directory
|
||||
|
||||
FileUtils.mkdir_p(repo_directory) unless Dir.exist?(repo_directory)
|
||||
|
||||
raise "#{repo_directory} is not writable" unless Pathname.new(repo_directory).writable?
|
||||
|
||||
delete_old_files
|
||||
end
|
||||
|
||||
# Removes DB files which are no longer used
|
||||
# this doesn't raise errors if they don't exist
|
||||
def delete_old_files
|
||||
OLD_FILES.each do |old_file|
|
||||
FileUtils.remove_file(local_file_path(old_file), true)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Time, nil ]
|
||||
def last_update
|
||||
Time.parse(File.read(last_update_file))
|
||||
rescue ArgumentError, Errno::ENOENT
|
||||
nil # returns nil if the file does not exist or contains invalid time data
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def last_update_file
|
||||
@last_update_file ||= File.join(repo_directory, '.last_update')
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def outdated?
|
||||
date = last_update
|
||||
|
||||
date.nil? || date < 5.days.ago
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def missing_files?
|
||||
FILES.each do |file|
|
||||
return true unless File.exist?(File.join(repo_directory, file))
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
def request_params
|
||||
{
|
||||
ssl_verifyhost: 2,
|
||||
ssl_verifypeer: true,
|
||||
timeout: 300,
|
||||
connecttimeout: 120,
|
||||
accept_encoding: 'gzip, deflate',
|
||||
cache_ttl: 0
|
||||
}
|
||||
end
|
||||
|
||||
# @return [ String ] The raw file URL associated with the given filename
|
||||
def remote_file_url(filename)
|
||||
"https://data.wpscan.org/#{filename}"
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the associated remote filename
|
||||
def remote_file_checksum(filename)
|
||||
url = "#{remote_file_url(filename)}.sha512"
|
||||
|
||||
res = Browser.get(url, request_params)
|
||||
raise DownloadError, res if res.timed_out? || res.code != 200
|
||||
|
||||
res.body.chomp
|
||||
end
|
||||
|
||||
def local_file_path(filename)
|
||||
File.join(repo_directory, filename.to_s)
|
||||
end
|
||||
|
||||
def local_file_checksum(filename)
|
||||
Digest::SHA512.file(local_file_path(filename)).hexdigest
|
||||
end
|
||||
|
||||
def backup_file_path(filename)
|
||||
File.join(repo_directory, "#{filename}.back")
|
||||
end
|
||||
|
||||
def create_backup(filename)
|
||||
return unless File.exist?(local_file_path(filename))
|
||||
|
||||
FileUtils.cp(local_file_path(filename), backup_file_path(filename))
|
||||
end
|
||||
|
||||
def restore_backup(filename)
|
||||
return unless File.exist?(backup_file_path(filename))
|
||||
|
||||
FileUtils.cp(backup_file_path(filename), local_file_path(filename))
|
||||
end
|
||||
|
||||
def delete_backup(filename)
|
||||
FileUtils.rm(backup_file_path(filename))
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the downloaded file
|
||||
def download(filename)
|
||||
file_path = local_file_path(filename)
|
||||
file_url = remote_file_url(filename)
|
||||
|
||||
res = Browser.get(file_url, request_params)
|
||||
raise DownloadError, res if res.timed_out? || res.code != 200
|
||||
|
||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||
|
||||
local_file_checksum(filename)
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The filenames updated
|
||||
def update
|
||||
updated = []
|
||||
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
|
||||
create_backup(filename)
|
||||
dl_checksum = download(filename)
|
||||
|
||||
raise "#{filename}: checksums do not match" unless dl_checksum == db_checksum
|
||||
|
||||
updated << filename
|
||||
rescue StandardError => e
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||
end
|
||||
end
|
||||
|
||||
File.write(last_update_file, Time.now)
|
||||
|
||||
updated
|
||||
end
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
18
lib/wpscan/db/wp_item.rb
Normal file
18
lib/wpscan/db/wp_item.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# WpItem - super DB class for Plugin, Theme and Version
|
||||
class WpItem
|
||||
# @param [ String ] identifier The plugin/theme slug or version number
|
||||
#
|
||||
# @return [ Hash ] The JSON data from the DB associated to the identifier
|
||||
def self.db_data(identifier)
|
||||
db[identifier] || {}
|
||||
end
|
||||
|
||||
# @return [ JSON ]
|
||||
def self.db
|
||||
@db ||= read_json_file(db_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
lib/wpscan/db/wp_items.rb
Normal file
21
lib/wpscan/db/wp_items.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# WP Items
|
||||
class WpItems
|
||||
# @return [ Array<String> ] The slug of all items
|
||||
def self.all_slugs
|
||||
db.keys
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The slug of all popular items
|
||||
def self.popular_slugs
|
||||
db.select { |_key, item| item['popular'] == true }.keys
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The slug of all vulnerable items
|
||||
def self.vulnerable_slugs
|
||||
db.reject { |_key, item| item['vulnerabilities'].empty? }.keys
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/wpscan/db/wp_version.rb
Normal file
11
lib/wpscan/db/wp_version.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module WPScan
|
||||
module DB
|
||||
# WP Version
|
||||
class Version < WpItem
|
||||
# @return [ String ]
|
||||
def self.db_file
|
||||
@db_file ||= File.join(DB_DIR, 'wordpresses.json')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
lib/wpscan/errors/http.rb
Normal file
34
lib/wpscan/errors/http.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
module WPScan
|
||||
# HTTP Error
|
||||
class HTTPError < StandardError
|
||||
attr_reader :response
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
def initialize(response)
|
||||
@response = response
|
||||
end
|
||||
|
||||
def failure_details
|
||||
msg = response.effective_url
|
||||
|
||||
msg += if response.code.zero? || response.timed_out?
|
||||
" (#{response.return_message})"
|
||||
else
|
||||
" (status: #{response.code})"
|
||||
end
|
||||
|
||||
msg
|
||||
end
|
||||
|
||||
def to_s
|
||||
"HTTP Error: #{failure_details}"
|
||||
end
|
||||
end
|
||||
|
||||
# Used in the Updater
|
||||
class DownloadError < HTTPError
|
||||
def to_s
|
||||
"Unable to get #{failure_details}"
|
||||
end
|
||||
end
|
||||
end
|
||||
8
lib/wpscan/errors/update.rb
Normal file
8
lib/wpscan/errors/update.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
module WPScan
|
||||
# Error raised when there is a missing DB file and --no-update supplied
|
||||
class MissingDatabaseFile < StandardError
|
||||
def to_s
|
||||
'Update required, you can not run a scan if a database file is missing.'
|
||||
end
|
||||
end
|
||||
end
|
||||
22
lib/wpscan/errors/wordpress.rb
Normal file
22
lib/wpscan/errors/wordpress.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
# WordPress hosted (*.wordpress.com)
|
||||
class WordPressHostedError < StandardError
|
||||
def to_s
|
||||
'Scanning *.wordpress.com hosted blogs is not supported.'
|
||||
end
|
||||
end
|
||||
|
||||
# Not WordPress Error
|
||||
class NotWordPressError < StandardError
|
||||
def to_s
|
||||
'The remote website is up, but does not seem to be running WordPress.'
|
||||
end
|
||||
end
|
||||
|
||||
# Invalid Wp Version (used in the WpVersion#new)
|
||||
class InvalidWordPressVersion < StandardError
|
||||
def to_s
|
||||
'The WordPress version is invalid'
|
||||
end
|
||||
end
|
||||
end
|
||||
26
lib/wpscan/finders.rb
Normal file
26
lib/wpscan/finders.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
require 'wpscan/finders/finder/wp_version/smart_url_checker'
|
||||
|
||||
require 'wpscan/finders/dynamic_finder/finder'
|
||||
require 'wpscan/finders/dynamic_finder/wp_items/finder'
|
||||
require 'wpscan/finders/dynamic_finder/version/finder'
|
||||
require 'wpscan/finders/dynamic_finder/version/xpath'
|
||||
require 'wpscan/finders/dynamic_finder/version/comment'
|
||||
require 'wpscan/finders/dynamic_finder/version/header_pattern'
|
||||
require 'wpscan/finders/dynamic_finder/version/body_pattern'
|
||||
require 'wpscan/finders/dynamic_finder/version/javascript_var'
|
||||
require 'wpscan/finders/dynamic_finder/version/query_parameter'
|
||||
require 'wpscan/finders/dynamic_finder/version/config_parser'
|
||||
require 'wpscan/finders/dynamic_finder/wp_item_version'
|
||||
require 'wpscan/finders/dynamic_finder/wp_version'
|
||||
|
||||
module WPScan
|
||||
# Custom Finders
|
||||
module Finders
|
||||
include CMSScanner::Finders
|
||||
|
||||
# Custom InterestingFindings
|
||||
module InterestingFindings
|
||||
include CMSScanner::Finders::InterestingFindings
|
||||
end
|
||||
end
|
||||
end
|
||||
66
lib/wpscan/finders/dynamic_finder/finder.rb
Normal file
66
lib/wpscan/finders/dynamic_finder/finder.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
# To be used as a base when creating a dynamic finder
|
||||
class Finder < CMSScanner::Finders::Finder
|
||||
# @param [ Array ] args
|
||||
def self.child_class_constant(*args)
|
||||
args.each do |arg|
|
||||
if arg.is_a?(Hash)
|
||||
child_class_constants.merge!(arg)
|
||||
else
|
||||
child_class_constants[arg] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Needed to have inheritance of the @child_class_constants
|
||||
# If inheritance is not needed, then the #child_class_constant can be used in the classe definition, ie
|
||||
# child_class_constant :FILES, PATTERN: /aaa/i
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= { PATH: nil }
|
||||
end
|
||||
|
||||
# @param [ Constant ] mod
|
||||
# @param [ Constant ] klass
|
||||
# @param [ Hash ] config
|
||||
def self.create_child_class(mod, klass, config)
|
||||
# Can't use the #child_class_constants directly in the Class.new(self) do; end below
|
||||
class_constants = child_class_constants
|
||||
|
||||
mod.const_set(
|
||||
klass, Class.new(self) do
|
||||
class_constants.each do |key, value|
|
||||
const_set(key, config[key.downcase.to_s] || value)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# This method has to be overriden in child classes
|
||||
#
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Mixed ]
|
||||
def find(_response, _opts = {})
|
||||
raise NoMethodError
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
def passive(opts = {})
|
||||
return if self.class::PATH
|
||||
|
||||
find(target.homepage_res, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
def aggressive(opts = {})
|
||||
return unless self.class::PATH
|
||||
|
||||
find(Browser.get(target.url(self.class::PATH)), opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/wpscan/finders/dynamic_finder/version/body_pattern.rb
Normal file
28
lib/wpscan/finders/dynamic_finder/version/body_pattern.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using Body Pattern method. Tipically used when the response is not
|
||||
# an HTML doc and Xpath can't be used
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(PATTERN: nil, CONFIDENCE: 60)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Version ]
|
||||
def find(response, _opts = {})
|
||||
return unless response.body =~ self.class::PATTERN
|
||||
|
||||
create_version(
|
||||
Regexp.last_match[:v],
|
||||
interesting_entries: ["#{response.effective_url}, Match: '#{Regexp.last_match}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/wpscan/finders/dynamic_finder/version/comment.rb
Normal file
16
lib/wpscan/finders/dynamic_finder/version/comment.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder in Comment, which is basically an Xpath one with a default
|
||||
# Xpath of //comment()
|
||||
class Comment < WPScan::Finders::DynamicFinder::Version::Xpath
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(PATTERN: nil, XPATH: '//comment()')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
56
lib/wpscan/finders/dynamic_finder/version/config_parser.rb
Normal file
56
lib/wpscan/finders/dynamic_finder/version/config_parser.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using by parsing config files, such as composer.json
|
||||
# and so on
|
||||
class ConfigParser < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
ALLOWED_PARSERS = [JSON, YAML].freeze
|
||||
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super.merge(
|
||||
PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 70
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
# @return [ Hash, nil ] The parsed body, with an available parser, if possible
|
||||
def parse(body)
|
||||
parsers = ALLOWED_PARSERS.include?(self.class::PARSER) ? [self.class::PARSER] : ALLOWED_PARSERS
|
||||
|
||||
parsers.each do |parser|
|
||||
begin
|
||||
parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
|
||||
|
||||
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
nil # Make sure nil is returned in case none of the parsers managed to parse the body correctly
|
||||
end
|
||||
|
||||
# No Passive way
|
||||
def passive(opts = {}); end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Version ]
|
||||
def find(response, _opts = {})
|
||||
parsed_body = parse(response.body)
|
||||
# Create indexes for the #dig, digits are converted to integers
|
||||
indexes = self.class::KEY.split(':').map { |e| e == e.to_i.to_s ? e.to_i : e }
|
||||
|
||||
return unless (data = parsed_body&.dig(*indexes)) && data =~ self.class::PATTERN
|
||||
|
||||
create_version(
|
||||
Regexp.last_match[:v],
|
||||
interesting_entries: ["#{response.effective_url}, Match: '#{Regexp.last_match}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
lib/wpscan/finders/dynamic_finder/version/finder.rb
Normal file
29
lib/wpscan/finders/dynamic_finder/version/finder.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# To be used as a base when creating
|
||||
# a dynamic finder to find the version of a WP Item (such as theme/plugin)
|
||||
class Finder < Finders::DynamicFinder::Finder
|
||||
protected
|
||||
|
||||
# @param [ String ] number
|
||||
# @param [ Hash ] finding_opts
|
||||
# @return [ WPScan::Version ]
|
||||
def create_version(number, finding_opts)
|
||||
WPScan::Version.new(number, version_finding_opts(finding_opts))
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @retutn [ Hash ]
|
||||
def version_finding_opts(opts)
|
||||
opts[:found_by] ||= found_by
|
||||
opts[:confidence] ||= self.class::CONFIDENCE
|
||||
|
||||
opts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/wpscan/finders/dynamic_finder/version/header_pattern.rb
Normal file
28
lib/wpscan/finders/dynamic_finder/version/header_pattern.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using Header Pattern method
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(HEADER: nil, PATTERN: nil, CONFIDENCE: 60)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Version ]
|
||||
def find(response, _opts = {})
|
||||
return unless response.headers && response.headers[self.class::HEADER]
|
||||
return unless response.headers[self.class::HEADER].to_s =~ self.class::PATTERN
|
||||
|
||||
create_version(
|
||||
Regexp.last_match[:v],
|
||||
interesting_entries: ["#{response.effective_url}, Match: '#{Regexp.last_match}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
56
lib/wpscan/finders/dynamic_finder/version/javascript_var.rb
Normal file
56
lib/wpscan/finders/dynamic_finder/version/javascript_var.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using JavaScript Variable method
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: '//script[not(@src)]', VERSION_KEY: nil,
|
||||
PATTERN: nil, CONFIDENCE: 60
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Version ]
|
||||
def find(response, _opts = {})
|
||||
target.xpath_pattern_from_page(
|
||||
self.class::XPATH, self.class::PATTERN, response
|
||||
) do |match_data, _node|
|
||||
next unless (version_number = version_number_from_match_data(match_data))
|
||||
|
||||
# If the text to be output in the interesting_entries is > 50 chars,
|
||||
# get 20 chars before and after (when possible) the detected version instead
|
||||
match = match_data.to_s
|
||||
match = match[/.*?(.{,20}#{Regexp.escape(version_number)}.{,20}).*/, 1] if match.size > 50
|
||||
|
||||
return create_version(
|
||||
version_number,
|
||||
interesting_entries: ["#{response.effective_url}, Match: '#{match.strip}'"]
|
||||
)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @param [ MatchData ] match_data
|
||||
# @return [ String ]
|
||||
def version_number_from_match_data(match_data)
|
||||
if self.class::VERSION_KEY
|
||||
begin
|
||||
json = JSON.parse("{#{match_data[:json].strip.chomp(',').tr("'", '"')}}")
|
||||
rescue JSON::ParserError
|
||||
return
|
||||
end
|
||||
|
||||
json.dig(*self.class::VERSION_KEY.split(':'))
|
||||
else
|
||||
match_data[:v]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
63
lib/wpscan/finders/dynamic_finder/version/query_parameter.rb
Normal file
63
lib/wpscan/finders/dynamic_finder/version/query_parameter.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using QueryParameter method
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)\=(?<v>\d+\.[\.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Array<Version>, nil ]
|
||||
def find(response, _opts = {})
|
||||
found = []
|
||||
|
||||
scan_response(response).each do |version_number, occurences|
|
||||
found << create_version(
|
||||
version_number,
|
||||
confidence: self.class::CONFIDENCE_PER_OCCURENCE * occurences.size,
|
||||
interesting_entries: occurences
|
||||
)
|
||||
end
|
||||
|
||||
found.compact
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @return [ Hash ]
|
||||
def scan_response(response)
|
||||
found = {}
|
||||
|
||||
target.in_scope_urls(response, xpath) do |url, _tag|
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
next unless uri.path =~ path_pattern && uri.query&.match(self.class::PATTERN)
|
||||
|
||||
version = Regexp.last_match[:v].to_s
|
||||
|
||||
found[version] ||= []
|
||||
found[version] << url
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def xpath
|
||||
@xpath ||= self.class::XPATH || '//link[@href]/@href|//script[@src]/@src'
|
||||
end
|
||||
|
||||
# @return [ Regexp ]
|
||||
def path_pattern
|
||||
@path_pattern ||= %r{/(?:#{self.class::FILES.join('|')})\z}i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
lib/wpscan/finders/dynamic_finder/version/xpath.rb
Normal file
34
lib/wpscan/finders/dynamic_finder/version/xpath.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module Version
|
||||
# Version finder using Xpath method
|
||||
class Xpath < WPScan::Finders::DynamicFinder::Version::Finder
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, PATTERN: /\A(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 60
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] opts
|
||||
# @return [ Version ]
|
||||
def find(response, _opts = {})
|
||||
target.xpath_pattern_from_page(
|
||||
self.class::XPATH, self.class::PATTERN, response
|
||||
) do |match_data, _node|
|
||||
next unless match_data[:v]
|
||||
|
||||
return create_version(
|
||||
match_data[:v],
|
||||
interesting_entries: ["#{response.effective_url}, Match: '#{match_data}'"]
|
||||
)
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
lib/wpscan/finders/dynamic_finder/wp_item_version.rb
Normal file
43
lib/wpscan/finders/dynamic_finder/wp_item_version.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module WpItemVersion
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::Version::BodyPattern
|
||||
end
|
||||
|
||||
class Comment < WPScan::Finders::DynamicFinder::Version::Comment
|
||||
end
|
||||
|
||||
class ConfigParser < WPScan::Finders::DynamicFinder::Version::ConfigParser
|
||||
end
|
||||
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::Version::HeaderPattern
|
||||
end
|
||||
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::Version::JavascriptVar
|
||||
end
|
||||
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::Version::QueryParameter
|
||||
# @return [ Regexp ]
|
||||
def path_pattern
|
||||
# TODO: consider the target.blog.themes_dir if the target is a Theme (maybe implement a WpItem#item_dir ?)
|
||||
@path_pattern ||= %r{
|
||||
#{Regexp.escape(target.blog.plugins_dir)}/
|
||||
#{Regexp.escape(target.slug)}/
|
||||
(?:#{self.class::FILES.join('|')})\z
|
||||
}ix
|
||||
end
|
||||
|
||||
def xpath
|
||||
@xpath ||= self.class::XPATH ||
|
||||
"//link[contains(@href,'#{target.slug}')]/@href" \
|
||||
"|//script[contains(@src,'#{target.slug}')]/@src"
|
||||
end
|
||||
end
|
||||
|
||||
class Xpath < WPScan::Finders::DynamicFinder::Version::Xpath
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
96
lib/wpscan/finders/dynamic_finder/wp_items/finder.rb
Normal file
96
lib/wpscan/finders/dynamic_finder/wp_items/finder.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module WpItems
|
||||
# Not really a dynamic finder in itself (hence not a child class of DynamicFinder::Finder)
|
||||
# but will use the dynamic finder DB configs to find collections of
|
||||
# WpItems (such as Plugins and Themes)
|
||||
#
|
||||
# Also used to factorise some code used between such finders.
|
||||
# The #process_response should be implemented in each child class, or the
|
||||
# #passive and #aggressive overriden
|
||||
class Finder < CMSScanner::Finders::Finder
|
||||
# @return [ Hash ] The related dynamic finder passive configurations
|
||||
# for the current class (all its usefullness comes from child classes)
|
||||
def passive_configs
|
||||
# So far only the Plugins have dynamic finders so using DB:: DynamicFinders::Plugin
|
||||
# is ok. However, when Themes have some, will need to create other child classes for them
|
||||
|
||||
method = "passive_#{self.class.to_s.demodulize.underscore}_finder_configs".to_sym
|
||||
|
||||
DB::DynamicFinders::Plugin.public_send(method)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin>, Array<Theme> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
passive_configs.each do |slug, configs|
|
||||
configs.each do |klass, config|
|
||||
item = process_response(opts, target.homepage_res, slug, klass, config)
|
||||
|
||||
found << item if item.is_a?(WpItem)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ Hash ] The related dynamic finder passive configurations
|
||||
# for the current class (all its usefullness comes from child classes)
|
||||
def aggressive_configs
|
||||
# So far only the Plugins have dynamic finders so using DB:: DynamicFinders::Plugin
|
||||
# is ok. However, when Themes have some, will need to create other child classes for them
|
||||
|
||||
method = "aggressive_#{self.class.to_s.demodulize.underscore}_finder_configs".to_sym
|
||||
|
||||
DB::DynamicFinders::Plugin.public_send(method)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin>, Array<Theme> ]
|
||||
def aggressive(_opts = {})
|
||||
# Disable this as it would make quite a lot of extra requests just to find plugins/themes
|
||||
# Kept the original method below for future implementation
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin>, Array<Theme> ]
|
||||
def aggressive_(opts = {})
|
||||
found = []
|
||||
|
||||
aggressive_configs.each do |slug, configs|
|
||||
configs.each do |klass, config|
|
||||
path = aggressive_path(slug, config)
|
||||
response = Browser.get(target.url(path))
|
||||
|
||||
item = process_response(opts, response, slug, klass, config)
|
||||
|
||||
found << item if item.is_a?(WpItem)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ String ] slug
|
||||
# @param [ Hash ] config from the YAML file with he 'path' key
|
||||
#
|
||||
# @return [ String ] The path related to the aggresive configuration
|
||||
# ie config['path'] if it's an absolute path (like /file.txt)
|
||||
# or the path from inside the related plugin directory
|
||||
def aggressive_path(slug, config)
|
||||
return config['path'] if config['path'][0] == '/'
|
||||
|
||||
# No need to set the correct plugins dir, it will be handled by target.url()
|
||||
"wp-content/plugins/#{slug}/#{config['path']}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
lib/wpscan/finders/dynamic_finder/wp_version.rb
Normal file
61
lib/wpscan/finders/dynamic_finder/wp_version.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DynamicFinder
|
||||
module WpVersion
|
||||
module Finder
|
||||
def create_version(number, finding_opts)
|
||||
return unless WPScan::WpVersion.valid?(number)
|
||||
|
||||
WPScan::WpVersion.new(number, version_finding_opts(finding_opts))
|
||||
end
|
||||
end
|
||||
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::Version::BodyPattern
|
||||
include Finder
|
||||
end
|
||||
|
||||
class Comment < WPScan::Finders::DynamicFinder::Version::Comment
|
||||
include Finder
|
||||
end
|
||||
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::Version::HeaderPattern
|
||||
include Finder
|
||||
end
|
||||
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::Version::JavascriptVar
|
||||
include Finder
|
||||
end
|
||||
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::Version::QueryParameter
|
||||
include Finder
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(PATTERN: /ver\=(?<v>\d+\.[\.\d]+)/i)
|
||||
end
|
||||
end
|
||||
|
||||
class WpItemQueryParameter < QueryParameter
|
||||
def xpath
|
||||
@xpath ||=
|
||||
self.class::XPATH ||
|
||||
"//link[contains(@href,'#{target.plugins_dir}') or contains(@href,'#{target.themes_dir}')]/@href" \
|
||||
"|//script[contains(@src,'#{target.plugins_dir}') or contains(@src,'#{target.themes_dir}')]/@src"
|
||||
end
|
||||
|
||||
def path_pattern
|
||||
@path_pattern ||= %r{
|
||||
(?:#{Regexp.escape(target.plugins_dir)}|#{Regexp.escape(target.themes_dir)})/
|
||||
[^/]+/
|
||||
.*\.(?:css|js)\z
|
||||
}ix
|
||||
end
|
||||
end
|
||||
|
||||
class Xpath < WPScan::Finders::DynamicFinder::Version::Xpath
|
||||
include Finder
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
lib/wpscan/finders/finder/wp_version/smart_url_checker.rb
Normal file
23
lib/wpscan/finders/finder/wp_version/smart_url_checker.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
class Finder
|
||||
module WpVersion
|
||||
# SmartURLChecker specific for the WP Version
|
||||
module SmartURLChecker
|
||||
include CMSScanner::Finders::Finder::SmartURLChecker
|
||||
|
||||
def create_version(number, opts = {})
|
||||
WPScan::WpVersion.new(
|
||||
number,
|
||||
found_by: opts[:found_by] || found_by,
|
||||
confidence: opts[:confidence] || 80,
|
||||
interesting_entries: opts[:entries]
|
||||
)
|
||||
rescue WPScan::InvalidWordPressVersion
|
||||
nil # Invalid Version returned as nil and will be ignored by Finders
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/wpscan/helper.rb
Normal file
16
lib/wpscan/helper.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
def read_json_file(file)
|
||||
JSON.parse(File.read(file))
|
||||
rescue StandardError => e
|
||||
raise "JSON parsing error in #{file} #{e}"
|
||||
end
|
||||
|
||||
# @return [ Symbol ]
|
||||
# @note As a class can not start with a digit or underscore, a D_ is
|
||||
# put as a prefix in such case. Ugly but well :x
|
||||
# Not only used to classify slugs though, but Dynamic Finder names as well
|
||||
def classify_slug(slug)
|
||||
classified = slug.to_s.tr('-', '_').camelize.to_s
|
||||
classified = "D_#{classified}" if classified[0] =~ /\d/
|
||||
|
||||
classified.to_sym
|
||||
end
|
||||
31
lib/wpscan/references.rb
Normal file
31
lib/wpscan/references.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
# References module (which should be included along with the CMSScanner::References)
|
||||
# to allow the use of the wpvulndb reference
|
||||
module References
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# See ActiveSupport::Concern
|
||||
module ClassMethods
|
||||
# @return [ Array<Symbol> ]
|
||||
def references_keys
|
||||
@references_keys ||= super << :wpvulndb
|
||||
end
|
||||
end
|
||||
|
||||
def references_urls
|
||||
wpvulndb_urls + super
|
||||
end
|
||||
|
||||
def wpvulndb_ids
|
||||
references[:wpvulndb] || []
|
||||
end
|
||||
|
||||
def wpvulndb_urls
|
||||
wpvulndb_ids.reduce([]) { |acc, elem| acc << wpvulndb_url(elem) }
|
||||
end
|
||||
|
||||
def wpvulndb_url(id)
|
||||
"https://wpvulndb.com/vulnerabilities/#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
94
lib/wpscan/target.rb
Normal file
94
lib/wpscan/target.rb
Normal file
@@ -0,0 +1,94 @@
|
||||
require 'wpscan/target/platform/wordpress'
|
||||
|
||||
module WPScan
|
||||
# Includes the WordPress Platform
|
||||
class Target < CMSScanner::Target
|
||||
include Platform::WordPress
|
||||
|
||||
# @return [ Boolean ]
|
||||
def vulnerable?
|
||||
[@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e|
|
||||
[*e].each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
||||
end
|
||||
|
||||
return true unless [*@config_backups].empty?
|
||||
return true unless [*@db_exports].empty?
|
||||
|
||||
[*@users].each { |u| return true if u.password }
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ XMLRPC, nil ]
|
||||
def xmlrpc
|
||||
@xmlrpc ||= interesting_findings&.select { |f| f.is_a?(WPScan::XMLRPC) }&.first
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WpVersion, false ] The WpVersion found or false if not detected
|
||||
def wp_version(opts = {})
|
||||
@wp_version = Finders::WpVersion::Base.find(self, opts) if @wp_version.nil?
|
||||
|
||||
@wp_version
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Theme ]
|
||||
def main_theme(opts = {})
|
||||
@main_theme = Finders::MainTheme::Base.find(self, opts) if @main_theme.nil?
|
||||
|
||||
@main_theme
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def plugins(opts = {})
|
||||
@plugins ||= Finders::Plugins::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def themes(opts = {})
|
||||
@themes ||= Finders::Themes::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Timthumb> ]
|
||||
def timthumbs(opts = {})
|
||||
@timthumbs ||= Finders::Timthumbs::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<ConfigBackup> ]
|
||||
def config_backups(opts = {})
|
||||
@config_backups ||= Finders::ConfigBackups::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<DBExport> ]
|
||||
def db_exports(opts = {})
|
||||
@db_exports ||= Finders::DbExports::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Media> ]
|
||||
def medias(opts = {})
|
||||
@medias ||= Finders::Medias::Base.find(self, opts)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def users(opts = {})
|
||||
@users ||= Finders::Users::Base.find(self, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
74
lib/wpscan/target/platform/wordpress.rb
Normal file
74
lib/wpscan/target/platform/wordpress.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
%w[custom_directories].each do |required|
|
||||
require "wpscan/target/platform/wordpress/#{required}"
|
||||
end
|
||||
|
||||
module WPScan
|
||||
class Target < CMSScanner::Target
|
||||
module Platform
|
||||
# Some WordPress specific implementation
|
||||
module WordPress
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
|
||||
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i
|
||||
|
||||
# These methods are used in the associated interesting_findings finders
|
||||
# to keep the boolean state of the finding rather than re-check the whole thing again
|
||||
attr_accessor :multisite, :registration_enabled, :mu_plugins
|
||||
alias multisite? multisite
|
||||
alias registration_enabled? registration_enabled
|
||||
alias mu_plugins? mu_plugins
|
||||
|
||||
# @return [ Boolean ]
|
||||
def wordpress?
|
||||
# res = Browser.get(url)
|
||||
|
||||
in_scope_urls(homepage_res) do |url|
|
||||
return true if Addressable::URI.parse(url).path.match(WORDPRESS_PATTERN)
|
||||
end
|
||||
|
||||
homepage_res.html.css('meta[name="generator"]').each do |node|
|
||||
return true if node['content'] =~ /wordpress/i
|
||||
end
|
||||
|
||||
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def registration_url
|
||||
multisite? ? url('wp-signup.php') : url('wp-login.php?action=register')
|
||||
end
|
||||
|
||||
def wordpress_hosted?
|
||||
uri.host =~ /wordpress.com$/i ? true : false
|
||||
end
|
||||
|
||||
# @param [ String ] username
|
||||
# @param [ String ] password
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def do_login(username, password)
|
||||
login_request(username, password).run
|
||||
end
|
||||
|
||||
# @param [ String ] username
|
||||
# @param [ String ] password
|
||||
#
|
||||
# @return [ Typhoeus::Request ]
|
||||
def login_request(username, password)
|
||||
Browser.instance.forge_request(
|
||||
login_url,
|
||||
method: :post,
|
||||
body: { log: username, pwd: password }
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ String ] The URL to the login page
|
||||
def login_url
|
||||
url('wp-login.php')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
108
lib/wpscan/target/platform/wordpress/custom_directories.rb
Normal file
108
lib/wpscan/target/platform/wordpress/custom_directories.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
module WPScan
|
||||
class Target < CMSScanner::Target
|
||||
module Platform
|
||||
# wp-content & plugins directory implementation
|
||||
module WordPress
|
||||
def content_dir=(dir)
|
||||
@content_dir = dir.chomp('/')
|
||||
end
|
||||
|
||||
def plugins_dir=(dir)
|
||||
@plugins_dir = dir.chomp('/')
|
||||
end
|
||||
|
||||
# @return [ String ] The wp-content directory
|
||||
def content_dir
|
||||
unless @content_dir
|
||||
escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
|
||||
pattern = %r{#{escaped_url}(.+?)\/(?:themes|plugins|uploads|cache)\/}i
|
||||
|
||||
in_scope_urls(homepage_res) do |url|
|
||||
return @content_dir = Regexp.last_match[1] if url.match(pattern)
|
||||
end
|
||||
end
|
||||
|
||||
@content_dir
|
||||
end
|
||||
|
||||
# @return [ Addressable::URI ]
|
||||
def content_uri
|
||||
uri.join("#{content_dir}/")
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def content_url
|
||||
content_uri.to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def plugins_dir
|
||||
@plugins_dir ||= "#{content_dir}/plugins"
|
||||
end
|
||||
|
||||
# @return [ Addressable::URI ]
|
||||
def plugins_uri
|
||||
uri.join("#{plugins_dir}/")
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def plugins_url
|
||||
plugins_uri.to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def themes_dir
|
||||
@themes_dir ||= "#{content_dir}/themes"
|
||||
end
|
||||
|
||||
# @return [ Addressable::URI ]
|
||||
def themes_uri
|
||||
uri.join("#{themes_dir}/")
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def themes_url
|
||||
themes_uri.to_s
|
||||
end
|
||||
|
||||
# TODO: Factorise the code and the content_dir one ?
|
||||
# @return [ String, False ] String of the sub_dir found, false otherwise
|
||||
# @note: nil can not be returned here, otherwise if there is no sub_dir
|
||||
# the check would be done each time
|
||||
def sub_dir
|
||||
unless @sub_dir
|
||||
escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
|
||||
pattern = %r{#{escaped_url}(.+?)\/(?:xmlrpc\.php|wp\-includes\/)}i
|
||||
|
||||
in_scope_urls(homepage_res) do |url|
|
||||
return @sub_dir = Regexp.last_match[1] if url.match(pattern)
|
||||
end
|
||||
|
||||
@sub_dir = false
|
||||
end
|
||||
|
||||
@sub_dir
|
||||
end
|
||||
|
||||
# Override of the WebSite#url to consider the custom WP directories
|
||||
#
|
||||
# @param [ String ] path Optional path to merge with the uri
|
||||
#
|
||||
# @return [ String ]
|
||||
def url(path = nil)
|
||||
return @uri.to_s unless path
|
||||
|
||||
if path =~ %r{wp\-content/plugins}i
|
||||
path.gsub!('wp-content/plugins', plugins_dir)
|
||||
elsif path =~ /wp\-content/i
|
||||
path.gsub!('wp-content', content_dir)
|
||||
elsif path[0] != '/' && sub_dir
|
||||
path = "#{sub_dir}/#{path}"
|
||||
end
|
||||
|
||||
super(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
4
lib/wpscan/version.rb
Normal file
4
lib/wpscan/version.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
# Version
|
||||
module WPScan
|
||||
VERSION = '3.3.0'.freeze
|
||||
end
|
||||
25
lib/wpscan/vulnerability.rb
Normal file
25
lib/wpscan/vulnerability.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
# Specific implementation
|
||||
class Vulnerability < CMSScanner::Vulnerability
|
||||
include References
|
||||
|
||||
# @param [ Hash ] json_data
|
||||
# @return [ Vulnerability ]
|
||||
def self.load_from_json(json_data)
|
||||
references = { wpvulndb: json_data['id'].to_s }
|
||||
|
||||
if json_data['references']
|
||||
references_keys.each do |key|
|
||||
references[key] = json_data['references'][key.to_s] if json_data['references'].key?(key.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
new(
|
||||
json_data['title'],
|
||||
references,
|
||||
json_data['vuln_type'],
|
||||
json_data['fixed_in']
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
10
lib/wpscan/vulnerable.rb
Normal file
10
lib/wpscan/vulnerable.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
module WPScan
|
||||
# Module to include in vulnerable WP item such as WpVersion.
|
||||
# the vulnerabilities method should be implemented
|
||||
module Vulnerable
|
||||
# @return [ Boolean ]
|
||||
def vulnerable?
|
||||
!vulnerabilities.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user