All Files (82.88% covered at 16.47 hits/line)
108 files in total.
1618 relevant lines.
1341 lines covered and
277 lines missed
- 1
require_relative 'models'
- 1
require_relative 'finders'
- 1
require_relative 'controllers'
- 1
require_relative 'controllers/core'
- 1
require_relative 'controllers/custom_directories'
- 1
require_relative 'controllers/wp_version'
- 1
require_relative 'controllers/main_theme'
- 1
require_relative 'controllers/enumeration'
- 1
require_relative 'controllers/brute_force'
- 1
module WPScan
- 1
module Controller
# Brute Force Controller
- 1
class BruteForce < CMSScanner::Controller::Base
- 1
def cli_options
[
OptFilePath.new(
['--passwords FILE-PATH', '-P',
'List of passwords to use during the brute forcing.',
'If no --username/s option supplied, user enumeration will be run'],
exists: true
- 3
),
OptString.new(['--username USERNAME', '-u', 'The username to brute force']),
OptFilePath.new(
['--usernames FILE-PATH', '-U', 'List of usernames to use during the brute forcing'],
exists: true
)
]
end
- 1
def run
- 1
return unless parsed_options[:passwords]
begin
found = []
brute_force(users, passwords(parsed_options[:passwords])) do |user|
found << user
output('found', user: user) if user_interaction?
end
ensure
output('users', users: found)
end
end
# @return [ Array<Users> ] The users to brute force
- 1
def users
- 3
return target.users unless parsed_options[:usernames] || parsed_options[:username]
- 2
if parsed_options[:username]
- 1
[User.new(parsed_options[:username])]
else
- 1
File.open(parsed_options[:usernames]).reduce([]) do |acc, elem|
- 2
acc << User.new(elem.chomp)
end
end
end
# the iteration should be on the passwords to be more efficient
# however, it's not that simple expecially when a combination is found:
# - the estimated number of requests (for the progressbar) has to be updated.
# - the user found has to be deleted from the loop
#
# @param [ Array<User> ] users
# @param [ Array<String> ] passwords
#
# @yield [ User ] when a valid combination is found
# rubocop:disable all
- 1
def brute_force(users, passwords)
hydra = Browser.instance.hydra
users.each do |user|
bar = progress_bar(passwords.size, user.username) if user_interaction?
passwords.each do |password|
request = target.login_request(user.username, password)
request.on_complete do |res|
bar.progress += 1 if user_interaction?
if res.code == 302
user.password = password
hydra.abort
yield user
next
elsif user_interaction? && res.code != 200
# Errors not displayed when using formats other than cli/cli-no-colour
output_error(res)
end
end
hydra.queue(request)
end
hydra.run
end
end
# rubocop:enable all
- 1
def progress_bar(size, username)
ProgressBar.create(
format: '%t %a <%B> (%c / %C) %P%% %e',
title: "Brute Forcing #{username}",
total: size
)
end
# @param [ String ] wordlist_path
#
# @return [ Array<String> ]
- 1
def passwords(wordlist_path)
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
acc << elem.chomp
end
end
# @param [ Typhoeus::Response ] response
- 1
def output_error(response)
return if response.body =~ /login_error/i
error = if response.timed_out?
'Request timed out.'
elsif response.code.zero?
"No response from remote server. WAF/IPS? (#{response.return_message})"
elsif response.code.to_s =~ /^50/
'Server error, try reducing the number of threads.'
else
"Unknown response received Code: #{response.code}\n Body: #{response.body}"
end
output('error', msg: error)
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Specific Core controller to include WordPress checks
- 1
class Core < CMSScanner::Controller::Core
# @return [ Array<OptParseValidator::Opt> ]
- 1
def cli_options
[OptURL.new(['--url URL', 'The URL of the blog to scan'], required_unless: :update, default_protocol: 'http')] +
super.drop(1) + # delete the --url from CMSScanner
[
OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'],
choices: %w(apache iis nginx),
normalize: [:downcase, :to_sym]),
OptBoolean.new(['--force', 'Do not check if the target is running WordPress']),
OptBoolean.new(['--[no-]update', 'Wether or not to update the Database'], required_unless: :url)
- 3
]
end
# @return [ DB::Updater ]
- 1
def local_db
- 26
@local_db ||= DB::Updater.new(DB_DIR)
end
# @return [ Boolean ]
- 1
def update_db_required?
- 10
if local_db.missing_files?
- 4
raise MissingDatabaseFile if parsed_options[:update] == false
- 3
return true
end
- 6
return parsed_options[:update] unless parsed_options[:update].nil?
- 4
return false unless user_interaction? && local_db.outdated?
- 2
output('@notice', msg: 'It seems like you have not updated the database for some time.')
- 2
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
- 2
Readline.readline =~ /^y/i ? true : false
end
- 1
def update_db
- 2
output('db_update_started')
- 2
output('db_update_finished', updated: local_db.update, verbose: parsed_options[:verbose])
- 2
exit(0) unless parsed_options[:url]
end
- 1
def before_scan
- 10
output('banner')
- 10
update_db if update_db_required?
- 9
super(false) # disable banner output
- 8
DB.init_db
- 8
load_server_module
- 8
check_wordpress_state
end
# Raises errors if the target is hosted on wordpress.com or is not running WordPress
# Also check if the homepage_url is still the install url
- 1
def check_wordpress_state
- 8
raise WordPressHostedError if target.wordpress_hosted?
- 7
if Addressable::URI.parse(target.homepage_url).path =~ %r{/wp-admin/install.php$}i
- 1
output('not_fully_configured', url: target.homepage_url)
- 1
exit(WPScan::ExitCode::VULNERABLE)
end
- 6
raise NotWordPressError unless target.wordpress? || parsed_options[:force]
end
# Loads the related server module in the target
# and includes it in the WpItem class which will be needed
# to check if directory listing is enabled etc
#
# @return [ Symbol ] The server module loaded
- 1
def load_server_module
- 9
server = target.server || :Apache # Tries to auto detect the server
# Force a specific server module to be loaded if supplied
- 9
case parsed_options[:server]
when :apache
- 1
server = :Apache
when :iis
- 1
server = :IIS
when :nginx
- 1
server = :Nginx
end
- 9
mod = CMSScanner::Target::Server.const_get(server)
- 9
target.extend mod
- 9
WPScan::WpItem.include mod
- 9
server
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Controller to ensure that the wp-content and wp-plugins
# directories are found
- 1
class CustomDirectories < CMSScanner::Controller::Base
- 1
def cli_options
[
- 3
OptString.new(['--wp-content-dir DIR']),
OptString.new(['--wp-plugins-dir DIR'])
]
end
- 1
def before_scan
- 2
target.content_dir = parsed_options[:wp_content_dir] if parsed_options[:wp_content_dir]
- 2
target.plugins_dir = parsed_options[:wp_plugins_dir] if parsed_options[:wp_plugins_dir]
- 2
return if target.content_dir
- 1
raise 'Unable to identify the wp-content dir, please supply it with --wp-content-dir'
end
end
end
end
- 1
require_relative 'enumeration/cli_options'
- 1
require_relative 'enumeration/enum_methods'
- 1
module WPScan
- 1
module Controller
# Enumeration Controller
- 1
class Enumeration < CMSScanner::Controller::Base
- 1
def before_scan
# Create the Dynamic Finders
- 1
DB::DynamicPluginFinders.db_data.each do |name, config|
- 45
%w(Comments).each do |klass|
- 45
next unless config[klass] && config[klass]['version']
- 26
constant_name = name.tr('-', '_').camelize
- 26
unless Finders::PluginVersion.constants.include?(constant_name.to_sym)
- 23
Finders::PluginVersion.const_set(constant_name, Module.new)
end
- 26
mod = WPScan::Finders::PluginVersion.const_get(constant_name)
- 26
raise "#{mod} has already a #{klass} class" if mod.constants.include?(klass.to_sym)
- 25
case klass
- 25
when 'Comments' then create_plugins_comments_finders(mod, config[klass])
end
end
end
end
- 1
def create_plugins_comments_finders(mod, config)
- 25
mod.const_set(
:Comments, Class.new(Finders::Finder::PluginVersion::Comments) do
- 25
const_set(:PATTERN, Regexp.new(config['pattern'], Regexp::IGNORECASE))
end
)
end
- 1
def run
- 12
enum = parsed_options[:enumerate] || {}
- 12
enum_plugins if enum_plugins?(enum)
- 12
enum_themes if enum_themes?(enum)
- 12
[:timthumbs, :config_backups, :medias].each do |key|
- 36
send("enum_#{key}".to_sym) if enum.key?(key)
end
- 12
enum_users if enum_users?(enum)
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Enumeration CLI Options
- 1
class Enumeration < CMSScanner::Controller::Base
- 1
def cli_options
cli_enum_choices + cli_plugins_opts + cli_themes_opts +
- 1
cli_timthumbs_opts + cli_config_backups_opts + cli_medias_opts + cli_users_opts
end
# @return [ Array<OptParseValidator::OptBase> ]
# rubocop:disable Metrics/MethodLength
- 1
def cli_enum_choices
[
OptMultiChoices.new(
['--enumerate [OPTS]', '-e', 'Enumeration Process'],
choices: {
vp: OptBoolean.new(['--vulnerable-plugins']),
ap: OptBoolean.new(['--all-plugins']),
p: OptBoolean.new(['--plugins']),
vt: OptBoolean.new(['--vulnerable-themes']),
at: OptBoolean.new(['--all-themes']),
t: OptBoolean.new(['--themes']),
tt: OptBoolean.new(['--timthumbs']),
cb: OptBoolean.new(['--config-backups']),
u: OptIntegerRange.new(['--users', 'User ids range. e.g: u1-5'], value_if_empty: '1-10'),
m: OptIntegerRange.new(['--medias', 'Media ids range. e.g m1-15'], value_if_empty: '1-100')
},
value_if_empty: 'vp,vt,tt,cb,u,m',
incompatible: [[:vp, :ap, :p], [:vt, :at, :t]]
- 2
),
OptRegexp.new(
[
'--exclude-content-based REGEXP_OR_STRING',
'Exclude all responses having their body matching (case insensitive) during parts of the enumeration.',
'Regexp delimiters are not required.'
], options: Regexp::IGNORECASE
)
]
end
# rubocop:enable Metrics/MethodLength
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_plugins_opts
[
- 1
OptFilePath.new(['--plugins-list FILE-PATH', 'List of plugins\' location to use'], exists: true),
OptChoice.new(
['--plugins-detection MODE',
'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
),
OptBoolean.new(['--plugins-version-all', 'Check all the plugins version locations'])
]
end
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_themes_opts
[
- 1
OptFilePath.new(['--themes-list FILE-PATH', 'List of themes\' location to use'], exists: true),
OptChoice.new(
['--themes-detection MODE',
'Use the supplied mode to enumerate Themes, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
),
OptBoolean.new(['--themes-version-all', 'Check all the themes version locations'])
]
end
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_timthumbs_opts
[
OptFilePath.new(
['--timthumbs-list FILE-PATH', 'List of timthumbs\' location to use'],
exists: true, default: File.join(DB_DIR, 'timthumbs-v3.txt')
- 1
),
OptChoice.new(
['--timthumbs-detection MODE',
'Use the supplied mode to enumerate Timthumbs, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
)
]
end
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_config_backups_opts
[
OptFilePath.new(
['--config-backups-list FILE-PATH', 'List of config backups\' filenames to use'],
exists: true, default: File.join(DB_DIR, 'config_backups.txt')
- 1
),
OptChoice.new(
['--config-backups-detection MODE',
'Use the supplied mode to enumerate Configs, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
)
]
end
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_medias_opts
[
OptChoice.new(
['--medias-detection MODE',
'Use the supplied mode to enumerate Medias, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
- 1
)
]
end
# @return [ Array<OptParseValidator::OptBase> ]
- 1
def cli_users_opts
[
OptFilePath.new(
['--users-list FILE-PATH',
'List of users to check during the users enumeration from the Login Error Messages'],
exists: true
- 1
),
OptChoice.new(
['--users-detection MODE',
'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive), normalize: :to_sym
)
]
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Enumeration Methods
- 1
class Enumeration < CMSScanner::Controller::Base
# @param [ String ] type (plugins or themes)
#
# @return [ String ] The related enumration message depending on the parsed_options and type supplied
- 1
def enum_message(type)
- 7
return unless type == 'plugins' || type == 'themes'
- 6
details = if parsed_options[:enumerate][:"vulnerable_#{type}"]
- 2
'Vulnerable'
- 4
elsif parsed_options[:enumerate][:"all_#{type}"]
- 2
'All'
else
- 2
'Most Popular'
end
- 6
"Enumerating #{details} #{type.capitalize}"
end
# @param [ String ] type (plugins, themes etc)
#
# @return [ Hash ]
- 1
def default_opts(type)
{
mode: parsed_options[:"#{type}_detection"] || parsed_options[:detection_mode],
exclude_content: parsed_options[:exclude_content_based],
show_progression: user_interaction?
- 2
}
end
# @param [ Hash ] opts
#
# @return [ Boolean ] Wether or not to enumerate the plugins
- 1
def enum_plugins?(opts)
- 12
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end
- 1
def enum_plugins
opts = default_opts('plugins').merge(
list: plugins_list_from_opts(parsed_options),
version_all: parsed_options[:plugins_version_all],
sort: true
)
output('@info', msg: enum_message('plugins')) if user_interaction?
# Enumerate the plugins & find their versions to avoid doing that when #version
# is called in the view
plugins = target.plugins(opts).each(&:version)
plugins.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_plugins]
output('plugins', plugins: plugins)
end
# @param [ Hash ] opts
#
# @return [ Array<String> ] The plugins list associated to the cli options
- 1
def plugins_list_from_opts(opts)
# List file provided by the user via the cli
return File.open(opts[:plugins_list]).map(&:chomp) if opts[:plugins_list]
if opts[:enumerate][:all_plugins]
DB::Plugins.all_slugs
elsif opts[:enumerate][:plugins]
DB::Plugins.popular_slugs
else
DB::Plugins.vulnerable_slugs
end
end
# @param [ Hash ] opts
#
# @return [ Boolean ] Wether or not to enumerate the themes
- 1
def enum_themes?(opts)
- 12
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
end
- 1
def enum_themes
opts = default_opts('themes').merge(
list: themes_list_from_opts(parsed_options),
version_all: parsed_options[:themes_version_all],
sort: true
)
output('@info', msg: enum_message('themes')) if user_interaction?
# Enumerate the themes & find their versions to avoid doing that when #version
# is called in the view
themes = target.themes(opts).each(&:version)
themes.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_themes]
output('themes', themes: themes)
end
# @param [ Hash ] opts
#
# @return [ Array<String> ] The themes list associated to the cli options
- 1
def themes_list_from_opts(opts)
# List file provided by the user via the cli
return File.open(opts[:themes_list]).map(&:chomp) if opts[:themes_list]
if opts[:enumerate][:all_themes]
DB::Themes.all_slugs
elsif opts[:enumerate][:themes]
DB::Themes.popular_slugs
else
DB::Themes.vulnerable_slugs
end
end
- 1
def enum_timthumbs
opts = default_opts('timthumbs').merge(list: parsed_options[:timthumbs_list])
output('@info', msg: 'Enumerating Timthumbs') if user_interaction?
output('timthumbs', timthumbs: target.timthumbs(opts))
end
- 1
def enum_config_backups
opts = default_opts('config_baclups').merge(list: parsed_options[:config_backups_list])
output('@info', msg: 'Enumerating Config Backups') if user_interaction?
output('config_backups', config_backups: target.config_backups(opts))
end
- 1
def enum_medias
opts = default_opts('medias').merge(range: parsed_options[:enumerate][:medias])
output('@info', msg: 'Enumerating Medias') if user_interaction?
output('medias', medias: target.medias(opts))
end
# @param [ Hash ] opts
#
# @return [ Boolean ] Wether or not to enumerate the users
- 1
def enum_users?(opts)
- 12
opts[:users] || (parsed_options[:passwords] && !parsed_options[:username] && !parsed_options[:usernames])
end
- 1
def enum_users
- 2
opts = default_opts('users').merge(
range: enum_users_range,
list: parsed_options[:users_list]
)
- 2
output('@info', msg: 'Enumerating Users') if user_interaction?
- 2
output('users', users: target.users(opts))
end
# @return [ Range ] The user ids range to enumerate
# If the --enumerate is used, the default value is handled by the Option
# However, when using --passwords alone, the default has to be set by the code below
- 1
def enum_users_range
- 2
parsed_options[:enumerate] ? parsed_options[:enumerate][:users] : cli_enum_choices[0].choices[:u].validate(nil)
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Main Theme Controller
- 1
class MainTheme < CMSScanner::Controller::Base
- 1
def cli_options
[
OptChoice.new(
['--main-theme-detection MODE',
'Use the supplied mode for the Main theme detection, instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive),
normalize: :to_sym
)
]
end
- 1
def run
output(
'theme',
theme: target.main_theme(
mode: parsed_options[:main_theme_detection] || parsed_options[:detection_mode]
),
verbose: parsed_options[:verbose]
)
end
end
end
end
- 1
module WPScan
- 1
module Controller
# Wp Version Controller
- 1
class WpVersion < CMSScanner::Controller::Base
- 1
def cli_options
[
- 3
OptBoolean.new(['--wp-version-all', 'Check all the version locations']),
OptChoice.new(
['--wp-version-detection MODE',
'Use the supplied mode for the WordPress version detection, ' \
'instead of the global (--detection-mode) mode.'],
choices: %w(mixed passive aggressive),
normalize: :to_sym
)
]
end
- 1
def run
- 5
output(
'version',
version: target.wp_version(
mode: parsed_options[:wp_version_detection] || parsed_options[:detection_mode],
- 5
confidence_threshold: parsed_options[:wp_version_all] ? 0 : 100,
show_progression: user_interaction?
)
)
end
end
end
end
- 1
require_relative 'finders/interesting_findings'
- 1
require_relative 'finders/wp_items'
- 1
require_relative 'finders/wp_version'
- 1
require_relative 'finders/main_theme'
- 1
require_relative 'finders/timthumb_version'
- 1
require_relative 'finders/timthumbs'
- 1
require_relative 'finders/config_backups'
- 1
require_relative 'finders/medias'
- 1
require_relative 'finders/users'
- 1
require_relative 'finders/plugins'
- 1
require_relative 'finders/plugin_version'
- 1
require_relative 'finders/theme_version'
- 1
require_relative 'finders/themes'
- 1
require_relative 'config_backups/known_filenames'
- 1
module WPScan
- 1
module Finders
- 1
module ConfigBackups
# Config Backup Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
- 1
finders << ConfigBackups::KnownFilenames.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module ConfigBackups
# Config Backup finder
- 1
class KnownFilenames < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ String ] :list
# @option opts [ Boolean ] :show_progression
#
# @return [ Array<InterestingFinding> ]
- 1
def aggressive(opts = {})
- 2
found = []
- 2
enumerate(potential_urls(opts), opts) do |res|
# Might need to improve that
- 6
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
- 2
found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
end
- 2
found
end
# @param [ Hash ] opts
# @option opts [ String ] :list Mandatory
#
# @return [ Hash ]
- 1
def potential_urls(opts = {})
- 4
urls = {}
- 4
File.open(opts[:list]).each_with_index do |file, index|
- 12
urls[target.url(file.chomp)] = index
end
- 4
urls
end
- 1
def create_progress_bar(opts = {})
- 2
super(opts.merge(title: ' Checking Config Backups -'))
end
end
end
end
end
- 1
require_relative 'interesting_findings/readme'
- 1
require_relative 'interesting_findings/multisite'
- 1
require_relative 'interesting_findings/debug_log'
- 1
require_relative 'interesting_findings/backup_db'
- 1
require_relative 'interesting_findings/mu_plugins'
- 1
require_relative 'interesting_findings/registration'
- 1
require_relative 'interesting_findings/tmm_db_migrate'
- 1
require_relative 'interesting_findings/upload_sql_dump'
- 1
require_relative 'interesting_findings/full_path_disclosure'
- 1
require_relative 'interesting_findings/duplicator_installer_log'
- 1
require_relative 'interesting_findings/upload_directory_listing'
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Interesting Files Finder
- 1
class Base < CMSScanner::Finders::InterestingFindings::Base
# @param [ WPScan::Target ] target
- 1
def initialize(target)
- 1
super(target)
%w(
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
UploadSQLDump
- 1
).each do |f|
- 11
finders << InterestingFindings.const_get(f).new(target)
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# BackupDB finder
- 1
class BackupDB < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 4
path = 'wp-content/backup-db/'
- 4
url = target.url(path)
- 4
res = Browser.get(url)
- 4
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
- 2
WPScan::InterestingFinding.new(
url,
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# debug.log finder
- 1
class DebugLog < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 2
path = 'wp-content/debug.log'
- 2
return unless target.debug_log?(path)
- 1
WPScan::InterestingFinding.new(
target.url(path),
confidence: 100, found_by: DIRECT_ACCESS
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# DuplicatorInstallerLog finder
- 1
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 2
url = target.url('installer-log.txt')
- 2
res = Browser.get(url)
- 2
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
- 1
WPScan::InterestingFinding.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Full Path Disclosure finder
- 1
class FullPathDisclosure < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 2
path = 'wp-includes/rss-functions.php'
- 2
fpd_entries = target.full_path_disclosure_entries(path)
- 2
return if fpd_entries.empty?
- 1
WPScan::InterestingFinding.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,
interesting_entries: fpd_entries
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Must Use Plugins Directory checker
- 1
class MuPlugins < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def passive(_opts = {})
pattern = %r{#{target.content_dir}/mu\-plugins/}i
target.in_scope_urls(target.homepage_res) do |url|
next unless Addressable::URI.parse(url).path =~ pattern
url = target.url('wp-content/mu-plugins/')
return WPScan::InterestingFinding.new(
url,
confidence: 70,
found_by: 'URLs In Homepage (Passive Detection)',
to_s: "This site has 'Must Use Plugins': #{url}",
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
)
end
nil
end
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
url = target.url('wp-content/mu-plugins/')
res = Browser.get_and_follow_location(url)
return unless [200, 401, 403].include?(res.code)
return if target.homepage_or_404?(res)
# TODO: add the check for --exclude-content once implemented ?
target.mu_plugins = true
WPScan::InterestingFinding.new(
url,
confidence: 80,
found_by: DIRECT_ACCESS,
to_s: "This site has 'Must Use Plugins': #{url}",
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Multisite checker
- 1
class Multisite < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
url = target.url('wp-signup.php')
res = Browser.get(url)
location = res.headers_hash['location']
return unless [200, 302].include?(res.code)
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
target.multisite = true
WPScan::InterestingFinding.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: 'This site seems to be a multisite',
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Readme.html finder
- 1
class Readme < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 2
potential_files.each do |file|
- 6
url = target.url(file)
- 6
res = Browser.get(url)
- 6
if res.code == 200 && res.body =~ /wordpress/i
- 1
return WPScan::InterestingFinding.new(url, confidence: 100, found_by: DIRECT_ACCESS)
end
end
nil
end
# @retun [ Array<String> ] The list of potential readme files
- 1
def potential_files
- 7
%w(readme.html olvasdel.html lisenssi.html liesmich.html)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Registration Enabled checker
- 1
class Registration < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def passive(_opts = {})
# Maybe check in the homepage if there is the registration url ?
end
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
res = Browser.get_and_follow_location(target.registration_url)
return unless res.code == 200
return if res.html.css('form#setupform').empty? &&
res.html.css('form#registerform').empty?
target.registration_enabled = true
WPScan::InterestingFinding.new(
res.effective_url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Registration is enabled: #{res.effective_url}"
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# Tmm DB Migrate finder
- 1
class TmmDbMigrate < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
url = target.url(path)
res = Browser.get(url)
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
WPScan::InterestingFinding.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
references: { packetstorm: 131_957 }
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# UploadDirectoryListing finder
- 1
class UploadDirectoryListing < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
path = 'wp-content/uploads/'
return unless target.directory_listing?(path)
url = target.url(path)
WPScan::InterestingFinding.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Upload directory has listing enabled: #{url}"
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module InterestingFindings
# UploadSQLDump finder
- 1
class UploadSQLDump < CMSScanner::Finders::Finder
- 1
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/
# @return [ InterestingFinding ]
- 1
def aggressive(_opts = {})
- 3
url = dump_url
- 3
res = Browser.get(url)
- 3
return unless res.code == 200 && res.body =~ SQL_PATTERN
- 1
WPScan::InterestingFinding.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS
)
end
- 1
def dump_url
- 7
target.url('wp-content/uploads/dump.sql')
end
end
end
end
end
- 1
require_relative 'main_theme/css_style'
- 1
require_relative 'main_theme/woo_framework_meta_generator'
- 1
require_relative 'main_theme/urls_in_homepage'
- 1
module WPScan
- 1
module Finders
- 1
module MainTheme
# Main Theme Finder
- 1
class Base
- 1
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
finders <<
MainTheme::CssStyle.new(target) <<
MainTheme::WooFrameworkMetaGenerator.new(target) <<
- 1
MainTheme::UrlsInHomepage.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module MainTheme
# From the css style
- 1
class CssStyle < CMSScanner::Finders::Finder
- 1
include Finders::WpItems::URLsInHomepage
- 1
def create_theme(name, style_url, opts)
- 2
WPScan::Theme.new(
name,
target,
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
)
end
- 1
def passive(opts = {})
- 3
passive_from_css_href(target.homepage_res, opts) || passive_from_style_code(target.homepage_res, opts)
end
- 1
def passive_from_css_href(res, opts)
- 3
target.in_scope_urls(res, '//style|//link') do |url|
- 7
next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
- 1
return create_theme(Regexp.last_match[1], url, opts)
end
nil
end
- 1
def passive_from_style_code(res, opts)
- 2
res.html.css('style').each do |tag|
- 1
code = tag.text.to_s
- 1
next if code.empty?
- 1
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
- 1
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
end
nil
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module MainTheme
# URLs In Homepage Finder
- 1
class UrlsInHomepage < CMSScanner::Finders::Finder
- 1
include WpItems::URLsInHomepage
# @param [ Hash ] opts
#
# @return [ Array<Theme> ]
- 1
def passive(opts = {})
- 1
found = []
- 1
names = items_from_links('themes', false) + items_from_codes('themes', false)
- 7
names.each_with_object(Hash.new(0)) { |name, counts| counts[name] += 1 }.each do |name, occurences|
- 3
found << WPScan::Theme.new(name, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
end
- 1
found
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module MainTheme
# From the WooFramework meta generators
- 1
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
- 1
THEME_PATTERN = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?"\s+/?>}
- 1
FRAMEWORK_PATTERN = %r{<meta name="generator" content="WooFramework\s?([^"]+)?"\s+/?>}
- 1
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i
- 1
def passive(opts = {})
- 2
return unless target.homepage_res.body =~ PATTERN
- 1
WPScan::Theme.new(
Regexp.last_match[1],
target,
opts.merge(found_by: found_by, confidence: 80)
)
end
end
end
end
end
- 1
require_relative 'medias/attachment_brute_forcing'
- 1
module WPScan
- 1
module Finders
- 1
module Medias
# Medias Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
- 1
finders << Medias::AttachmentBruteForcing.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Medias
# Medias Finder
- 1
class AttachmentBruteForcing < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ Range ] :range Mandatory
#
# @return [ Array<Media> ]
- 1
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts) do |res|
next unless res.code == 200
found << WPScan::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
end
found
end
# @param [ Hash ] opts
# @option opts [ Range ] :range Mandatory
#
# @return [ Hash ]
- 1
def target_urls(opts = {})
- 1
urls = {}
- 1
opts[:range].each do |id|
- 2
urls[target.uri.join("?attachment_id=#{id}").to_s] = id
end
- 1
urls
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: ' Brute Forcing Attachment Ids -'))
end
end
end
end
end
- 1
require_relative 'plugin_version/readme'
# Plugins Specific
- 1
require_relative 'plugin_version/layer_slider/translation_file'
- 1
require_relative 'plugin_version/revslider/release_log'
- 1
require_relative 'plugin_version/sitepress_multilingual_cms/version_parameter'
- 1
require_relative 'plugin_version/sitepress_multilingual_cms/meta_generator'
- 1
require_relative 'plugin_version/w3_total_cache/headers'
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
# Plugin Version Finder
- 1
class Base
- 1
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Plugin ] plugin
- 1
def initialize(plugin)
- 5
finders << PluginVersion::Readme.new(plugin)
- 5
load_specific_finders(plugin)
end
# Load the finders associated with the plugin
#
# @param [ WPScan::Plugin ] plugin
- 1
def load_specific_finders(plugin)
- 5
module_name = plugin.classify_name.to_sym
- 5
return unless Finders::PluginVersion.constants.include?(module_name)
- 4
mod = Finders::PluginVersion.const_get(module_name)
- 4
mod.constants.each do |constant|
- 7
c = mod.const_get(constant)
- 7
next unless c.is_a?(Class)
- 7
finders << c.new(plugin)
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
- 1
module LayerSlider
# Version from a Translation file
#
# See https://github.com/wpscanteam/wpscan/issues/765
- 1
class TranslationFile < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def aggressive(_opts = {})
- 4
potential_urls.each do |url|
- 8
res = Browser.get(url)
- 8
next unless res.code == 200 && res.body =~ /Project-Id-Version: LayerSlider WP v?([0-9\.][^\\\s]+)/
return WPScan::Version.new(
Regexp.last_match[1],
found_by: 'Translation File (Aggressive Detection)',
confidence: 90,
interesting_entries: ["#{url}, Match: '#{Regexp.last_match}'"]
- 2
)
end
nil
end
# @return [ Array<String> ] The potential URLs where the version is disclosed
- 1
def potential_urls
# Recent versions seem to use the 'locales' directory instead of the 'languages' one.
# Maybe also check other locales ?
- 8
%w(locales languages).reduce([]) do |a, e|
- 16
a << target.url("#{e}/LayerSlider-en_US.po")
end
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
# Plugin Version Finder from the readme.txt file
- 1
class Readme < CMSScanner::Finders::Finder
# @return [ Version ]
- 1
def aggressive(_opts = {})
- 18
found_by_msg = 'Readme - %s (Aggressive Detection)'
- 18
WPScan::WpItem::READMES.each do |file|
- 79
url = target.url(file)
- 79
res = Browser.get(url)
- 79
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
return numbers.reduce([]) do |a, e|
a << WPScan::Version.new(
e[0],
found_by: format(found_by_msg, e[1]),
confidence: e[2],
interesting_entries: [url]
- 15
)
- 14
end
end
nil
end
# @return [ Array<String, String, Integer> ] number, found_by, confidence
- 1
def version_numbers(body)
- 18
numbers = []
- 18
if (number = from_stable_tag(body))
- 4
numbers << [number, 'Stable Tag', 80]
end
- 18
if (number = from_changelog_section(body))
- 11
numbers << [number, 'ChangeLog Section', 50]
end
- 18
numbers
end
# @param [ String ] body
#
# @return [ String, nil ] The version number detected from the stable tag
- 1
def from_stable_tag(body)
- 18
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
- 7
number = Regexp.last_match[1]
- 7
number if number =~ /[0-9]+/
end
# @param [ String ] body
#
# @return [ String, nil ] The best version number detected from the changelog section
- 1
def from_changelog_section(body)
- 18
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
- 18
return if extracted_versions.nil? || extracted_versions.empty?
- 11
extracted_versions.flatten!
# must contain at least one number
- 289
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
- 11
sorted = extracted_versions.sort do |x, y|
- 882
begin
- 882
Gem::Version.new(x) <=> Gem::Version.new(y)
rescue
- 2
0
end
end
- 11
sorted.last
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
- 1
module Revslider
# Version from the release_log.html
#
# See https://github.com/wpscanteam/wpscan/issues/817
- 1
class ReleaseLog < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def aggressive(_opts = {})
- 2
res = Browser.get(release_log_url)
- 2
res.html.css('h3.version-number:first').each do |node|
- 1
next unless node.text =~ /\AVersion ([0-9\.]+).*\z/i
return WPScan::Version.new(
Regexp.last_match[1],
found_by: found_by,
confidence: 90,
interesting_entries: ["#{release_log_url}, Match: '#{Regexp.last_match}'"]
- 1
)
end
nil
end
- 1
def release_log_url
- 6
target.url('release_log.html')
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
- 1
module SitepressMultilingualCms
# Version from the meta generator
- 1
class MetaGenerator < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def passive(_opts = {})
- 3
target.target.homepage_res.html.css('meta[name="generator"]').each do |node|
- 2
next unless node['content'] =~ /\AWPML\sver:([0-9\.]+)\sstt/i
return WPScan::Version.new(
Regexp.last_match(1),
found_by: 'Meta Generator (Passive detection)',
confidence: 50,
interesting_entries: ["#{target.target.url}, Match: '#{node}'"]
- 1
)
end
nil
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
- 1
module SitepressMultilingualCms
# Version from the v parameter in href / src of stylesheets / scripts
- 1
class VersionParameter < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def passive(_opts = {})
- 2
pattern = %r{#{Regexp.escape(target.target.plugins_dir)}/sitepress-multilingual-cms/}i
- 2
target.target.in_scope_urls(target.target.homepage_res, '//link|//script') do |url|
- 4
uri = Addressable::URI.parse(url)
- 4
next unless uri.path =~ pattern && uri.query =~ /v=([0-9\.]+)/
return WPScan::Version.new(
Regexp.last_match[1],
found_by: found_by,
confidence: 50,
interesting_entries: [url]
- 1
)
end
nil
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module PluginVersion
- 1
module W3TotalCache
# Version from Headers
- 1
class Headers < CMSScanner::Finders::Finder
- 1
PATTERN = %r{W3 Total Cache/([0-9.]+)}i
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def passive(_opts = {})
headers = target.target.headers
return unless headers && headers['X-Powered-By'].to_s =~ PATTERN
WPScan::Version.new(
Regexp.last_match[1],
found_by: found_by,
confidence: 80,
interesting_entries: ["#{target.target.url}, Match: '#{Regexp.last_match}'"]
)
end
end
end
end
end
end
- 1
require_relative 'plugins/urls_in_homepage'
- 1
require_relative 'plugins/headers'
- 1
require_relative 'plugins/comments'
- 1
require_relative 'plugins/known_locations'
- 1
module WPScan
- 1
module Finders
- 1
module Plugins
# Plugins Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
finders <<
Plugins::UrlsInHomepage.new(target) <<
Plugins::Headers.new(target) <<
Plugins::Comments.new(target) <<
- 1
Plugins::KnownLocations.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Plugins
# Plugins from Comments Finder
- 1
class Comments < CMSScanner::Finders::Finder
# @param [ Hash ] opts
# @option opts [ Boolean ] :unique Default: true
#
# @return [ Array<Plugin> ]
- 1
def passive(opts = {})
- 3
found = []
- 3
opts[:unique] = true unless opts.key?(:unique)
- 3
target.homepage_res.html.xpath('//comment()').each do |node|
- 131
comment = node.text.to_s.strip
- 131
DB::DynamicPluginFinders.comments.each do |name, config|
- 5764
next unless comment =~ config['pattern']
- 122
plugin = WPScan::Plugin.new(name, target, opts.merge(found_by: found_by, confidence: 70))
- 122
found << plugin unless opts[:unique] && found.include?(plugin)
end
end
- 3
found
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Plugins
# Plugins from Headers Finder
- 1
class Headers < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Array<Plugin> ]
- 1
def passive(opts = {})
- 3
plugin_names_from_headers(opts).reduce([]) do |a, e|
- 3
a << WPScan::Plugin.new(e, target, opts.merge(found_by: found_by, confidence: 60))
end
end
# X-Powered-By: W3 Total Cache/0.9.2.5
# WP-Super-Cache: Served supercache file from PHP
#
# @return [ Array<String> ]
- 1
def plugin_names_from_headers(_opts = {})
- 3
found = []
- 3
headers = target.homepage_res.headers
- 3
if headers
- 2
powered_by = headers['X-Powered-By'].to_s
- 2
wp_super_cache = headers['wp-super-cache'].to_s
- 2
found << 'w3-total-cache' if powered_by =~ Finders::PluginVersion::W3TotalCache::Headers::PATTERN
- 2
found << 'wp-super-cache' if wp_super_cache =~ /supercache/i
end
- 3
found
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Plugins
# Known Locations Plugins Finder
- 1
class KnownLocations < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ String ] :list
#
# @return [ Array<Plugin> ]
- 1
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts) do |res, name|
# TODO: follow the location (from enumerate()) and remove the 301 here ?
# As a result, it might remove false positive due to redirection to the homepage
next unless [200, 401, 403, 301].include?(res.code)
found << WPScan::Plugin.new(name, target, opts.merge(found_by: found_by, confidence: 80))
end
found
end
# @param [ Hash ] opts
# @option opts [ String ] :list
#
# @return [ Hash ]
- 1
def target_urls(opts = {})
names = opts[:list] || DB::Plugins.vulnerable_slugs
urls = {}
plugins_url = target.plugins_url
names.each do |name|
urls["#{plugins_url}#{URI.encode(name)}/"] = name
end
urls
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: ' Checking Known Locations -'))
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Plugins
# URLs In Homepage Finder
- 1
class UrlsInHomepage < CMSScanner::Finders::Finder
- 1
include WpItems::URLsInHomepage
# @param [ Hash ] opts
#
# @return [ Array<Plugin> ]
- 1
def passive(opts = {})
- 1
found = []
- 1
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |name|
- 10
found << Plugin.new(name, target, opts.merge(found_by: found_by, confidence: 80))
end
- 1
DB::DynamicPluginFinders.urls_in_page.each do |name, config|
- 1
next unless target.homepage_res.html.xpath(config['xpath']).any?
- 1
found << Plugin.new(name, target, opts.merge(found_by: found_by, confidence: 100))
end
- 1
found
end
end
end
end
end
- 1
require_relative 'theme_version/style'
- 1
require_relative 'theme_version/woo_framework_meta_generator'
- 1
module WPScan
- 1
module Finders
- 1
module ThemeVersion
# Theme Version Finder
- 1
class Base
- 1
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Theme ] theme
- 1
def initialize(theme)
finders <<
ThemeVersion::Style.new(theme) <<
- 1
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
- 1
load_specific_finders(theme)
end
# Load the finders associated with the theme
#
# @param [ WPScan::Theme ] theme
- 1
def load_specific_finders(theme)
- 1
module_name = theme.classify_name.to_sym
- 1
return unless Finders::ThemeVersion.constants.include?(module_name)
mod = Finders::ThemeVersion.const_get(module_name)
mod.constants.each do |constant|
c = mod.const_get(constant)
next unless c.is_a?(Class)
finders << c.new(theme)
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module ThemeVersion
# Theme Version Finder from the style.css file
- 1
class Style < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def passive(_opts = {})
- 2
return unless cached_style?
- 1
style_version
end
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def aggressive(_opts = {})
- 2
return if cached_style?
- 1
style_version
end
# @return [ Boolean ]
- 1
def cached_style?
- 1
Typhoeus::Config.cache.get(browser.forge_request(target.style_url)) ? true : false
end
# @return [ Version ]
- 1
def style_version
- 6
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
- 3
WPScan::Version.new(
Regexp.last_match[1],
found_by: found_by,
confidence: 80,
interesting_entries: ["#{target.style_url}, Match: '#{Regexp.last_match}'"]
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module ThemeVersion
# Theme Version Finder from the WooFramework generators
- 1
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Version ]
- 1
def passive(_opts = {})
- 2
return unless target.target.homepage_res.body =~ Finders::MainTheme::WooFrameworkMetaGenerator::PATTERN
- 2
return unless Regexp.last_match[1] == target.name
- 1
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
end
end
end
end
end
- 1
require_relative 'themes/urls_in_homepage'
- 1
require_relative 'themes/known_locations'
- 1
module WPScan
- 1
module Finders
- 1
module Themes
# themes Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
finders <<
Themes::UrlsInHomepage.new(target) <<
- 1
Themes::KnownLocations.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Themes
# Known Locations Themes Finder
- 1
class KnownLocations < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ String ] :list
#
# @return [ Array<Theme> ]
- 1
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts) do |res, name|
# TODO: follow the location (from enumerate()) and remove the 301 here ?
# As a result, it might remove false positive due to redirection to the homepage
next unless [200, 401, 403, 301].include?(res.code)
found << WPScan::Theme.new(name, target, opts.merge(found_by: found_by, confidence: 80))
end
found
end
# @param [ Hash ] opts
# @option opts [ String ] :list
#
# @return [ Hash ]
- 1
def target_urls(opts = {})
names = opts[:list] || DB::Themes.vulnerable_slugs
urls = {}
themes_url = target.url('wp-content/themes/')
names.each do |name|
urls["#{themes_url}#{URI.encode(name)}/"] = name
end
urls
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: ' Checking Known Locations -'))
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Themes
# URLs In Homepage Finder
- 1
class UrlsInHomepage < CMSScanner::Finders::Finder
- 1
include WpItems::URLsInHomepage
# @param [ Hash ] opts
#
# @return [ Array<Theme> ]
- 1
def passive(opts = {})
found = []
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |name|
found << WPScan::Theme.new(name, target, opts.merge(found_by: found_by, confidence: 80))
end
found
end
end
end
end
end
- 1
require_relative 'timthumb_version/bad_request'
- 1
module WPScan
- 1
module Finders
- 1
module TimthumbVersion
# Timthumb Version Finder
- 1
class Base
- 1
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Timthumb ] target
- 1
def initialize(target)
- 1
finders << TimthumbVersion::BadRequest.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module TimthumbVersion
# Timthumb Version Finder from the body of a bad request
# See https://code.google.com/p/timthumb/source/browse/trunk/timthumb.php#435
- 1
class BadRequest < CMSScanner::Finders::Finder
# @return [ Version ]
- 1
def aggressive(_opts = {})
- 2
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
- 1
WPScan::Version.new(
Regexp.last_match[2],
found_by: 'Bad Request (Aggressive Detection)',
confidence: 90,
interesting_entries: ["#{target.url}, Match: '#{Regexp.last_match[1]}'"]
)
end
end
end
end
end
- 1
require_relative 'timthumbs/known_locations'
- 1
module WPScan
- 1
module Finders
- 1
module Timthumbs
# Timthumbs Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
- 1
finders << Timthumbs::KnownLocations.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Timthumbs
# Known Locations Timthumbs Finder
- 1
class KnownLocations < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ String ] :list Mandatory
#
# @return [ Array<Timthumb> ]
- 1
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts) do |res|
next unless res.code == 400 && res.body =~ /no image specified/i
found << WPScan::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
end
found
end
# @param [ Hash ] opts
# @option opts [ String ] :list Mandatory
#
# @return [ Hash ]
- 1
def target_urls(opts = {})
urls = {}
File.open(opts[:list]).each_with_index do |path, index|
urls[target.url(path.chomp)] = index
end
# Add potential timthumbs located in the main theme
if target.main_theme
main_theme_timthumbs_paths.each do |path|
urls[target.main_theme.url(path)] = 1 # index not important there
end
end
urls
end
- 1
def main_theme_timthumbs_paths
%w(timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
scripts/timthumb.php tools/timthumb.php functions/timthumb.php)
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: ' Checking Known Locations -'))
end
end
end
end
end
- 1
require_relative 'users/author_posts'
- 1
require_relative 'users/wp_json_api'
- 1
require_relative 'users/author_id_brute_forcing'
- 1
require_relative 'users/login_error_messages'
- 1
module WPScan
- 1
module Finders
- 1
module Users
# Users Finder
- 1
class Base
- 1
include CMSScanner::Finders::SameTypeFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
finders <<
Users::AuthorPosts.new(target) <<
Users::WpJsonApi.new(target) <<
Users::AuthorIdBruteForcing.new(target) <<
- 1
Users::LoginErrorMessages.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Users
# Author Id Brute Forcing
- 1
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Enumerator
# @param [ Hash ] opts
# @option opts [ Range ] :range Mandatory
#
# @return [ Array<User> ]
- 1
def aggressive(opts = {})
found = []
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
enumerate(target_urls(opts), opts) do |res, id|
username, found_by, confidence = potential_username(res)
next unless username
found << WPScan::User.new(
username,
id: id,
found_by: format(found_by_msg, found_by),
confidence: confidence
)
end
found
end
# @param [ Hash ] opts
# @option opts [ Range ] :range
#
# @return [ Hash ]
- 1
def target_urls(opts = {})
- 1
urls = {}
- 1
opts[:range].each do |id|
- 2
urls[target.uri.join("?author=#{id}").to_s] = id
end
- 1
urls
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: ' Brute Forcing Author Ids -'))
end
- 1
def request_params
{ followlocation: true }
end
# @param [ Typhoeus::Response ] res
#
# @return [ Array<String, String, Integer>, nil ] username, found_by, confidence
- 1
def potential_username(res)
username = username_from_author_url(res.effective_url) || username_from_response(res)
return username, 'Author Pattern', 100 if username
username = display_name_from_body(res.body)
return username, 'Display Name', 50 if username
end
# @param [ String ] url
#
# @return [ String, nil ]
- 1
def username_from_author_url(url)
- 64
url[%r{/author/([^/\b]+)/?}i, 1]
end
# @param [ Typhoeus::Response ] res
#
# @return [ String, nil ] The username found
- 1
def username_from_response(res)
# Permalink enabled
- 6
target.in_scope_urls(res, '//link|//a') do |url|
- 64
username = username_from_author_url(url)
- 64
return username if username
end
# No permalink
- 3
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
end
# @param [ String ] body
#
# @return [ String, nil ]
- 1
def display_name_from_body(body)
- 9
page = Nokogiri::HTML.parse(body)
# WP >= 3.0
- 9
page.css('h1.page-title span').each do |node|
- 4
return node.text.to_s
end
# WP < 3.0
- 5
page.xpath('//link[@rel="alternate" and @type="application/rss+xml"]').each do |node|
- 13
title = node['title']
- 13
next unless title =~ /Posts by (.*) Feed\z/i
- 3
return Regexp.last_match[1] unless Regexp.last_match[1].empty?
end
nil
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Users
# Author Posts
- 1
class AuthorPosts < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Array<User> ]
- 1
def passive(opts = {})
found_by_msg = 'Author Posts - %s (Passive Detection)'
usernames(opts).reduce([]) do |a, e|
a << WPScan::User.new(
e[0],
found_by: format(found_by_msg, e[1]),
confidence: e[2]
)
end
end
# @param [ Hash ] opts
#
# @return [ Array<Array>> ]
- 1
def usernames(_opts = {})
found = potential_usernames(target.homepage_res)
return found unless found.empty?
target.homepage_res.html.css('header.entry-header a').each do |post_url_node|
url = post_url_node['href']
next if url.nil? || url.empty?
found += potential_usernames(Browser.get(url))
end
found.compact.uniq
end
# @param [ Typhoeus::Response ] res
#
# @return [ Array<Array> ]
- 1
def potential_usernames(res)
- 1
usernames = []
- 1
target.in_scope_urls(res, '//a', %w(href)) do |url, node|
- 5
uri = Addressable::URI.parse(url)
- 5
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
- 2
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
- 3
elsif uri.query =~ /author=[0-9]+/
- 2
usernames << [node.text.to_s.strip, 'Display Name', 30]
end
end
- 1
usernames.uniq
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Users
# Login Error Messages
#
# Existing username:
# WP < 3.1 - Incorrect password.
# WP >= 3.1 - The password you entered for the username admin is incorrect.
# Non existent username: Invalid username.
#
- 1
class LoginErrorMessages < CMSScanner::Finders::Finder
# @param [ Hash ] opts
# @option opts [ String ] :list
#
# @return [ Array<User> ]
- 1
def aggressive(opts = {})
found = []
usernames(opts).each do |username|
res = target.do_login(username, SecureRandom.hex[0, 8])
return found unless res.code == 200
error = res.html.css('div#login_error').text.strip
return found if error.empty? # Protection plugin / error disabled
next unless error =~ /The password you entered for the username|Incorrect Password/i
found << WPScan::User.new(username, found_by: found_by, confidence: 100)
end
found
end
# @return [ Array<String> ] List of usernames to check
- 1
def usernames(opts = {})
# usernames from the potential Users found
unames = opts[:found].map(&:username)
if opts[:list]
File.open(opts[:list]).each { |uname| unames << uname.chomp }
end
unames.uniq
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module Users
# WP JSON API
#
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
#
- 1
class WpJsonApi < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Array<User> ]
- 1
def aggressive(_opts = {})
found = []
JSON.parse(Browser.get(api_url).body).each do |user|
found << WPScan::User.new(user['slug'], id: user['id'], found_by: found_by, confidence: 100)
end
found
rescue JSON::ParserError
found
end
# @return [ String ] The URL of the API listing the Users
- 1
def api_url
@api_url ||= target.url('wp-json/wp/v2/users/')
end
end
end
end
end
- 1
require_relative 'wp_items/urls_in_homepage'
- 1
module WPScan
- 1
module Finders
- 1
module WpItems
# URLs In Homepage Module to use in plugins & themes finders
- 1
module URLsInHomepage
# @param [ String ] type plugins / themes
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
#
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
- 1
def items_from_links(type, uniq = true)
- 8
found = []
- 8
target.in_scope_urls(target.homepage_res) do |url|
- 41
next unless url =~ item_attribute_pattern(type)
- 20
found << Regexp.last_match[1]
end
- 8
uniq ? found.uniq.sort : found.sort
end
# @param [ String ] type plugins / themes
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
#
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
- 1
def items_from_codes(type, uniq = true)
- 8
found = []
- 8
target.homepage_res.html.css('script,style').each do |tag|
- 36
code = tag.text.to_s
- 36
next if code.empty?
- 41
code.scan(item_code_pattern(type)).flatten.uniq.each { |name| found << name }
end
- 8
uniq ? found.uniq.sort : found.sort
end
# @param [ String ] type
#
# @return [ Regexp ]
- 1
def item_attribute_pattern(type)
- 41
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
end
# @param [ String ] type
#
# @return [ Regexp ]
- 1
def item_code_pattern(type)
- 21
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
end
# @param [ String ] type
#
# @return [ Regexp ]
- 1
def item_url_pattern(type)
- 14
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
- 14
item_url = type == 'plugins' ? target.plugins_url : target.content_url
- 14
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
- 14
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
- 14
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
end
end
end
end
end
- 1
require_relative 'wp_version/meta_generator'
- 1
require_relative 'wp_version/rss_generator'
- 1
require_relative 'wp_version/atom_generator'
- 1
require_relative 'wp_version/rdf_generator'
- 1
require_relative 'wp_version/readme'
- 1
require_relative 'wp_version/sitemap_generator'
- 1
require_relative 'wp_version/opml_generator'
- 1
require_relative 'wp_version/stylesheets'
- 1
require_relative 'wp_version/unique_fingerprinting'
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Wp Version Finder
- 1
class Base
- 1
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Target ] target
- 1
def initialize(target)
finders <<
WpVersion::MetaGenerator.new(target) <<
WpVersion::RSSGenerator.new(target) <<
WpVersion::AtomGenerator.new(target) <<
WpVersion::Stylesheets.new(target) <<
WpVersion::RDFGenerator.new(target) <<
WpVersion::Readme.new(target) <<
WpVersion::SitemapGenerator.new(target) <<
WpVersion::OpmlGenerator.new(target) <<
- 1
WpVersion::UniqueFingerprinting.new(target)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Atom Generator Version Finder
- 1
class AtomGenerator < CMSScanner::Finders::Finder
- 1
include Finder::WpVersion::SmartURLChecker
- 1
def process_urls(urls, _opts = {})
found = Findings.new
urls.each do |url|
res = Browser.get_and_follow_location(url)
res.html.css('generator').each do |node|
next unless node.text.to_s.strip.casecmp('wordpress').zero?
found << create_version(
node['version'],
found_by: found_by,
entries: ["#{res.effective_url}, #{node}"]
)
end
end
found
end
- 1
def passive_urls_xpath
'//link[@rel="alternate" and @type="application/atom+xml"]'
end
- 1
def aggressive_urls(_opts = {})
%w(feed/atom/ ?feed=atom).reduce([]) do |a, uri|
a << target.url(uri)
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Meta Generator Version Finder
- 1
class MetaGenerator < CMSScanner::Finders::Finder
# @return [ WpVersion ]
- 1
def passive(_opts = {})
- 4
target.homepage_res.html.css('meta[name="generator"]').each do |node|
- 3
next unless node.attribute('content').to_s =~ /wordpress ([0-9\.]+)/i
- 3
number = Regexp.last_match(1)
- 3
next unless WPScan::WpVersion.valid?(number)
return WPScan::WpVersion.new(
number,
found_by: 'Meta Generator (Passive detection)',
confidence: 80,
interesting_entries: ["#{target.url}, Match: '#{node}'"]
- 2
)
end
nil
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Sitemap Generator Version Finder
- 1
class OpmlGenerator < CMSScanner::Finders::Finder
# @return [ WpVersion ]
- 1
def aggressive(_opts = {})
- 3
target.comments_from_page(%r{\Agenerator="wordpress/([^"]+)"\z}i, 'wp-links-opml.php') do |match, node|
- 2
next unless WPScan::WpVersion.valid?(match[1])
return WPScan::WpVersion.new(
match[1],
found_by: 'OPML Generator (Aggressive Detection)',
confidence: 80,
interesting_entries: ["#{target.url('wp-links-opml.php')}, Match: '#{node}'"]
- 1
)
end
nil
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# RDF Generator Version Finder
- 1
class RDFGenerator < CMSScanner::Finders::Finder
- 1
include Finder::WpVersion::SmartURLChecker
- 1
def process_urls(urls, _opts = {})
found = Findings.new
urls.each do |url|
res = Browser.get_and_follow_location(url)
res.html.xpath('//generatoragent').each do |node|
next unless node['rdf:resource'] =~ %r{\Ahttps?://wordpress\.(?:[a-z.]+)/\?v=(.*)\z}i
found << create_version(
Regexp.last_match[1],
found_by: found_by,
entries: ["#{res.effective_url}, #{node}"]
)
end
end
found
end
- 1
def passive_urls_xpath
'//a[contains(@href, "rdf")]'
end
- 1
def aggressive_urls(_opts = {})
[target.url('feed/rdf/')]
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Readme Version Finder
- 1
class Readme < CMSScanner::Finders::Finder
# @return [ WpVersion ]
- 1
def aggressive(_opts = {})
- 3
readme_url = target.url('readme.html') # Maybe move this into the Target ?
- 3
node = Browser.get(readme_url).html.css('h1#logo').last
- 3
return unless node && node.text.to_s.strip =~ /\AVersion (.*)\z/i
- 2
number = Regexp.last_match(1)
- 2
return unless WPScan::WpVersion.valid?(number)
- 1
WPScan::WpVersion.new(
number,
found_by: 'Readme (Aggressive Detection)',
confidence: 90,
interesting_entries: ["#{readme_url}, Match: '#{node.text.to_s.strip}'"]
)
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# RSS Generator Version Finder
- 1
class RSSGenerator < CMSScanner::Finders::Finder
- 1
include Finder::WpVersion::SmartURLChecker
- 1
def process_urls(urls, _opts = {})
found = Findings.new
urls.each do |url|
res = Browser.get_and_follow_location(url)
res.html.xpath('//comment()[contains(., "wordpress")] | //generator').each do |node|
node_text = node.text.to_s.strip
next unless node_text =~ %r{\Ahttps?://wordpress\.(?:[a-z]+)/\?v=(.*)\z}i ||
node_text =~ %r{\Agenerator="wordpress/([^"]+)"\z}i
found << create_version(
Regexp.last_match[1],
found_by: found_by,
entries: ["#{res.effective_url}, #{node}"]
)
end
end
found
end
- 1
def passive_urls_xpath
'//link[@rel="alternate" and @type="application/rss+xml"]'
end
- 1
def aggressive_urls(_opts = {})
%w(feed/ comments/feed/ feed/rss/ feed/rss2/).reduce([]) do |a, uri|
a << target.url(uri)
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Sitemap Generator Version Finder
- 1
class SitemapGenerator < CMSScanner::Finders::Finder
# @return [ WpVersion ]
- 1
def aggressive(_opts = {})
- 4
target.comments_from_page(%r{\Agenerator="wordpress/([^"]+)"\z}i, 'sitemap.xml') do |match, node|
- 2
next unless WPScan::WpVersion.valid?(match[1])
return WPScan::WpVersion.new(
match[1],
found_by: 'Sitemap Generator (Aggressive Detection)',
confidence: 80,
interesting_entries: ["#{target.url('sitemap.xml')}, #{node}"]
- 1
)
end
nil
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Stylesheets Version Finder
- 1
class Stylesheets < CMSScanner::Finders::Finder
# @return [ WpVersion ]
- 1
def passive(_opts = {})
found = []
scan_page(target.homepage_url).each do |version_number, occurences|
next unless WPScan::WpVersion.valid?(version_number) # Skip invalid versions
found << WPScan::WpVersion.new(
version_number,
found_by: 'Stylesheet Numbers (Passive Detection)',
confidence: 5 * occurences,
interesting_entries: [target.homepage_url]
)
end
found
end
- 1
protected
# TODO: use target.in_scope_urls to get the URLs
# @param [ String ] url
#
# @return [ Hash ]
- 1
def scan_page(url)
found = {}
pattern = /\bver=([0-9\.]+)/i
Browser.get(url).html.css('link,script').each do |tag|
%w(href src).each do |attribute|
attr_value = tag.attribute(attribute).to_s
next if attr_value.nil? || attr_value.empty?
uri = Addressable::URI.parse(attr_value)
next unless uri.query && uri.query.match(pattern)
version = Regexp.last_match[1].to_s
found[version] ||= 0
found[version] += 1
end
end
found
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
module WpVersion
# Unique Fingerprinting Version Finder
- 1
class UniqueFingerprinting < CMSScanner::Finders::Finder
- 1
include CMSScanner::Finders::Finder::Fingerprinter
- 1
QUERY = 'SELECT md5_hash, path_id, version_id, ' \
'versions.number AS version,' \
'paths.value AS path ' \
'FROM fingerprints ' \
'LEFT JOIN versions ON version_id = versions.id ' \
'LEFT JOIN paths on path_id = paths.id ' \
'WHERE md5_hash IN ' \
'(SELECT md5_hash FROM fingerprints GROUP BY md5_hash HAVING COUNT(*) = 1) ' \
'ORDER BY version DESC'.freeze
# @return [ WpVersion ]
- 1
def aggressive(opts = {})
fingerprint(unique_fingerprints, opts) do |version_number, url, md5sum|
hydra.abort
progress_bar.finish
return WPScan::WpVersion.new(
version_number,
found_by: 'Unique Fingerprinting (Aggressive Detection)',
confidence: 100,
interesting_entries: ["#{url} md5sum is #{md5sum}"]
)
end
nil
end
# @return [ Hash ] The unique fingerprints across all versions in the DB
#
# 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
# }
# }
- 1
def unique_fingerprints
fingerprints = {}
repository(:default).adapter.select(QUERY).each do |f|
fingerprints[f.path] ||= {}
fingerprints[f.path][f.md5_hash] = f.version
end
fingerprints
end
- 1
def create_progress_bar(opts = {})
super(opts.merge(title: 'Fingerprinting the version -'))
end
end
end
end
end
- 1
require_relative 'models/interesting_finding'
- 1
require_relative 'models/wp_version'
- 1
require_relative 'models/xml_rpc'
- 1
require_relative 'models/wp_item'
- 1
require_relative 'models/timthumb'
- 1
require_relative 'models/media'
- 1
require_relative 'models/user'
- 1
require_relative 'models/plugin'
- 1
require_relative 'models/theme'
- 1
require_relative 'models/config_backup'
- 1
module WPScan
# Config Backup
- 1
class ConfigBackup < InterestingFinding
end
end
- 1
module WPScan
# Custom class to include the WPScan::References module
- 1
class InterestingFinding < CMSScanner::InterestingFinding
- 1
include References
end
end
- 1
module WPScan
# Media
- 1
class Media < InterestingFinding
end
end
- 1
module WPScan
# WordPress Plugin
- 1
class Plugin < WpItem
# See WpItem
- 1
def initialize(name, target, opts = {})
- 299
super(name, target, opts)
- 299
@uri = Addressable::URI.parse(target.url("wp-content/plugins/#{name}/"))
end
# @return [ JSON ]
- 1
def db_data
- 15
DB::Plugin.db_data(name)
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
- 1
def version(opts = {})
- 5
@version = Finders::PluginVersion::Base.find(self, detection_opts.merge(opts)) if @version.nil?
- 5
@version
end
end
end
- 1
module WPScan
# WordPress Theme
- 1
class Theme < WpItem
- 1
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
:license, :license_uri, :tags, :text_domain
# See WpItem
- 1
def initialize(name, target, opts = {})
- 64
super(name, target, opts)
- 64
@uri = Addressable::URI.parse(target.url("wp-content/themes/#{name}/"))
- 64
@style_url = opts[:style_url] || url('style.css')
- 64
parse_style
end
# @return [ JSON ]
- 1
def db_data
- 22
DB::Theme.db_data(name)
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
- 1
def version(opts = {})
- 5
@version = Finders::ThemeVersion::Base.find(self, detection_opts.merge(opts)) if @version.nil?
- 5
@version
end
# @return [ Theme ]
- 1
def parent_theme
- 9
return unless template
- 2
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
- 2
opts = detection_opts.merge(
style_url: url(Regexp.last_match[1]),
found_by: 'Parent Themes (Passive Detection)',
confidence: 100
)
- 2
self.class.new(template, target, opts)
end
# @param [ Integer ] depth
#
# @retun [ Array<Theme> ]
- 1
def parent_themes(depth = 3)
- 6
theme = self
- 6
found = []
- 6
(1..depth).each do |_|
- 6
parent = theme.parent_theme
- 6
break unless parent
found << parent
theme = parent
end
- 6
found
end
- 1
def style_body
- 642
@style_body ||= Browser.get(style_url).body
end
- 1
def parse_style
{
style_name: 'Theme Name',
style_uri: 'Theme URI',
author: 'Author',
author_uri: 'Author URI',
template: 'Template',
description: 'Description',
license: 'License',
license_uri: 'License URI',
tags: 'Tags',
text_domain: 'Text Domain'
- 64
}.each do |attribute, tag|
- 640
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
end
end
# @param [ String ] bofy
# @param [ String ] tag
#
# @return [ String ]
- 1
def parse_style_tag(body, tag)
- 640
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
- 640
value && !value.strip.empty? ? value.strip : nil
end
- 1
def ==(other)
- 17
super(other) && style_url == other.style_url
end
end
end
- 1
module WPScan
# Timthumb
- 1
class Timthumb < InterestingFinding
- 1
include Vulnerable
# Opts used to detect the version
- 1
attr_reader :detection_opts
# @param [ String ] url
# @param [ Hash ] opts
# @option opts [ String ] :detection_mode
- 1
def initialize(url, opts = {})
- 28
super(url, opts)
- 28
@detection_opts = { mode: opts[:mode] }
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
- 1
def version(opts = {})
- 46
if @version.nil?
- 14
@version = Finders::TimthumbVersion::Base.find(self, detection_opts.merge(opts))
end
- 46
@version
end
# @return [ Array<Vulnerability> ]
- 1
def vulnerabilities
- 18
vulns = []
- 18
vulns << rce_webshot_vuln if false == version || version > '1.35' && version < '2.8.14' && webshot_enabled?
- 18
vulns << rce_132_vuln if false == version || version < '1.33'
- 18
vulns
end
# @return [ Vulnerability ] The RCE in the <= 1.32
- 1
def rce_132_vuln
- 8
Vulnerability.new(
'Timthumb <= 1.32 Remote Code Execution',
{ exploitdb: ['17602'] },
'RCE',
'1.33'
)
end
# @return [ Vulnerability ] The RCE due to the WebShot in the > 1.35 (or >= 2.0) and <= 2.8.13
- 1
def rce_webshot_vuln
- 8
Vulnerability.new(
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
{
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
cve: '2014-4663'
},
'RCE',
'2.8.14'
)
end
# @return [ Boolean ]
- 1
def webshot_enabled?
- 2
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
- 2
res.body =~ /WEBSHOT_ENABLED == true/ ? false : true
end
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
- 1
def default_allowed_domains
- 2
%w(flickr.com picasa.com img.youtube.com upload.wikimedia.org)
end
end
end
- 1
module WPScan
# WordPress User
- 1
class User
- 1
include Finders::Finding
- 1
attr_accessor :password
- 1
attr_reader :id, :username
# @param [ String ] username
# @param [ Hash ] opts
# @option opts [ Integer ] :id
# @option opts [ String ] :password
- 1
def initialize(username, opts = {})
- 18
@username = username
- 18
@password = opts[:password]
- 18
@id = opts[:id]
- 18
parse_finding_options(opts)
end
- 1
def ==(other)
- 6
return false unless self.class == other.class
- 5
username == other.username
end
- 1
def to_s
- 1
username
end
end
end
- 1
module WPScan
# WpItem (superclass of Plugin & Theme)
- 1
class WpItem
- 1
include Vulnerable
- 1
include Finders::Finding
- 1
include CMSScanner::Target::Platform::PHP
- 1
include CMSScanner::Target::Server::Generic
- 1
READMES = %w(readme.txt README.txt Readme.txt ReadMe.txt README.TXT readme.TXT).freeze
- 1
CHANGELOGS = %w(changelog.txt Changelog.txt ChangeLog.txt CHANGELOG.txt).freeze
- 1
attr_reader :uri, :name, :detection_opts, :target, :db_data
# @param [ String ] name The plugin/theme name
# @param [ Target ] target The targeted blog
# @param [ Hash ] opts
# @option opts [ String ] :detection_mode
# @option opts [ Boolean ] :version_all Wether or not to
# @option opts [ String ] :url The URL of the item
- 1
def initialize(name, target, opts = {})
- 384
@name = URI.decode(name)
- 384
@target = target
- 384
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
# Options used to detect the version
- 384
@detection_opts = { mode: opts[:mode], confidence_threshold: opts[:version_all] ? 0 : 100 }
- 384
parse_finding_options(opts)
end
# @return [ Array<Vulnerabily> ]
- 1
def vulnerabilities
- 16
return @vulnerabilities if @vulnerabilities
- 11
@vulnerabilities = []
- 11
[*db_data['vulnerabilities']].each do |json_vuln|
- 8
vulnerability = Vulnerability.load_from_json(json_vuln)
- 8
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end
- 11
@vulnerabilities
end
# Checks if the wp_item is vulnerable to a specific vulnerability
#
# @param [ Vulnerability ] vuln Vulnerability to check the item against
#
# @return [ Boolean ]
- 1
def vulnerable_to?(vuln)
- 8
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
- 2
version < vuln.fixed_in ? true : false
end
# @return [ String ]
- 1
def latest_version
- 17
@latest_version ||= db_data['latest_version'] ? WPScan::Version.new(db_data['latest_version']) : nil
end
# Not used anywhere ATM
# @return [ Boolean ]
- 1
def popular?
@popular ||= db_data['popular']
end
# @return [ String ]
- 1
def last_updated
- 8
@last_updated ||= db_data['last_updated']
end
# @return [ Boolean ]
- 1
def outdated?
- 8
@outdated ||= if version && latest_version
- 2
version < latest_version
else
- 6
false
- 8
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 ]
- 1
def url(path = nil)
- 219
return unless @uri
- 218
return @uri.to_s unless path
- 204
@uri.join(URI.encode(path)).to_s
end
# @return [ Boolean ]
- 1
def ==(other)
- 1532
return false unless self.class == other.class
- 1531
name == other.name
end
- 1
def to_s
- 1240
name
end
# @return [ Symbol ] The Class name associated to the item name
- 1
def classify_name
- 7
name.to_s.tr('-', '_').camelize.to_s.to_sym
end
# @return [ String ] The readme url if found
- 1
def readme_url
- 9
return if detection_opts[:mode] == :passive
- 9
if @readme_url.nil?
- 6
READMES.each do |path|
- 6
return @readme_url = url(path) if Browser.get(url(path)).code == 200
end
end
- 3
@readme_url
end
# @return [ String, false ] The changelog urr if found
- 1
def changelog_url
- 9
return if detection_opts[:mode] == :passive
- 9
if @changelog_url.nil?
- 6
CHANGELOGS.each do |path|
- 6
return @changelog_url = url(path) if Browser.get(url(path)).code == 200
end
end
- 3
@changelog_url
end
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
- 1
def directory_listing?(path = nil, params = {})
- 6
return if detection_opts[:mode] == :passive
- 6
super(path, params)
end
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
- 1
def error_log?(path = 'error_log', params = {})
- 6
return if detection_opts[:mode] == :passive
- 6
super(path, params)
end
end
end
- 1
module WPScan
# WP Version
- 1
class WpVersion < CMSScanner::Version
- 1
include Vulnerable
- 1
attr_reader :db_data
- 1
def initialize(number, opts = {})
- 34
raise InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
- 33
super(number, opts)
end
# @param [ String ] number
#
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
- 1
def self.valid?(number)
- 45
all.include?(number)
end
# @return [ Array<String> ] All the version numbers
- 1
def self.all
- 45
return @all_numbers if @all_numbers
- 1
@all_numbers = []
- 102
DB::Version.all.each { |v| @all_numbers << v.number }
- 1
@all_numbers
end
# @return [ JSON ]
- 1
def db_data
- 15
DB::Version.db_data(number)
end
# @return [ Array<Vulnerability> ]
- 1
def vulnerabilities
- 17
return @vulnerabilities if @vulnerabilities
- 15
@vulnerabilities = []
- 15
[*db_data['vulnerabilities']].each do |json_vuln|
- 9
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
end
- 15
@vulnerabilities
end
end
end
- 1
module WPScan
# Override of the CMSScanner::XMLRPC to include the references
- 1
class XMLRPC < CMSScanner::XMLRPC
- 1
include References # To be able to use the :wpvulndb reference if needed
# @return [ Hash ]
- 1
def references
{
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
metasploit: [
'auxiliary/scanner/http/wordpress_ghost_scanner',
'auxiliary/dos/http/wordpress_xmlrpc_dos',
'auxiliary/scanner/http/wordpress_xmlrpc_login',
'auxiliary/scanner/http/wordpress_pingback_access'
]
- 1
}
end
end
end
# Gems
# Believe it or not, active_support MUST be the first one,
# otherwise encoding issues can happen when using JSON format.
# Not kidding.
- 1
require 'active_support/all'
- 1
require 'cms_scanner'
- 1
require 'yajl/json_gem'
- 1
require 'addressable/uri'
# Standard Lib
- 1
require 'uri'
- 1
require 'time'
- 1
require 'readline'
- 1
require 'securerandom'
# Custom Libs
- 1
require 'wpscan/helper'
- 1
require 'wpscan/db'
- 1
require 'wpscan/version'
- 1
require 'wpscan/errors/wordpress'
- 1
require 'wpscan/errors/http'
- 1
require 'wpscan/errors/update'
- 1
require 'wpscan/browser'
- 1
require 'wpscan/target'
- 1
require 'wpscan/finders'
- 1
require 'wpscan/controller'
- 1
require 'wpscan/controllers'
- 1
require 'wpscan/references'
- 1
require 'wpscan/vulnerable'
- 1
require 'wpscan/vulnerability'
- 1
Encoding.default_external = Encoding::UTF_8
# WPScan
- 1
module WPScan
- 1
include CMSScanner
- 1
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
- 1
DB_DIR = File.join(Dir.home, '.wpscan', 'db')
# Override, otherwise it would be returned as 'wp_scan'
#
# @return [ String ]
- 1
def self.app_name
- 83
'wpscan'
end
end
- 1
require "#{WPScan::APP_DIR}/app"
- 1
module WPScan
# Custom Browser
- 1
class Browser < CMSScanner::Browser
- 1
extend Actions
# @return [ String ] The path to the user agents list
- 1
def user_agents_list
- 2
@user_agents_list ||= File.join(DB_DIR, 'user-agents.txt')
end
# @return [ String ]
- 1
def default_user_agent
- 11
"WPScan v#{VERSION} (http://wpscan.org/)"
end
end
end
- 1
module WPScan
# Needed to load at least the Core controller
# Otherwise, the following error will be raised:
# `initialize': uninitialized constant WPScan::Controller::Core (NameError)
- 1
module Controller
- 1
include CMSScanner::Controller
end
end
- 1
module WPScan
# Override to set the OptParser's summary width to 45 (instead of 40 from the CMSScanner)
- 1
class Controllers < CMSScanner::Controllers
- 1
def initialize(option_parser = OptParseValidator::OptParser.new(nil, 45))
super(option_parser)
end
end
end
- 1
require 'dm-core'
- 1
require 'dm-migrations'
- 1
require 'dm-constraints'
- 1
require 'dm-sqlite-adapter'
- 1
require 'wpscan/db/wp_item'
- 1
require 'wpscan/db/schema'
- 1
require 'wpscan/db/updater'
- 1
require 'wpscan/db/wp_items'
- 1
require 'wpscan/db/plugins'
- 1
require 'wpscan/db/themes'
- 1
require 'wpscan/db/plugin'
- 1
require 'wpscan/db/theme'
- 1
require 'wpscan/db/wp_version'
- 1
require 'wpscan/db/dynamic_finders'
- 1
module WPScan
# DB
- 1
module DB
- 1
def self.init_db
- 9
db_file ||= File.join(DB_DIR, 'wordpress.db')
# DataMapper::Logger.new($stdout, :debug)
- 9
DataMapper.setup(:default, "sqlite://#{db_file}")
- 9
DataMapper.auto_upgrade!
end
end
end
- 1
module WPScan
- 1
module DB
# Dynamic Finders
- 1
class DynamicFinders
# @return [ String ]
- 1
def self.db_file
- 2
@db_file ||= File.join(DB_DIR, 'dynamic_finders.yml')
end
# @return [ Hash ]
- 1
def self.db_data
- 2
@db_data ||= YAML.load_file(db_file)
end
# @return [ Hash ]
- 1
def self.finder_configs(finder_klass)
- 2
configs = {}
- 2
db_data.each do |slug, config|
- 90
next unless config[finder_klass]
- 45
configs[slug] = config[finder_klass].dup
end
- 2
configs
end
end
# Dynamic Plugin Finders
- 1
class DynamicPluginFinders < DynamicFinders
# @return [ Hash ]
- 1
def self.db_data
- 4
@db_data ||= super['plugins'] || {}
end
# @return [ Hash ]
- 1
def self.comments
- 135
unless @comments
- 1
@comments = finder_configs('Comments')
- 1
@comments.each do |slug, config|
- 44
@comments[slug]['pattern'] = Regexp.new(config['pattern'], Regexp::IGNORECASE)
end
end
- 135
@comments
end
# @return [ Hash ]
- 1
def self.urls_in_page
- 4
@urls_in_page ||= finder_configs('UrlsInPage')
end
end
# Dynamic Theme Finders (none ATM)
- 1
class DynamicThemeFinders < DynamicFinders
# @return [ Hash ]
- 1
def self.db_data
- 1
@db_data ||= super['themes'] || {}
end
end
end
end
- 1
module WPScan
- 1
module DB
# Plugin DB
- 1
class Plugin < WpItem
# @return [ String ]
- 1
def self.db_file
- 1
@db_file ||= File.join(DB_DIR, 'plugins.json')
end
end
end
end
- 1
module WPScan
- 1
module DB
# WP Plugins
- 1
class Plugins < WpItems
# @return [ JSON ]
- 1
def self.db
- 3
Plugin.db
end
end
end
end
- 1
module WPScan
- 1
module DB
# WP Version
- 1
class Version < WpItem
- 1
include DataMapper::Resource
- 1
storage_names[:default] = 'versions'
- 1
has n, :fingerprints, constraint: :destroy
- 1
property :id, Serial
- 1
property :number, String, required: true, unique: true
end
# Path
- 1
class Path
- 1
include DataMapper::Resource
- 1
storage_names[:default] = 'paths'
- 1
has n, :fingerprints, constraint: :destroy
- 1
property :id, Serial
- 1
property :value, String, required: true, unique: true
end
# Fingerprint
- 1
class Fingerprint
- 1
include DataMapper::Resource
- 1
storage_names[:default] = 'fingerprints'
- 1
belongs_to :version, key: true
- 1
belongs_to :path, key: true
- 1
property :md5_hash, String, required: true, length: 32
end
end
end
- 1
module WPScan
- 1
module DB
# Theme DB
- 1
class Theme < WpItem
# @return [ String ]
- 1
def self.db_file
- 1
@db_file ||= File.join(DB_DIR, 'themes.json')
end
end
end
end
- 1
module WPScan
- 1
module DB
# WP Themes
- 1
class Themes < WpItems
# @return [ JSON ]
- 1
def self.db
- 3
Theme.db
end
end
end
end
- 1
module WPScan
- 1
module DB
# Class used to perform DB updates
# :nocov:
- skipped
class Updater
- skipped
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
- skipped
FILES = %w(
- skipped
plugins.json themes.json wordpresses.json
- skipped
timthumbs-v3.txt user-agents.txt config_backups.txt
- skipped
dynamic_finders.yml wordpress.db LICENSE
- skipped
).freeze
- skipped
- skipped
attr_reader :repo_directory
- skipped
- skipped
def initialize(repo_directory)
- skipped
@repo_directory = repo_directory
- skipped
- skipped
FileUtils.mkdir_p(repo_directory) unless Dir.exist?(repo_directory)
- skipped
- skipped
raise "#{repo_directory} is not writable" unless Pathname.new(repo_directory).writable?
- skipped
end
- skipped
- skipped
# @return [ Time, nil ]
- skipped
def last_update
- skipped
Time.parse(File.read(last_update_file))
- skipped
rescue ArgumentError, Errno::ENOENT
- skipped
nil # returns nil if the file does not exist or contains invalid time data
- skipped
end
- skipped
- skipped
# @return [ String ]
- skipped
def last_update_file
- skipped
@last_update_file ||= File.join(repo_directory, '.last_update')
- skipped
end
- skipped
- skipped
# @return [ Boolean ]
- skipped
def outdated?
- skipped
date = last_update
- skipped
- skipped
date.nil? || date < 5.days.ago
- skipped
end
- skipped
- skipped
# @return [ Boolean ]
- skipped
def missing_files?
- skipped
FILES.each do |file|
- skipped
return true unless File.exist?(File.join(repo_directory, file))
- skipped
end
- skipped
false
- skipped
end
- skipped
- skipped
# @return [ Hash ] The params for Typhoeus::Request
- skipped
def request_params
- skipped
{
- skipped
ssl_verifyhost: 2,
- skipped
ssl_verifypeer: true,
- skipped
timeout: 300,
- skipped
connecttimeout: 120,
- skipped
accept_encoding: 'gzip, deflate',
- skipped
cache_ttl: 0
- skipped
}
- skipped
end
- skipped
- skipped
# @return [ String ] The raw file URL associated with the given filename
- skipped
def remote_file_url(filename)
- skipped
"https://data.wpscan.org/#{filename}"
- skipped
end
- skipped
- skipped
# @return [ String ] The checksum of the associated remote filename
- skipped
def remote_file_checksum(filename)
- skipped
url = "#{remote_file_url(filename)}.sha512"
- skipped
- skipped
res = Browser.get(url, request_params)
- skipped
raise DownloadError, res if res.timed_out? || res.code != 200
- skipped
res.body.chomp
- skipped
end
- skipped
- skipped
def local_file_path(filename)
- skipped
File.join(repo_directory, filename.to_s)
- skipped
end
- skipped
- skipped
def local_file_checksum(filename)
- skipped
Digest::SHA512.file(local_file_path(filename)).hexdigest
- skipped
end
- skipped
- skipped
def backup_file_path(filename)
- skipped
File.join(repo_directory, "#{filename}.back")
- skipped
end
- skipped
- skipped
def create_backup(filename)
- skipped
return unless File.exist?(local_file_path(filename))
- skipped
FileUtils.cp(local_file_path(filename), backup_file_path(filename))
- skipped
end
- skipped
- skipped
def restore_backup(filename)
- skipped
return unless File.exist?(backup_file_path(filename))
- skipped
FileUtils.cp(backup_file_path(filename), local_file_path(filename))
- skipped
end
- skipped
- skipped
def delete_backup(filename)
- skipped
FileUtils.rm(backup_file_path(filename))
- skipped
end
- skipped
- skipped
# @return [ String ] The checksum of the downloaded file
- skipped
def download(filename)
- skipped
file_path = local_file_path(filename)
- skipped
file_url = remote_file_url(filename)
- skipped
- skipped
res = Browser.get(file_url, request_params)
- skipped
raise DownloadError, res if res.timed_out? || res.code != 200
- skipped
- skipped
File.open(file_path, 'wb') { |f| f.write(res.body) }
- skipped
- skipped
local_file_checksum(filename)
- skipped
end
- skipped
- skipped
# rubocop:disable MethodLength
- skipped
# @return [ Array<String> ] The filenames updated
- skipped
def update
- skipped
updated = []
- skipped
- skipped
FILES.each do |filename|
- skipped
begin
- skipped
db_checksum = remote_file_checksum(filename)
- skipped
- skipped
# Checking if the file needs to be updated
- skipped
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
- skipped
- skipped
create_backup(filename)
- skipped
dl_checksum = download(filename)
- skipped
- skipped
raise "#{filename}: checksums do not match" unless dl_checksum == db_checksum
- skipped
updated << filename
- skipped
rescue => e
- skipped
restore_backup(filename)
- skipped
raise e
- skipped
ensure
- skipped
delete_backup(filename) if File.exist?(backup_file_path(filename))
- skipped
end
- skipped
end
- skipped
- skipped
File.write(last_update_file, Time.now)
- skipped
- skipped
updated
- skipped
end
- skipped
# rubocop:enable MethodLength
- skipped
end
- skipped
end
# :nocov:
end
- 1
module WPScan
- 1
module DB
# WpItem - super DB class for Plugin, Theme and Version
- 1
class WpItem
# @param [ String ] identifier The plugin/theme slug or version number
#
# @return [ Hash ] The JSON data from the DB associated to the identifier
- 1
def self.db_data(identifier)
- 52
db[identifier] || {}
end
# @return [ JSON ]
- 1
def self.db
- 58
@db ||= read_json_file(db_file)
end
end
end
end
- 1
module WPScan
- 1
module DB
# WP Items
- 1
class WpItems
# @return [ Array<String> ] The slug of all items
- 1
def self.all_slugs
- 2
db.keys
end
# @return [ Array<String> ] The slug of all popular items
- 1
def self.popular_slugs
- 7
db.select { |_key, item| item['popular'] == true }.keys
end
# @return [ Array<String> ] The slug of all vulnerable items
- 1
def self.vulnerable_slugs
- 7
db.select { |_key, item| !item['vulnerabilities'].empty? }.keys
end
end
end
end
- 1
module WPScan
- 1
module DB
# WP Version
- 1
class Version < WpItem
# @return [ String ]
- 1
def self.db_file
- 1
@db_file ||= File.join(DB_DIR, 'wordpresses.json')
end
end
end
end
- 1
module WPScan
# HTTP Error
- 1
class HTTPError < StandardError
- 1
attr_reader :response
# @param [ Typhoeus::Response ] res
- 1
def initialize(response)
@response = response
end
- 1
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
- 1
def to_s
"HTTP Error: #{failure_details}"
end
end
# Used in the Updater
- 1
class DownloadError < HTTPError
- 1
def to_s
"Unable to get #{failure_details}"
end
end
end
- 1
module WPScan
# Error raised when there is a missing DB file and --no-update supplied
- 1
class MissingDatabaseFile < StandardError
- 1
def to_s
'Update required, you can not run a scan if a database file is missing.'
end
end
end
- 1
module WPScan
# WordPress hosted (*.wordpress.com)
- 1
class WordPressHostedError < StandardError
- 1
def to_s
'Scanning *.wordpress.com hosted blogs is not supported.'
end
end
# Not WordPress Error
- 1
class NotWordPressError < StandardError
- 1
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)
- 1
class InvalidWordPressVersion < StandardError
- 1
def to_s
'The WordPress version is invalid'
end
end
end
- 1
require 'wpscan/finders/finder/wp_version/smart_url_checker'
- 1
require 'wpscan/finders/finder/plugin_version/comments'
- 1
module WPScan
# Custom Finders
- 1
module Finders
- 1
include CMSScanner::Finders
# Custom InterestingFindings
- 1
module InterestingFindings
- 1
include CMSScanner::Finders::InterestingFindings
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
class Finder
- 1
module PluginVersion
# Plugin Version from the Comments in the homepage, used in dynamic PluginVersion finders
- 1
class Comments < CMSScanner::Finders::Finder
- 1
def passive(_opts = {})
- 1
target.target.comments_from_page(self.class::PATTERN) do |match|
# Avoid nil version, i.e a pattern allowing both versionable and non
# versionable string to be detected
- 1
next unless match[1]
return WPScan::Version.new(
match[1],
found_by: found_by,
confidence: 80,
interesting_entries: ["#{target.target.url}, Match: '#{match}'"]
- 1
)
end
end
end
end
end
end
end
- 1
module WPScan
- 1
module Finders
- 1
class Finder
- 1
module WpVersion
# SmartURLChecker specific for the WP Version
- 1
module SmartURLChecker
- 1
include CMSScanner::Finders::Finder::SmartURLChecker
- 1
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
- 1
module WPScan
# References module (which should be included along with the CMSScanner::References)
# to allow the use of the wpvulndb reference
- 1
module References
- 1
extend ActiveSupport::Concern
# See ActiveSupport::Concern
- 1
module ClassMethods
# @return [ Array<Symbol> ]
- 1
def references_keys
- 74
@references_keys ||= super << :wpvulndb
end
end
- 1
def references_urls
- 9
wpvulndb_urls + super
end
- 1
def wpvulndb_ids
- 17
references[:wpvulndb] || []
end
- 1
def wpvulndb_urls
- 20
wpvulndb_ids.reduce([]) { |acc, elem| acc << wpvulndb_url(elem) }
end
- 1
def wpvulndb_url(id)
- 7
"https://wpvulndb.com/vulnerabilities/#{id}"
end
end
end
- 1
require 'wpscan/target/platform/wordpress'
- 1
module WPScan
# Includes the WordPress Platform
- 1
class Target < CMSScanner::Target
- 1
include Platform::WordPress
# @return [ Boolean ]
- 1
def vulnerable?
- 7
[@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e|
- 34
[*e].each { |ae| return true if ae && ae.vulnerable? }
end
- 6
return true unless [*@config_backups].empty?
- 9
[*@users].each { |u| return true if u.password }
- 4
false
end
# @param [ Hash ] opts
#
# @return [ WpVersion, false ] The WpVersion found or false if not detected
- 1
def wp_version(opts = {})
- 4
@wp_version = Finders::WpVersion::Base.find(self, opts) if @wp_version.nil?
- 4
@wp_version
end
# @param [ Hash ] opts
#
# @return [ Theme ]
- 1
def main_theme(opts = {})
- 4
@main_theme = Finders::MainTheme::Base.find(self, opts) if @main_theme.nil?
- 4
@main_theme
end
# @param [ Hash ] opts
#
# @return [ Array<Plugin> ]
- 1
def plugins(opts = {})
- 4
@plugins ||= Finders::Plugins::Base.find(self, opts)
end
# @param [ Hash ] opts
#
# @return [ Array<Theme> ]
- 1
def themes(opts = {})
- 4
@themes ||= Finders::Themes::Base.find(self, opts)
end
# @param [ Hash ] opts
#
# @return [ Array<Timthumb> ]
- 1
def timthumbs(opts = {})
- 4
@timthumbs ||= Finders::Timthumbs::Base.find(self, opts)
end
# @param [ Hash ] opts
#
# @return [ Array<ConfigBackup> ]
- 1
def config_backups(opts = {})
- 4
@config_backups ||= Finders::ConfigBackups::Base.find(self, opts)
end
# @param [ Hash ] opts
#
# @return [ Array<Media> ]
- 1
def medias(opts = {})
- 4
@medias ||= Finders::Medias::Base.find(self, opts)
end
# @param [ Hash ] opts
#
# @return [ Array<User> ]
- 1
def users(opts = {})
- 5
@users ||= Finders::Users::Base.find(self, opts)
end
end
end
- 1
%w(custom_directories).each do |required|
- 1
require "wpscan/target/platform/wordpress/#{required}"
end
- 1
module WPScan
- 1
class Target < CMSScanner::Target
- 1
module Platform
# Some WordPress specific implementation
- 1
module WordPress
- 1
include CMSScanner::Target::Platform::PHP
- 1
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
- 1
attr_accessor :multisite, :registration_enabled, :mu_plugins
- 1
alias multisite? multisite
- 1
alias registration_enabled? registration_enabled
- 1
alias mu_plugins? mu_plugins
# @return [ Boolean ]
- 1
def wordpress?
# res = Browser.get(url)
- 7
in_scope_urls(homepage_res) do |url|
- 4
return true if Addressable::URI.parse(url).path.match(WORDPRESS_PATTERN)
end
- 3
homepage_res.html.css('meta[name="generator"]').each do |node|
- 1
return true if node['content'] =~ /wordpress/i
end
- 2
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
- 1
false
end
# @return [ String ]
- 1
def registration_url
multisite? ? url('wp-signup.php') : url('wp-login.php?action=register')
end
- 1
def wordpress_hosted?
- 10
uri.host =~ /wordpress.com$/i ? true : false
end
# @param [ String ] username
# @param [ String ] password
#
# @return [ Typhoeus::Response ]
- 1
def do_login(username, password)
login_request(username, password).run
end
# @param [ String ] username
# @param [ String ] password
#
# @return [ Typhoeus::Request ]
- 1
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
- 1
def login_url
url('wp-login.php')
end
end
end
end
end
- 1
module WPScan
- 1
class Target < CMSScanner::Target
- 1
module Platform
# wp-content & plugins directory implementation
- 1
module WordPress
- 1
def content_dir=(dir)
- 21
@content_dir = dir.chomp('/')
end
- 1
def plugins_dir=(dir)
- 10
@plugins_dir = dir.chomp('/')
end
# @return [ String ] The wp-content directory
- 1
def content_dir
- 34
unless @content_dir
- 8
escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
- 8
pattern = %r{#{escaped_url}(.+?)\/(?:themes|plugins|uploads)\/}i
- 8
in_scope_urls(homepage_res) do |url|
- 16
return @content_dir = Regexp.last_match[1] if url.match(pattern)
end
end
- 30
@content_dir
end
# @return [ Addressable::URI ]
- 1
def content_uri
- 11
uri.join("#{content_dir}/")
end
# @return [ String ]
- 1
def content_url
- 10
content_uri.to_s
end
# @return [ String ]
- 1
def plugins_dir
- 321
@plugins_dir ||= "#{content_dir}/plugins"
end
# @return [ Addressable::URI ]
- 1
def plugins_uri
- 7
uri.join("#{plugins_dir}/")
end
# @return [ String ]
- 1
def plugins_url
- 6
plugins_uri.to_s
end
# TODO: Factorise the code and the content_dir one ?
# @return [ String, False ] The sub_dir is found, false otherwise
# @note: nil can not be returned here, otherwise if there is no sub_dir
# the check would be done each time
- 1
def sub_dir
- 2
unless @sub_dir
- 2
escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
- 2
pattern = %r{#{escaped_url}(.+?)\/(?:xmlrpc\.php|wp\-includes\/)}i
- 2
in_scope_urls(homepage_res) do |url|
- 10
return @sub_dir = Regexp.last_match[1] if url.match(pattern)
end
- 1
@sub_dir = false
end
- 1
@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 ]
- 1
def url(path = nil)
- 588
return @uri.to_s unless path
- 459
if path =~ %r{wp\-content/plugins}i
- 303
path.gsub!('wp-content/plugins', plugins_dir)
- 156
elsif path =~ /wp\-content/i
- 96
path.gsub!('wp-content', content_dir)
- 60
elsif path[0] != '/' && sub_dir
- 1
path = "#{sub_dir}/#{path}"
end
- 459
super(path)
end
end
end
end
end
- 1
module WPScan
# Specific implementation
- 1
class Vulnerability < CMSScanner::Vulnerability
- 1
include References
# @param [ Hash ] json_data
# @return [ Vulnerability ]
- 1
def self.load_from_json(json_data)
- 17
references = { wpvulndb: json_data['id'].to_s }
- 17
if json_data['references']
- 7
references_keys.each do |key|
- 63
references[key] = json_data['references'][key.to_s] if json_data['references'].key?(key.to_s)
end
end
- 17
new(
json_data['title'],
references,
json_data['vuln_type'],
json_data['fixed_in']
)
end
end
end
- 1
module WPScan
# Module to include in vulnerable WP item such as WpVersion.
# the vulnerabilities method should be implemented
- 1
module Vulnerable
# @return [ Boolean ]
- 1
def vulnerable?
- 14
!vulnerabilities.empty?
end
end
end