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