Compare commits

..

3 Commits

Author SHA1 Message Date
erwanlr
82db02a688 Updates spec for #1342 2019-05-03 14:25:17 +01:00
erwanlr
2c07de8c6b Updates class comment 2019-05-03 14:23:04 +01:00
erwanlr
4b0b8fa624 Fixes #1342 2019-05-03 14:04:50 +01:00
340 changed files with 680 additions and 94990 deletions

View File

@@ -8,12 +8,10 @@ ClassVars:
Enabled: false Enabled: false
LineLength: LineLength:
Max: 120 Max: 120
Lint/UriEscapeUnescape:
Enabled: false
MethodLength: MethodLength:
Max: 20 Max: 20
Exclude: Lint/UriEscapeUnescape:
- 'app/controllers/enumeration/cli_options.rb' Enabled: false
Metrics/AbcSize: Metrics/AbcSize:
Max: 25 Max: 25
Metrics/BlockLength: Metrics/BlockLength:
@@ -21,14 +19,9 @@ Metrics/BlockLength:
- 'spec/**/*' - 'spec/**/*'
Metrics/ClassLength: Metrics/ClassLength:
Max: 150 Max: 150
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 8 Max: 8
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Style/FormatStringToken: Style/FormatStringToken:
Enabled: false Enabled: false
Style/NumericPredicate:
Exclude:
- 'app/controllers/vuln_api.rb'

View File

@@ -1,4 +1,4 @@
FROM ruby:2.6.3-alpine AS builder FROM ruby:2.6.2-alpine3.9 AS builder
LABEL maintainer="WPScan Team <team@wpscan.org>" LABEL maintainer="WPScan Team <team@wpscan.org>"
ARG BUNDLER_ARGS="--jobs=8 --without test development" ARG BUNDLER_ARGS="--jobs=8 --without test development"
@@ -19,7 +19,7 @@ RUN rake install --trace
RUN chmod -R a+r /usr/local/bundle RUN chmod -R a+r /usr/local/bundle
FROM ruby:2.6.3-alpine FROM ruby:2.6.2-alpine3.9
LABEL maintainer="WPScan Team <team@wpscan.org>" LABEL maintainer="WPScan Team <team@wpscan.org>"
RUN adduser -h /wpscan -g WPScan -D wpscan RUN adduser -h /wpscan -g WPScan -D wpscan

View File

@@ -17,6 +17,7 @@
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a> <a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
<a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a> <a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a>
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a> <a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
<a href="https://www.patreon.com/wpscan" target="_blank"><img src="https://img.shields.io/badge/patreon-donate-green.svg"></a>
</p> </p>
# INSTALL # INSTALL
@@ -29,7 +30,6 @@
- Curl >= 7.21 - Recommended: latest - Curl >= 7.21 - Recommended: latest
- The 7.29 has a segfault - The 7.29 has a segfault
- RubyGems - Recommended: latest - RubyGems - Recommended: latest
- Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
### From RubyGems (Recommended) ### From RubyGems (Recommended)
@@ -77,60 +77,41 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
# Usage # 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. ```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.
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'. 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) 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 The DB is located at ~/.wpscan/db
## Vulnerability Database
The WPScan CLI tool uses the [WPVulnDB API](https://wpvulndb.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPVulnDB](https://wpvulndb.com/users/sign_up). Up to 50 API requests per day are given free of charge to registered users. Once the 50 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data. Users can upgrade to paid API usage to increase their API limits within their user profile on [WPVulnDB](https://wpvulndb.com/).
## Load CLI options from file/s
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last): WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
- ~/.wpscan/scan.json - ~/.wpscan/cli_options.json
- ~/.wpscan/scan.yml - ~/.wpscan/cli_options.yml
- pwd/.wpscan/scan.json - pwd/.wpscan/cli_options.json
- pwd/.wpscan/scan.yml - pwd/.wpscan/cli_options.yml
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice. If those files exist, options from them will be loaded and overridden if found twice.
e.g: e.g:
~/.wpscan/scan.yml: ~/.wpscan/cli_options.yml:
```yml ```yml
cli_options: proxy: 'http://127.0.0.1:8080'
proxy: 'http://127.0.0.1:8080' verbose: true
verbose: true
``` ```
pwd/.wpscan/scan.yml: pwd/.wpscan/cli_options.yml:
```yml ```yml
cli_options: proxy: 'socks5://127.0.0.1:9090'
proxy: 'socks5://127.0.0.1:9090' url: 'http://target.tld'
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``` Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
## Save API Token in a file Enumerating usernames
The feature mentioned above is useful to keep the API Token in a config file and not have to supply it via the CLI each time. To do so, create the ~/.wpscan/scan.yml file containing the below:
```yml
cli_options:
api_token: YOUR_API_TOKEN
```
## Enumerating usernames
```shell ```shell
wpscan --url https://target.tld/ --enumerate u wpscan --url https://target.tld/ --enumerate u

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'controllers/core' require_relative 'controllers/core'
require_relative 'controllers/vuln_api'
require_relative 'controllers/custom_directories' require_relative 'controllers/custom_directories'
require_relative 'controllers/wp_version' require_relative 'controllers/wp_version'
require_relative 'controllers/main_theme' require_relative 'controllers/main_theme'

View File

@@ -7,6 +7,15 @@ module WPScan
module Controller module Controller
# Enumeration Controller # Enumeration Controller
class Enumeration < CMSScanner::Controller::Base class Enumeration < CMSScanner::Controller::Base
def before_scan
DB::DynamicFinders::Plugin.create_versions_finders
DB::DynamicFinders::Theme.create_versions_finders
# Force the Garbage Collector to run due to the above method being
# quite heavy in objects allocation
GC.start
end
def run def run
enum = ParsedCli.enumerate || {} enum = ParsedCli.enumerate || {}

View File

@@ -11,6 +11,7 @@ module WPScan
end end
# @return [ Array<OptParseValidator::OptBase> ] # @return [ Array<OptParseValidator::OptBase> ]
# rubocop:disable Metrics/MethodLength
def cli_enum_choices def cli_enum_choices
[ [
OptMultiChoices.new( OptMultiChoices.new(
@@ -18,10 +19,10 @@ module WPScan
choices: { choices: {
vp: OptBoolean.new(['--vulnerable-plugins']), vp: OptBoolean.new(['--vulnerable-plugins']),
ap: OptBoolean.new(['--all-plugins']), ap: OptBoolean.new(['--all-plugins']),
p: OptBoolean.new(['--popular-plugins']), p: OptBoolean.new(['--plugins']),
vt: OptBoolean.new(['--vulnerable-themes']), vt: OptBoolean.new(['--vulnerable-themes']),
at: OptBoolean.new(['--all-themes']), at: OptBoolean.new(['--all-themes']),
t: OptBoolean.new(['--popular-themes']), t: OptBoolean.new(['--themes']),
tt: OptBoolean.new(['--timthumbs']), tt: OptBoolean.new(['--timthumbs']),
cb: OptBoolean.new(['--config-backups']), cb: OptBoolean.new(['--config-backups']),
dbe: OptBoolean.new(['--db-exports']), dbe: OptBoolean.new(['--db-exports']),
@@ -44,6 +45,7 @@ module WPScan
) )
] ]
end end
# rubocop:enable Metrics/MethodLength
# @return [ Array<OptParseValidator::OptBase> ] # @return [ Array<OptParseValidator::OptBase> ]
def cli_plugins_opts def cli_plugins_opts
@@ -65,11 +67,6 @@ module WPScan
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \ 'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
'or --plugins-detection modes.'], 'or --plugins-detection modes.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
),
OptInteger.new(
['--plugins-threshold THRESHOLD',
'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
'Set to 0 to ignore the threshold.'], default: 100
) )
] ]
end end
@@ -94,11 +91,6 @@ module WPScan
'Use the supplied mode to check themes versions instead of the --detection-mode ' \ 'Use the supplied mode to check themes versions instead of the --detection-mode ' \
'or --themes-detection modes.'], 'or --themes-detection modes.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
),
OptInteger.new(
['--themes-threshold THRESHOLD',
'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
'Set to 0 to ignore the threshold.'], default: 20
) )
] ]
end end

View File

@@ -56,13 +56,12 @@ module WPScan
# #
# @return [ Boolean ] Wether or not to enumerate the plugins # @return [ Boolean ] Wether or not to enumerate the plugins
def enum_plugins?(opts) def enum_plugins?(opts)
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins] opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end end
def enum_plugins def enum_plugins
opts = default_opts('plugins').merge( opts = default_opts('plugins').merge(
list: plugins_list_from_opts(ParsedCli.options), list: plugins_list_from_opts(ParsedCli.options),
threshold: ParsedCli.plugins_threshold,
sort: true sort: true
) )
@@ -92,7 +91,7 @@ module WPScan
if opts[:enumerate][:all_plugins] if opts[:enumerate][:all_plugins]
DB::Plugins.all_slugs DB::Plugins.all_slugs
elsif opts[:enumerate][:popular_plugins] elsif opts[:enumerate][:plugins]
DB::Plugins.popular_slugs DB::Plugins.popular_slugs
else else
DB::Plugins.vulnerable_slugs DB::Plugins.vulnerable_slugs
@@ -103,13 +102,12 @@ module WPScan
# #
# @return [ Boolean ] Wether or not to enumerate the themes # @return [ Boolean ] Wether or not to enumerate the themes
def enum_themes?(opts) def enum_themes?(opts)
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes] opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
end end
def enum_themes def enum_themes
opts = default_opts('themes').merge( opts = default_opts('themes').merge(
list: themes_list_from_opts(ParsedCli.options), list: themes_list_from_opts(ParsedCli.options),
threshold: ParsedCli.themes_threshold,
sort: true sort: true
) )
@@ -139,7 +137,7 @@ module WPScan
if opts[:enumerate][:all_themes] if opts[:enumerate][:all_themes]
DB::Themes.all_slugs DB::Themes.all_slugs
elsif opts[:enumerate][:popular_themes] elsif opts[:enumerate][:themes]
DB::Themes.popular_slugs DB::Themes.popular_slugs
else else
DB::Themes.vulnerable_slugs DB::Themes.vulnerable_slugs

View File

@@ -65,43 +65,30 @@ module WPScan
case ParsedCli.password_attack case ParsedCli.password_attack
when :wp_login when :wp_login
Finders::Passwords::WpLogin.new(target) WPScan::Finders::Passwords::WpLogin.new(target)
when :xmlrpc when :xmlrpc
raise Error::XMLRPCNotDetected unless xmlrpc raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPC.new(xmlrpc) WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
when :xmlrpc_multicall when :xmlrpc_multicall
raise Error::XMLRPCNotDetected unless xmlrpc raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPCMulticall.new(xmlrpc) WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
end
end
# @return [ Boolean ]
def xmlrpc_get_users_blogs_enabled?
if xmlrpc&.enabled? &&
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
.run.body !~ /XML\-RPC services are disabled/
true
else
false
end end
end end
# @return [ CMSScanner::Finders::Finder ] # @return [ CMSScanner::Finders::Finder ]
def attacker_from_automatic_detection def attacker_from_automatic_detection
if xmlrpc_get_users_blogs_enabled? if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
wp_version = target.wp_version wp_version = target.wp_version
if wp_version && wp_version < '4.4' if wp_version && wp_version < '4.4'
Finders::Passwords::XMLRPCMulticall.new(xmlrpc) WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
else else
Finders::Passwords::XMLRPC.new(xmlrpc) WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
end end
else else
Finders::Passwords::WpLogin.new(target) WPScan::Finders::Passwords::WpLogin.new(target)
end end
end end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class VulnApi < CMSScanner::Controller::Base
def cli_options
[
OptString.new(['--api-token TOKEN', 'The WPVulnDB API Token to display vulnerability data'])
]
end
def before_scan
return unless ParsedCli.api_token
DB::VulnApi.token = ParsedCli.api_token
api_status = DB::VulnApi.status
raise Error::InvalidApiToken if api_status['error']
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
raise api_status['http_error'] if api_status['http_error']
end
def after_scan
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
end
end
end
end

View File

@@ -17,7 +17,7 @@ module WPScan
end end
def before_scan def before_scan
DB::DynamicFinders::Wordpress.create_versions_finders WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
end end
def run def run

View File

@@ -20,9 +20,9 @@ module WPScan
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res| enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
if res.effective_url.end_with?('.zip') if res.effective_url.end_with?('.zip')
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type']) next unless res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
else else
next unless SQL_PATTERN.match?(res.body) next unless res.body =~ SQL_PATTERN
end end
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100) found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)

View File

@@ -4,7 +4,7 @@ require_relative 'interesting_findings/readme'
require_relative 'interesting_findings/wp_cron' require_relative 'interesting_findings/wp_cron'
require_relative 'interesting_findings/multisite' require_relative 'interesting_findings/multisite'
require_relative 'interesting_findings/debug_log' require_relative 'interesting_findings/debug_log'
require_relative 'interesting_findings/backup_db' require_relative 'interesting_findings/plugin_backup_folders'
require_relative 'interesting_findings/mu_plugins' require_relative 'interesting_findings/mu_plugins'
require_relative 'interesting_findings/registration' require_relative 'interesting_findings/registration'
require_relative 'interesting_findings/tmm_db_migrate' require_relative 'interesting_findings/tmm_db_migrate'
@@ -24,7 +24,7 @@ module WPScan
super(target) super(target)
%w[ %w[
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog Readme DebugLog FullPathDisclosure PluginBackupFolders DuplicatorInstallerLog
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
UploadSQLDump EmergencyPwdResetScript WPCron UploadSQLDump EmergencyPwdResetScript WPCron
].each do |f| ].each do |f|

View File

@@ -1,26 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# BackupDB finder
class BackupDB < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'wp-content/backup-db/'
res = target.head_and_get(path, [200, 403])
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
Model::BackupDB.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
)
end
end
end
end
end

View File

@@ -9,7 +9,7 @@ module WPScan
def aggressive(_opts = {}) def aggressive(_opts = {})
path = 'installer-log.txt' path = 'installer-log.txt'
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body) return unless target.head_and_get(path).body =~ /DUPLICATOR INSTALL-LOG/
Model::DuplicatorInstallerLog.new( Model::DuplicatorInstallerLog.new(
target.url(path), target.url(path),

View File

@@ -10,7 +10,7 @@ module WPScan
pattern = %r{#{target.content_dir}/mu\-plugins/}i pattern = %r{#{target.content_dir}/mu\-plugins/}i
target.in_scope_uris(target.homepage_res) do |uri| target.in_scope_uris(target.homepage_res) do |uri|
next unless uri.path&.match?(pattern) next unless uri.path =~ pattern
url = target.url('wp-content/mu-plugins/') url = target.url('wp-content/mu-plugins/')

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# Known Backup Folders from Plugin finder
class PluginBackupFolders < CMSScanner::Finders::Finder
PATHS = %w[wp-content/backup-db/ wp-content/backups-dup-pro/ wp-content/updraft/].freeze
# @return [ InterestingFinding ]
def aggressive(_opts = {})
found = []
PATHS.each do |path|
res = target.head_and_get(path, [200, 403])
next unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
found << Model::PluginBackupFolder.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),
references: { url: ['https://github.com/wpscanteam/wpscan/issues/422',
'https://github.com/wpscanteam/wpscan/issues/1342'] }
)
end
found
end
end
end
end
end

View File

@@ -12,7 +12,7 @@ module WPScan
path = 'wp-content/uploads/dump.sql' path = 'wp-content/uploads/dump.sql'
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } }) res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
return unless SQL_PATTERN.match?(res.body) return unless res.body =~ SQL_PATTERN
Model::UploadSQLDump.new( Model::UploadSQLDump.new(
target.url(path), target.url(path),

View File

@@ -13,7 +13,7 @@ module WPScan
def valid_credentials?(response) def valid_credentials?(response)
response.code == 302 && response.code == 302 &&
[*response.headers['Set-Cookie']]&.any? { |cookie| cookie =~ /wordpress_logged_in_/i } response.headers['Set-Cookie']&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
end end
def errored_response?(response) def errored_response?(response)

View File

@@ -8,7 +8,7 @@ module WPScan
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
def login_request(username, password) def login_request(username, password)
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0) target.method_call('wp.getUsersBlogs', [username, password])
end end
def valid_credentials?(response) def valid_credentials?(response)

View File

@@ -19,7 +19,7 @@ module WPScan
end end
end end
target.multi_call(methods, cache_ttl: 0).run target.multi_call(methods).run
end end
# @param [ Array<Model::User> ] users # @param [ Array<Model::User> ] users

View File

@@ -13,15 +13,25 @@ module WPScan
def initialize(plugin) def initialize(plugin)
finders << PluginVersion::Readme.new(plugin) finders << PluginVersion::Readme.new(plugin)
create_and_load_dynamic_versions_finders(plugin) load_specific_finders(plugin)
end end
# Create the dynamic version finders related to the plugin and register them # Load the finders associated with the plugin
# #
# @param [ Model::Plugin ] plugin # @param [ Model::Plugin ] plugin
def create_and_load_dynamic_versions_finders(plugin) def load_specific_finders(plugin)
DB::DynamicFinders::Plugin.create_versions_finders(plugin.slug).each do |finder| module_name = plugin.classify
finders << finder.new(plugin)
return unless Finders::PluginVersion.constants.include?(module_name)
mod = Finders::PluginVersion.const_get(module_name)
mod.constants.each do |constant|
c = mod.const_get(constant)
next unless c.is_a?(Class)
finders << c.new(plugin)
end end
end end
end end

View File

@@ -11,7 +11,7 @@ module WPScan
# The target(plugin)#readme_url can't be used directly here # The target(plugin)#readme_url can't be used directly here
# as if the --detection-mode is passive, it will always return nil # as if the --detection-mode is passive, it will always return nil
target.potential_readme_filenames.each do |file| Model::WpItem::READMES.each do |file|
res = target.head_and_get(file) res = target.head_and_get(file)
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty? next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
@@ -52,7 +52,7 @@ module WPScan
number = Regexp.last_match[1] number = Regexp.last_match[1]
number if /[0-9]+/.match?(number) number if number =~ /[0-9]+/
end end
# @param [ String ] body # @param [ String ] body

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'BodyPattern' # Plugins finder from Dynamic Finder 'BodyPattern'
class BodyPattern < Finders::DynamicFinder::WpItems::Finder class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -15,7 +15,7 @@ module WPScan
# #
# @return [ Plugin ] The detected plugin in the response, related to the config # @return [ Plugin ] The detected plugin in the response, related to the config
def process_response(opts, response, slug, klass, config) def process_response(opts, response, slug, klass, config)
return unless response.body&.match?(config['pattern']) return unless response.body =~ config['pattern']
Model::Plugin.new( Model::Plugin.new(
slug, slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'Comment' # Plugins finder from the Dynamic Finder 'Comment'
class Comment < Finders::DynamicFinder::WpItems::Finder class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -18,7 +18,7 @@ module WPScan
response.html.xpath(config['xpath'] || '//comment()').each do |node| response.html.xpath(config['xpath'] || '//comment()').each do |node|
comment = node.text.to_s.strip comment = node.text.to_s.strip
next unless comment&.match?(config['pattern']) next unless comment =~ config['pattern']
return Model::Plugin.new( return Model::Plugin.new(
slug, slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'ConfigParser' # Plugins finder from Dynamic Finder 'ConfigParser'
class ConfigParser < Finders::DynamicFinder::WpItems::Finder class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40 DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'HeaderPattern' # Plugins finder from Dynamic Finder 'HeaderPattern'
class HeaderPattern < Finders::DynamicFinder::WpItems::Finder class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts # @param [ Hash ] opts

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'JavascriptVar' # Plugins finder from the Dynamic Finder 'JavascriptVar'
class JavascriptVar < Finders::DynamicFinder::WpItems::Finder class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 60 DEFAULT_CONFIDENCE = 60
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -21,8 +21,6 @@ module WPScan
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug| enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80)) found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
end end
found found

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'QueryParameter' # Plugins finder from Dynamic Finder 'QueryParameter'
class QueryParameter < Finders::DynamicFinder::WpItems::Finder class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 10 DEFAULT_CONFIDENCE = 10
def passive(_opts = {}) def passive(_opts = {})

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'Xpath' # Plugins finder from the Dynamic Finder 'Xpath'
class Xpath < Finders::DynamicFinder::WpItems::Finder class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40 DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -16,15 +16,25 @@ module WPScan
ThemeVersion::Style.new(theme) << ThemeVersion::Style.new(theme) <<
ThemeVersion::WooFrameworkMetaGenerator.new(theme) ThemeVersion::WooFrameworkMetaGenerator.new(theme)
create_and_load_dynamic_versions_finders(theme) load_specific_finders(theme)
end end
# Create the dynamic version finders related to the theme and register them # Load the finders associated with the theme
# #
# @param [ Model::Theme ] theme # @param [ Model::Theme ] theme
def create_and_load_dynamic_versions_finders(theme) def load_specific_finders(theme)
DB::DynamicFinders::Theme.create_versions_finders(theme.slug).each do |finder| module_name = theme.classify
finders << finder.new(theme)
return unless Finders::ThemeVersion.constants.include?(module_name)
mod = Finders::ThemeVersion.const_get(module_name)
mod.constants.each do |constant|
c = mod.const_get(constant)
next unless c.is_a?(Class)
finders << c.new(theme)
end end
end end
end end

View File

@@ -21,8 +21,6 @@ module WPScan
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug| enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80)) found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
end end
found found

View File

@@ -22,7 +22,7 @@ module WPScan
found = [] found = []
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res| enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
next unless /no image specified/i.match?(res.body) next unless res.body =~ /no image specified/i
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100)) found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
end end

View File

@@ -24,7 +24,7 @@ module WPScan
return found if error.empty? # Protection plugin / error disabled return found if error.empty? # Protection plugin / error disabled
next unless /The password you entered for the username|Incorrect Password/i.match?(error) next unless error =~ /The password you entered for the username|Incorrect Password/i
found << Model::User.new(username, found_by: found_by, confidence: 100) found << Model::User.new(username, found_by: found_by, confidence: 100)
end end

View File

@@ -6,7 +6,7 @@ module WPScan
# Users disclosed from the dc:creator field in the RSS # Users disclosed from the dc:creator field in the RSS
# The names disclosed are display names, however depending on the configuration of the blog, # The names disclosed are display names, however depending on the configuration of the blog,
# they can be the same than usernames # they can be the same than usernames
class RSSGenerator < Finders::WpVersion::RSSGenerator class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
def process_urls(urls, _opts = {}) def process_urls(urls, _opts = {})
found = [] found = []

View File

@@ -28,7 +28,7 @@ module WPScan
# @param [ WPScan::Target ] target # @param [ WPScan::Target ] target
def initialize(target) def initialize(target)
(%w[RSSGenerator AtomGenerator RDFGenerator] + (%w[RSSGenerator AtomGenerator RDFGenerator] +
DB::DynamicFinders::Wordpress.versions_finders_configs.keys + WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
%w[Readme UniqueFingerprinting] %w[Readme UniqueFingerprinting]
).each do |finder_name| ).each do |finder_name|
finders << WpVersion.const_get(finder_name.to_sym).new(target) finders << WpVersion.const_get(finder_name.to_sym).new(target)

View File

@@ -10,7 +10,7 @@ module WPScan
# #
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent) # Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
# #
class BackupDB < InterestingFinding class PluginBackupFolder < InterestingFinding
end end
class DebugLog < InterestingFinding class DebugLog < InterestingFinding

View File

@@ -15,16 +15,9 @@ module WPScan
@uri = Addressable::URI.parse(blog.url(path_from_blog)) @uri = Addressable::URI.parse(blog.url(path_from_blog))
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given), # @return [ JSON ]
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.plugin_data(slug) @db_data ||= DB::Plugin.db_data(slug)
end end
# @param [ Hash ] opts # @param [ Hash ] opts
@@ -35,11 +28,6 @@ module WPScan
@version @version
end end
# @return [ Array<String> ]
def potential_readme_filenames
@potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
end
end end
end end
end end

View File

@@ -21,16 +21,9 @@ module WPScan
parse_style parse_style
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ JSON ] # @return [ JSON ]
def metadata
@metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.theme_data(slug) @db_data ||= DB::Theme.db_data(slug)
end end
# @param [ Hash ] opts # @param [ Hash ] opts

View File

@@ -9,7 +9,6 @@ module WPScan
include CMSScanner::Target::Platform::PHP include CMSScanner::Target::Platform::PHP
include CMSScanner::Target::Server::Generic include CMSScanner::Target::Server::Generic
# Most common readme filenames, based on checking all public plugins and themes.
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
@@ -60,18 +59,18 @@ module WPScan
# @return [ String ] # @return [ String ]
def latest_version def latest_version
@latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil @latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
end end
# Not used anywhere ATM # Not used anywhere ATM
# @return [ Boolean ] # @return [ Boolean ]
def popular? def popular?
@popular ||= metadata['popular'] ? true : false @popular ||= db_data['popular']
end end
# @return [ String ] # @return [ String ]
def last_updated def last_updated
@last_updated ||= metadata['last_updated'] @last_updated ||= db_data['last_updated']
end end
# @return [ Boolean ] # @return [ Boolean ]
@@ -118,7 +117,7 @@ module WPScan
return @readme_url unless @readme_url.nil? return @readme_url unless @readme_url.nil?
potential_readme_filenames.each do |path| READMES.each do |path|
t_url = url(path) t_url = url(path)
return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200 return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
@@ -127,10 +126,6 @@ module WPScan
@readme_url = false @readme_url = false
end end
def potential_readme_filenames
@potential_readme_filenames ||= READMES
end
# @param [ String ] path # @param [ String ] path
# @param [ Hash ] params The request params # @param [ Hash ] params The request params
# #

View File

@@ -35,16 +35,9 @@ module WPScan
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) } @all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given), # @return [ JSON ]
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.wordpress_data(number) @db_data ||= DB::Version.db_data(number)
end end
# @return [ Array<Vulnerability> ] # @return [ Array<Vulnerability> ]
@@ -62,12 +55,12 @@ module WPScan
# @return [ String ] # @return [ String ]
def release_date def release_date
@release_date ||= metadata['release_date'] || 'Unknown' @release_date ||= db_data['release_date'] || 'Unknown'
end end
# @return [ String ] # @return [ String ]
def status def status
@status ||= metadata['status'] || 'Unknown' @status ||= db_data['status'] || 'Unknown'
end end
end end
end end

View File

@@ -8,7 +8,7 @@ _______________________________________________________________
WordPress Security Scanner by the WPScan Team WordPress Security Scanner by the WPScan Team
Version <%= WPScan::VERSION %> Version <%= WPScan::VERSION %>
<%= ' ' * ((63 - WPScan::DB::Sponsor.text.length)/2) + WPScan::DB::Sponsor.text %> Sponsored by Sucuri - https://sucuri.net
@_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_ @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
_______________________________________________________________ _______________________________________________________________

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Config Backup(s) Identified: <%= notice_icon %> Config Backup(s) Identified:
<% @config_backups.each do |config_backup| -%> <% @config_backups.each do |config_backup| -%>
<%= critical_icon %> <%= config_backup %> <%= info_icon %> <%= config_backup %>
<%= render('@finding', item: config_backup) -%> <%= render('@finding', item: config_backup) -%>
<% end -%> <% end -%>
<% end %> <% end %>

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Db Export(s) Identified: <%= notice_icon %> Db Export(s) Identified:
<% @db_exports.each do |db_export| -%> <% @db_exports.each do |db_export| -%>
<%= critical_icon %> <%= db_export %> <%= info_icon %> <%= db_export %>
<%= render('@finding', item: db_export) -%> <%= render('@finding', item: db_export) -%>
<% end -%> <% end -%>
<% end %> <% end %>

View File

@@ -1,13 +0,0 @@
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
<%= critical_icon %> WPVulnDB API, <%= @status['http_error'].to_s %>
<% else -%>
<%= info_icon %> WPVulnDB API OK
| Plan: <%= @status['plan'] %>
| Requests Done (during the scan): <%= @api_requests %>
| Requests Remaining: <%= @status['requests_remaining'] %>
<% end -%>
<% else -%>
<%= warning_icon %> No WPVulnDB API Token given, as a result vulnerability data has not been output.
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register.
<% end -%>

View File

@@ -7,5 +7,5 @@
"@erwan_lr", "@erwan_lr",
"@_FireFart_" "@_FireFart_"
], ],
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %> "sponsored_by": "Sucuri - https://sucuri.net"
}, },

View File

@@ -11,10 +11,9 @@
}<% unless index == last_index %>,<% end -%> }<% unless index == last_index %>,<% end -%>
<% end -%> <% end -%>
<% end -%> <% end -%>
} },
<% if @item.respond_to?(:vulnerabilities) -%> "vulnerabilities": [
,"vulnerabilities": [ <% if @item.respond_to?(:vulnerabilities) && !(vulns = @item.vulnerabilities).empty? -%>
<% unless (vulns = @item.vulnerabilities).empty? -%>
<% last_index = vulns.size - 1 -%> <% last_index = vulns.size - 1 -%>
<% vulns.each_with_index do |v, index| -%> <% vulns.each_with_index do |v, index| -%>
{ {
@@ -24,5 +23,4 @@
}<% unless index == last_index -%>,<% end -%> }<% unless index == last_index -%>,<% end -%>
<% end -%> <% end -%>
<% end -%> <% end -%>
] ]
<% end -%>

View File

@@ -1,13 +0,0 @@
"vuln_api": {
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
"http_error": <%= @status['http_error'].to_s.to_json %>
<% else -%>
"plan": <%= @status['plan'].to_json %>,
"requests_done_during_scan": <%= @api_requests.to_json %>,
"requests_remaining": <%= @status['requests_remaining'].to_json %>
<% end -%>
<% else -%>
"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register."
<% end -%>
},

View File

@@ -5,7 +5,6 @@ require 'wpscan'
WPScan::Scan.new do |s| WPScan::Scan.new do |s|
s.controllers << s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new << WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new << WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new << WPScan::Controller::WpVersion.new <<

View File

@@ -7,7 +7,6 @@ require 'wpscan'
report = MemoryProfiler.report(top: 15) do report = MemoryProfiler.report(top: 15) do
WPScan::Scan.new do |s| WPScan::Scan.new do |s|
s.controllers << s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new << WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new << WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new << WPScan::Controller::WpVersion.new <<

View File

@@ -12,7 +12,6 @@ StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do
# require_relative 'wpscan' doesn't work # require_relative 'wpscan' doesn't work
WPScan::Scan.new do |s| WPScan::Scan.new do |s|
s.controllers << s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new << WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new << WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new << WPScan::Controller::WpVersion.new <<

View File

@@ -13,8 +13,7 @@ require 'uri'
require 'time' require 'time'
require 'readline' require 'readline'
require 'securerandom' require 'securerandom'
# Monkey Patches/Fixes/Override
require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method
# Custom Libs # Custom Libs
require 'wpscan/helper' require 'wpscan/helper'
require 'wpscan/db' require 'wpscan/db'
@@ -39,28 +38,12 @@ module WPScan
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db') DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db')
Typhoeus.on_complete do |response|
next if response.cached? || !response.from_vuln_api?
self.api_requests += 1
end
# Override, otherwise it would be returned as 'wp_scan' # Override, otherwise it would be returned as 'wp_scan'
# #
# @return [ String ] # @return [ String ]
def self.app_name def self.app_name
'wpscan' 'wpscan'
end end
# @return [ Integer ]
def self.api_requests
@@api_requests ||= 0
end
# @param [ Integer ] value
def self.api_requests=(value)
@@api_requests = value
end
end end
require "#{WPScan::APP_DIR}/app" require "#{WPScan::APP_DIR}/app"

View File

@@ -7,7 +7,7 @@ module WPScan
# @return [ String ] # @return [ String ]
def default_user_agent def default_user_agent
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)" "WPScan v#{VERSION} (https://wpscan.org/)"
end end
end end
end end

View File

@@ -7,12 +7,9 @@ require_relative 'db/plugins'
require_relative 'db/themes' require_relative 'db/themes'
require_relative 'db/plugin' require_relative 'db/plugin'
require_relative 'db/theme' require_relative 'db/theme'
require_relative 'db/sponsor'
require_relative 'db/wp_version' require_relative 'db/wp_version'
require_relative 'db/fingerprints' require_relative 'db/fingerprints'
require_relative 'db/vuln_api'
require_relative 'db/dynamic_finders/base' require_relative 'db/dynamic_finders/base'
require_relative 'db/dynamic_finders/plugin' require_relative 'db/dynamic_finders/plugin'
require_relative 'db/dynamic_finders/theme' require_relative 'db/dynamic_finders/theme'

View File

@@ -5,19 +5,18 @@ module WPScan
module DynamicFinders module DynamicFinders
class Base class Base
# @return [ String ] # @return [ String ]
def self.df_file def self.db_file
@df_file ||= DB_DIR.join('dynamic_finders.yml').to_s @db_file ||= DB_DIR.join('dynamic_finders.yml').to_s
end end
# @return [ Hash ] # @return [ Hash ]
def self.all_df_data def self.db_data
@all_df_data ||= YAML.safe_load(File.read(df_file), [Regexp]) # true allows aliases to be loaded
@db_data ||= YAML.safe_load(File.read(db_file), [Regexp], [], true)
end end
# @return [ Array<Symbol> ] # @return [ Array<Symbol> ]
def self.allowed_classes def self.allowed_classes
# The Readme is not put in there as it's not a Real DF, but rather using the DF system
# to get the list of potential filenames for a given slug
@allowed_classes ||= %i[Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter ConfigParser] @allowed_classes ||= %i[Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter ConfigParser]
end end

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders module DynamicFinders
class Plugin < Base class Plugin < Base
# @return [ Hash ] # @return [ Hash ]
def self.df_data def self.db_data
@df_data ||= all_df_data['plugins'] || {} @db_data ||= super['plugins'] || {}
end end
def self.version_finder_module def self.version_finder_module
@@ -21,7 +21,7 @@ module WPScan
return configs unless allowed_classes.include?(finder_class) return configs unless allowed_classes.include?(finder_class)
df_data.each do |slug, finders| db_data.each do |slug, finders|
# Quite sure better can be done with some kind of logic statement in the select # Quite sure better can be done with some kind of logic statement in the select
fs = if aggressive fs = if aggressive
finders.reject { |_f, c| c['path'].nil? } finders.reject { |_f, c| c['path'].nil? }
@@ -48,7 +48,7 @@ module WPScan
@versions_finders_configs = {} @versions_finders_configs = {}
df_data.each do |slug, finders| db_data.each do |slug, finders|
finders.each do |finder_name, config| finders.each do |finder_name, config|
next unless config.key?('version') next unless config.key?('version')
@@ -73,33 +73,23 @@ module WPScan
version_finder_module.const_get(constant_name) version_finder_module.const_get(constant_name)
end end
# Create the dynamic finders related to the given slug, and return the created classes def self.create_versions_finders
# versions_finders_configs.each do |slug, finders|
# @param [ String ] slug mod = maybe_create_module(slug)
#
# @return [ Array<Class> ] The created classes
def self.create_versions_finders(slug)
created = []
mod = maybe_create_module(slug)
versions_finders_configs[slug]&.each do |finder_class, config| finders.each do |finder_class, config|
klass = config['class'] || finder_class klass = config['class'] || finder_class
# Instead of raising exceptions, skip unallowed/already defined finders # Instead of raising exceptions, skip unallowed/already defined finders
# So that, when new DF configs are put in the .yml # So that, when new DF configs are put in the .yml
# users with old version of WPScan will still be able to scan blogs # users with old version of WPScan will still be able to scan blogs
# when updating the DB but not the tool # when updating the DB but not the tool
next if mod.constants.include?(finder_class.to_sym) ||
!allowed_classes.include?(klass.to_sym)
next unless allowed_classes.include?(klass.to_sym) version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
end
created << if mod.constants.include?(finder_class.to_sym)
mod.const_get(finder_class.to_sym)
else
version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
end
end end
created
end end
# The idea here would be to check if the class exist in # The idea here would be to check if the class exist in

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders module DynamicFinders
class Theme < Plugin class Theme < Plugin
# @return [ Hash ] # @return [ Hash ]
def self.df_data def self.db_data
@df_data ||= all_df_data['themes'] || {} @db_data ||= super['themes'] || {}
end end
def self.version_finder_module def self.version_finder_module

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders module DynamicFinders
class Wordpress < Base class Wordpress < Base
# @return [ Hash ] # @return [ Hash ]
def self.df_data def self.db_data
@df_data ||= all_df_data['wordpress'] || {} @db_data ||= super['wordpress'] || {}
end end
# @return [ Constant ] # @return [ Constant ]
@@ -30,9 +30,9 @@ module WPScan
return configs unless allowed_classes.include?(finder_class) return configs unless allowed_classes.include?(finder_class)
finders = if aggressive finders = if aggressive
df_data.reject { |_f, c| c['path'].nil? } db_data.reject { |_f, c| c['path'].nil? }
else else
df_data.select { |_f, c| c['path'].nil? } db_data.select { |_f, c| c['path'].nil? }
end end
finders.each do |finder_name, config| finders.each do |finder_name, config|
@@ -48,7 +48,7 @@ module WPScan
# @return [ Hash ] # @return [ Hash ]
def self.versions_finders_configs def self.versions_finders_configs
@versions_finders_configs ||= df_data.select { |_finder_name, config| config.key?('version') } @versions_finders_configs ||= db_data.select { |_finder_name, config| config.key?('version') }
end end
def self.create_versions_finders def self.create_versions_finders

View File

@@ -4,9 +4,9 @@ module WPScan
module DB module DB
# Plugin DB # Plugin DB
class Plugin < WpItem class Plugin < WpItem
# @return [ Hash ] # @return [ String ]
def self.metadata def self.db_file
@metadata ||= super['plugins'] || {} @db_file ||= DB_DIR.join('plugins.json').to_s
end end
end end
end end

View File

@@ -5,8 +5,8 @@ module WPScan
# WP Plugins # WP Plugins
class Plugins < WpItems class Plugins < WpItems
# @return [ JSON ] # @return [ JSON ]
def self.metadata def self.db
Plugin.metadata Plugin.db
end end
end end
end end

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module WPScan
module DB
class Sponsor
# @return [ Hash ]
def self.text
@text ||= file_path.exist? ? File.read(file_path).chomp : ''
end
def self.file_path
@file_path ||= DB_DIR.join('sponsor.txt')
end
end
end
end

View File

@@ -4,9 +4,9 @@ module WPScan
module DB module DB
# Theme DB # Theme DB
class Theme < WpItem class Theme < WpItem
# @return [ Hash ] # @return [ String ]
def self.metadata def self.db_file
@metadata ||= super['themes'] || {} @db_file ||= DB_DIR.join('themes.json').to_s
end end
end end
end end

View File

@@ -5,8 +5,8 @@ module WPScan
# WP Themes # WP Themes
class Themes < WpItems class Themes < WpItems
# @return [ JSON ] # @return [ JSON ]
def self.metadata def self.db
Theme.metadata Theme.db
end end
end end
end end

View File

@@ -7,15 +7,12 @@ module WPScan
class Updater class Updater
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here # /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
FILES = %w[ FILES = %w[
metadata.json wp_fingerprints.json plugins.json themes.json wordpresses.json
timthumbs-v3.txt config_backups.txt db_exports.txt timthumbs-v3.txt config_backups.txt db_exports.txt
dynamic_finders.yml LICENSE sponsor.txt dynamic_finders.yml wp_fingerprints.json LICENSE
].freeze ].freeze
OLD_FILES = %w[ OLD_FILES = %w[wordpress.db user-agents.txt dynamic_finders_01.yml].freeze
wordpress.db user-agents.txt dynamic_finders_01.yml
wordpresses.json plugins.json themes.json
].freeze
attr_reader :repo_directory attr_reader :repo_directory
@@ -67,12 +64,11 @@ module WPScan
# @return [ Hash ] The params for Typhoeus::Request # @return [ Hash ] The params for Typhoeus::Request
# @note Those params can't be overriden by CLI options # @note Those params can't be overriden by CLI options
def request_params def request_params
@request_params ||= { {
timeout: 600, timeout: 600,
connecttimeout: 300, connecttimeout: 300,
accept_encoding: 'gzip, deflate', accept_encoding: 'gzip, deflate',
cache_ttl: 0, cache_ttl: 0
headers: { 'User-Agent' => Browser.instance.default_user_agent, 'Referer' => nil }
} }
end end

View File

@@ -1,79 +0,0 @@
# frozen_string_literal: true
module WPScan
module DB
# WPVulnDB API
class VulnApi
NON_ERROR_CODES = [200, 401].freeze
class << self
attr_accessor :token
end
# @return [ Addressable::URI ]
def self.uri
@uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/')
end
# @param [ String ] path
# @param [ Hash ] params
#
# @return [ Hash ]
def self.get(path, params = {})
return {} unless token
res = Browser.get(uri.join(path), params.merge(request_params))
return {} if res.code == 404 # This is for API inconsistencies when dots in path
return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code)
raise Error::HTTP, res
rescue Error::HTTP => e
retries ||= 0
if (retries += 1) <= 3
sleep(1)
retry
end
{ 'http_error' => e }
end
# @return [ Hash ]
def self.plugin_data(slug)
get("plugins/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.theme_data(slug)
get("themes/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.wordpress_data(version_number)
get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {}
end
# @return [ Hash ]
def self.status
json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0)
json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1
json
end
# @return [ Hash ]
def self.request_params
{
headers: {
'Host' => uri.host, # Reset in case user provided a --vhost for the target
'Referer' => nil, # Removes referer set by the cmsscanner to the target url
'User-Agent' => Browser.instance.default_user_agent,
'Authorization' => "Token token=#{token}"
}
}
end
end
end
end

View File

@@ -6,19 +6,14 @@ module WPScan
class WpItem class WpItem
# @param [ String ] identifier The plugin/theme slug or version number # @param [ String ] identifier The plugin/theme slug or version number
# #
# @return [ Hash ] The JSON data from the metadata associated to the identifier # @return [ Hash ] The JSON data from the DB associated to the identifier
def self.metadata_at(identifier) def self.db_data(identifier)
metadata[identifier] || {} db[identifier] || {}
end end
# @return [ JSON ] # @return [ JSON ]
def self.metadata def self.db
@metadata ||= read_json_file(metadata_file) @db ||= read_json_file(db_file)
end
# @return [ String ]
def self.metadata_file
@metadata_file ||= DB_DIR.join('metadata.json').to_s
end end
end end
end end

View File

@@ -6,17 +6,17 @@ module WPScan
class WpItems class WpItems
# @return [ Array<String> ] The slug of all items # @return [ Array<String> ] The slug of all items
def self.all_slugs def self.all_slugs
metadata.keys db.keys
end end
# @return [ Array<String> ] The slug of all popular items # @return [ Array<String> ] The slug of all popular items
def self.popular_slugs def self.popular_slugs
metadata.select { |_key, item| item['popular'] == true }.keys db.select { |_key, item| item['popular'] == true }.keys
end end
# @return [ Array<String> ] The slug of all vulnerable items # @return [ Array<String> ] The slug of all vulnerable items
def self.vulnerable_slugs def self.vulnerable_slugs
metadata.select { |_key, item| item['vulnerabilities'] == true }.keys db.reject { |_key, item| item['vulnerabilities'].empty? }.keys
end end
end end
end end

View File

@@ -4,9 +4,9 @@ module WPScan
module DB module DB
# WP Version # WP Version
class Version < WpItem class Version < WpItem
# @return [ Hash ] # @return [ String ]
def self.metadata def self.db_file
@metadata ||= super['wordpress'] || {} @db_file ||= DB_DIR.join('wordpresses.json').to_s
end end
end end
end end

View File

@@ -9,9 +9,7 @@ module WPScan
end end
end end
require_relative 'errors/enumeration'
require_relative 'errors/http' require_relative 'errors/http'
require_relative 'errors/update' require_relative 'errors/update'
require_relative 'errors/vuln_api'
require_relative 'errors/wordpress' require_relative 'errors/wordpress'
require_relative 'errors/xmlrpc' require_relative 'errors/xmlrpc'

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module WPScan
module Error
class PluginsThresholdReached < Standard
def to_s
"The number of plugins detected reached the threshold of #{ParsedCli.plugins_threshold} " \
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
'option to ignore the bad responses.'
end
end
class ThemesThresholdReached < Standard
def to_s
"The number of themes detected reached the threshold of #{ParsedCli.themes_threshold} " \
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
'option to ignore the bad responses.'
end
end
end
end

View File

@@ -1,20 +0,0 @@
# frozen_string_literal: true
module WPScan
module Error
# Error raised when the token given via --api-token is invalid
class InvalidApiToken < Standard
def to_s
'The API token provided is invalid'
end
end
# Error raised when the number of API requests has been reached
# currently not implemented on the API side
class ApiLimitReached < Standard
def to_s
'Your API limit has been reached'
end
end
end
end

View File

@@ -4,9 +4,9 @@ module WPScan
module Finders module Finders
module DynamicFinder module DynamicFinder
module Version module Version
# Version finder using Body Pattern method. Typically used when the response is not # Version finder using Body Pattern method. Tipically used when the response is not
# an HTML doc and Xpath can't be used # an HTML doc and Xpath can't be used
class BodyPattern < Finders::DynamicFinder::Version::Finder class BodyPattern < WPScan::Finders::DynamicFinder::Version::Finder
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge(PATTERN: nil, CONFIDENCE: 60) @child_class_constants ||= super().merge(PATTERN: nil, CONFIDENCE: 60)
@@ -16,7 +16,7 @@ module WPScan
# @param [ Hash ] opts # @param [ Hash ] opts
# @return [ Version ] # @return [ Version ]
def find(response, _opts = {}) def find(response, _opts = {})
return unless response.code != 404 && response.body =~ self.class::PATTERN return unless response.body =~ self.class::PATTERN
create_version( create_version(
Regexp.last_match[:v], Regexp.last_match[:v],

View File

@@ -6,7 +6,7 @@ module WPScan
module Version module Version
# Version finder in Comment, which is basically an Xpath one with a default # Version finder in Comment, which is basically an Xpath one with a default
# Xpath of //comment() # Xpath of //comment()
class Comment < Finders::DynamicFinder::Version::Xpath class Comment < WPScan::Finders::DynamicFinder::Version::Xpath
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge(PATTERN: nil, XPATH: '//comment()') @child_class_constants ||= super().merge(PATTERN: nil, XPATH: '//comment()')

View File

@@ -6,7 +6,7 @@ module WPScan
module Version module Version
# Version finder using by parsing config files, such as composer.json # Version finder using by parsing config files, such as composer.json
# and so on # and so on
class ConfigParser < Finders::DynamicFinder::Version::Finder class ConfigParser < WPScan::Finders::DynamicFinder::Version::Finder
ALLOWED_PARSERS = [JSON, YAML].freeze ALLOWED_PARSERS = [JSON, YAML].freeze
def self.child_class_constants def self.child_class_constants

View File

@@ -5,7 +5,7 @@ module WPScan
module DynamicFinder module DynamicFinder
module Version module Version
# Version finder using Header Pattern method # Version finder using Header Pattern method
class HeaderPattern < Finders::DynamicFinder::Version::Finder class HeaderPattern < WPScan::Finders::DynamicFinder::Version::Finder
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge(HEADER: nil, PATTERN: nil, CONFIDENCE: 60) @child_class_constants ||= super().merge(HEADER: nil, PATTERN: nil, CONFIDENCE: 60)

View File

@@ -5,7 +5,7 @@ module WPScan
module DynamicFinder module DynamicFinder
module Version module Version
# Version finder using JavaScript Variable method # Version finder using JavaScript Variable method
class JavascriptVar < Finders::DynamicFinder::Version::Finder class JavascriptVar < WPScan::Finders::DynamicFinder::Version::Finder
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge( @child_class_constants ||= super().merge(

View File

@@ -5,7 +5,7 @@ module WPScan
module DynamicFinder module DynamicFinder
module Version module Version
# Version finder using QueryParameter method # Version finder using QueryParameter method
class QueryParameter < Finders::DynamicFinder::Version::Finder class QueryParameter < WPScan::Finders::DynamicFinder::Version::Finder
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge( @child_class_constants ||= super().merge(

View File

@@ -5,7 +5,7 @@ module WPScan
module DynamicFinder module DynamicFinder
module Version module Version
# Version finder using Xpath method # Version finder using Xpath method
class Xpath < Finders::DynamicFinder::Version::Finder class Xpath < WPScan::Finders::DynamicFinder::Version::Finder
# @return [ Hash ] # @return [ Hash ]
def self.child_class_constants def self.child_class_constants
@child_class_constants ||= super().merge( @child_class_constants ||= super().merge(

View File

@@ -4,22 +4,22 @@ module WPScan
module Finders module Finders
module DynamicFinder module DynamicFinder
module WpItemVersion module WpItemVersion
class BodyPattern < Finders::DynamicFinder::Version::BodyPattern class BodyPattern < WPScan::Finders::DynamicFinder::Version::BodyPattern
end end
class Comment < Finders::DynamicFinder::Version::Comment class Comment < WPScan::Finders::DynamicFinder::Version::Comment
end end
class ConfigParser < Finders::DynamicFinder::Version::ConfigParser class ConfigParser < WPScan::Finders::DynamicFinder::Version::ConfigParser
end end
class HeaderPattern < Finders::DynamicFinder::Version::HeaderPattern class HeaderPattern < WPScan::Finders::DynamicFinder::Version::HeaderPattern
end end
class JavascriptVar < Finders::DynamicFinder::Version::JavascriptVar class JavascriptVar < WPScan::Finders::DynamicFinder::Version::JavascriptVar
end end
class QueryParameter < Finders::DynamicFinder::Version::QueryParameter class QueryParameter < WPScan::Finders::DynamicFinder::Version::QueryParameter
# @return [ Regexp ] # @return [ Regexp ]
def path_pattern def path_pattern
# TODO: consider the target.blog.themes_dir if the target is a Theme (maybe implement a WpItem#item_dir ?) # TODO: consider the target.blog.themes_dir if the target is a Theme (maybe implement a WpItem#item_dir ?)
@@ -37,7 +37,7 @@ module WPScan
end end
end end
class Xpath < Finders::DynamicFinder::Version::Xpath class Xpath < WPScan::Finders::DynamicFinder::Version::Xpath
end end
end end
end end

View File

@@ -12,23 +12,23 @@ module WPScan
end end
end end
class BodyPattern < Finders::DynamicFinder::Version::BodyPattern class BodyPattern < WPScan::Finders::DynamicFinder::Version::BodyPattern
include Finder include Finder
end end
class Comment < Finders::DynamicFinder::Version::Comment class Comment < WPScan::Finders::DynamicFinder::Version::Comment
include Finder include Finder
end end
class HeaderPattern < Finders::DynamicFinder::Version::HeaderPattern class HeaderPattern < WPScan::Finders::DynamicFinder::Version::HeaderPattern
include Finder include Finder
end end
class JavascriptVar < Finders::DynamicFinder::Version::JavascriptVar class JavascriptVar < WPScan::Finders::DynamicFinder::Version::JavascriptVar
include Finder include Finder
end end
class QueryParameter < Finders::DynamicFinder::Version::QueryParameter class QueryParameter < WPScan::Finders::DynamicFinder::Version::QueryParameter
include Finder include Finder
# @return [ Hash ] # @return [ Hash ]

View File

@@ -6,15 +6,13 @@ rescue StandardError => e
raise "JSON parsing error in #{file} #{e}" raise "JSON parsing error in #{file} #{e}"
end end
# Sanitize and classify a slug
# @note As a class can not start with a digit or underscore, a D_ is
# put as a prefix in such case. Ugly but well :x
# Not only used to classify slugs though, but Dynamic Finder names as well
#
# @return [ Symbol ] # @return [ Symbol ]
# @note As a class can not start with a digit or underscore, a D_ is
# put as a prefix in such case. Ugly but well :x
# Not only used to classify slugs though, but Dynamic Finder names as well
def classify_slug(slug) def classify_slug(slug)
classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/\-{1,}/, '_').camelize.to_s classified = slug.to_s.tr('-', '_').camelize.to_s
classified = "D_#{classified}" if /\d/.match?(classified[0]) classified = "D_#{classified}" if classified[0] =~ /\d/
classified.to_sym classified.to_sym
end end

View File

@@ -29,7 +29,7 @@ module WPScan
end end
homepage_res.html.css('meta[name="generator"]').each do |node| homepage_res.html.css('meta[name="generator"]').each do |node|
return true if /wordpress/i.match?(node['content']) return true if node['content'] =~ /wordpress/i
end end
return true unless comments_from_page(/wordpress/i, homepage_res).empty? return true unless comments_from_page(/wordpress/i, homepage_res).empty?
@@ -109,7 +109,6 @@ module WPScan
Browser.instance.forge_request( Browser.instance.forge_request(
login_url, login_url,
method: :post, method: :post,
cache_ttl: 0,
body: { log: username, pwd: password } body: { log: username, pwd: password }
) )
end end

View File

@@ -99,19 +99,20 @@ module WPScan
# @return [ String, False ] String of the sub_dir found, false otherwise # @return [ String, False ] String of the sub_dir found, false otherwise
# @note: nil can not be returned here, otherwise if there is no sub_dir # @note: nil can not be returned here, otherwise if there is no sub_dir
# the check would be done each time, which would make enumeration of # the check would be done each time
# long list of items very slow to generate
def sub_dir def sub_dir
return @sub_dir unless @sub_dir.nil? unless @sub_dir
# url_pattern is from CMSScanner::Target
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
# url_pattern is from CMSScanner::Target in_scope_uris(homepage_res) do |uri|
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end
in_scope_uris(homepage_res) do |uri| @sub_dir = false
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end end
@sub_dir = false @sub_dir
end end
# Override of the WebSite#url to consider the custom WP directories # Override of the WebSite#url to consider the custom WP directories

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
module Typhoeus
# Custom Response class
class Response
# @note: Ignores requests done to the /status endpoint of the API
#
# @return [ Boolean ]
def from_vuln_api?
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) && !effective_url.include?('/status')
end
end
end

View File

@@ -2,5 +2,5 @@
# Version # Version
module WPScan module WPScan
VERSION = '3.7.1' VERSION = '3.5.3'
end end

View File

@@ -70,8 +70,8 @@ describe WPScan::Controller::Enumeration do
it 'contains the correct options' do it 'contains the correct options' do
expect(controller.cli_options.map(&:to_sym)).to eql( expect(controller.cli_options.map(&:to_sym)).to eql(
%i[enumerate exclude_content_based %i[enumerate exclude_content_based
plugins_list plugins_detection plugins_version_all plugins_version_detection plugins_threshold plugins_list plugins_detection plugins_version_all plugins_version_detection
themes_list themes_detection themes_version_all themes_version_detection themes_threshold themes_list themes_detection themes_version_all themes_version_detection
timthumbs_list timthumbs_detection timthumbs_list timthumbs_detection
config_backups_list config_backups_detection config_backups_list config_backups_detection
db_exports_list db_exports_detection db_exports_list db_exports_detection
@@ -102,6 +102,15 @@ describe WPScan::Controller::Enumeration do
end end
end end
describe '#before_scan' do
it 'creates the Dynamic Finders' do
expect(WPScan::DB::DynamicFinders::Plugin).to receive(:create_versions_finders)
expect(WPScan::DB::DynamicFinders::Theme).to receive(:create_versions_finders)
controller.before_scan
end
end
describe '#run' do describe '#run' do
context 'when no :enumerate' do context 'when no :enumerate' do
before do before do

View File

@@ -52,60 +52,6 @@ describe WPScan::Controller::PasswordAttack do
end end
end end
describe '#xmlrpc_get_users_blogs_enabled?' do
before { expect(controller.target).to receive(:xmlrpc).and_return(xmlrpc) }
context 'when xmlrpc not found' do
let(:xmlrpc) { nil }
its(:xmlrpc_get_users_blogs_enabled?) { should be false }
end
context 'when xmlrpc not enabled' do
let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php") }
it 'returns false' do
expect(xmlrpc).to receive(:enabled?).and_return(false)
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
end
end
context 'when xmlrpc enabled' do
let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php") }
before { expect(xmlrpc).to receive(:enabled?).and_return(true) }
context 'when wp.getUsersBlogs methods not listed' do
it 'returns false' do
expect(xmlrpc).to receive(:available_methods).and_return(%w[m1 m2])
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
end
end
context 'when wp.getUsersBlogs method listed' do
before { expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2]) }
context 'when wp.getUsersBlogs method disabled' do
it 'returns false' do
stub_request(:post, xmlrpc.url).to_return(body: 'XML-RPC services are disabled on this site.')
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
end
end
context 'when wp.getUsersBlogs method enabled' do
it 'returns true' do
stub_request(:post, xmlrpc.url).to_return(body: 'Incorrect username or password.')
expect(controller.xmlrpc_get_users_blogs_enabled?).to be true
end
end
end
end
end
describe '#attacker' do describe '#attacker' do
context 'when --password-attack provided' do context 'when --password-attack provided' do
let(:cli_args) { "#{super()} --password-attack #{attack}" } let(:cli_args) { "#{super()} --password-attack #{attack}" }
@@ -146,7 +92,7 @@ describe WPScan::Controller::PasswordAttack do
before do before do
expect(controller.target) expect(controller.target)
.to receive(:xmlrpc) .to receive(:xmlrpc)
.and_return(WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php")) .and_return(WPScan::Model::XMLRPC.new("#{target_url}/xmlrpc.php"))
end end
context 'when single xmlrpc' do context 'when single xmlrpc' do
@@ -171,50 +117,73 @@ describe WPScan::Controller::PasswordAttack do
end end
context 'when automatic detection' do context 'when automatic detection' do
context 'when xmlrpc_get_users_blogs_enabled? is false' do before { expect(controller.target).to receive(:xmlrpc).and_return(xmlrpc) }
context 'when xmlrpc not found' do
let(:xmlrpc) { nil }
it 'returns the WpLogin' do it 'returns the WpLogin' do
expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(false) expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
expect(controller.attacker.target).to be_a WPScan::Target
end
end
context 'when xmlrpc not enabled' do
let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}/xmlrpc.php") }
it 'returns the WpLogin' do
expect(xmlrpc).to receive(:enabled?).and_return(false)
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
expect(controller.attacker.target).to be_a WPScan::Target expect(controller.attacker.target).to be_a WPScan::Target
end end
end end
context 'when xmlrpc_get_users_blogs_enabled? is true' do context 'when xmlrpc enabled' do
before do let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}/xmlrpc.php") }
expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(true)
expect(controller.target) before { expect(xmlrpc).to receive(:enabled?).and_return(true) }
.to receive(:xmlrpc).and_return(WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php"))
end
context 'when WP version not found' do context 'when wp.getUsersBlogs methods not available' do
it 'returns the XMLRPC' do it 'returns the WpLogin' do
expect(controller.target).to receive(:wp_version).and_return(false) expect(xmlrpc).to receive(:available_methods).and_return(%w[m1 m2])
expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC expect(controller.attacker.target).to be_a WPScan::Target
end end
end end
context 'when WP version found' do context 'when wp.getUsersBlogs method evailable' do
before { expect(controller.target).to receive(:wp_version).and_return(wp_version) } before { expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2]) }
context 'when WP < 4.4' do context 'when WP version not found' do
let(:wp_version) { WPScan::Model::WpVersion.new('3.8.1') } it 'returns the XMLRPC' do
expect(controller.target).to receive(:wp_version).and_return(false)
it 'returns the XMLRPCMulticall' do expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC
expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPCMulticall
expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
end end
end end
context 'when WP >= 4.4' do context 'when WP version found' do
let(:wp_version) { WPScan::Model::WpVersion.new('4.4') } before { expect(controller.target).to receive(:wp_version).and_return(wp_version) }
it 'returns the XMLRPC' do context 'when WP < 4.4' do
expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC let(:wp_version) { WPScan::Model::WpVersion.new('3.8.1') }
expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
it 'returns the XMLRPCMulticall' do
expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPCMulticall
expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
end
end
context 'when WP >= 4.4' do
let(:wp_version) { WPScan::Model::WpVersion.new('4.4') }
it 'returns the XMLRPC' do
expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC
expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
end
end end
end end
end end

View File

@@ -1,93 +0,0 @@
# frozen_string_literal: true
describe WPScan::Controller::VulnApi do
subject(:controller) { described_class.new }
let(:target_url) { 'http://ex.lo/' }
let(:cli_args) { "--url #{target_url}" }
before do
WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
end
describe '#cli_options' do
its(:cli_options) { should_not be_empty }
its(:cli_options) { should be_a Array }
it 'contains to correct options' do
expect(controller.cli_options.map(&:to_sym)).to eq %i[api_token]
end
end
describe '#before_scan' do
context 'when no --api-token provided' do
its(:before_scan) { should be nil }
end
context 'when --api-token given' do
let(:cli_args) { "#{super()} --api-token token" }
context 'when the token is invalid' do
before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('error' => 'HTTP Token: Access denied.') }
it 'raise an InvalidApiToken error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::InvalidApiToken)
end
end
context 'when the token is valid' do
context 'when the limit has been reached' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 0)
end
it 'raises an ApiLimitReached error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::ApiLimitReached)
end
end
context 'when a HTTP error, like a timeout' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return(
'http_error' => WPScan::Error::HTTP.new(
Typhoeus::Response.new(effective_url: 'mock-url', return_code: 28)
)
)
end
it 'raises an HTTP error' do
expect { controller.before_scan }
.to raise_error(WPScan::Error::HTTP, 'HTTP Error: mock-url (Timeout was reached)')
end
end
context 'when the token is valid and no HTTP error' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => requests)
end
context 'when limited requests' do
let(:requests) { 100 }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
context 'when unlimited requests' do
let(:requests) { 'Unlimited' }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
end
end
end
end
end
end
end

View File

@@ -1,73 +0,0 @@
# frozen_string_literal: true
describe WPScan::Finders::InterestingFindings::BackupDB do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) }
let(:url) { 'http://ex.lo/' }
let(:fixtures) { FINDERS_FIXTURES.join('interesting_findings', 'backup_db') }
let(:wp_content) { 'wp-content' }
let(:dir_url) { target.url("#{wp_content}/backup-db/") }
before do
expect(target).to receive(:content_dir).at_least(1).and_return(wp_content)
expect(target).to receive(:head_or_get_params).and_return(method: :head)
end
describe '#aggressive' do
context 'when not a 200 or 403' do
it 'returns nil' do
stub_request(:head, dir_url).to_return(status: 404)
expect(finder.aggressive).to eql nil
end
end
context 'when 200 and matching the homepage' do
it 'returns nil' do
stub_request(:head, dir_url)
stub_request(:get, dir_url)
expect(target).to receive(:homepage_or_404?).and_return(true)
expect(finder.aggressive).to eql nil
end
end
context 'when 200 or 403' do
before do
stub_request(:head, dir_url)
stub_request(:get, dir_url).and_return(body: body)
expect(target).to receive(:homepage_or_404?).and_return(false)
end
after do
found = finder.aggressive
expect(found).to eql WPScan::Model::BackupDB.new(
dir_url,
confidence: 70,
found_by: described_class::DIRECT_ACCESS
)
expect(found.interesting_entries).to eq @expected_entries
end
context 'when no directory listing' do
let(:body) { '' }
it 'returns an empty interesting_findings attribute' do
@expected_entries = []
end
end
context 'when directory listing enabled' do
let(:body) { File.read(fixtures.join('dir_listing.html')) }
it 'returns the expected interesting_findings attribute' do
@expected_entries = %w[sqldump.sql test.txt]
end
end
end
end
end

View File

@@ -0,0 +1,76 @@
# frozen_string_literal: true
describe WPScan::Finders::InterestingFindings::PluginBackupFolders do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) }
let(:url) { 'http://ex.lo/' }
let(:fixtures) { FINDERS_FIXTURES.join('interesting_findings', 'plugin_backup_folders') }
before do
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content')
finder.class::PATHS.each { |path| stub_request(:head, target.url(path)).to_return(status: 404) }
allow(target).to receive(:head_or_get_params).and_return(method: :head)
end
describe '#aggressive' do
context 'when none of them exist' do
it 'returns an empty array' do
expect(finder.aggressive).to eql([])
end
end
context 'when one exist but matches the homepage' do
let(:existing_url) { target.url(finder.class::PATHS.sample) }
it 'ignores it' do
stub_request(:head, existing_url)
stub_request(:get, existing_url)
expect(target).to receive(:homepage_or_404?).and_return(true)
expect(finder.aggressive).to eql([])
end
end
context 'when 200 or 403' do
let(:existing_url) { target.url(finder.class::PATHS.sample) }
before do
stub_request(:head, existing_url)
stub_request(:get, existing_url).and_return(body: body)
expect(target).to receive(:homepage_or_404?).and_return(false)
end
after do
found = finder.aggressive
expect(found.size).to eql 1
expect(found).to eql([WPScan::Model::PluginBackupFolder.new(existing_url,
confidence: 70,
found_by: described_class::DIRECT_ACCESS)])
expect(found.first.interesting_entries).to eq @expected_entries
end
context 'when no directory listing' do
let(:body) { '' }
it 'returns an empty interesting_findings attribute' do
@expected_entries = []
end
end
context 'when directory listing enabled' do
let(:body) { File.read(fixtures.join('dir_listing.html')) }
it 'returns the expected interesting_findings attribute' do
@expected_entries = %w[sqldump.sql test.txt]
end
end
end
end
end

View File

@@ -1,61 +0,0 @@
# frozen_string_literal: true
describe WPScan::Finders::Passwords::WpLogin do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'http://ex.lo/' }
describe '#valid_credentials?' do
context 'when a non 302' do
it 'returns false' do
expect(finder.valid_credentials?(Typhoeus::Response.new(code: 200, headers: {}))).to be_falsey
end
end
context 'when a 302' do
let(:response) { Typhoeus::Response.new(code: 302, headers: headers) }
context 'when no cookies set' do
let(:headers) { {} }
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
context 'when no logged_in cookie set' do
context 'when only one cookie set' do
let(:headers) { 'Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/' }
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
context 'when multiple cookies set' do
let(:headers) do
"Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/\r\n" \
'Set-Cookie: something=value; path=/'
end
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
end
context 'when logged_in cookie set' do
let(:headers) do
"Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/\r\r" \
"Set-Cookie: wordpress_xxx=yyy; path=/wp-content/plugins; httponly\r\n" \
"Set-Cookie: wordpress_xxx=yyy; path=/wp-admin; httponly\r\n" \
'Set-Cookie: wordpress_logged_in_xxx=yyy; path=/; httponly'
end
it 'returns false' do
expect(finder.valid_credentials?(response)).to eql true
end
end
end
end
end

View File

@@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
# If this file is tested alone (rspec path-to-this-file), then there will be an error about
# constants not being intilialized. This is due to the Dynamic Finders.
describe WPScan::Finders::PluginVersion::Base do describe WPScan::Finders::PluginVersion::Base do
subject(:plugin_version) { described_class.new(plugin) } subject(:plugin_version) { described_class.new(plugin) }
let(:plugin) { WPScan::Model::Plugin.new(slug, target) } let(:plugin) { WPScan::Model::Plugin.new(slug, target) }
@@ -12,7 +15,7 @@ describe WPScan::Finders::PluginVersion::Base do
expect(plugin_version.finders.map { |f| f.class.to_s.demodulize }).to match_array @expected expect(plugin_version.finders.map { |f| f.class.to_s.demodulize }).to match_array @expected
end end
context 'when no related dynamic finders' do context 'when no related specific finders' do
let(:slug) { 'spec' } let(:slug) { 'spec' }
it 'contains the default finders' do it 'contains the default finders' do
@@ -22,13 +25,19 @@ describe WPScan::Finders::PluginVersion::Base do
# Dynamic Version Finders are not tested here, they are in # Dynamic Version Finders are not tested here, they are in
# spec/lib/finders/dynamic_finder/plugin_versions_spec # spec/lib/finders/dynamic_finder/plugin_versions_spec
context 'when dynamic finders' do context 'when specific finders' do
let(:specific) do
{
# None so far
}
end
WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |plugin_slug, configs| WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |plugin_slug, configs|
context "when #{plugin_slug} plugin" do context "when #{plugin_slug} plugin" do
let(:slug) { plugin_slug } let(:slug) { plugin_slug }
it 'contains the expected finders (default + the dynamic ones)' do it 'contains the expected finders (default + specific + the dynamic ones)' do
@expected = default_finders + configs.keys @expected = default_finders + [*specific[plugin_slug]] + configs.keys
end end
end end
end end

View File

@@ -13,21 +13,20 @@ describe WPScan::Finders::ThemeVersion::Base do
expect(theme_version.finders.map { |f| f.class.to_s.demodulize }).to eql @expected expect(theme_version.finders.map { |f| f.class.to_s.demodulize }).to eql @expected
end end
context 'when no related dynamic finders' do context 'when no related specific finders' do
it 'contains the default finders' do it 'contains the default finders' do
@expected = default_finders @expected = default_finders
end end
end end
# Dynamic Version Finders are not tested here, they are in context 'when specific finders' do
# spec/lib/finders/dynamic_finder/theme_versions_spec {
context 'when dynamic finders' do }.each do |theme_slug, specific_finders|
WPScan::DB::DynamicFinders::Theme.versions_finders_configs.each do |theme_slug, configs|
context "when #{theme_slug} theme" do context "when #{theme_slug} theme" do
let(:slug) { theme_slug } let(:slug) { theme_slug }
it 'contains the expected finders (default + the dynamic ones)' do it 'contains the expected finders' do
@expected = default_finders + configs.keys @expected = default_finders + specific_finders
end end
end end
end end

View File

@@ -60,60 +60,25 @@ describe WPScan::Model::Plugin do
end end
end end
describe 'potential_readme_filenames' do
context 'when not set in the DF file' do
its(:potential_readme_filenames) { should eql described_class::READMES }
end
context 'when set in the DF file' do
context 'as a string' do
let(:slug) { 'photoblocks-grid-gallery' }
its(:potential_readme_filenames) { should eql %w[README.txt] }
end
context 'as an array' do
let(:slug) { 'customerlabs-actionrecorder' }
its(:potential_readme_filenames) { should eql %w[Readme.txt Readme.md] }
end
end
end
describe '#latest_version, #last_updated, #popular' do describe '#latest_version, #last_updated, #popular' do
before { allow(plugin).to receive(:db_data).and_return(db_data) } context 'when none' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil } its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil } its(:last_updated) { should be_nil }
its(:popular?) { should be false } its(:popular?) { should be false }
end end
context 'when no db_data but metadata' do context 'when values' do
let(:slug) { 'no-vulns-popular' } let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') } its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' } its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true } its(:popular?) { should be true }
end end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.1') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end end
describe '#outdated?' do describe '#outdated?' do
before { allow(plugin).to receive(:db_data).and_return({}) }
context 'when last_version' do context 'when last_version' do
let(:slug) { 'no-vulns-popular' } let(:slug) { 'no-vulns-popular' }
@@ -131,13 +96,13 @@ describe WPScan::Model::Plugin do
.and_return(WPScan::Model::Version.new(version_number)) .and_return(WPScan::Model::Version.new(version_number))
end end
context 'when version < latest_version' do context 'when version < last_version' do
let(:version_number) { '1.2' } let(:version_number) { '1.2' }
its(:outdated?) { should eql true } its(:outdated?) { should eql true }
end end
context 'when version >= latest_version' do context 'when version >= last_version' do
let(:version_number) { '3.0' } let(:version_number) { '3.0' }
its(:outdated?) { should eql false } its(:outdated?) { should eql false }
@@ -145,7 +110,7 @@ describe WPScan::Model::Plugin do
end end
end end
context 'when no latest_version' do context 'when no last_version' do
let(:slug) { 'vulnerable-not-popular' } let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do context 'when no version' do
@@ -168,16 +133,13 @@ describe WPScan::Model::Plugin do
end end
describe '#vulnerabilities' do describe '#vulnerabilities' do
before { allow(plugin).to receive(:db_data).and_return(db_data) }
after do after do
expect(plugin.vulnerabilities).to eq @expected expect(plugin.vulnerabilities).to eq @expected
expect(plugin.vulnerable?).to eql @expected.empty? ? false : true expect(plugin.vulnerable?).to eql @expected.empty? ? false : true
end end
context 'when plugin not in the DB' do context 'when plugin not in the DB' do
let(:slug) { 'not-in-db' } let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do it 'returns an empty array' do
@expected = [] @expected = []
@@ -186,8 +148,7 @@ describe WPScan::Model::Plugin do
context 'when in the DB' do context 'when in the DB' do
context 'when no vulnerabilities' do context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' } let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
it 'returns an empty array' do it 'returns an empty array' do
@expected = [] @expected = []
@@ -195,13 +156,11 @@ describe WPScan::Model::Plugin do
end end
context 'when vulnerabilities' do context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' } let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('plugins/vulnerable-not-popular') }
let(:all_vulns) do let(:all_vulns) do
[ [
WPScan::Vulnerability.new( WPScan::Vulnerability.new(
'First Vuln <= 6.3.10 - LFI', 'First Vuln',
{ wpvulndb: '1' }, { wpvulndb: '1' },
'LFI', 'LFI',
'6.3.10' '6.3.10'

View File

@@ -86,179 +86,8 @@ describe WPScan::Model::Theme do
end end
end end
describe '#latest_version, #last_updated, #popular' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil }
its(:popular?) { should be false }
end
context 'when no db_data but metadata' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true }
end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.2') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end
describe '#outdated?' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return({})
end
context 'when last_version' do
let(:slug) { 'no-vulns-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(version_number))
end
context 'when version < latest_version' do
let(:version_number) { '1.2' }
its(:outdated?) { should eql true }
end
context 'when version >= latest_version' do
let(:version_number) { '3.0' }
its(:outdated?) { should eql false }
end
end
end
context 'when no latest_version' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new('1.0'))
end
its(:outdated?) { should eql false }
end
end
end
describe '#vulnerabilities' do describe '#vulnerabilities' do
before do xit
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
after do
expect(theme.vulnerabilities).to eq @expected
expect(theme.vulnerable?).to eql @expected.empty? ? false : true
end
context 'when theme not in the DB' do
let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do
@expected = []
end
end
context 'when in the DB' do
context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
it 'returns an empty array' do
@expected = []
end
end
context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('themes/vulnerable-not-popular') }
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'First Vuln',
{ wpvulndb: '1' },
'LFI',
'6.3.10'
),
WPScan::Vulnerability.new('No Fixed In', wpvulndb: '2')
]
end
context 'when no theme version' do
before { expect(theme).to receive(:version).at_least(1).and_return(false) }
it 'returns all the vulnerabilities' do
@expected = all_vulns
end
end
context 'when theme version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(number))
end
context 'when < to a fixed_in' do
let(:number) { '5.0' }
it 'returns it' do
@expected = all_vulns
end
end
context 'when >= to a fixed_in' do
let(:number) { '6.3.10' }
it 'does not return it ' do
@expected = [all_vulns.last]
end
end
end
end
end
end end
describe '#parent_theme' do describe '#parent_theme' do

View File

@@ -40,13 +40,11 @@ describe WPScan::Model::WpVersion do
describe '#vulnerabilities' do describe '#vulnerabilities' do
subject(:version) { described_class.new(number) } subject(:version) { described_class.new(number) }
before { allow(version).to receive(:db_data).and_return(db_data) }
context 'when no vulns' do context 'when no vulns' do
let(:number) { '4.4' } let(:number) { '4.4' }
let(:db_data) { { 'vulnerabilities' => [] } }
its(:vulnerabilities) { should be_empty } its(:vulnerabilities) { should eql([]) }
end end
context 'when vulnerable' do context 'when vulnerable' do
@@ -55,30 +53,13 @@ describe WPScan::Model::WpVersion do
expect(version).to be_vulnerable expect(version).to be_vulnerable
end end
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 1',
{ wpvulndb: '1' },
'SQLI'
),
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 2',
{ url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' },
nil,
'3.8.2'
)
]
end
context 'when a signle vuln' do context 'when a signle vuln' do
let(:number) { '3.8.1' } let(:number) { '3.8' }
let(:db_data) { vuln_api_data_for('wordpresses/38') }
it 'returns the expected result' do it 'returns the expected result' do
@expected = [WPScan::Vulnerability.new( @expected = [WPScan::Vulnerability.new(
'WP 3.8 - Vuln 1', 'WP 3.8 - Vuln 1',
{ url: %w[url-4], wpvulndb: '3' }, { url: %w[url-4], osvdb: %w[11], wpvulndb: '3' },
'AUTHBYPASS' 'AUTHBYPASS'
)] )]
end end
@@ -86,7 +67,6 @@ describe WPScan::Model::WpVersion do
context 'when multiple vulns' do context 'when multiple vulns' do
let(:number) { '3.8.1' } let(:number) { '3.8.1' }
let(:db_data) { vuln_api_data_for('wordpresses/381') }
it 'returns the expected results' do it 'returns the expected results' do
@expected = [ @expected = [
@@ -97,7 +77,7 @@ describe WPScan::Model::WpVersion do
), ),
WPScan::Vulnerability.new( WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 2', 'WP 3.8.1 - Vuln 2',
{ url: %w[url-2 url-3], cve: %w[2014-0166], wpvulndb: '2' }, { url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' },
nil, nil,
'3.8.2' '3.8.2'
) )
@@ -107,30 +87,27 @@ describe WPScan::Model::WpVersion do
end end
end end
describe '#metadata, #release_date, #status' do describe '#release_date' do
subject(:version) { described_class.new('3.8.1') } subject(:version) { described_class.new('3.8.1') }
before { allow(version).to receive(:db_data).and_return(db_data) } its(:release_date) { should eql '2014-01-23' }
context 'when no db_data' do context 'when the version is not in the DB' do
let(:db_data) { {} } subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql '2014-01-23' } its(:release_date) { should eql 'Unknown' }
its(:status) { should eql 'outdated' }
context 'when the version is not in the metadata' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
its(:status) { should eql 'Unknown' }
end
end end
end
context 'when db_data' do describe '#status' do
let(:db_data) { vuln_api_data_for('wordpresses/381') } subject(:version) { described_class.new('3.8.1') }
its(:release_date) { should eql '2014-01-23-via-api' } its(:status) { should eql 'outdated' }
its(:status) { should eql 'outdated-via-api' }
context 'when the version is not in the DB' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
end end
end end
end end

View File

@@ -9,7 +9,6 @@ describe 'App::Views' do
# in the expected output. # in the expected output.
%i[JSON CliNoColour].each do |formatter| %i[JSON CliNoColour].each do |formatter|
context "when #{formatter}" do context "when #{formatter}" do
it_behaves_like 'App::Views::VulnApi'
it_behaves_like 'App::Views::WpVersion' it_behaves_like 'App::Views::WpVersion'
it_behaves_like 'App::Views::MainTheme' it_behaves_like 'App::Views::MainTheme'
it_behaves_like 'App::Views::Enumeration' it_behaves_like 'App::Views::Enumeration'

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
{
"wordpress": {
"4.0": {
"release_date": "2014-09-04",
"status": "latest"
},
"3.8.1": {
"release_date": "2014-01-23",
"status": "outdated"
},
"3.8": {
"release_date": "2013-12-12",
"status": "insecure"
}
},
"plugins": {
"no-vulns-popular": {
"vulnerabilities": false,
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z"
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities": true
}
},
"themes": {
"no-vulns-popular": {
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z",
"vulnerabilities": false
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities": true
},
"dignitas-themes": {
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : true
},
"yaaburnee-themes": {
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : true
}
}
}

25
spec/fixtures/db/plugins.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"no-vulns-popular": {
"vulnerabilities": [],
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z"
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}
}

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