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,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
class Finder
|
|
6
|
+
# Module to provide an easy way to perform password attacks
|
|
7
|
+
module BreadthFirstDictionaryAttack
|
|
8
|
+
# Tracks progress for password attack
|
|
9
|
+
class ProgressTracker
|
|
10
|
+
attr_reader :progress_bar
|
|
11
|
+
|
|
12
|
+
def initialize(progress_bar:, total_passwords:)
|
|
13
|
+
@progress_bar = progress_bar
|
|
14
|
+
@total_passwords = total_passwords
|
|
15
|
+
@user_requests_count = Hash.new(0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def record_request(username)
|
|
19
|
+
@user_requests_count[username] += 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def requests_for_user(username)
|
|
23
|
+
@user_requests_count[username]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update_title(username, password, retry_count, max_retries)
|
|
27
|
+
retry_info = retry_count.positive? ? " (retry #{retry_count}/#{max_retries})" : ''
|
|
28
|
+
@progress_bar.title = "Trying #{username} / #{password}#{retry_info}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def increment
|
|
32
|
+
@progress_bar.increment unless @progress_bar.progress == @progress_bar.total
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def adjust_total_on_success(username)
|
|
36
|
+
@progress_bar.total -= @total_passwords - @user_requests_count[username]
|
|
37
|
+
rescue ProgressBar::InvalidProgressError
|
|
38
|
+
# Due to Typhoeus threads, progress bar might be in invalid state
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def log(message)
|
|
42
|
+
@progress_bar.log(message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
@progress_bar.stop
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Configuration bundle for LoginAttempt
|
|
51
|
+
LoginConfig = Struct.new(:tracker, :request_builder, :credentials_validator,
|
|
52
|
+
:error_checker, :error_handler, :hydra, :opts,
|
|
53
|
+
keyword_init: true)
|
|
54
|
+
|
|
55
|
+
# Handles a single login attempt with retry support
|
|
56
|
+
class LoginAttempt
|
|
57
|
+
attr_reader :user, :password, :max_retries
|
|
58
|
+
attr_accessor :retry_count
|
|
59
|
+
|
|
60
|
+
def initialize(user:, password:, max_retries:, config:)
|
|
61
|
+
@user = user
|
|
62
|
+
@password = password
|
|
63
|
+
@max_retries = max_retries
|
|
64
|
+
@retry_count = 0
|
|
65
|
+
@config = config
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def execute(&block)
|
|
69
|
+
request = @config.request_builder.call(@user.username, @password)
|
|
70
|
+
@config.tracker.record_request(@user.username) if @retry_count.zero?
|
|
71
|
+
|
|
72
|
+
request.on_complete do |response|
|
|
73
|
+
handle_response(response, &block)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@config.hydra.queue(request)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def handle_response(response, &)
|
|
82
|
+
@config.tracker.update_title(@user.username, @password, @retry_count, @max_retries)
|
|
83
|
+
|
|
84
|
+
if @config.credentials_validator.call(response)
|
|
85
|
+
handle_success(&)
|
|
86
|
+
elsif @config.error_checker.call(response)
|
|
87
|
+
handle_error(response, &)
|
|
88
|
+
else
|
|
89
|
+
handle_invalid_credentials
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def handle_success
|
|
94
|
+
@config.tracker.increment
|
|
95
|
+
@user.password = @password
|
|
96
|
+
@config.tracker.adjust_total_on_success(@user.username)
|
|
97
|
+
|
|
98
|
+
yield @user if block_given?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_error(response, &)
|
|
102
|
+
if @user.password
|
|
103
|
+
@config.tracker.increment
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if should_retry?
|
|
108
|
+
retry_attempt(&)
|
|
109
|
+
else
|
|
110
|
+
finalize_failed_attempt(response)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def should_retry?
|
|
115
|
+
@retry_count < @max_retries
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def retry_attempt(&)
|
|
119
|
+
@retry_count += 1
|
|
120
|
+
|
|
121
|
+
if @config.opts[:show_progression] && WPScan::ParsedCli.verbose?
|
|
122
|
+
@config.tracker.log("[RETRY #{@retry_count}/#{@max_retries}] #{@user.username} / #{@password}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
execute(&)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def finalize_failed_attempt(response)
|
|
129
|
+
@config.error_handler.call(response)
|
|
130
|
+
@config.tracker.increment
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def handle_invalid_credentials
|
|
134
|
+
@config.tracker.increment
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param [ Array<WPScan::Model::User> ] users
|
|
139
|
+
# @param [ String ] wordlist_path
|
|
140
|
+
# @param [ Hash ] opts
|
|
141
|
+
# @option opts [ Boolean ] :show_progression
|
|
142
|
+
# @option opts [ Integer ] :wordlist_skip Number of passwords to skip from the beginning
|
|
143
|
+
# @option opts [ Integer ] :max_retries Maximum retry attempts for failed requests
|
|
144
|
+
#
|
|
145
|
+
# @yield [ WPScan::User ] When a valid combination is found
|
|
146
|
+
#
|
|
147
|
+
# Due to Typhoeus threads shenanigans, in rare cases the progress-bar might
|
|
148
|
+
# be incorrectly updated, hence the 'rescue ProgressBar::InvalidProgressError'
|
|
149
|
+
#
|
|
150
|
+
# TODO: Make rubocop happy about metrics etc
|
|
151
|
+
#
|
|
152
|
+
# rubocop:disable all
|
|
153
|
+
def attack(users, wordlist_path, opts = {})
|
|
154
|
+
wordlist = File.open(wordlist_path)
|
|
155
|
+
skip_count = opts[:wordlist_skip] || 0
|
|
156
|
+
max_retries = opts[:max_retries] || 0
|
|
157
|
+
|
|
158
|
+
# Calculate total, accounting for skipped passwords
|
|
159
|
+
total_passwords = wordlist.count
|
|
160
|
+
effective_passwords = [total_passwords - skip_count, 0].max
|
|
161
|
+
|
|
162
|
+
create_progress_bar(total: users.size * effective_passwords, show_progression: opts[:show_progression])
|
|
163
|
+
tracker = ProgressTracker.new(progress_bar: progress_bar, total_passwords: effective_passwords)
|
|
164
|
+
config = create_login_config(tracker, opts)
|
|
165
|
+
|
|
166
|
+
# Show skip progress if skipping
|
|
167
|
+
if skip_count.positive? && opts[:show_progression]
|
|
168
|
+
tracker.log("[INFO] Skipping first #{skip_count} password(s) from wordlist...")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
queue_count = 0
|
|
172
|
+
|
|
173
|
+
File.foreach(wordlist, chomp: true).lazy.drop(skip_count).each do |password|
|
|
174
|
+
remaining_users = users.select { |u| u.password.nil? }
|
|
175
|
+
break if remaining_users.empty?
|
|
176
|
+
|
|
177
|
+
remaining_users.each do |user|
|
|
178
|
+
attempt = LoginAttempt.new(user: user, password: password, max_retries: max_retries, config: config)
|
|
179
|
+
attempt.execute { |found_user| yield found_user if block_given? }
|
|
180
|
+
queue_count += 1
|
|
181
|
+
|
|
182
|
+
if queue_count >= hydra.max_concurrency
|
|
183
|
+
hydra.run
|
|
184
|
+
queue_count = 0
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
hydra.run
|
|
190
|
+
tracker.stop
|
|
191
|
+
end
|
|
192
|
+
# rubocop:enable all
|
|
193
|
+
|
|
194
|
+
# Create login configuration with all dependencies
|
|
195
|
+
#
|
|
196
|
+
# @param [ ProgressTracker ] tracker
|
|
197
|
+
# @param [ Hash ] opts
|
|
198
|
+
#
|
|
199
|
+
# @return [ LoginConfig ]
|
|
200
|
+
#
|
|
201
|
+
def create_login_config(tracker, opts)
|
|
202
|
+
LoginConfig.new(
|
|
203
|
+
tracker: tracker,
|
|
204
|
+
request_builder: method(:login_request).to_proc,
|
|
205
|
+
credentials_validator: method(:valid_credentials?).to_proc,
|
|
206
|
+
error_checker: method(:errored_response?).to_proc,
|
|
207
|
+
error_handler: method(:output_error).to_proc,
|
|
208
|
+
hydra: hydra,
|
|
209
|
+
opts: opts
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @param [ String ] username
|
|
214
|
+
# param [ String ] password
|
|
215
|
+
#
|
|
216
|
+
# @return [ Typhoeus::Request ]
|
|
217
|
+
def login_request(username, password)
|
|
218
|
+
# To Implement in the finder related to the attack
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @param [ Typhoeus::Response ] response
|
|
222
|
+
#
|
|
223
|
+
# @return [ Boolean ] Whether or not credentials related to the request are valid
|
|
224
|
+
def valid_credentials?(response)
|
|
225
|
+
# To Implement in the finder related to the attack
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @param [ Typhoeus::Response ] response
|
|
229
|
+
#
|
|
230
|
+
# @return [ Boolean ] Whether or not something wrong happened
|
|
231
|
+
# other than wrong credentials
|
|
232
|
+
def errored_response?(response)
|
|
233
|
+
# To Implement in the finder related to the attack
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
protected
|
|
237
|
+
|
|
238
|
+
# @param [ Typhoeus::Response ] response
|
|
239
|
+
def output_error(response)
|
|
240
|
+
error = if response.timed_out?
|
|
241
|
+
'Request timed out.'
|
|
242
|
+
elsif response.code.zero?
|
|
243
|
+
"No response from remote server. WAF/IPS? (#{response.return_message})"
|
|
244
|
+
elsif response.code.to_s.start_with?('50')
|
|
245
|
+
'Server error, try reducing the number of threads.'
|
|
246
|
+
elsif WPScan::ParsedCli.verbose?
|
|
247
|
+
"Unknown response received Code: #{response.code}\nBody: #{response.body}"
|
|
248
|
+
else
|
|
249
|
+
"Unknown response received Code: #{response.code}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
progress_bar.log("Error: #{error}")
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
class Finder
|
|
6
|
+
# Module to provide an easy way to enumerate items such as plugins, themes etc
|
|
7
|
+
module Enumerator
|
|
8
|
+
# @return [ Hash ]
|
|
9
|
+
def head_or_get_request_params
|
|
10
|
+
# Disabling the cache, as it causes a 'stack level too deep' exception
|
|
11
|
+
# with a large number of requests.
|
|
12
|
+
# See https://github.com/typhoeus/typhoeus/issues/408
|
|
13
|
+
@head_or_get_request_params ||= target.head_or_get_params.merge(cache_ttl: 0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [ Array<Integer> ]
|
|
17
|
+
def valid_response_codes
|
|
18
|
+
@valid_response_codes ||= [200]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param [ Hash ] The target urls
|
|
22
|
+
# @param [ Hash ] opts
|
|
23
|
+
# @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
|
|
24
|
+
# @option opts [ Regexp ] :exclude_content
|
|
25
|
+
# @option opts [ Boolean, Array, String ] :check_full_response
|
|
26
|
+
#
|
|
27
|
+
# @yield [ Typhoeus::Response, String ]
|
|
28
|
+
def enumerate(urls, opts = {})
|
|
29
|
+
create_progress_bar(opts.merge(total: urls.size))
|
|
30
|
+
|
|
31
|
+
urls.each do |url, id|
|
|
32
|
+
request = browser.forge_request(url, head_or_get_request_params)
|
|
33
|
+
|
|
34
|
+
request.on_complete do |head_res|
|
|
35
|
+
progress_bar.increment
|
|
36
|
+
|
|
37
|
+
next unless valid_response_codes.include?(head_res.code)
|
|
38
|
+
|
|
39
|
+
next if opts[:exclude_content] && head_res.response_headers&.match(opts[:exclude_content])
|
|
40
|
+
|
|
41
|
+
head_or_full_res = maybe_get_full_response(head_res, opts)
|
|
42
|
+
|
|
43
|
+
yield head_or_full_res, id if head_or_full_res
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
hydra.queue(request)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
hydra.run
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param [ Typhoeus::Response ] head_res
|
|
53
|
+
# @param [ Hash ] opts
|
|
54
|
+
#
|
|
55
|
+
# @return [ Typhoeus::Response, nil ]
|
|
56
|
+
def maybe_get_full_response(head_res, opts)
|
|
57
|
+
return head_res unless opts[:check_full_response] == true ||
|
|
58
|
+
Array(opts[:check_full_response]).include?(head_res.code)
|
|
59
|
+
|
|
60
|
+
full_res = WPScan::Browser.get(head_res.effective_url, full_request_params)
|
|
61
|
+
|
|
62
|
+
return unless valid_response_codes.include?(full_res.code)
|
|
63
|
+
|
|
64
|
+
return if target.homepage_or_404?(full_res) ||
|
|
65
|
+
(opts[:exclude_content] && full_res.body&.match(opts[:exclude_content]))
|
|
66
|
+
|
|
67
|
+
full_res
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [ Hash ]
|
|
71
|
+
def full_request_params
|
|
72
|
+
@full_request_params ||= {}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
class Finder
|
|
6
|
+
# Module to provide an easy way to fingerprint things such as versions
|
|
7
|
+
module Fingerprinter
|
|
8
|
+
include Enumerator
|
|
9
|
+
|
|
10
|
+
# @param [ Hash ] fingerprints The fingerprints
|
|
11
|
+
# Format should be like the following:
|
|
12
|
+
# {
|
|
13
|
+
# file_path_1: {
|
|
14
|
+
# md5_hash_1: version_1,
|
|
15
|
+
# md5_hash_2: [version_2]
|
|
16
|
+
# },
|
|
17
|
+
# file_path_2: {
|
|
18
|
+
# md5_hash_3: [version_1, version_2],
|
|
19
|
+
# md5_hash_4: version_3
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
# Note that the version can either be an array or a string
|
|
23
|
+
#
|
|
24
|
+
# @param [ Hash ] opts
|
|
25
|
+
# @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
|
|
26
|
+
#
|
|
27
|
+
# @yield [ Mixed, String, String ] version/s, url, hash The version associated to the
|
|
28
|
+
# fingerprint of the url
|
|
29
|
+
def fingerprint(fingerprints, opts = {})
|
|
30
|
+
enum_opts = opts.merge(check_full_response: 200)
|
|
31
|
+
|
|
32
|
+
enumerate(fingerprints.transform_keys { |k| target.url(k) }, enum_opts) do |res, fingerprint|
|
|
33
|
+
md5sum = hexdigest(res.body)
|
|
34
|
+
|
|
35
|
+
next unless fingerprint.key?(md5sum)
|
|
36
|
+
|
|
37
|
+
yield fingerprint[md5sum], res.effective_url, md5sum
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [ String ] The hashed value for the given body
|
|
42
|
+
def hexdigest(body)
|
|
43
|
+
Digest::MD5.hexdigest(body)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
class Finder
|
|
6
|
+
module SmartURLChecker
|
|
7
|
+
# Findings
|
|
8
|
+
class Findings < Array
|
|
9
|
+
def <<(finding)
|
|
10
|
+
return self unless finding
|
|
11
|
+
|
|
12
|
+
each do |f|
|
|
13
|
+
next unless f == finding && f.found_by == finding.found_by
|
|
14
|
+
|
|
15
|
+
# This makes sure entries added are unique
|
|
16
|
+
# and prevent pages redirecting to the same one to be added twice
|
|
17
|
+
entries_to_add = finding.interesting_entries - f.interesting_entries
|
|
18
|
+
return self if entries_to_add.empty?
|
|
19
|
+
|
|
20
|
+
entries_to_add.each { |entry| f.interesting_entries << entry }
|
|
21
|
+
|
|
22
|
+
f.confidence += finding.confidence
|
|
23
|
+
|
|
24
|
+
return self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'wpscan/finders/finder/smart_url_checker/findings'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
module Finders
|
|
7
|
+
class Finder
|
|
8
|
+
# Smart URL Checker
|
|
9
|
+
# Typically used when some URLs are potentially in the homepage. If they are found
|
|
10
|
+
# in it, they will be checked in the #passive (like a browser/client would do when loading the page).
|
|
11
|
+
# Otherwise they will be checked in the #aggressive
|
|
12
|
+
module SmartURLChecker
|
|
13
|
+
# @param [ Array<String> ] urls
|
|
14
|
+
# @param [ Hash ] opts
|
|
15
|
+
#
|
|
16
|
+
# @return []
|
|
17
|
+
def process_urls(_urls, _opts = {})
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param [ Hash ] opts
|
|
22
|
+
#
|
|
23
|
+
# @return [ Array<Finding> ]
|
|
24
|
+
def passive(opts = {})
|
|
25
|
+
process_urls(passive_urls(opts), opts)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param [ Hash ] opts
|
|
29
|
+
#
|
|
30
|
+
# @return [ Array<String> ]
|
|
31
|
+
def passive_urls(_opts = {})
|
|
32
|
+
target.in_scope_uris(target.homepage_res, passive_urls_xpath).map(&:to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [ String ]
|
|
36
|
+
def passive_urls_xpath
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param [ Hash ] opts
|
|
41
|
+
#
|
|
42
|
+
# @return [ Array<Finding> ]
|
|
43
|
+
def aggressive(opts = {})
|
|
44
|
+
# To avoid scanning the same twice
|
|
45
|
+
urls = aggressive_urls(opts)
|
|
46
|
+
urls -= passive_urls(opts) if opts[:mode] == :mixed
|
|
47
|
+
|
|
48
|
+
process_urls(urls, opts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [ Hash ] opts
|
|
52
|
+
#
|
|
53
|
+
# @return [ Array<String> ]
|
|
54
|
+
def aggressive_urls(_opts = {})
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -6,7 +6,7 @@ module WPScan
|
|
|
6
6
|
module WpVersion
|
|
7
7
|
# SmartURLChecker specific for the WP Version
|
|
8
8
|
module SmartURLChecker
|
|
9
|
-
include
|
|
9
|
+
include WPScan::Finders::Finder::SmartURLChecker
|
|
10
10
|
|
|
11
11
|
def create_version(number, opts = {})
|
|
12
12
|
Model::WpVersion.new(
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'wpscan/finders/finder/smart_url_checker'
|
|
4
|
+
require 'wpscan/finders/finder/enumerator'
|
|
5
|
+
require 'wpscan/finders/finder/fingerprinter'
|
|
6
|
+
require 'wpscan/finders/finder/breadth_first_dictionary_attack'
|
|
7
|
+
|
|
8
|
+
module WPScan
|
|
9
|
+
module Finders
|
|
10
|
+
# Finder
|
|
11
|
+
class Finder
|
|
12
|
+
# Constants for common found_by
|
|
13
|
+
DIRECT_ACCESS = 'Direct Access (Aggressive Detection)'
|
|
14
|
+
|
|
15
|
+
attr_accessor :target, :progress_bar
|
|
16
|
+
|
|
17
|
+
def initialize(target)
|
|
18
|
+
@target = target
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [ String ] The titleized name of the finder
|
|
22
|
+
def titleize
|
|
23
|
+
# Put a _ char before any digits except those at the end, which will be replaced by a space
|
|
24
|
+
# Otherwise, class such as Error404Page are returned as Error404 Page instead of Error 404 page
|
|
25
|
+
# The keep_id_suffix is to concevert classes such as CssId to Css Id instead of Css
|
|
26
|
+
|
|
27
|
+
@titleize ||= self.class.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [ Hash ] _opts
|
|
31
|
+
def passive(_opts = {}); end
|
|
32
|
+
|
|
33
|
+
# @param [ Hash ] _opts
|
|
34
|
+
def aggressive(_opts = {}); end
|
|
35
|
+
|
|
36
|
+
# @param [ Hash ] opts See https://github.com/jfelchner/ruby-progressbar/wiki/Options
|
|
37
|
+
# @option opts [ Boolean ] :show_progression
|
|
38
|
+
#
|
|
39
|
+
# @return [ ProgressBar::Base ]
|
|
40
|
+
def create_progress_bar(opts = {})
|
|
41
|
+
bar_opts = { format: '%t %a <%B> (%c / %C) %P%% %e' }
|
|
42
|
+
bar_opts[:output] = ProgressBarNullOutput unless opts[:show_progression]
|
|
43
|
+
|
|
44
|
+
@progress_bar = ::ProgressBar.create(bar_opts.merge(opts))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [ Browser ]
|
|
48
|
+
def browser
|
|
49
|
+
@browser ||= WPScan::Browser.instance
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [ Typhoeus::Hydra ]
|
|
53
|
+
def hydra
|
|
54
|
+
@hydra ||= browser.hydra
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param [String, Class ] klass
|
|
58
|
+
# @return [ String ]
|
|
59
|
+
def found_by(klass = self.class)
|
|
60
|
+
labels = %w[aggressive passive]
|
|
61
|
+
|
|
62
|
+
caller_locations.each do |call|
|
|
63
|
+
label = call.label
|
|
64
|
+
# Since ruby 3.4, the label contains the full name, including module and class
|
|
65
|
+
# rather than just the method like in ruby < 3.4
|
|
66
|
+
label = label[/#(.*)/i, 1] if label.include?('#')
|
|
67
|
+
|
|
68
|
+
next unless labels.include? label
|
|
69
|
+
|
|
70
|
+
title = klass.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
|
|
71
|
+
|
|
72
|
+
return "#{title} (#{label.capitalize} Detection)"
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# Finding
|
|
6
|
+
module Finding
|
|
7
|
+
# Fix for "Double/Dynamic Inclusion Problem"
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.include References
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
FINDING_OPTS = %i[confidence confirmed_by references found_by interesting_entries].freeze
|
|
14
|
+
|
|
15
|
+
attr_accessor(*FINDING_OPTS)
|
|
16
|
+
|
|
17
|
+
# @return [ Array ]
|
|
18
|
+
def confirmed_by
|
|
19
|
+
@confirmed_by ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Should be overriden in child classes
|
|
23
|
+
# @return [ Array ]
|
|
24
|
+
def interesting_entries
|
|
25
|
+
@interesting_entries ||= []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [ Integer ]
|
|
29
|
+
def confidence
|
|
30
|
+
@confidence ||= 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [ Integer ] value
|
|
34
|
+
def confidence=(value)
|
|
35
|
+
@confidence = [value, 100].min
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param [ Hash ] opts
|
|
39
|
+
def parse_finding_options(opts = {})
|
|
40
|
+
FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# TODO: maybe also check for interesting_entries and confirmed_by ?
|
|
44
|
+
# So far this is used in specs only
|
|
45
|
+
def eql?(other)
|
|
46
|
+
self == other && confidence == other.confidence && found_by == other.found_by
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def <=>(other)
|
|
50
|
+
to_s.downcase <=> other.to_s.downcase
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# Findings container
|
|
6
|
+
class Findings < Array
|
|
7
|
+
# Optional callable invoked with each newly-appended finding (not invoked
|
|
8
|
+
# when a duplicate is merged into an existing one via confirmed_by). Used
|
|
9
|
+
# by enumeration controllers to stream findings as they are discovered.
|
|
10
|
+
attr_accessor :on_append
|
|
11
|
+
|
|
12
|
+
# Override to include the confirmed_by logic
|
|
13
|
+
#
|
|
14
|
+
# @param [ Finding ] finding
|
|
15
|
+
def <<(finding)
|
|
16
|
+
return self unless finding
|
|
17
|
+
|
|
18
|
+
each do |found|
|
|
19
|
+
next unless found == finding
|
|
20
|
+
|
|
21
|
+
found.confirmed_by << finding
|
|
22
|
+
found.confidence += finding.confidence
|
|
23
|
+
|
|
24
|
+
return self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
super
|
|
28
|
+
@on_append&.call(finding)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Finders
|
|
5
|
+
# Independent Finder
|
|
6
|
+
module IndependentFinder
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
# See ActiveSupport::Concern
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def find(target, opts = {}, &)
|
|
12
|
+
new(target).find(opts, &)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [ Hash ] opts
|
|
17
|
+
# @option opts [ Symbol ] mode (:mixed, :passive, :aggressive)
|
|
18
|
+
# @yield [ Finding ] Optional block called for each finding the moment
|
|
19
|
+
# it is first appended to the result set (used to
|
|
20
|
+
# stream enumeration findings as they are discovered).
|
|
21
|
+
#
|
|
22
|
+
# @return [ Findings ]
|
|
23
|
+
def find(opts = {}, &)
|
|
24
|
+
finders.run(opts, &)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [ Array ]
|
|
28
|
+
def finders
|
|
29
|
+
@finders ||= WPScan::Finders::IndependentFinders.new
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|