HELLO v3!!!
This commit is contained in:
3
app/app.rb
Normal file
3
app/app.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
require_relative 'models'
|
||||
require_relative 'finders'
|
||||
require_relative 'controllers'
|
||||
7
app/controllers.rb
Normal file
7
app/controllers.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require_relative 'controllers/core'
|
||||
require_relative 'controllers/custom_directories'
|
||||
require_relative 'controllers/wp_version'
|
||||
require_relative 'controllers/main_theme'
|
||||
require_relative 'controllers/enumeration'
|
||||
require_relative 'controllers/password_attack'
|
||||
require_relative 'controllers/aliases'
|
||||
13
app/controllers/aliases.rb
Normal file
13
app/controllers/aliases.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Controller to add the aliases in the CLI
|
||||
class Aliases < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
[
|
||||
OptAlias.new(['--stealthy'],
|
||||
alias_for: '--random-user-agent --detection-mode passive --plugins-version-detection passive')
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
104
app/controllers/core.rb
Normal file
104
app/controllers/core.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Specific Core controller to include WordPress checks
|
||||
class Core < CMSScanner::Controller::Core
|
||||
# @return [ Array<OptParseValidator::Opt> ]
|
||||
def cli_options
|
||||
[OptURL.new(['--url URL', 'The URL of the blog to scan'],
|
||||
required_unless: %i[update help version], 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: %i[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: %i[url help version])
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ DB::Updater ]
|
||||
def local_db
|
||||
@local_db ||= DB::Updater.new(DB_DIR)
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def update_db_required?
|
||||
if local_db.missing_files?
|
||||
raise MissingDatabaseFile if parsed_options[:update] == false
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return parsed_options[:update] unless parsed_options[:update].nil?
|
||||
|
||||
return false unless user_interaction? && local_db.outdated?
|
||||
|
||||
output('@notice', msg: 'It seems like you have not updated the database for some time.')
|
||||
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
|
||||
|
||||
Readline.readline =~ /^y/i ? true : false
|
||||
end
|
||||
|
||||
def update_db
|
||||
output('db_update_started')
|
||||
output('db_update_finished', updated: local_db.update, verbose: parsed_options[:verbose])
|
||||
|
||||
exit(0) unless parsed_options[:url]
|
||||
end
|
||||
|
||||
def before_scan
|
||||
@last_update = local_db.last_update
|
||||
|
||||
maybe_output_banner_help_and_version # From CMS Scanner
|
||||
|
||||
update_db if update_db_required?
|
||||
setup_cache
|
||||
check_target_availability
|
||||
load_server_module
|
||||
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
|
||||
def check_wordpress_state
|
||||
raise WordPressHostedError if target.wordpress_hosted?
|
||||
|
||||
if Addressable::URI.parse(target.homepage_url).path =~ %r{/wp-admin/install.php$}i
|
||||
|
||||
output('not_fully_configured', url: target.homepage_url)
|
||||
|
||||
exit(WPScan::ExitCode::VULNERABLE)
|
||||
end
|
||||
|
||||
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
|
||||
def load_server_module
|
||||
server = target.server || :Apache # Tries to auto detect the server
|
||||
|
||||
# Force a specific server module to be loaded if supplied
|
||||
case parsed_options[:server]
|
||||
when :apache
|
||||
server = :Apache
|
||||
when :iis
|
||||
server = :IIS
|
||||
when :nginx
|
||||
server = :Nginx
|
||||
end
|
||||
|
||||
mod = CMSScanner::Target::Server.const_get(server)
|
||||
|
||||
target.extend mod
|
||||
WPScan::WpItem.include mod
|
||||
|
||||
server
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/controllers/custom_directories.rb
Normal file
23
app/controllers/custom_directories.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Controller to ensure that the wp-content and wp-plugins
|
||||
# directories are found
|
||||
class CustomDirectories < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
[
|
||||
OptString.new(['--wp-content-dir DIR']),
|
||||
OptString.new(['--wp-plugins-dir DIR'])
|
||||
]
|
||||
end
|
||||
|
||||
def before_scan
|
||||
target.content_dir = parsed_options[:wp_content_dir] if parsed_options[:wp_content_dir]
|
||||
target.plugins_dir = parsed_options[:wp_plugins_dir] if parsed_options[:wp_plugins_dir]
|
||||
|
||||
return if target.content_dir
|
||||
|
||||
raise 'Unable to identify the wp-content dir, please supply it with --wp-content-dir'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/controllers/enumeration.rb
Normal file
27
app/controllers/enumeration.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
require_relative 'enumeration/cli_options'
|
||||
require_relative 'enumeration/enum_methods'
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Enumeration Controller
|
||||
class Enumeration < CMSScanner::Controller::Base
|
||||
def before_scan
|
||||
DB::DynamicFinders::Plugin.create_versions_finders
|
||||
DB::DynamicFinders::Theme.create_versions_finders
|
||||
end
|
||||
|
||||
def run
|
||||
enum = parsed_options[:enumerate] || {}
|
||||
|
||||
enum_plugins if enum_plugins?(enum)
|
||||
enum_themes if enum_themes?(enum)
|
||||
|
||||
%i[timthumbs config_backups db_exports medias].each do |key|
|
||||
send("enum_#{key}".to_sym) if enum.key?(key)
|
||||
end
|
||||
|
||||
enum_users if enum_users?(enum)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
163
app/controllers/enumeration/cli_options.rb
Normal file
163
app/controllers/enumeration/cli_options.rb
Normal file
@@ -0,0 +1,163 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Enumeration CLI Options
|
||||
class Enumeration < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
cli_enum_choices + cli_plugins_opts + cli_themes_opts +
|
||||
cli_timthumbs_opts + cli_config_backups_opts + cli_db_exports_opts +
|
||||
cli_medias_opts + cli_users_opts
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
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']),
|
||||
dbe: OptBoolean.new(['--db-exports']),
|
||||
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,dbe,u,m',
|
||||
incompatible: [%i[vp ap p], %i[vt at t]],
|
||||
default: { all_plugins: true, config_backups: true }
|
||||
),
|
||||
OptRegexp.new(
|
||||
[
|
||||
'--exclude-content-based REGEXP_OR_STRING',
|
||||
'Exclude all responses matching the Regexp (case insensitive) during parts of the enumeration.',
|
||||
'Both the headers and body are checked. Regexp delimiters are not required.'
|
||||
], options: Regexp::IGNORECASE
|
||||
)
|
||||
]
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
def cli_plugins_opts
|
||||
[
|
||||
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate']),
|
||||
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, default: :passive
|
||||
),
|
||||
OptBoolean.new(
|
||||
['--plugins-version-all',
|
||||
'Check all the plugins version locations according to the choosen mode (--detection-mode, ' \
|
||||
'--plugins-detection and --plugins-version-detection)']
|
||||
),
|
||||
OptChoice.new(
|
||||
['--plugins-version-detection MODE',
|
||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
||||
'or --plugins-detection modes.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
def cli_themes_opts
|
||||
[
|
||||
OptSmartList.new(['--themes-list LIST', 'List of themes to enumerate']),
|
||||
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 according to the choosen mode (--detection-mode, ' \
|
||||
'--themes-detection and --themes-version-detection)']
|
||||
),
|
||||
OptChoice.new(
|
||||
['--themes-version-detection MODE',
|
||||
'Use the supplied mode to check themes versions instead of the --detection-mode ' \
|
||||
'or --themes-detection modes.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
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')
|
||||
),
|
||||
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> ]
|
||||
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')
|
||||
),
|
||||
OptChoice.new(
|
||||
['--config-backups-detection MODE',
|
||||
'Use the supplied mode to enumerate Config Backups, instead of the global (--detection-mode) mode.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
def cli_db_exports_opts
|
||||
[
|
||||
OptFilePath.new(
|
||||
['--db-exports-list FILE-PATH', 'List of DB exports\' paths to use'],
|
||||
exists: true, default: File.join(DB_DIR, 'db_exports.txt')
|
||||
),
|
||||
OptChoice.new(
|
||||
['--db-exports-detection MODE',
|
||||
'Use the supplied mode to enumerate DB Exports, instead of the global (--detection-mode) mode.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
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
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
def cli_users_opts
|
||||
[
|
||||
OptSmartList.new(
|
||||
['--users-list LIST',
|
||||
'List of users to check during the users enumeration from the Login Error Messages']
|
||||
),
|
||||
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
|
||||
178
app/controllers/enumeration/enum_methods.rb
Normal file
178
app/controllers/enumeration/enum_methods.rb
Normal file
@@ -0,0 +1,178 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Enumeration Methods
|
||||
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
|
||||
def enum_message(type)
|
||||
return unless %w[plugins themes].include?(type)
|
||||
|
||||
details = if parsed_options[:enumerate][:"vulnerable_#{type}"]
|
||||
'Vulnerable'
|
||||
elsif parsed_options[:enumerate][:"all_#{type}"]
|
||||
'All'
|
||||
else
|
||||
'Most Popular'
|
||||
end
|
||||
|
||||
"Enumerating #{details} #{type.capitalize}"
|
||||
end
|
||||
|
||||
# @param [ String ] type (plugins, themes etc)
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def default_opts(type)
|
||||
mode = parsed_options[:"#{type}_detection"] || parsed_options[:detection_mode]
|
||||
|
||||
{
|
||||
mode: mode,
|
||||
exclude_content: parsed_options[:exclude_content_based],
|
||||
show_progression: user_interaction?,
|
||||
version_detection: {
|
||||
mode: parsed_options[:"#{type}_version_detection"] || mode,
|
||||
confidence_threshold: parsed_options[:"#{type}_version_all"] ? 0 : 100
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Boolean ] Wether or not to enumerate the plugins
|
||||
def enum_plugins?(opts)
|
||||
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
||||
end
|
||||
|
||||
def enum_plugins
|
||||
opts = default_opts('plugins').merge(
|
||||
list: plugins_list_from_opts(parsed_options),
|
||||
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)
|
||||
|
||||
output('@info', msg: 'Checking Plugin Versions') if user_interaction? && !plugins.empty?
|
||||
|
||||
plugins.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
|
||||
def plugins_list_from_opts(opts)
|
||||
# List file provided by the user via the cli
|
||||
return opts[:plugins_list] 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
|
||||
def enum_themes?(opts)
|
||||
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
||||
end
|
||||
|
||||
def enum_themes
|
||||
opts = default_opts('themes').merge(
|
||||
list: themes_list_from_opts(parsed_options),
|
||||
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)
|
||||
|
||||
output('@info', msg: 'Checking Theme Versions') if user_interaction? && !themes.empty?
|
||||
|
||||
themes.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
|
||||
def themes_list_from_opts(opts)
|
||||
# List file provided by the user via the cli
|
||||
return opts[:themes_list] 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
|
||||
|
||||
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
|
||||
|
||||
def enum_config_backups
|
||||
opts = default_opts('config_backups').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
|
||||
|
||||
def enum_db_exports
|
||||
opts = default_opts('db_exports').merge(list: parsed_options[:db_exports_list])
|
||||
|
||||
output('@info', msg: 'Enumerating DB Exports') if user_interaction?
|
||||
output('db_exports', db_exports: target.db_exports(opts))
|
||||
end
|
||||
|
||||
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
|
||||
def enum_users?(opts)
|
||||
opts[:users] || (parsed_options[:passwords] && !parsed_options[:username] && !parsed_options[:usernames])
|
||||
end
|
||||
|
||||
def enum_users
|
||||
opts = default_opts('users').merge(
|
||||
range: enum_users_range,
|
||||
list: parsed_options[:users_list]
|
||||
)
|
||||
|
||||
output('@info', msg: 'Enumerating Users') if user_interaction?
|
||||
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
|
||||
def enum_users_range
|
||||
parsed_options[:enumerate][:users] || cli_enum_choices[0].choices[:u].validate(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/controllers/main_theme.rb
Normal file
27
app/controllers/main_theme.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Main Theme Controller
|
||||
class MainTheme < CMSScanner::Controller::Base
|
||||
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
|
||||
|
||||
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
|
||||
108
app/controllers/password_attack.rb
Normal file
108
app/controllers/password_attack.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Password Attack Controller
|
||||
class PasswordAttack < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
[
|
||||
OptFilePath.new(
|
||||
['--passwords FILE-PATH', '-P',
|
||||
'List of passwords to use during the password attack.',
|
||||
'If no --username/s option supplied, user enumeration will be run.'],
|
||||
exists: true
|
||||
),
|
||||
OptSmartList.new(['--usernames LIST', '-U', 'List of usernames to use during the password attack.']),
|
||||
OptInteger.new(['--multicall-max-passwords MAX_PWD',
|
||||
'Maximum number of passwords to send by request with XMLRPC multicall'],
|
||||
default: 500),
|
||||
OptChoice.new(['--password-attack ATTACK',
|
||||
'Force the supplied attack to be used rather than automatically determining one.'],
|
||||
choices: %w[wp-login xmlrpc xmlrpc-multicall],
|
||||
normalize: %i[downcase underscore to_sym])
|
||||
]
|
||||
end
|
||||
|
||||
def run
|
||||
return unless parsed_options[:passwords]
|
||||
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||
end
|
||||
|
||||
attack_opts = {
|
||||
show_progression: user_interaction?,
|
||||
multicall_max_passwords: parsed_options[:multicall_max_passwords]
|
||||
}
|
||||
|
||||
begin
|
||||
found = []
|
||||
|
||||
attacker.attack(users, passwords(parsed_options[:passwords]), attack_opts) do |user|
|
||||
found << user
|
||||
|
||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||
end
|
||||
ensure
|
||||
output('users', users: found)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ CMSScanner::Finders::Finder ] The finder used to perform the attack
|
||||
def attacker
|
||||
@attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
|
||||
end
|
||||
|
||||
# @return [ WPScan::XMLRPC ]
|
||||
def xmlrpc
|
||||
@xmlrpc ||= target.xmlrpc
|
||||
end
|
||||
|
||||
# @return [ CMSScanner::Finders::Finder ]
|
||||
def attacker_from_cli_options
|
||||
return unless parsed_options[:password_attack]
|
||||
|
||||
case parsed_options[:password_attack]
|
||||
when :wp_login
|
||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
||||
when :xmlrpc
|
||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
when :xmlrpc_multicall
|
||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ CMSScanner::Finders::Finder ]
|
||||
def attacker_from_automatic_detection
|
||||
if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
|
||||
wp_version = target.wp_version
|
||||
|
||||
if wp_version && wp_version < '4.4'
|
||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
else
|
||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
end
|
||||
else
|
||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Array<Users> ] The users to brute force
|
||||
def users
|
||||
return target.users unless parsed_options[:usernames]
|
||||
|
||||
parsed_options[:usernames].reduce([]) do |acc, elem|
|
||||
acc << CMSScanner::User.new(elem.chomp)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] wordlist_path
|
||||
#
|
||||
# @return [ Array<String> ]
|
||||
def passwords(wordlist_path)
|
||||
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
|
||||
acc << elem.chomp
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/controllers/wp_version.rb
Normal file
34
app/controllers/wp_version.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
module WPScan
|
||||
module Controller
|
||||
# Wp Version Controller
|
||||
class WpVersion < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
[
|
||||
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
|
||||
|
||||
def before_scan
|
||||
WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
|
||||
end
|
||||
|
||||
def run
|
||||
output(
|
||||
'version',
|
||||
version: target.wp_version(
|
||||
mode: parsed_options[:wp_version_detection] || parsed_options[:detection_mode],
|
||||
confidence_threshold: parsed_options[:wp_version_all] ? 0 : 100,
|
||||
show_progression: user_interaction?
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/finders.rb
Normal file
15
app/finders.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require_relative 'finders/interesting_findings'
|
||||
require_relative 'finders/wp_items'
|
||||
require_relative 'finders/wp_version'
|
||||
require_relative 'finders/main_theme'
|
||||
require_relative 'finders/timthumb_version'
|
||||
require_relative 'finders/timthumbs'
|
||||
require_relative 'finders/config_backups'
|
||||
require_relative 'finders/db_exports'
|
||||
require_relative 'finders/medias'
|
||||
require_relative 'finders/users'
|
||||
require_relative 'finders/plugins'
|
||||
require_relative 'finders/plugin_version'
|
||||
require_relative 'finders/theme_version'
|
||||
require_relative 'finders/themes'
|
||||
require_relative 'finders/passwords'
|
||||
17
app/finders/config_backups.rb
Normal file
17
app/finders/config_backups.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'config_backups/known_filenames'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ConfigBackups
|
||||
# Config Backup Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << ConfigBackups::KnownFilenames.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
46
app/finders/config_backups/known_filenames.rb
Normal file
46
app/finders/config_backups/known_filenames.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ConfigBackups
|
||||
# Config Backup finder
|
||||
class KnownFilenames < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Array<ConfigBackup> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(potential_urls(opts), opts) do |res|
|
||||
# Might need to improve that
|
||||
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
|
||||
|
||||
found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
File.open(opts[:list]).each_with_index do |file, index|
|
||||
urls[target.url(file.chomp)] = index
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Config Backups -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/db_exports.rb
Normal file
17
app/finders/db_exports.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'db_exports/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module DbExports
|
||||
# DB Exports Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << DbExports::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/db_exports/known_locations.rb
Normal file
49
app/finders/db_exports/known_locations.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DbExports
|
||||
# DB Exports finder
|
||||
# See https://github.com/wpscanteam/wpscan-v3/issues/62
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Array<DBExport> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(potential_urls(opts), opts) do |res|
|
||||
next unless res.code == 200 && res.body =~ /INSERT INTO/
|
||||
|
||||
found << WPScan::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
domain_name = target.uri.host[/(^[\w|-]+)/, 1]
|
||||
|
||||
File.open(opts[:list]).each_with_index do |path, index|
|
||||
path.gsub!('{domain_name}', domain_name)
|
||||
|
||||
urls[target.url(path.chomp)] = index
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking DB Exports -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/finders/interesting_findings.rb
Normal file
34
app/finders/interesting_findings.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require_relative 'interesting_findings/readme'
|
||||
require_relative 'interesting_findings/multisite'
|
||||
require_relative 'interesting_findings/debug_log'
|
||||
require_relative 'interesting_findings/backup_db'
|
||||
require_relative 'interesting_findings/mu_plugins'
|
||||
require_relative 'interesting_findings/registration'
|
||||
require_relative 'interesting_findings/tmm_db_migrate'
|
||||
require_relative 'interesting_findings/upload_sql_dump'
|
||||
require_relative 'interesting_findings/full_path_disclosure'
|
||||
require_relative 'interesting_findings/duplicator_installer_log'
|
||||
require_relative 'interesting_findings/upload_directory_listing'
|
||||
require_relative 'interesting_findings/emergency_pwd_reset_script'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Interesting Files Finder
|
||||
class Base < CMSScanner::Finders::InterestingFindings::Base
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
super(target)
|
||||
|
||||
%w[
|
||||
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
||||
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
||||
UploadSQLDump EmergencyPwdResetScript
|
||||
].each do |f|
|
||||
finders << InterestingFindings.const_get(f).new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/interesting_findings/backup_db.rb
Normal file
25
app/finders/interesting_findings/backup_db.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# BackupDB finder
|
||||
class BackupDB < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/backup-db/'
|
||||
url = target.url(path)
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
|
||||
|
||||
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
|
||||
20
app/finders/interesting_findings/debug_log.rb
Normal file
20
app/finders/interesting_findings/debug_log.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# debug.log finder
|
||||
class DebugLog < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/debug.log'
|
||||
|
||||
return unless target.debug_log?(path)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
target.url(path),
|
||||
confidence: 100, found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/interesting_findings/duplicator_installer_log.rb
Normal file
23
app/finders/interesting_findings/duplicator_installer_log.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# DuplicatorInstallerLog finder
|
||||
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('installer-log.txt')
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Emergency Password Reset Script finder
|
||||
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('/emergency.php')
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.code == 200 && !target.homepage_or_404?(res)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: res.body =~ /password/i ? 100 : 40,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: {
|
||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/interesting_findings/full_path_disclosure.rb
Normal file
23
app/finders/interesting_findings/full_path_disclosure.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Full Path Disclosure finder
|
||||
class FullPathDisclosure < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-includes/rss-functions.php'
|
||||
fpd_entries = target.full_path_disclosure_entries(path)
|
||||
|
||||
return if fpd_entries.empty?
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: fpd_entries
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/interesting_findings/mu_plugins.rb
Normal file
49
app/finders/interesting_findings/mu_plugins.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Must Use Plugins Directory checker
|
||||
class MuPlugins < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
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 ]
|
||||
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
|
||||
29
app/finders/interesting_findings/multisite.rb
Normal file
29
app/finders/interesting_findings/multisite.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Multisite checker
|
||||
class Multisite < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
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
|
||||
26
app/finders/interesting_findings/readme.rb
Normal file
26
app/finders/interesting_findings/readme.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Readme.html finder
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
potential_files.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
|
||||
if res.code == 200 && res.body =~ /wordpress/i
|
||||
return WPScan::InterestingFinding.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @retun [ Array<String> ] The list of potential readme files
|
||||
def potential_files
|
||||
%w[readme.html olvasdel.html lisenssi.html liesmich.html]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/interesting_findings/registration.rb
Normal file
31
app/finders/interesting_findings/registration.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Registration Enabled checker
|
||||
class Registration < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def passive(_opts = {})
|
||||
# Maybe check in the homepage if there is the registration url ?
|
||||
end
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
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
|
||||
24
app/finders/interesting_findings/tmm_db_migrate.rb
Normal file
24
app/finders/interesting_findings/tmm_db_migrate.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Tmm DB Migrate finder
|
||||
class TmmDbMigrate < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
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
|
||||
24
app/finders/interesting_findings/upload_directory_listing.rb
Normal file
24
app/finders/interesting_findings/upload_directory_listing.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# UploadDirectoryListing finder
|
||||
class UploadDirectoryListing < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
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
|
||||
28
app/finders/interesting_findings/upload_sql_dump.rb
Normal file
28
app/finders/interesting_findings/upload_sql_dump.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# UploadSQLDump finder
|
||||
class UploadSQLDump < CMSScanner::Finders::Finder
|
||||
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = dump_url
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.code == 200 && res.body =~ SQL_PATTERN
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
|
||||
def dump_url
|
||||
target.url('wp-content/uploads/dump.sql')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/main_theme.rb
Normal file
22
app/finders/main_theme.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require_relative 'main_theme/css_style'
|
||||
require_relative 'main_theme/woo_framework_meta_generator'
|
||||
require_relative 'main_theme/urls_in_homepage'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# Main Theme Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
MainTheme::CssStyle.new(target) <<
|
||||
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
||||
MainTheme::UrlsInHomepage.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/finders/main_theme/css_style.rb
Normal file
43
app/finders/main_theme/css_style.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# From the css style
|
||||
class CssStyle < CMSScanner::Finders::Finder
|
||||
include Finders::WpItems::URLsInHomepage
|
||||
|
||||
def create_theme(slug, style_url, opts)
|
||||
WPScan::Theme.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
|
||||
)
|
||||
end
|
||||
|
||||
def passive(opts = {})
|
||||
passive_from_css_href(target.homepage_res, opts) || passive_from_style_code(target.homepage_res, opts)
|
||||
end
|
||||
|
||||
def passive_from_css_href(res, opts)
|
||||
target.in_scope_urls(res, '//style/@src|//link/@href') do |url|
|
||||
next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], url, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def passive_from_style_code(res, opts)
|
||||
res.html.css('style').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/main_theme/urls_in_homepage.rb
Normal file
25
app/finders/main_theme/urls_in_homepage.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# URLs In Homepage Finder
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
||||
|
||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/main_theme/woo_framework_meta_generator.rb
Normal file
22
app/finders/main_theme/woo_framework_meta_generator.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# From the WooFramework meta generators
|
||||
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
|
||||
THEME_PATTERN = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?"\s+/?>}
|
||||
FRAMEWORK_PATTERN = %r{<meta name="generator" content="WooFramework\s?([^"]+)?"\s+/?>}
|
||||
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i
|
||||
|
||||
def passive(opts = {})
|
||||
return unless target.homepage_res.body =~ PATTERN
|
||||
|
||||
WPScan::Theme.new(
|
||||
Regexp.last_match[1],
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 80)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/medias.rb
Normal file
17
app/finders/medias.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'medias/attachment_brute_forcing'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Medias
|
||||
# Medias Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << Medias::AttachmentBruteForcing.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/finders/medias/attachment_brute_forcing.rb
Normal file
44
app/finders/medias/attachment_brute_forcing.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Medias
|
||||
# Medias Finder
|
||||
class AttachmentBruteForcing < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
# @return [ Array<Media> ]
|
||||
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 ]
|
||||
def target_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
opts[:range].each do |id|
|
||||
urls[target.uri.join("?attachment_id=#{id}").to_s] = id
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Brute Forcing Attachment IDs -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
app/finders/passwords.rb
Normal file
3
app/finders/passwords.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
require_relative 'passwords/wp_login'
|
||||
require_relative 'passwords/xml_rpc'
|
||||
require_relative 'passwords/xml_rpc_multicall'
|
||||
22
app/finders/passwords/wp_login.rb
Normal file
22
app/finders/passwords/wp_login.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the wp-login.php
|
||||
class WpLogin < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||
|
||||
def login_request(username, password)
|
||||
target.login_request(username, password)
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 302
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/passwords/xml_rpc.rb
Normal file
22
app/finders/passwords/xml_rpc.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the XMLRPC interface
|
||||
class XMLRPC < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||
|
||||
def login_request(username, password)
|
||||
target.method_call('wp.getUsersBlogs', [username, password])
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 200 && response.body =~ /blogName/
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
102
app/finders/passwords/xml_rpc_multicall.rb
Normal file
102
app/finders/passwords/xml_rpc_multicall.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the XMLRPC interface with the multicall method
|
||||
# WP < 4.4 is vulnerable to such attack
|
||||
class XMLRPCMulticall < CMSScanner::Finders::Finder
|
||||
# @param [ Array<User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def do_multi_call(users, passwords)
|
||||
methods = []
|
||||
|
||||
users.each do |user|
|
||||
passwords.each do |password|
|
||||
methods << ['wp.getUsersBlogs', user.username, password]
|
||||
end
|
||||
end
|
||||
|
||||
target.multi_call(methods).run
|
||||
end
|
||||
|
||||
# @param [ Array<CMSScanner::User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
# @option opts [ Integer ] :multicall_max_passwords
|
||||
#
|
||||
# @yield [ CMSScanner::User ] When a valid combination is found
|
||||
#
|
||||
# TODO: Make rubocop happy about metrics etc
|
||||
#
|
||||
# rubocop:disable all
|
||||
def attack(users, passwords, opts = {})
|
||||
wordlist_index = 0
|
||||
max_passwords = opts[:multicall_max_passwords]
|
||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
||||
|
||||
create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
|
||||
show_progression: opts[:show_progression])
|
||||
|
||||
loop do
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
||||
wordlist_index += current_passwords_size
|
||||
|
||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||
|
||||
res = do_multi_call(current_users, current_passwords)
|
||||
|
||||
progress_bar.increment
|
||||
|
||||
check_and_output_errors(res)
|
||||
|
||||
# Avoid to parse the response and iterate over all the structs in the document
|
||||
# if there isn't any tag matching a valid combination
|
||||
next unless res.body =~ /isAdmin/ # maybe a better one ?
|
||||
|
||||
Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|
|
||||
next if struct.text =~ /faultCode/
|
||||
|
||||
user = current_users[index / current_passwords.size]
|
||||
user.password = current_passwords[index % current_passwords.size]
|
||||
|
||||
yield user
|
||||
|
||||
# Updates the current_passwords_size and progress_bar#total
|
||||
# given that less requests will be done due to a valid combination found.
|
||||
current_passwords_size = passwords_size(max_passwords, current_users.size - 1)
|
||||
|
||||
if current_passwords_size == 0
|
||||
progress_bar.log('All Found') # remove ?
|
||||
progress_bar.stop
|
||||
break
|
||||
end
|
||||
|
||||
progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
|
||||
end
|
||||
end
|
||||
# Maybe a progress_bar.stop ?
|
||||
end
|
||||
# rubocop:disable all
|
||||
|
||||
def passwords_size(max_passwords, users_size)
|
||||
return 1 if max_passwords < users_size
|
||||
return 0 if users_size == 0
|
||||
|
||||
max_passwords / users_size
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
def check_and_output_errors(res)
|
||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
||||
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
|
||||
|
||||
progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/plugin_version.rb
Normal file
38
app/finders/plugin_version.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require_relative 'plugin_version/readme'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module PluginVersion
|
||||
# Plugin Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
def initialize(plugin)
|
||||
finders << PluginVersion::Readme.new(plugin)
|
||||
|
||||
load_specific_finders(plugin)
|
||||
end
|
||||
|
||||
# Load the finders associated with the plugin
|
||||
#
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
def load_specific_finders(plugin)
|
||||
module_name = plugin.classify
|
||||
|
||||
return unless Finders::PluginVersion.constants.include?(module_name)
|
||||
|
||||
mod = Finders::PluginVersion.const_get(module_name)
|
||||
|
||||
mod.constants.each do |constant|
|
||||
c = mod.const_get(constant)
|
||||
|
||||
next unless c.is_a?(Class)
|
||||
|
||||
finders << c.new(plugin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/finders/plugin_version/readme.rb
Normal file
79
app/finders/plugin_version/readme.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module PluginVersion
|
||||
# Plugin Version Finder from the readme.txt file
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
found_by_msg = 'Readme - %s (Aggressive Detection)'
|
||||
|
||||
WPScan::WpItem::READMES.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
|
||||
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]
|
||||
)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Array<String, String, Integer> ] number, found_by, confidence
|
||||
def version_numbers(body)
|
||||
numbers = []
|
||||
|
||||
if (number = from_stable_tag(body))
|
||||
numbers << [number, 'Stable Tag', 80]
|
||||
end
|
||||
|
||||
if (number = from_changelog_section(body))
|
||||
numbers << [number, 'ChangeLog Section', 50]
|
||||
end
|
||||
|
||||
numbers
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ] The version number detected from the stable tag
|
||||
def from_stable_tag(body)
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
|
||||
|
||||
number = Regexp.last_match[1]
|
||||
|
||||
number if number =~ /[0-9]+/
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ] The best version number detected from the changelog section
|
||||
def from_changelog_section(body)
|
||||
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
|
||||
|
||||
return if extracted_versions.nil? || extracted_versions.empty?
|
||||
|
||||
extracted_versions.flatten!
|
||||
# must contain at least one number
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
|
||||
sorted = extracted_versions.sort do |x, y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
sorted.last
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
33
app/finders/plugins.rb
Normal file
33
app/finders/plugins.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require_relative 'plugins/urls_in_homepage'
|
||||
require_relative 'plugins/known_locations'
|
||||
# From the DynamicFinders
|
||||
require_relative 'plugins/comment'
|
||||
require_relative 'plugins/xpath'
|
||||
require_relative 'plugins/header_pattern'
|
||||
require_relative 'plugins/body_pattern'
|
||||
require_relative 'plugins/javascript_var'
|
||||
require_relative 'plugins/query_parameter'
|
||||
require_relative 'plugins/config_parser' # Not loaded below as not implemented
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Plugins::UrlsInHomepage.new(target) <<
|
||||
Plugins::HeaderPattern.new(target) <<
|
||||
Plugins::Comment.new(target) <<
|
||||
Plugins::Xpath.new(target) <<
|
||||
Plugins::BodyPattern.new(target) <<
|
||||
Plugins::JavascriptVar.new(target) <<
|
||||
Plugins::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/finders/plugins/body_pattern.rb
Normal file
27
app/finders/plugins/body_pattern.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'BodyPattern'
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
return unless response.body =~ config['pattern']
|
||||
|
||||
Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/plugins/comment.rb
Normal file
31
app/finders/plugins/comment.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Comment'
|
||||
class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
||||
comment = node.text.to_s.strip
|
||||
|
||||
next unless comment =~ config['pattern']
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/plugins/config_parser.rb
Normal file
31
app/finders/plugins/config_parser.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'ConfigParser'
|
||||
class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def _process_response(_opts, _response, slug, klass, config)
|
||||
#
|
||||
# TODO. Currently not implemented, and not even loaded by the Finders, as this
|
||||
# finder only has an aggressive method, which has been disabled (globally)
|
||||
# when checking for plugins
|
||||
#
|
||||
|
||||
Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/finders/plugins/header_pattern.rb
Normal file
41
app/finders/plugins/header_pattern.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'HeaderPattern'
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
headers = target.homepage_res.headers
|
||||
|
||||
return found if headers.empty?
|
||||
|
||||
DB::DynamicFinders::Plugin.passive_header_pattern_finder_configs.each do |slug, configs|
|
||||
configs.each do |klass, config|
|
||||
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
|
||||
|
||||
found << Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ nil ]
|
||||
def aggressive(_opts = {})
|
||||
# None
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/plugins/javascript_var.rb
Normal file
29
app/finders/plugins/javascript_var.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'JavascriptVar'
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 60
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/finders/plugins/known_locations.rb
Normal file
48
app/finders/plugins/known_locations.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Known Locations Plugins Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# 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(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Plugins.vulnerable_slugs
|
||||
urls = {}
|
||||
plugins_url = target.plugins_url
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{plugins_url}#{URI.encode(slug)}/"] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/plugins/query_parameter.rb
Normal file
25
app/finders/plugins/query_parameter.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'QueryParameter'
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 10
|
||||
|
||||
def passive(_opts = {})
|
||||
# Handled by UrlsInHomePage, so no need to check this twice
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
# TODO: when a real case will be found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/plugins/urls_in_homepage.rb
Normal file
25
app/finders/plugins/urls_in_homepage.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# URLs In Homepage Finder
|
||||
# Typically, the items detected from URLs like
|
||||
# /wp-content/plugins/<slug>/
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
|
||||
found << Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/plugins/xpath.rb
Normal file
29
app/finders/plugins/xpath.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Xpath'
|
||||
class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath']).each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/finders/theme_version.rb
Normal file
41
app/finders/theme_version.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
require_relative 'theme_version/style'
|
||||
require_relative 'theme_version/woo_framework_meta_generator'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Theme ] theme
|
||||
def initialize(theme)
|
||||
finders <<
|
||||
ThemeVersion::Style.new(theme) <<
|
||||
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
|
||||
|
||||
load_specific_finders(theme)
|
||||
end
|
||||
|
||||
# Load the finders associated with the theme
|
||||
#
|
||||
# @param [ WPScan::Theme ] theme
|
||||
def load_specific_finders(theme)
|
||||
module_name = theme.classify
|
||||
|
||||
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
|
||||
43
app/finders/theme_version/style.rb
Normal file
43
app/finders/theme_version/style.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder from the style.css file
|
||||
class Style < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def passive(_opts = {})
|
||||
return unless cached_style?
|
||||
|
||||
style_version
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
return if cached_style?
|
||||
|
||||
style_version
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def cached_style?
|
||||
Typhoeus::Config.cache.get(browser.forge_request(target.style_url)) ? true : false
|
||||
end
|
||||
|
||||
# @return [ Version ]
|
||||
def style_version
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
||||
|
||||
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
|
||||
19
app/finders/theme_version/woo_framework_meta_generator.rb
Normal file
19
app/finders/theme_version/woo_framework_meta_generator.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder from the WooFramework generators
|
||||
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def passive(_opts = {})
|
||||
return unless target.blog.homepage_res.body =~ Finders::MainTheme::WooFrameworkMetaGenerator::PATTERN
|
||||
|
||||
return unless Regexp.last_match[1] == target.slug
|
||||
|
||||
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/finders/themes.rb
Normal file
20
app/finders/themes.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require_relative 'themes/urls_in_homepage'
|
||||
require_relative 'themes/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# themes Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Themes::UrlsInHomepage.new(target) <<
|
||||
Themes::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/finders/themes/known_locations.rb
Normal file
48
app/finders/themes/known_locations.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# Known Locations Themes Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# 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(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
||||
urls = {}
|
||||
themes_url = target.url('wp-content/themes/')
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{themes_url}#{URI.encode(slug)}/"] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/themes/urls_in_homepage.rb
Normal file
23
app/finders/themes/urls_in_homepage.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# URLs In Homepage Finder
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |slug|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/timthumb_version.rb
Normal file
17
app/finders/timthumb_version.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'timthumb_version/bad_request'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module TimthumbVersion
|
||||
# Timthumb Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Timthumb ] target
|
||||
def initialize(target)
|
||||
finders << TimthumbVersion::BadRequest.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/finders/timthumb_version/bad_request.rb
Normal file
21
app/finders/timthumb_version/bad_request.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
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
|
||||
class BadRequest < CMSScanner::Finders::Finder
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
|
||||
|
||||
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
|
||||
17
app/finders/timthumbs.rb
Normal file
17
app/finders/timthumbs.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'timthumbs/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Timthumbs
|
||||
# Timthumbs Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << Timthumbs::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
56
app/finders/timthumbs/known_locations.rb
Normal file
56
app/finders/timthumbs/known_locations.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Timthumbs
|
||||
# Known Locations Timthumbs Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Array<Timthumb> ]
|
||||
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 ]
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/finders/users.rb
Normal file
28
app/finders/users.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
require_relative 'users/author_posts'
|
||||
require_relative 'users/wp_json_api'
|
||||
require_relative 'users/oembed_api'
|
||||
require_relative 'users/rss_generator'
|
||||
require_relative 'users/author_id_brute_forcing'
|
||||
require_relative 'users/login_error_messages'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Users Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Users::AuthorPosts.new(target) <<
|
||||
Users::WpJsonApi.new(target) <<
|
||||
Users::OembedApi.new(target) <<
|
||||
Users::RSSGenerator.new(target) <<
|
||||
Users::AuthorIdBruteForcing.new(target) <<
|
||||
Users::LoginErrorMessages.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
111
app/finders/users/author_id_brute_forcing.rb
Normal file
111
app/finders/users/author_id_brute_forcing.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Author Id Brute Forcing
|
||||
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
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 << CMSScanner::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 ]
|
||||
def target_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
opts[:range].each do |id|
|
||||
urls[target.uri.join("?author=#{id}").to_s] = id
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Brute Forcing Author IDs -'))
|
||||
end
|
||||
|
||||
def request_params
|
||||
{ followlocation: true }
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
#
|
||||
# @return [ Array<String, String, Integer>, nil ] username, found_by, confidence
|
||||
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 ]
|
||||
def username_from_author_url(url)
|
||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
#
|
||||
# @return [ String, nil ] The username found
|
||||
def username_from_response(res)
|
||||
# Permalink enabled
|
||||
target.in_scope_urls(res, '//link/@href|//a/@href') do |url|
|
||||
username = username_from_author_url(url)
|
||||
return username if username
|
||||
end
|
||||
|
||||
# No permalink
|
||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ]
|
||||
def display_name_from_body(body)
|
||||
page = Nokogiri::HTML.parse(body)
|
||||
# WP >= 3.0
|
||||
page.css('h1.page-title span').each do |node|
|
||||
return node.text.to_s
|
||||
end
|
||||
|
||||
# WP < 3.0
|
||||
page.xpath('//link[@rel="alternate" and @type="application/rss+xml"]').each do |node|
|
||||
title = node['title']
|
||||
|
||||
next unless title =~ /Posts by (.*) Feed\z/i
|
||||
|
||||
return Regexp.last_match[1] unless Regexp.last_match[1].empty?
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
app/finders/users/author_posts.rb
Normal file
61
app/finders/users/author_posts.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Author Posts
|
||||
class AuthorPosts < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def passive(opts = {})
|
||||
found_by_msg = 'Author Posts - %s (Passive Detection)'
|
||||
|
||||
usernames(opts).reduce([]) do |a, e|
|
||||
a << CMSScanner::User.new(
|
||||
e[0],
|
||||
found_by: format(found_by_msg, e[1]),
|
||||
confidence: e[2]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Array>> ]
|
||||
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> ]
|
||||
def potential_usernames(res)
|
||||
usernames = []
|
||||
|
||||
target.in_scope_urls(res, '//a/@href') do |url, node|
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||
elsif uri.query =~ /author=[0-9]+/
|
||||
usernames << [node.text.to_s.strip, 'Display Name', 30]
|
||||
end
|
||||
end
|
||||
|
||||
usernames.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
45
app/finders/users/login_error_messages.rb
Normal file
45
app/finders/users/login_error_messages.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
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.
|
||||
#
|
||||
class LoginErrorMessages < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
usernames(opts).each do |username|
|
||||
res = target.do_login(username, SecureRandom.hex[0, 8])
|
||||
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 << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] List of usernames to check
|
||||
def usernames(opts = {})
|
||||
# usernames from the potential Users found
|
||||
unames = opts[:found].map(&:username)
|
||||
|
||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
||||
|
||||
unames.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/users/oembed_api.rb
Normal file
49
app/finders/users/oembed_api.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Since WP 4.4, the oembed API can disclose a user
|
||||
# https://github.com/wpscanteam/wpscan/issues/1049
|
||||
class OembedApi < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def passive(_opts = {})
|
||||
# TODO: get the api_url from the Homepage and query it if present,
|
||||
# then discard the aggressive check if same/similar URL
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# TODO: make this code pretty :x
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
found_by_msg = 'Oembed API - %s (Aggressive Detection)'
|
||||
|
||||
oembed_data = JSON.parse(Browser.get(api_url).body)
|
||||
|
||||
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
||||
details = [Regexp.last_match[1], 'Author URL', 90]
|
||||
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
||||
details = [oembed_data['author_name'].delete(' '), 'Author Name', 70]
|
||||
end
|
||||
|
||||
return unless details
|
||||
|
||||
found << CMSScanner::User.new(details[0],
|
||||
found_by: format(found_by_msg, details[1]),
|
||||
confidence: details[2],
|
||||
interesting_entries: [api_url])
|
||||
rescue JSON::ParserError
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the API listing the Users
|
||||
def api_url
|
||||
@api_url ||= target.url("wp-json/oembed/1.0/embed?url=#{target.url}&format=json")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/users/rss_generator.rb
Normal file
38
app/finders/users/rss_generator.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Users disclosed from the dc:creator field in the RSS
|
||||
# The names disclosed are display names, however depending on the configuration of the blog,
|
||||
# they can be the same than usernames
|
||||
class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
|
||||
def process_urls(urls, _opts = {})
|
||||
found = []
|
||||
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
next unless res.code == 200 && res.body =~ /<dc\:creator>/i
|
||||
|
||||
potential_usernames = []
|
||||
|
||||
begin
|
||||
res.xml.xpath('//item/dc:creator').each do |node|
|
||||
potential_usernames << node.text.to_s unless node.text.to_s.length > 40
|
||||
end
|
||||
rescue Nokogiri::XML::XPath::SyntaxError
|
||||
next
|
||||
end
|
||||
|
||||
potential_usernames.uniq.each do |potential_username|
|
||||
found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/finders/users/wp_json_api.rb
Normal file
35
app/finders/users/wp_json_api.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
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)
|
||||
#
|
||||
class WpJsonApi < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
JSON.parse(Browser.get(api_url).body)&.each do |user|
|
||||
found << CMSScanner::User.new(user['slug'],
|
||||
id: user['id'],
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [api_url])
|
||||
end
|
||||
|
||||
found
|
||||
rescue JSON::ParserError, TypeError
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the API listing the Users
|
||||
def api_url
|
||||
@api_url ||= target.url('wp-json/wp/v2/users/')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
1
app/finders/wp_items.rb
Normal file
1
app/finders/wp_items.rb
Normal file
@@ -0,0 +1 @@
|
||||
require_relative 'wp_items/urls_in_homepage'
|
||||
68
app/finders/wp_items/urls_in_homepage.rb
Normal file
68
app/finders/wp_items/urls_in_homepage.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpItems
|
||||
# URLs In Homepage Module to use in plugins & themes finders
|
||||
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
|
||||
def items_from_links(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.in_scope_urls(target.homepage_res) do |url|
|
||||
next unless url =~ item_attribute_pattern(type)
|
||||
|
||||
found << Regexp.last_match[1]
|
||||
end
|
||||
|
||||
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
|
||||
def items_from_codes(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.homepage_res.html.css('script,style').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
code.scan(item_code_pattern(type)).flatten.uniq.each { |slug| found << slug }
|
||||
end
|
||||
|
||||
uniq ? found.uniq.sort : found.sort
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_attribute_pattern(type)
|
||||
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_code_pattern(type)
|
||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_url_pattern(type)
|
||||
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||
|
||||
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
app/finders/wp_version.rb
Normal file
42
app/finders/wp_version.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
require_relative 'wp_version/rss_generator'
|
||||
require_relative 'wp_version/atom_generator'
|
||||
require_relative 'wp_version/rdf_generator'
|
||||
require_relative 'wp_version/readme'
|
||||
require_relative 'wp_version/unique_fingerprinting'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
# Specific Finders container to filter the version detected
|
||||
# and remove the one with low confidence to avoid false
|
||||
# positive when there is not enought information to accurately
|
||||
# determine it.
|
||||
class WpVersionFinders < UniqueFinders
|
||||
def filter_findings
|
||||
best_finding = super
|
||||
|
||||
best_finding && best_finding.confidence >= 40 ? best_finding : false
|
||||
end
|
||||
end
|
||||
|
||||
module WpVersion
|
||||
# Wp Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
(%w[RSSGenerator AtomGenerator RDFGenerator] +
|
||||
WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
||||
%w[Readme UniqueFingerprinting]
|
||||
).each do |finder_name|
|
||||
finders << WpVersion.const_get(finder_name.to_sym).new(target)
|
||||
end
|
||||
end
|
||||
|
||||
def finders
|
||||
@finders ||= Finders::WpVersionFinders.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
40
app/finders/wp_version/atom_generator.rb
Normal file
40
app/finders/wp_version/atom_generator.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Atom Generator Version Finder
|
||||
class AtomGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
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.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//link[@rel="alternate" and @type="application/atom+xml"]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
%w[feed/atom/ ?feed=atom].reduce([]) do |a, uri|
|
||||
a << target.url(uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/wp_version/rdf_generator.rb
Normal file
38
app/finders/wp_version/rdf_generator.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# RDF Generator Version Finder
|
||||
class RDFGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
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.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//a[contains(@href, "rdf")]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
[target.url('feed/rdf/')]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/wp_version/readme.rb
Normal file
29
app/finders/wp_version/readme.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Readme Version Finder
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ WpVersion ]
|
||||
def aggressive(_opts = {})
|
||||
readme_url = target.url('readme.html') # Maybe move this into the Target ?
|
||||
|
||||
node = Browser.get(readme_url).html.css('h1#logo').last
|
||||
|
||||
return unless node&.text.to_s.strip =~ /\AVersion (.*)\z/i
|
||||
|
||||
number = Regexp.last_match(1)
|
||||
|
||||
return unless WPScan::WpVersion.valid?(number)
|
||||
|
||||
WPScan::WpVersion.new(
|
||||
number,
|
||||
found_by: 'Readme (Aggressive Detection)',
|
||||
# Since WP 4.7, the Readme only contains the major version (ie 4.7, 4.8 etc)
|
||||
confidence: number >= '4.7' ? 10 : 90,
|
||||
interesting_entries: ["#{readme_url}, Match: '#{node.text.to_s.strip}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/finders/wp_version/rss_generator.rb
Normal file
43
app/finders/wp_version/rss_generator.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# RSS Generator Version Finder
|
||||
class RSSGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
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.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//link[@rel="alternate" and @type="application/rss+xml"]/@href'
|
||||
end
|
||||
|
||||
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
|
||||
30
app/finders/wp_version/unique_fingerprinting.rb
Normal file
30
app/finders/wp_version/unique_fingerprinting.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Unique Fingerprinting Version Finder
|
||||
class UniqueFingerprinting < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Fingerprinter
|
||||
|
||||
# @return [ WpVersion ]
|
||||
def aggressive(opts = {})
|
||||
fingerprint(DB::Fingerprints.wp_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
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: 'Fingerprinting the version -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
10
app/models.rb
Normal file
10
app/models.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
require_relative 'models/interesting_finding'
|
||||
require_relative 'models/wp_version'
|
||||
require_relative 'models/xml_rpc'
|
||||
require_relative 'models/wp_item'
|
||||
require_relative 'models/timthumb'
|
||||
require_relative 'models/media'
|
||||
require_relative 'models/plugin'
|
||||
require_relative 'models/theme'
|
||||
require_relative 'models/config_backup'
|
||||
require_relative 'models/db_export'
|
||||
5
app/models/config_backup.rb
Normal file
5
app/models/config_backup.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module WPScan
|
||||
# Config Backup
|
||||
class ConfigBackup < InterestingFinding
|
||||
end
|
||||
end
|
||||
5
app/models/db_export.rb
Normal file
5
app/models/db_export.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module WPScan
|
||||
# DB Export
|
||||
class DbExport < InterestingFinding
|
||||
end
|
||||
end
|
||||
6
app/models/interesting_finding.rb
Normal file
6
app/models/interesting_finding.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module WPScan
|
||||
# Custom class to include the WPScan::References module
|
||||
class InterestingFinding < CMSScanner::InterestingFinding
|
||||
include References
|
||||
end
|
||||
end
|
||||
5
app/models/media.rb
Normal file
5
app/models/media.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module WPScan
|
||||
# Media
|
||||
class Media < InterestingFinding
|
||||
end
|
||||
end
|
||||
25
app/models/plugin.rb
Normal file
25
app/models/plugin.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
# WordPress Plugin
|
||||
class Plugin < WpItem
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
|
||||
@uri = Addressable::URI.parse(blog.url("wp-content/plugins/#{slug}/"))
|
||||
end
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Plugin.db_data(slug)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::PluginVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
end
|
||||
end
|
||||
99
app/models/theme.rb
Normal file
99
app/models/theme.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
module WPScan
|
||||
# WordPress Theme
|
||||
class Theme < WpItem
|
||||
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
|
||||
:license, :license_uri, :tags, :text_domain
|
||||
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
|
||||
@uri = Addressable::URI.parse(blog.url("wp-content/themes/#{slug}/"))
|
||||
@style_url = opts[:style_url] || url('style.css')
|
||||
|
||||
parse_style
|
||||
end
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Theme.db_data(slug)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::ThemeVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ Theme ]
|
||||
def parent_theme
|
||||
return unless template
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
||||
|
||||
opts = detection_opts.merge(
|
||||
style_url: url(Regexp.last_match[1]),
|
||||
found_by: 'Parent Themes (Passive Detection)',
|
||||
confidence: 100
|
||||
).merge(version_detection: version_detection_opts)
|
||||
|
||||
self.class.new(template, blog, opts)
|
||||
end
|
||||
|
||||
# @param [ Integer ] depth
|
||||
#
|
||||
# @retun [ Array<Theme> ]
|
||||
def parent_themes(depth = 3)
|
||||
theme = self
|
||||
found = []
|
||||
|
||||
(1..depth).each do |_|
|
||||
parent = theme.parent_theme
|
||||
|
||||
break unless parent
|
||||
|
||||
found << parent
|
||||
theme = parent
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def style_body
|
||||
@style_body ||= Browser.get(style_url).body
|
||||
end
|
||||
|
||||
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'
|
||||
}.each do |attribute, tag|
|
||||
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] bofy
|
||||
# @param [ String ] tag
|
||||
#
|
||||
# @return [ String ]
|
||||
def parse_style_tag(body, tag)
|
||||
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
|
||||
|
||||
value && !value.strip.empty? ? value.strip : nil
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
super(other) && style_url == other.style_url
|
||||
end
|
||||
end
|
||||
end
|
||||
71
app/models/timthumb.rb
Normal file
71
app/models/timthumb.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
module WPScan
|
||||
# Timthumb
|
||||
class Timthumb < InterestingFinding
|
||||
include Vulnerable
|
||||
|
||||
attr_reader :version_detection_opts
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The mode to use to detect the version
|
||||
def initialize(url, opts = {})
|
||||
super(url, opts)
|
||||
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::TimthumbVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
vulns = []
|
||||
|
||||
vulns << rce_webshot_vuln if version == false || version > '1.35' && version < '2.8.14' && webshot_enabled?
|
||||
vulns << rce_132_vuln if version == false || version < '1.33'
|
||||
|
||||
vulns
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE in the <= 1.32
|
||||
def rce_132_vuln
|
||||
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
|
||||
def rce_webshot_vuln
|
||||
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 ]
|
||||
def webshot_enabled?
|
||||
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
|
||||
|
||||
res.body =~ /WEBSHOT_ENABLED == true/ ? false : true
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
def default_allowed_domains
|
||||
%w[flickr.com picasa.com img.youtube.com upload.wikimedia.org]
|
||||
end
|
||||
end
|
||||
end
|
||||
158
app/models/wp_item.rb
Normal file
158
app/models/wp_item.rb
Normal file
@@ -0,0 +1,158 @@
|
||||
module WPScan
|
||||
# WpItem (superclass of Plugin & Theme)
|
||||
class WpItem
|
||||
include Vulnerable
|
||||
include Finders::Finding
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
include CMSScanner::Target::Server::Generic
|
||||
|
||||
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
|
||||
CHANGELOGS = %w[changelog.txt CHANGELOG.md changelog.md].freeze
|
||||
|
||||
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :db_data
|
||||
|
||||
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_urls, to: :blog
|
||||
|
||||
# @param [ String ] slug The plugin/theme slug
|
||||
# @param [ Target ] blog The targeted blog
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The detection mode to use
|
||||
# @option opts [ Hash ] :version_detection The options to use when looking for the version
|
||||
# @option opts [ String ] :url The URL of the item
|
||||
def initialize(slug, blog, opts = {})
|
||||
@slug = URI.decode(slug)
|
||||
@blog = blog
|
||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||
|
||||
@detection_opts = { mode: opts[:mode] }
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
|
||||
parse_finding_options(opts)
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerabily> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
|
||||
# Checks if the wp_item is vulnerable to a specific vulnerability
|
||||
#
|
||||
# @param [ Vulnerability ] vuln Vulnerability to check the item against
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def vulnerable_to?(vuln)
|
||||
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
|
||||
|
||||
version < vuln.fixed_in
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def latest_version
|
||||
@latest_version ||= db_data['latest_version'] ? WPScan::Version.new(db_data['latest_version']) : nil
|
||||
end
|
||||
|
||||
# Not used anywhere ATM
|
||||
# @return [ Boolean ]
|
||||
def popular?
|
||||
@popular ||= db_data['popular']
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def last_updated
|
||||
@last_updated ||= db_data['last_updated']
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def outdated?
|
||||
@outdated ||= if version && latest_version
|
||||
version < latest_version
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# URI.encode is preferered over Addressable::URI.encode as it will encode
|
||||
# leading # character:
|
||||
# URI.encode('#t#') => %23t%23
|
||||
# Addressable::URI.encode('#t#') => #t%23
|
||||
#
|
||||
# @param [ String ] path Optional path to merge with the uri
|
||||
#
|
||||
# @return [ String ]
|
||||
def url(path = nil)
|
||||
return unless @uri
|
||||
return @uri.to_s unless path
|
||||
|
||||
@uri.join(URI.encode(path)).to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
self.class == other.class && slug == other.slug
|
||||
end
|
||||
|
||||
def to_s
|
||||
slug
|
||||
end
|
||||
|
||||
# @return [ Symbol ] The Class symbol associated to the item
|
||||
def classify
|
||||
@classify ||= classify_slug(slug)
|
||||
end
|
||||
|
||||
# @return [ String ] The readme url if found
|
||||
def readme_url
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
if @readme_url.nil?
|
||||
READMES.each do |path|
|
||||
return @readme_url = url(path) if Browser.get(url(path)).code == 200
|
||||
end
|
||||
end
|
||||
|
||||
@readme_url
|
||||
end
|
||||
|
||||
# @return [ String, false ] The changelog urr if found
|
||||
def changelog_url
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
if @changelog_url.nil?
|
||||
CHANGELOGS.each do |path|
|
||||
return @changelog_url = url(path) if Browser.get(url(path)).code == 200
|
||||
end
|
||||
end
|
||||
|
||||
@changelog_url
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def directory_listing?(path = nil, params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
super(path, params)
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def error_log?(path = 'error_log', params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
super(path, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
54
app/models/wp_version.rb
Normal file
54
app/models/wp_version.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
module WPScan
|
||||
# WP Version
|
||||
class WpVersion < CMSScanner::Version
|
||||
include Vulnerable
|
||||
|
||||
def initialize(number, opts = {})
|
||||
raise InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
|
||||
|
||||
super(number, opts)
|
||||
end
|
||||
|
||||
# @param [ String ] number
|
||||
#
|
||||
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
|
||||
def self.valid?(number)
|
||||
all.include?(number)
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] All the version numbers
|
||||
def self.all
|
||||
return @all_numbers if @all_numbers
|
||||
|
||||
@all_numbers = []
|
||||
|
||||
DB::Fingerprints.wp_fingerprints.each_value do |fp|
|
||||
fp.each_value do |versions|
|
||||
versions.each do |version|
|
||||
@all_numbers << version unless @all_numbers.include?(version)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
||||
end
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Version.db_data(number)
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/models/xml_rpc.rb
Normal file
19
app/models/xml_rpc.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module WPScan
|
||||
# Override of the CMSScanner::XMLRPC to include the references
|
||||
class XMLRPC < CMSScanner::XMLRPC
|
||||
include References # To be able to use the :wpvulndb reference if needed
|
||||
|
||||
# @return [ Hash ]
|
||||
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'
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/views/cli/core/banner.erb
Normal file
14
app/views/cli/core/banner.erb
Normal file
@@ -0,0 +1,14 @@
|
||||
_______________________________________________________________
|
||||
__ _______ _____
|
||||
\ \ / / __ \ / ____|
|
||||
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
|
||||
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
|
||||
\ /\ / | | ____) | (__| (_| | | | |
|
||||
\/ \/ |_| |_____/ \___|\__,_|_| |_|
|
||||
|
||||
WordPress Security Scanner by the WPScan Team
|
||||
Version <%= WPScan::VERSION %>
|
||||
Sponsored by Sucuri - https://sucuri.net
|
||||
@_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
|
||||
_______________________________________________________________
|
||||
|
||||
8
app/views/cli/core/db_update_finished.erb
Normal file
8
app/views/cli/core/db_update_finished.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<% if @verbose && !@updated.empty? -%>
|
||||
<%= notice_icon %> File(s) Updated:
|
||||
<% @updated.each do |file| -%>
|
||||
| <%= file %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<%= notice_icon %> Update completed.
|
||||
|
||||
1
app/views/cli/core/db_update_started.erb
Normal file
1
app/views/cli/core/db_update_started.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= notice_icon %> Updating the Database ...
|
||||
1
app/views/cli/core/not_fully_configured.erb
Normal file
1
app/views/cli/core/not_fully_configured.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= critical_icon %> The Website is not fully configured and currently in install mode. Create a new admin user at <%= @url %>
|
||||
5
app/views/cli/core/version.erb
Normal file
5
app/views/cli/core/version.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
Current Version: <%= WPScan::VERSION %>
|
||||
<% if @last_update -%>
|
||||
Last DB Update: <%= @last_update.strftime('%Y-%m-%d') %>
|
||||
<% end -%>
|
||||
|
||||
11
app/views/cli/enumeration/config_backups.erb
Normal file
11
app/views/cli/enumeration/config_backups.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<% if @config_backups.empty? -%>
|
||||
<%= notice_icon %> No Config Backups Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Config Backup(s) Identified:
|
||||
<% @config_backups.each do |config_backup| -%>
|
||||
|
||||
<%= info_icon %> <%= config_backup %>
|
||||
<%= render('@finding', item: config_backup) -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
11
app/views/cli/enumeration/db_exports.erb
Normal file
11
app/views/cli/enumeration/db_exports.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<% if @db_exports.empty? -%>
|
||||
<%= notice_icon %> No DB Exports Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Db Export(s) Identified:
|
||||
<% @db_exports.each do |db_export| -%>
|
||||
|
||||
<%= info_icon %> <%= db_export %>
|
||||
<%= render('@finding', item: db_export) -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
11
app/views/cli/enumeration/medias.erb
Normal file
11
app/views/cli/enumeration/medias.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<% if @medias.empty? -%>
|
||||
<%= notice_icon %> No Medias Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Medias(s) Identified:
|
||||
<% @medias.each do |media| -%>
|
||||
|
||||
<%= info_icon %> <%= media %>
|
||||
<%= render('@finding', item: media) -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
20
app/views/cli/enumeration/plugins.erb
Normal file
20
app/views/cli/enumeration/plugins.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
<% if @plugins.empty? -%>
|
||||
<%= notice_icon %> No plugins Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Plugin(s) Identified:
|
||||
<% @plugins.each do |plugin| -%>
|
||||
|
||||
<%= info_icon %> <%= plugin %>
|
||||
<%= render('@wp_item', wp_item: plugin) -%>
|
||||
|
|
||||
<%= render('@finding', item: plugin) -%>
|
||||
|
|
||||
<% if plugin.version -%>
|
||||
| Version: <%= plugin.version %> (<%= plugin.version.confidence %>% confidence)
|
||||
<%= render('@finding', item: plugin.version) -%>
|
||||
<% else -%>
|
||||
| The version could not be determined.
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
11
app/views/cli/enumeration/themes.erb
Normal file
11
app/views/cli/enumeration/themes.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<% if @themes.empty? -%>
|
||||
<%= notice_icon %> No themes Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Theme(s) Identified:
|
||||
<% @themes.each do |theme| -%>
|
||||
|
||||
<%= info_icon %> <%= theme %>
|
||||
<%= render('@theme', theme: theme, show_parents: false) -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
18
app/views/cli/enumeration/timthumbs.erb
Normal file
18
app/views/cli/enumeration/timthumbs.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
<% if @timthumbs.empty? -%>
|
||||
<%= notice_icon %> No Timthumbs Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Timthumb(s) Identified:
|
||||
<% @timthumbs.each do |timthumb| -%>
|
||||
|
||||
<%= info_icon %> <%= timthumb %>
|
||||
<%= render('@finding', item: timthumb) -%>
|
||||
|
|
||||
<% if timthumb.version -%>
|
||||
| Version: <%= timthumb.version %>
|
||||
<%= render('@finding', item: timthumb.version) -%>
|
||||
<% else -%>
|
||||
| The version could not be determined.
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
11
app/views/cli/enumeration/users.erb
Normal file
11
app/views/cli/enumeration/users.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<% if @users.empty? -%>
|
||||
<%= notice_icon %> No Users Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> User(s) Identified:
|
||||
<% @users.each do |user| -%>
|
||||
|
||||
<%= info_icon %> <%= user %>
|
||||
<%= render('@finding', item: user) -%>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
32
app/views/cli/finding.erb
Normal file
32
app/views/cli/finding.erb
Normal file
@@ -0,0 +1,32 @@
|
||||
| Detected By: <%= @item.found_by %>
|
||||
<% @item.interesting_entries.each do |entry| -%>
|
||||
| - <%= entry %>
|
||||
<% end -%>
|
||||
<% unless (confirmed = @item.confirmed_by).empty? -%>
|
||||
<% if confirmed.size == 1 -%>
|
||||
| Confirmed By: <%= confirmed.first.found_by %>
|
||||
<% confirmed.first.interesting_entries.each do |entry| -%>
|
||||
| - <%= entry %>
|
||||
<% end -%>
|
||||
<% else -%>
|
||||
| Confirmed By:
|
||||
<% confirmed.each do |c| -%>
|
||||
| <%= c.found_by %>
|
||||
<% c.interesting_entries.each do |entry| -%>
|
||||
| - <%= entry %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% if @item.respond_to?(:vulnerabilities) && !(vulns = @item.vulnerabilities).empty? -%>
|
||||
<% vulns_size = vulns.size -%>
|
||||
|
|
||||
| <%= critical_icon %> <%= vulns_size %> <%= vulns_size == 1 ? 'vulnerability' : 'vulnerabilities' %> identified:
|
||||
|
|
||||
<% vulns.each_with_index do |vulnerability, index| -%>
|
||||
<%= render('@vulnerability', v: vulnerability) -%>
|
||||
<% if index != vulns_size -1 -%>
|
||||
|
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user