HELLO v3!!!
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user