HELLO v3!!!

This commit is contained in:
Ryan Dewhurst
2018-09-26 21:12:01 +02:00
parent 28b9c15256
commit d268a86795
1871 changed files with 988118 additions and 0 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
git/
bundle/
.idea/
.yardoc/
cache/
coverage/
spec/
.*
**/*.md
*.md
Dockerfile
**/*.orig
*.orig
bin/wpscan-docker*
.wpscan/

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM ruby:2.5-alpine
MAINTAINER WPScan Team <team@wpscan.org>
ARG BUNDLER_ARGS="--jobs=8 --without test development"
RUN adduser -h /wpscan -g WPScan -D wpscan
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
COPY . /wpscan
RUN chown -R wpscan:wpscan /wpscan
# runtime dependencies
RUN apk add --no-cache libcurl procps sqlite-libs && \
# build dependencies
apk add --no-cache --virtual build-deps git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
bundle install --system --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
apk del --no-cache build-deps
WORKDIR /wpscan
RUN rake install --trace
USER wpscan
RUN /usr/local/bundle/bin/wpscan --update --verbose
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
CMD ["--help"]

2
Gemfile Normal file
View File

@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec

74
LICENSE Normal file
View File

@@ -0,0 +1,74 @@
WPScan Public Source License
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
1. Definitions
1.1 “License” means this document.
1.2 “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
1.3 “WPScan Team” means WPScans core developers, an updated list of whom can be found within the CREDITS file.
2. Commercialization
A commercial use is one intended for commercial advantage or monetary compensation.
Example cases of commercialization are:
- Using WPScan to provide commercial managed/Software-as-a-Service services.
- Distributing WPScan as a commercial product or as part of one.
- Using WPScan as a value added service/product.
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
- Using WPScan to test your own systems.
- Any non-commercial use of WPScan.
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
Free-use Terms and Conditions;
3. Redistribution
Redistribution is permitted under the following conditions:
- Unmodified License is provided with WPScan.
- Unmodified Copyright notices are provided with WPScan.
- Does not conflict with the commercialization clause.
4. Copying
Copying is permitted so long as it does not conflict with the Redistribution clause.
5. Modification
Modification is permitted so long as it does not conflict with the Redistribution clause.
6. Contributions
Any Contributions assume the Contributor grants the WPScan Team the unlimited, non-exclusive right to reuse, modify and relicense the Contributor's content.
7. Support
WPScan is provided under an AS-IS basis and without any support, updates or maintenance. Support, updates and maintenance may be given according to the sole discretion of the WPScan Team.
8. Disclaimer of Warranty
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
9. Limitation of Liability
To the extent permitted under Law, WPScan is provided under an AS-IS basis. The WPScan Team shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred as a result of WPScan's actions, failure, bugs and/or any other interaction between WPScan and end-equipment, computers, other software or any 3rd party, end-equipment, computer or services.
10. Disclaimer
Running WPScan against websites without prior mutual consent may be illegal in your country. The WPScan Team accept no liability and are not responsible for any misuse or damage caused by WPScan.
11. Trademark
The "wpscan" term is a registered trademark. This License does not grant the use of the "wpscan" trademark or the use of the WPScan logo.

157
README.md Normal file
View File

@@ -0,0 +1,157 @@
![alt text](https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/wpscan_logo_407x80.png "WPScan - WordPress Security Scanner") v3 BETA
[![Gem Version](https://badge.fury.io/rb/wpscan.svg)](https://badge.fury.io/rb/wpscan)
[![Build Status](https://travis-ci.org/wpscanteam/wpscan-v3.svg?branch=master)](https://travis-ci.org/wpscanteam/wpscan-v3)
[![Code Climate](https://codeclimate.com/github/wpscanteam/wpscan-v3/badges/gpa.svg)](https://codeclimate.com/github/wpscanteam/wpscan-v3)
[![Patreon Donate](https://img.shields.io/badge/patreon-donate-green.svg)](https://www.patreon.com/wpscan)
# INSTALL
## Prerequisites:
- Ruby >= 2.2.2 - Recommended: 2.3.3
- Curl >= 7.21 - Recommended: latest - FYI the 7.29 has a segfault
- RubyGems - Recommended: latest
### From RubyGems:
```
gem install wpscan
```
### From sources:
Prerequisites: Git
```
git clone https://github.com/wpscanteam/wpscan-v3 wpscan
cd wpscan/
bundle install && rake install
```
# Docker
Pull the repo with ```docker pull wpscanteam/wpscan-v3```
# Usage
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings. If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
The DB is located at ~/.wpscan/db
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
* ~/.wpscan/cli_options.json
* ~/.wpscan/cli_options.yml
* pwd/.wpscan/cli_options.json
* pwd/.wpscan/cli_options.yml
If those files exist, options from them will be loaded and overridden if found twice.
e.g:
~/.wpscan/cli_options.yml:
```
proxy: 'http://127.0.0.1:8080'
verbose: true
```
pwd/.wpscan/cli_options.yml:
```
proxy: 'socks5://127.0.0.1:9090'
url: 'http://target.tld'
```
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
# PROJECT HOME
[https://wpscan.org](https://wpscan.org)
# VULNERABILITY DATABASE
[https://wpvulndb.com](https://wpvulndb.com)
# LICENSE
## WPScan Public Source License
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
### 1. Definitions
1.1 "License" means this document.
1.2 "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
1.3 "WPScan Team" means WPScans core developers, an updated list of whom can be found within the CREDITS file.
### 2. Commercialization
A commercial use is one intended for commercial advantage or monetary compensation.
Example cases of commercialization are:
- Using WPScan to provide commercial managed/Software-as-a-Service services.
- Distributing WPScan as a commercial product or as part of one.
- Using WPScan as a value added service/product.
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
- Using WPScan to test your own systems.
- Any non-commercial use of WPScan.
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
Free-use Terms and Conditions;
### 3. Redistribution
Redistribution is permitted under the following conditions:
- Unmodified License is provided with WPScan.
- Unmodified Copyright notices are provided with WPScan.
- Does not conflict with the commercialization clause.
### 4. Copying
Copying is permitted so long as it does not conflict with the Redistribution clause.
### 5. Modification
Modification is permitted so long as it does not conflict with the Redistribution clause.
### 6. Contributions
Any Contributions assume the Contributor grants the WPScan Team the unlimited, non-exclusive right to reuse, modify and relicense the Contributor's content.
### 7. Support
WPScan is provided under an AS-IS basis and without any support, updates or maintenance. Support, updates and maintenance may be given according to the sole discretion of the WPScan Team.
### 8. Disclaimer of Warranty
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
### 9. Limitation of Liability
To the extent permitted under Law, WPScan is provided under an AS-IS basis. The WPScan Team shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred as a result of WPScan's actions, failure, bugs and/or any other interaction between WPScan and end-equipment, computers, other software or any 3rd party, end-equipment, computer or services.
### 10. Disclaimer
Running WPScan against websites without prior mutual consent may be illegal in your country. The WPScan Team accept no liability and are not responsible for any misuse or damage caused by WPScan.
### 11. Trademark
The "wpscan" term is a registered trademark. This License does not grant the use of the "wpscan" trademark or the use of the WPScan logo.

24
Rakefile Normal file
View File

@@ -0,0 +1,24 @@
# rubocop:disable all
require 'bundler/gem_tasks'
exec = []
begin
require 'rubocop/rake_task'
RuboCop::RakeTask.new
exec << :rubocop
rescue LoadError
end
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
exec << :spec
rescue LoadError
end
# Run rubocop & rspec before the build (only if installed)
task build: exec
# rubocop:enable all

3
app/app.rb Normal file
View File

@@ -0,0 +1,3 @@
require_relative 'models'
require_relative 'finders'
require_relative 'controllers'

7
app/controllers.rb Normal file
View 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'

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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'

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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
View 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

View 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
View File

@@ -0,0 +1,3 @@
require_relative 'passwords/wp_login'
require_relative 'passwords/xml_rpc'
require_relative 'passwords/xml_rpc_multicall'

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View File

@@ -0,0 +1 @@
require_relative 'wp_items/urls_in_homepage'

View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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'

View File

@@ -0,0 +1,5 @@
module WPScan
# Config Backup
class ConfigBackup < InterestingFinding
end
end

5
app/models/db_export.rb Normal file
View File

@@ -0,0 +1,5 @@
module WPScan
# DB Export
class DbExport < InterestingFinding
end
end

View 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
View File

@@ -0,0 +1,5 @@
module WPScan
# Media
class Media < InterestingFinding
end
end

25
app/models/plugin.rb Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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_
_______________________________________________________________

View 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.

View File

@@ -0,0 +1 @@
<%= notice_icon %> Updating the Database ...

View File

@@ -0,0 +1 @@
<%= critical_icon %> The Website is not fully configured and currently in install mode. Create a new admin user at <%= @url %>

View File

@@ -0,0 +1,5 @@
Current Version: <%= WPScan::VERSION %>
<% if @last_update -%>
Last DB Update: <%= @last_update.strftime('%Y-%m-%d') %>
<% end -%>

View 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 %>

View 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 %>

Some files were not shown because too many files have changed in this diff Show More