wpscan 3.8.28 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +104 -30
- data/app/app.rb +26 -0
- data/app/controllers/aliases.rb +2 -2
- data/app/controllers/authenticated_inventory.rb +43 -0
- data/app/controllers/core/cli_options.rb +151 -0
- data/app/controllers/core.rb +200 -25
- data/app/controllers/custom_directories.rb +1 -1
- data/app/controllers/enumeration/cli_options.rb +21 -31
- data/app/controllers/enumeration/enum_methods.rb +145 -38
- data/app/controllers/enumeration.rb +26 -3
- data/app/controllers/interesting_findings.rb +25 -0
- data/app/controllers/main_theme.rb +1 -1
- data/app/controllers/password_attack.rb +14 -6
- data/app/controllers/vuln_api.rb +9 -3
- data/app/controllers/wp_version.rb +1 -1
- data/app/controllers.rb +1 -0
- data/app/finders/backup_folders/known_locations.rb +66 -0
- data/app/finders/backup_folders.rb +19 -0
- data/app/finders/config_backups/known_filenames.rb +6 -4
- data/app/finders/config_backups.rb +1 -1
- data/app/finders/db_exports/known_locations.rb +16 -14
- data/app/finders/db_exports.rb +1 -1
- data/app/finders/interesting_findings/backup_db.rb +1 -1
- data/app/finders/interesting_findings/debug_log.rb +1 -1
- data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
- data/app/finders/interesting_findings/emergency_pwd_reset_script.rb +1 -1
- data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
- data/app/finders/interesting_findings/full_path_disclosure.rb +1 -1
- data/app/finders/interesting_findings/headers.rb +17 -0
- data/app/finders/interesting_findings/mu_plugins.rb +1 -1
- data/app/finders/interesting_findings/multisite.rb +1 -1
- data/app/finders/interesting_findings/php_disabled.rb +2 -2
- data/app/finders/interesting_findings/readme.rb +1 -1
- data/app/finders/interesting_findings/registration.rb +1 -1
- data/app/finders/interesting_findings/robots_txt.rb +20 -0
- data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
- data/app/finders/interesting_findings/tmm_db_migrate.rb +1 -1
- data/app/finders/interesting_findings/upload_directory_listing.rb +1 -1
- data/app/finders/interesting_findings/upload_sql_dump.rb +2 -2
- data/app/finders/interesting_findings/wp_cron.rb +1 -1
- data/app/finders/interesting_findings/xml_rpc.rb +61 -0
- data/app/finders/interesting_findings.rb +13 -4
- data/app/finders/main_theme/css_style_in_homepage.rb +1 -1
- data/app/finders/main_theme/urls_in_homepage.rb +3 -7
- data/app/finders/main_theme/woo_framework_meta_generator.rb +4 -4
- data/app/finders/main_theme.rb +1 -1
- data/app/finders/medias/attachment_brute_forcing.rb +2 -2
- data/app/finders/medias.rb +1 -1
- data/app/finders/passwords/wp_login.rb +2 -2
- data/app/finders/passwords/xml_rpc.rb +2 -2
- data/app/finders/passwords/xml_rpc_multicall.rb +1 -1
- data/app/finders/plugin_version/readme.rb +1 -1
- data/app/finders/plugin_version.rb +1 -1
- data/app/finders/plugins/known_locations.rb +17 -7
- data/app/finders/plugins/urls_in_homepage.rb +3 -7
- data/app/finders/plugins/wp_json_api.rb +85 -0
- data/app/finders/plugins.rb +2 -1
- data/app/finders/theme_version/style.rb +1 -1
- data/app/finders/theme_version/woo_framework_meta_generator.rb +1 -1
- data/app/finders/theme_version.rb +1 -1
- data/app/finders/themes/known_locations.rb +12 -6
- data/app/finders/themes/urls_in_homepage.rb +3 -7
- data/app/finders/themes/wp_json_api.rb +74 -0
- data/app/finders/themes.rb +2 -1
- data/app/finders/timthumb_version/bad_request.rb +1 -1
- data/app/finders/timthumb_version.rb +1 -1
- data/app/finders/timthumbs/known_locations.rb +6 -4
- data/app/finders/timthumbs.rb +1 -1
- data/app/finders/users/author_id_brute_forcing.rb +11 -7
- data/app/finders/users/author_posts.rb +1 -1
- data/app/finders/users/author_sitemap.rb +1 -1
- data/app/finders/users/login_error_messages.rb +1 -1
- data/app/finders/users/oembed_api.rb +3 -1
- data/app/finders/users/wp_json_api.rb +11 -7
- data/app/finders/users.rb +1 -1
- data/app/finders/wp_version/atom_generator.rb +1 -1
- data/app/finders/wp_version/rdf_generator.rb +1 -1
- data/app/finders/wp_version/readme.rb +1 -1
- data/app/finders/wp_version/rss_generator.rb +1 -1
- data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
- data/app/finders/wp_version.rb +1 -1
- data/app/finders.rb +1 -0
- data/app/formatters/cli.rb +79 -0
- data/app/formatters/cli_no_color.rb +9 -0
- data/app/formatters/cli_no_colour.rb +17 -0
- data/app/formatters/json.rb +14 -0
- data/app/formatters/jsonl.rb +29 -0
- data/app/formatters/sarif.rb +311 -0
- data/app/models/backup_folder.rb +39 -0
- data/app/models/fantastico_fileslist.rb +34 -0
- data/app/models/headers.rb +44 -0
- data/app/models/interesting_finding.rb +41 -2
- data/app/models/plugin.rb +8 -2
- data/app/models/robots_txt.rb +31 -0
- data/app/models/search_replace_db_2.rb +17 -0
- data/app/models/theme.rb +9 -2
- data/app/models/timthumb.rb +2 -2
- data/app/models/user.rb +35 -0
- data/app/models/version.rb +49 -0
- data/app/models/wp_item/wordpress_org_data.rb +55 -0
- data/app/models/wp_item.rb +109 -9
- data/app/models/wp_version.rb +2 -2
- data/app/models/xml_rpc.rb +73 -3
- data/app/models.rb +2 -1
- data/app/user_agents.txt +46 -0
- data/app/views/cli/core/banner.erb +3 -3
- data/app/views/cli/core/finished.erb +15 -0
- data/app/views/cli/core/help.erb +4 -0
- data/app/views/cli/core/started.erb +11 -0
- data/app/views/cli/enumeration/backup_folders.erb +11 -0
- data/app/views/cli/enumeration/plugin.erb +13 -0
- data/app/views/cli/enumeration/plugins.erb +1 -12
- data/app/views/cli/enumeration/theme.erb +4 -0
- data/app/views/cli/enumeration/themes.erb +1 -3
- data/app/views/cli/enumeration/user.erb +4 -0
- data/app/views/cli/enumeration/users.erb +1 -3
- data/app/views/cli/finding.erb +1 -1
- data/app/views/cli/interesting_findings/_array.erb +10 -0
- data/app/views/cli/interesting_findings/findings.erb +23 -0
- data/app/views/cli/scan_aborted.erb +5 -0
- data/app/views/cli/update_aborted.erb +5 -0
- data/app/views/cli/vuln_api/status.erb +2 -0
- data/app/views/cli/vulnerability.erb +6 -0
- data/app/views/cli/wp_item.erb +4 -1
- data/app/views/json/core/banner.erb +2 -8
- data/app/views/json/core/finished.erb +13 -0
- data/app/views/json/core/help.erb +4 -0
- data/app/views/json/core/started.erb +10 -0
- data/app/views/json/enumeration/backup_folders.erb +11 -0
- data/app/views/json/enumeration/plugin.erb +15 -0
- data/app/views/json/enumeration/theme.erb +5 -0
- data/app/views/json/enumeration/user.erb +6 -0
- data/app/views/json/finding.erb +8 -2
- data/app/views/json/interesting_findings/findings.erb +24 -0
- data/app/views/json/notice.erb +1 -0
- data/app/views/json/scan_aborted.erb +5 -0
- data/app/views/json/update_aborted.erb +5 -0
- data/app/views/json/vuln_api/status.erb +2 -0
- data/app/views/json/wp_item.erb +4 -1
- data/bin/wpscan +1 -0
- data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
- data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
- data/lib/opt_parse_validator/errors.rb +9 -0
- data/lib/opt_parse_validator/hacks.rb +19 -0
- data/lib/opt_parse_validator/opts/alias.rb +28 -0
- data/lib/opt_parse_validator/opts/array.rb +34 -0
- data/lib/opt_parse_validator/opts/base.rb +142 -0
- data/lib/opt_parse_validator/opts/boolean.rb +19 -0
- data/lib/opt_parse_validator/opts/choice.rb +43 -0
- data/lib/opt_parse_validator/opts/credentials.rb +15 -0
- data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
- data/lib/opt_parse_validator/opts/file_path.rb +34 -0
- data/lib/opt_parse_validator/opts/headers.rb +33 -0
- data/lib/opt_parse_validator/opts/integer.rb +15 -0
- data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
- data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
- data/lib/opt_parse_validator/opts/path.rb +78 -0
- data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
- data/lib/opt_parse_validator/opts/proxy.rb +7 -0
- data/lib/opt_parse_validator/opts/regexp.rb +14 -0
- data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
- data/lib/opt_parse_validator/opts/string.rb +8 -0
- data/lib/opt_parse_validator/opts/uri.rb +41 -0
- data/lib/opt_parse_validator/opts/url.rb +11 -0
- data/lib/opt_parse_validator/opts.rb +9 -0
- data/lib/opt_parse_validator/version.rb +6 -0
- data/lib/opt_parse_validator.rb +161 -0
- data/lib/wpscan/browser/actions.rb +48 -0
- data/lib/wpscan/browser/options.rb +92 -0
- data/lib/wpscan/browser.rb +87 -2
- data/lib/wpscan/browser_authenticator.rb +64 -0
- data/lib/wpscan/cache/file_store.rb +77 -0
- data/lib/wpscan/cache/typhoeus.rb +25 -0
- data/lib/wpscan/controller.rb +100 -4
- data/lib/wpscan/controllers.rb +78 -3
- data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
- data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
- data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
- data/lib/wpscan/db/fingerprints.rb +2 -2
- data/lib/wpscan/db/updater.rb +23 -13
- data/lib/wpscan/db/vuln_api.rb +19 -7
- data/lib/wpscan/db/wp_item.rb +2 -2
- data/lib/wpscan/errors/enumeration.rb +4 -4
- data/lib/wpscan/errors/http.rb +82 -3
- data/lib/wpscan/errors/saml.rb +28 -0
- data/lib/wpscan/errors/scan.rb +14 -0
- data/lib/wpscan/errors/update.rb +11 -3
- data/lib/wpscan/errors/vuln_api.rb +24 -0
- data/lib/wpscan/errors/wordpress.rb +2 -2
- data/lib/wpscan/errors/wp_auth.rb +37 -0
- data/lib/wpscan/errors.rb +4 -3
- data/lib/wpscan/exit_code.rb +25 -0
- data/lib/wpscan/finders/base_finders.rb +45 -0
- data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
- data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
- data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
- data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
- data/lib/wpscan/finders/finder/enumerator.rb +77 -0
- data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
- data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
- data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
- data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
- data/lib/wpscan/finders/finder.rb +78 -0
- data/lib/wpscan/finders/finding.rb +54 -0
- data/lib/wpscan/finders/findings.rb +33 -0
- data/lib/wpscan/finders/independent_finder.rb +33 -0
- data/lib/wpscan/finders/independent_finders.rb +26 -0
- data/lib/wpscan/finders/same_type_finder.rb +19 -0
- data/lib/wpscan/finders/same_type_finders.rb +28 -0
- data/lib/wpscan/finders/unique_finder.rb +19 -0
- data/lib/wpscan/finders/unique_finders.rb +47 -0
- data/lib/wpscan/finders.rb +11 -12
- data/lib/wpscan/formatter/buffer.rb +17 -0
- data/lib/wpscan/formatter.rb +152 -0
- data/lib/wpscan/helper.rb +7 -1
- data/lib/wpscan/http_status_tracking.rb +128 -0
- data/lib/wpscan/numeric.rb +13 -0
- data/lib/wpscan/parsed_cli.rb +31 -2
- data/lib/wpscan/progressbar_null_output.rb +23 -0
- data/lib/wpscan/public_suffix/domain.rb +44 -0
- data/lib/wpscan/references.rb +118 -4
- data/lib/wpscan/scan.rb +127 -0
- data/lib/wpscan/target/hashes.rb +45 -0
- data/lib/wpscan/target/platform/php.rb +124 -0
- data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
- data/lib/wpscan/target/platform/wordpress.rb +7 -8
- data/lib/wpscan/target/platform.rb +3 -0
- data/lib/wpscan/target/scope.rb +103 -0
- data/lib/wpscan/target/server/apache.rb +27 -0
- data/lib/wpscan/target/server/generic.rb +72 -0
- data/lib/wpscan/target/server/iis.rb +29 -0
- data/lib/wpscan/target/server/nginx.rb +27 -0
- data/lib/wpscan/target/server.rb +6 -0
- data/lib/wpscan/target.rb +129 -9
- data/lib/wpscan/typhoeus/hydra.rb +12 -0
- data/lib/wpscan/typhoeus/response.rb +24 -1
- data/lib/wpscan/version.rb +1 -1
- data/lib/wpscan/vulnerability.rb +49 -3
- data/lib/wpscan/vulnerability_filter.rb +68 -0
- data/lib/wpscan/vulnerable.rb +13 -1
- data/lib/wpscan/web_site.rb +152 -0
- data/lib/wpscan.rb +126 -29
- metadata +362 -20
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# This class is designed to handle independent results
|
|
6
|
+
# which are not related with each others
|
|
7
|
+
# e.g: interesting files
|
|
8
|
+
class IndependentFinders < BaseFinders
|
|
9
|
+
# @param [ Hash ] opts
|
|
10
|
+
# @option opts [ Symbol ] mode :mixed, :passive or :aggressive
|
|
11
|
+
#
|
|
12
|
+
# @return [ Findings ]
|
|
13
|
+
def run(opts = {})
|
|
14
|
+
methods = symbols_from_mode(opts[:mode])
|
|
15
|
+
|
|
16
|
+
each do |finder|
|
|
17
|
+
methods.each do |symbol|
|
|
18
|
+
run_finder(finder, symbol, opts)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
filter_findings
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# Same Type Finder
|
|
6
|
+
module SameTypeFinder
|
|
7
|
+
def self.included(klass)
|
|
8
|
+
klass.class_eval do
|
|
9
|
+
include IndependentFinder
|
|
10
|
+
|
|
11
|
+
# @return [ Array ]
|
|
12
|
+
def finders
|
|
13
|
+
@finders ||= WPScan::Finders::SameTypeFinders.new
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# This class is designed to handle same type results, such as enumeration of plugins,
|
|
6
|
+
# themes etc.
|
|
7
|
+
class SameTypeFinders < BaseFinders
|
|
8
|
+
# @param [ Hash ] opts
|
|
9
|
+
# @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
|
|
10
|
+
# @option opts [ Boolean ] :sort Wether or not to sort the findings
|
|
11
|
+
#
|
|
12
|
+
# @return [ Findings ]
|
|
13
|
+
def run(opts = {}, &block)
|
|
14
|
+
findings.on_append = block if block
|
|
15
|
+
|
|
16
|
+
symbols_from_mode(opts[:mode]).each do |symbol|
|
|
17
|
+
each do |finder|
|
|
18
|
+
run_finder(finder, symbol, opts)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
findings.sort! if opts[:sort]
|
|
23
|
+
|
|
24
|
+
filter_findings
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# Unique Finder
|
|
6
|
+
module UniqueFinder
|
|
7
|
+
def self.included(klass)
|
|
8
|
+
klass.class_eval do
|
|
9
|
+
include IndependentFinder
|
|
10
|
+
|
|
11
|
+
# @return [ Array ]
|
|
12
|
+
def finders
|
|
13
|
+
@finders ||= WPScan::Finders::UniqueFinders.new
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# This class is designed to return a unique result such as a version
|
|
6
|
+
# Note: Finders contained can return multiple results but the #run will only
|
|
7
|
+
# returned the best finding
|
|
8
|
+
class UniqueFinders < BaseFinders
|
|
9
|
+
# @param [ Hash ] opts
|
|
10
|
+
# @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
|
|
11
|
+
# @option opts [ Int ] :confidence_threshold If a finding's confidence reaches this value,
|
|
12
|
+
# it will be returned as the best finding.
|
|
13
|
+
# Default is 100.
|
|
14
|
+
# If <= 0, all finders will be ran.
|
|
15
|
+
#
|
|
16
|
+
# @return [ Object, false ] The best finding or false if none
|
|
17
|
+
def run(opts = {})
|
|
18
|
+
opts[:confidence_threshold] ||= 100
|
|
19
|
+
|
|
20
|
+
symbols_from_mode(opts[:mode]).each do |symbol|
|
|
21
|
+
each do |finder|
|
|
22
|
+
run_finder(finder, symbol, opts)
|
|
23
|
+
|
|
24
|
+
next if opts[:confidence_threshold] <= 0
|
|
25
|
+
|
|
26
|
+
findings.each { |f| return f if f.confidence >= opts[:confidence_threshold] }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
filter_findings
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
protected
|
|
34
|
+
|
|
35
|
+
# @return [ Object, false ] The best finding or false if none
|
|
36
|
+
def filter_findings
|
|
37
|
+
# results are sorted by confidence ASC
|
|
38
|
+
findings.sort_by!(&:confidence)
|
|
39
|
+
|
|
40
|
+
# If all findings have the same confidence, false is returned
|
|
41
|
+
return false if findings.size > 1 && findings.first.confidence == findings.last.confidence
|
|
42
|
+
|
|
43
|
+
findings.last || false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/wpscan/finders.rb
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'wpscan/finders/finder'
|
|
4
|
+
require 'wpscan/finders/finding'
|
|
5
|
+
require 'wpscan/finders/findings'
|
|
6
|
+
require 'wpscan/finders/base_finders'
|
|
7
|
+
require 'wpscan/finders/independent_finders'
|
|
8
|
+
require 'wpscan/finders/independent_finder'
|
|
9
|
+
require 'wpscan/finders/unique_finders'
|
|
10
|
+
require 'wpscan/finders/unique_finder'
|
|
11
|
+
require 'wpscan/finders/same_type_finders'
|
|
12
|
+
require 'wpscan/finders/same_type_finder'
|
|
13
|
+
|
|
3
14
|
require 'wpscan/finders/finder/wp_version/smart_url_checker'
|
|
4
15
|
|
|
5
16
|
require 'wpscan/finders/dynamic_finder/finder'
|
|
@@ -14,15 +25,3 @@ require 'wpscan/finders/dynamic_finder/version/query_parameter'
|
|
|
14
25
|
require 'wpscan/finders/dynamic_finder/version/config_parser'
|
|
15
26
|
require 'wpscan/finders/dynamic_finder/wp_item_version'
|
|
16
27
|
require 'wpscan/finders/dynamic_finder/wp_version'
|
|
17
|
-
|
|
18
|
-
module WPScan
|
|
19
|
-
# Custom Finders
|
|
20
|
-
module Finders
|
|
21
|
-
include CMSScanner::Finders
|
|
22
|
-
|
|
23
|
-
# Custom InterestingFindings
|
|
24
|
-
module InterestingFindings
|
|
25
|
-
include CMSScanner::Finders::InterestingFindings
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Formatter
|
|
5
|
+
# Module used to output the rendered views into a buffer
|
|
6
|
+
# and beautify it a the end of the scan
|
|
7
|
+
module Buffer
|
|
8
|
+
def output(tpl, vars = {}, controller_name = nil)
|
|
9
|
+
buffer << render(tpl, vars, controller_name).encode('UTF-8', invalid: :replace, undef: :replace)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def buffer
|
|
13
|
+
@buffer ||= +''
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'wpscan/formatter/buffer'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
# Formatter
|
|
7
|
+
module Formatter
|
|
8
|
+
# Module to be able to do Formatter.load() & Formatter.availables
|
|
9
|
+
# and do that as well when the Formatter is included in another module
|
|
10
|
+
module ClassMethods
|
|
11
|
+
# @param [ String ] format
|
|
12
|
+
# @param [ Array<String> ] custom_views
|
|
13
|
+
#
|
|
14
|
+
# @return [ Formatter::Base ]
|
|
15
|
+
def load(format = nil, custom_views = nil)
|
|
16
|
+
format ||= 'cli'
|
|
17
|
+
custom_views ||= []
|
|
18
|
+
|
|
19
|
+
f = const_get(format.tr('-', '_').camelize).new
|
|
20
|
+
custom_views.each { |v| f.views_directories << v }
|
|
21
|
+
f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [ Array<String> ] The list of the available formatters (except the Base one)
|
|
25
|
+
# @note: the #load method above should then be used to create the associated formatter
|
|
26
|
+
def availables
|
|
27
|
+
formatters = WPScan::Formatter.constants.select do |const|
|
|
28
|
+
name = WPScan::Formatter.const_get(const)
|
|
29
|
+
name.is_a?(Class) && name != WPScan::Formatter::Base
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
formatters.map { |sym| sym.to_s.underscore.dasherize }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
extend ClassMethods
|
|
37
|
+
|
|
38
|
+
def self.included(base)
|
|
39
|
+
base.extend(ClassMethods)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# This module should be implemented in the code which uses this Framework to
|
|
43
|
+
# be able to override/implements instance methods for all the Formatters
|
|
44
|
+
# w/o having to include/write the methods in each formatters.
|
|
45
|
+
#
|
|
46
|
+
# Example: to override the #views_directories (see the wpscan-v3/lib/wpscan/formatter.rb)
|
|
47
|
+
module InstanceMethods
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Base Formatter
|
|
51
|
+
class Base
|
|
52
|
+
attr_reader :controller_name
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
# Can't put this at the top level of the class, due to the WPScan::
|
|
56
|
+
extend WPScan::Formatter::InstanceMethods
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [ String ] The underscored name of the class
|
|
60
|
+
def format
|
|
61
|
+
self.class.name.demodulize.underscore
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [ Boolean ]
|
|
65
|
+
def user_interaction?
|
|
66
|
+
format == 'cli'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [ Boolean ]
|
|
70
|
+
# Whether this formatter can render findings incrementally as they
|
|
71
|
+
# arrive (cli, jsonl), or needs to receive the full result set first
|
|
72
|
+
# (json, sarif — they emit a single well-formed document at end-of-scan).
|
|
73
|
+
def streams?
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [ String ] The underscored format to use as a base
|
|
78
|
+
def base_format; end
|
|
79
|
+
|
|
80
|
+
# @return [ Array<String> ]
|
|
81
|
+
def formats
|
|
82
|
+
[format, base_format].compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# This is called after the scan
|
|
86
|
+
# and used in some formatters (e.g JSON)
|
|
87
|
+
# to indent results
|
|
88
|
+
def beautify; end
|
|
89
|
+
|
|
90
|
+
# @see #render
|
|
91
|
+
def output(tpl, vars = {}, controller_name = nil)
|
|
92
|
+
puts render(tpl, vars, controller_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ERB_SUPPORTS_KVARGS = ::ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
|
|
96
|
+
|
|
97
|
+
# @param [ String ] tpl
|
|
98
|
+
# @param [ Hash ] vars
|
|
99
|
+
# @param [ String ] controller_name
|
|
100
|
+
def render(tpl, vars = {}, controller_name = nil)
|
|
101
|
+
template_vars(vars)
|
|
102
|
+
@controller_name = controller_name if controller_name
|
|
103
|
+
|
|
104
|
+
# '-' disables new lines when -%> is used.
|
|
105
|
+
# See http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/ERB.html
|
|
106
|
+
ERB.new(File.read(view_path(tpl)), trim_mode: '-').result(binding)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param [ Hash ] vars
|
|
110
|
+
#
|
|
111
|
+
# @return [ Void ]
|
|
112
|
+
def template_vars(vars)
|
|
113
|
+
vars.each do |key, value|
|
|
114
|
+
instance_variable_set("@#{key}", value) unless key == :views_directories
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param [ String ] tpl
|
|
119
|
+
#
|
|
120
|
+
# @return [ String ] The path of the view
|
|
121
|
+
def view_path(tpl)
|
|
122
|
+
if tpl[0, 1] == '@' # Global Template
|
|
123
|
+
tpl = tpl.delete('@')
|
|
124
|
+
else
|
|
125
|
+
raise 'The controller_name can not be nil' unless controller_name
|
|
126
|
+
|
|
127
|
+
tpl = "#{controller_name}/#{tpl}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise "Wrong tpl format: '#{tpl}'" unless %r{\A[\w/_]+\z}.match?(tpl)
|
|
131
|
+
|
|
132
|
+
views_directories.reverse_each do |dir|
|
|
133
|
+
formats.each do |format|
|
|
134
|
+
potential_file = File.join(dir, format, "#{tpl}.erb")
|
|
135
|
+
|
|
136
|
+
return potential_file if File.exist?(potential_file)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
raise "View not found for #{format}/#{tpl}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [ Array<String> ] The directories to look into for views
|
|
144
|
+
def views_directories
|
|
145
|
+
@views_directories ||= [
|
|
146
|
+
APP_DIR, WPScan::APP_DIR,
|
|
147
|
+
File.join(Dir.home, ".#{WPScan.app_name}"), File.join(Dir.pwd, ".#{WPScan.app_name}")
|
|
148
|
+
].uniq.reduce([]) { |acc, elem| acc << Pathname.new(elem).join('views').to_s }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/wpscan/helper.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# @param [ String ] file The file path
|
|
4
|
+
def redirect_output_to_file(file)
|
|
5
|
+
$stdout.reopen(file, 'w')
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
end
|
|
8
|
+
|
|
3
9
|
def read_json_file(file)
|
|
4
10
|
JSON.parse(File.read(file))
|
|
5
11
|
rescue StandardError => e
|
|
@@ -13,7 +19,7 @@ end
|
|
|
13
19
|
#
|
|
14
20
|
# @return [ Symbol ]
|
|
15
21
|
def classify_slug(slug)
|
|
16
|
-
classified = slug.to_s.gsub(/[^a-z\d
|
|
22
|
+
classified = slug.to_s.gsub(/[^a-z\d-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
|
|
17
23
|
classified = "D_#{classified}" if /\d/.match?(classified[0])
|
|
18
24
|
|
|
19
25
|
# Special case for slugs with all non-latin characters.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Module for HTTP status code tracking and error detection
|
|
5
|
+
module HttpStatusTracking
|
|
6
|
+
# Tracking for HTTP status codes
|
|
7
|
+
def status_codes
|
|
8
|
+
@@status_codes_mutex ||= Mutex.new
|
|
9
|
+
@@status_codes ||= Hash.new(0)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Reset status codes (mainly for testing)
|
|
13
|
+
def reset_status_codes
|
|
14
|
+
@@status_codes_mutex ||= Mutex.new
|
|
15
|
+
@@status_codes_mutex.synchronize do
|
|
16
|
+
@@status_codes = Hash.new(0)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Thread-safe increment of status code count
|
|
21
|
+
def increment_status_code(code)
|
|
22
|
+
@@status_codes_mutex ||= Mutex.new
|
|
23
|
+
@@status_codes_mutex.synchronize do
|
|
24
|
+
status_codes[code] += 1
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Thread-safe set of status code count (mainly for testing)
|
|
29
|
+
def set_status_code(code, count)
|
|
30
|
+
@@status_codes_mutex ||= Mutex.new
|
|
31
|
+
@@status_codes_mutex.synchronize do
|
|
32
|
+
status_codes[code] = count
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get top N status codes by frequency
|
|
37
|
+
def top_status_codes(limit = 5)
|
|
38
|
+
@@status_codes_mutex ||= Mutex.new
|
|
39
|
+
@@status_codes_mutex.synchronize do
|
|
40
|
+
return {} if status_codes.empty?
|
|
41
|
+
|
|
42
|
+
status_codes.sort_by { |_code, count| -count }.first(limit).to_h
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Helper to count specific error types
|
|
47
|
+
def error_counts
|
|
48
|
+
@@status_codes_mutex ||= Mutex.new
|
|
49
|
+
@@status_codes_mutex.synchronize do
|
|
50
|
+
{
|
|
51
|
+
failed: status_codes[0] || 0,
|
|
52
|
+
rate_limit: status_codes[429] || 0,
|
|
53
|
+
server_errors: status_codes.select { |code, _| code >= 500 }.values.sum,
|
|
54
|
+
client_errors: status_codes.select { |code, _count| code >= 400 && code < 500 && code != 404 }.values.sum
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Format status codes for display (converts code 0 to "failed")
|
|
60
|
+
def format_status_codes(codes_hash)
|
|
61
|
+
codes_hash.to_h do |code, count|
|
|
62
|
+
label = code.zero? ? 'failed' : code.to_s
|
|
63
|
+
[label, count]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get all applicable warning messages based on error types
|
|
68
|
+
def error_warning_messages
|
|
69
|
+
return [] if total_requests.zero?
|
|
70
|
+
|
|
71
|
+
messages = []
|
|
72
|
+
counts = error_counts
|
|
73
|
+
|
|
74
|
+
check_specific_error_conditions(messages, counts)
|
|
75
|
+
check_generic_error_rate(messages, counts)
|
|
76
|
+
|
|
77
|
+
messages
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Determine if warning should be shown for concerning error codes
|
|
81
|
+
def concerning_error_codes?
|
|
82
|
+
return false if total_requests.zero?
|
|
83
|
+
|
|
84
|
+
counts = error_counts
|
|
85
|
+
# Total errors excluding 404s but including failed requests (code 0)
|
|
86
|
+
total_errors = counts[:client_errors] + counts[:server_errors] + counts[:failed]
|
|
87
|
+
error_percentage = total_errors.to_f / total_requests
|
|
88
|
+
|
|
89
|
+
# Warning conditions
|
|
90
|
+
error_percentage > 0.2 ||
|
|
91
|
+
counts[:rate_limit] > 10 ||
|
|
92
|
+
counts[:server_errors] > 10 ||
|
|
93
|
+
counts[:failed] > 10
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def check_specific_error_conditions(messages, counts)
|
|
99
|
+
if counts[:failed] > 10
|
|
100
|
+
messages << 'Too many failed requests (no response) could indicate network issues, ' \
|
|
101
|
+
'WAF/IPS blocking, or an unavailable target'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if counts[:rate_limit] > 10
|
|
105
|
+
messages << 'Rate limiting detected (429 responses). Consider using --throttle or reducing --max-threads'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if counts[:server_errors] > 10
|
|
109
|
+
messages << 'Too many server errors (5xx). The target may be experiencing issues or blocking requests'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check for other 4xx client errors (excluding 404 and 429 which are handled separately)
|
|
113
|
+
other_client_errors = counts[:client_errors] - counts[:rate_limit]
|
|
114
|
+
return unless other_client_errors > 10
|
|
115
|
+
|
|
116
|
+
messages << 'Too many client errors (4xx). This could indicate access restrictions, ' \
|
|
117
|
+
'authentication issues, or WAF blocking'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def check_generic_error_rate(messages, counts)
|
|
121
|
+
error_percentage = (counts[:client_errors] + counts[:server_errors] + counts[:failed]).to_f / total_requests
|
|
122
|
+
return unless error_percentage > 0.2 && messages.empty?
|
|
123
|
+
|
|
124
|
+
messages << 'Too many errors detected. This could indicate network issues, rate limiting, ' \
|
|
125
|
+
'or security protection (e.g. WAF)'
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hack of the Numeric class
|
|
4
|
+
class Numeric
|
|
5
|
+
# @return [ String ] A human readable string of the value
|
|
6
|
+
def bytes_to_human
|
|
7
|
+
units = %w[B KB MB GB TB]
|
|
8
|
+
e = abs.zero? ? abs : (Math.log(abs) / Math.log(1024)).floor
|
|
9
|
+
s = format('%<s>.3f', s: (abs.to_f / (1024**e)))
|
|
10
|
+
|
|
11
|
+
s.sub(/\.?0*$/, " #{units[e]}")
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/wpscan/parsed_cli.rb
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
#
|
|
5
|
-
class
|
|
4
|
+
# Holds the parsed CLI options and exposes them via class methods (e.g. #verbose?)
|
|
5
|
+
# rather than from the raw hash. Similar to an OpenStruct but class-wise, with extra
|
|
6
|
+
# logic to push browser-relevant options into the Browser singleton.
|
|
7
|
+
class ParsedCli
|
|
8
|
+
# @return [ Hash ]
|
|
9
|
+
def self.options
|
|
10
|
+
@options ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Sets the CLI options and propagates them to the Browser.
|
|
14
|
+
# @param [ Hash ] options
|
|
15
|
+
def self.options=(options)
|
|
16
|
+
@options = options.dup || {}
|
|
17
|
+
|
|
18
|
+
WPScan::Browser.reset
|
|
19
|
+
WPScan::Browser.instance(@options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [ Boolean ]
|
|
23
|
+
def self.verbose?
|
|
24
|
+
options[:verbose] ? true : false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Unknown methods return nil — expected behaviour for option lookups.
|
|
28
|
+
# rubocop:disable Style/MissingRespondToMissing
|
|
29
|
+
def self.method_missing(method_name, *_args, &)
|
|
30
|
+
super if method_name == :new
|
|
31
|
+
|
|
32
|
+
options[method_name.to_sym]
|
|
33
|
+
end
|
|
34
|
+
# rubocop:enable Style/MissingRespondToMissing
|
|
6
35
|
end
|
|
7
36
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby-progressbar/outputs/null'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
# Adds the feature to log message sent to #log
|
|
7
|
+
# So they can be retrieved at some point, like after a password attack with a JSON output
|
|
8
|
+
# which won't display the progressbar but still call #log for errors etc
|
|
9
|
+
class ProgressBarNullOutput < ::ProgressBar::Outputs::Null
|
|
10
|
+
# @retutn [ Array<String> ]
|
|
11
|
+
def logs
|
|
12
|
+
@logs ||= []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Override of parent method
|
|
16
|
+
# @return [ Array<String> ] return the logs when no string provided
|
|
17
|
+
def log(string = nil)
|
|
18
|
+
return logs if string.nil?
|
|
19
|
+
|
|
20
|
+
logs << string unless logs.include?(string)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PublicSuffix
|
|
4
|
+
# Monkey Patch to include the match logic
|
|
5
|
+
class Domain
|
|
6
|
+
# For Sanity
|
|
7
|
+
def ==(other)
|
|
8
|
+
name == other.name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [ Boolean ]
|
|
12
|
+
#
|
|
13
|
+
# rubocop:disable Naming/PredicateMethod
|
|
14
|
+
def match(pattern)
|
|
15
|
+
pattern = PublicSuffix.parse(pattern) unless pattern.is_a?(PublicSuffix::Domain)
|
|
16
|
+
|
|
17
|
+
return name == pattern.name unless pattern.trd
|
|
18
|
+
return false unless tld == pattern.tld && sld == pattern.sld
|
|
19
|
+
|
|
20
|
+
matching_pattern?(pattern)
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Naming/PredicateMethod
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
# @rturn [ Boolean ]
|
|
27
|
+
def matching_pattern?(pattern)
|
|
28
|
+
pattern_trds = pattern.trd.split('.')
|
|
29
|
+
domain_trds = trd.split('.')
|
|
30
|
+
|
|
31
|
+
case pattern_trds.first
|
|
32
|
+
when '*'
|
|
33
|
+
pattern_trds[1..] == domain_trds[1..]
|
|
34
|
+
when '**'
|
|
35
|
+
pa = pattern_trds[1..]
|
|
36
|
+
pa_size = pa.size
|
|
37
|
+
|
|
38
|
+
domain_trds[domain_trds.size - pa_size, pa_size] == pa
|
|
39
|
+
else
|
|
40
|
+
name == pattern.name
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|