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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Options available in the Browser
|
|
5
|
+
class Browser
|
|
6
|
+
OPTIONS = %i[
|
|
7
|
+
cache_ttl
|
|
8
|
+
cookie_jar
|
|
9
|
+
cookie_string
|
|
10
|
+
connect_timeout
|
|
11
|
+
disable_tls_checks
|
|
12
|
+
headers
|
|
13
|
+
http_auth
|
|
14
|
+
max_threads
|
|
15
|
+
proxy
|
|
16
|
+
proxy_auth
|
|
17
|
+
random_user_agent
|
|
18
|
+
request_timeout
|
|
19
|
+
throttle
|
|
20
|
+
url
|
|
21
|
+
user_agent
|
|
22
|
+
user_agents_list
|
|
23
|
+
vhost
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
attr_accessor(*OPTIONS)
|
|
27
|
+
|
|
28
|
+
# @return [ String ]
|
|
29
|
+
def default_user_agent
|
|
30
|
+
"#{NS} v#{WPScan::VERSION}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [ Typhoeus::Hydra ]
|
|
34
|
+
def hydra
|
|
35
|
+
@hydra ||= Typhoeus::Hydra.new(max_concurrency: max_threads || 1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param [ Hash ] options
|
|
39
|
+
def load_options(options = {})
|
|
40
|
+
OPTIONS.each do |sym|
|
|
41
|
+
send("#{sym}=", options[sym]) if options.key?(sym)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set the threads attribute and update hydra accordinly
|
|
46
|
+
# If the throttle attribute is > 0, max_threads will be forced to 1
|
|
47
|
+
#
|
|
48
|
+
# @param [ Integer ] number
|
|
49
|
+
def max_threads=(number)
|
|
50
|
+
@max_threads = number.to_i.positive? && throttle.zero? ? number.to_i : 1
|
|
51
|
+
|
|
52
|
+
hydra.max_concurrency = @max_threads
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [ String ] The user agent
|
|
56
|
+
def user_agent
|
|
57
|
+
@user_agent ||= random_user_agent ? user_agents.sample : default_user_agent
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [ Array<String> ]
|
|
61
|
+
def user_agents
|
|
62
|
+
return @user_agents if @user_agents
|
|
63
|
+
|
|
64
|
+
@user_agents = []
|
|
65
|
+
|
|
66
|
+
# The user_agents_list is managed by the CLI options, with the default being
|
|
67
|
+
# APP_DIR/user_agents.txt
|
|
68
|
+
File.open(user_agents_list) do |f|
|
|
69
|
+
f.each do |line|
|
|
70
|
+
next if line == "\n" || line[0, 1] == '#'
|
|
71
|
+
|
|
72
|
+
@user_agents << line.chomp
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@user_agents
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param [ value ] The throttle time in milliseconds
|
|
80
|
+
#
|
|
81
|
+
# if value > 0, the max_threads will be set to 1
|
|
82
|
+
def throttle=(value)
|
|
83
|
+
@throttle = value.to_i.abs / 1000.0
|
|
84
|
+
|
|
85
|
+
self.max_threads = 1 if @throttle.positive?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def trottle!
|
|
89
|
+
sleep(throttle) if throttle.positive?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/wpscan/browser.rb
CHANGED
|
@@ -1,13 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'wpscan/browser/actions'
|
|
4
|
+
require 'wpscan/browser/options'
|
|
5
|
+
|
|
3
6
|
module WPScan
|
|
4
|
-
#
|
|
5
|
-
class Browser
|
|
7
|
+
# Singleton used to perform HTTP/HTTPS requests to the target.
|
|
8
|
+
class Browser
|
|
6
9
|
extend Actions
|
|
7
10
|
|
|
11
|
+
def initialize(parsed_options = {})
|
|
12
|
+
self.throttle = 0
|
|
13
|
+
|
|
14
|
+
load_options(parsed_options.dup)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private_class_method :new
|
|
18
|
+
|
|
19
|
+
# @param [ Hash ] parsed_options
|
|
20
|
+
#
|
|
21
|
+
# @return [ Browser ] The instance
|
|
22
|
+
def self.instance(parsed_options = {})
|
|
23
|
+
@@instance ||= new(parsed_options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.reset
|
|
27
|
+
@@instance = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
# @return [ String ]
|
|
9
31
|
def default_user_agent
|
|
10
32
|
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.com/wordpress-security-scanner)"
|
|
11
33
|
end
|
|
34
|
+
|
|
35
|
+
# @param [ String ] url
|
|
36
|
+
# @param [ Hash ] params
|
|
37
|
+
#
|
|
38
|
+
# @return [ Typhoeus::Request ]
|
|
39
|
+
def forge_request(url, params = {})
|
|
40
|
+
Typhoeus::Request.new(url, request_params(params))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [ Hash ] The request params used to connect to the target as well as other systems (e.g. API).
|
|
44
|
+
def default_connect_request_params
|
|
45
|
+
params = {}
|
|
46
|
+
|
|
47
|
+
if disable_tls_checks
|
|
48
|
+
# See http://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
|
|
49
|
+
params[:ssl_verifypeer] = false
|
|
50
|
+
params[:ssl_verifyhost] = 0
|
|
51
|
+
# TLSv1.0 and plus, allows to use a protocol potentially lower than the OS default
|
|
52
|
+
params[:sslversion] = :tlsv1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
connecttimeout: :connect_timeout, cache_ttl: :cache_ttl,
|
|
57
|
+
proxy: :proxy, timeout: :request_timeout
|
|
58
|
+
}.each do |typhoeus_opt, browser_opt|
|
|
59
|
+
attr_value = public_send(browser_opt)
|
|
60
|
+
params[typhoeus_opt] = attr_value unless attr_value.nil?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
params
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [ Hash ]
|
|
67
|
+
# The params are not cached (using @params ||= for example) so they are set accordingly if updated
|
|
68
|
+
# by a controller / other piece of code.
|
|
69
|
+
def default_request_params
|
|
70
|
+
params = default_connect_request_params.merge(
|
|
71
|
+
headers: { 'User-Agent' => user_agent, 'Referer' => url }.merge(headers || {}),
|
|
72
|
+
accept_encoding: 'gzip, deflate',
|
|
73
|
+
method: :get
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
{ cookiejar: :cookie_jar, cookiefile: :cookie_jar, cookie: :cookie_string }.each do |typhoeus_opt, browser_opt|
|
|
77
|
+
attr_value = public_send(browser_opt)
|
|
78
|
+
params[typhoeus_opt] = attr_value unless attr_value.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
params[:proxyuserpwd] = "#{proxy_auth[:username]}:#{proxy_auth[:password]}" if proxy_auth
|
|
82
|
+
params[:userpwd] = "#{http_auth[:username]}:#{http_auth[:password]}" if http_auth
|
|
83
|
+
|
|
84
|
+
params[:headers]['Host'] = vhost if vhost
|
|
85
|
+
|
|
86
|
+
params
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param [ Hash ] params
|
|
90
|
+
#
|
|
91
|
+
# @return [ Hash ]
|
|
92
|
+
def request_params(params = {})
|
|
93
|
+
default_request_params.merge(params) do |key, oldval, newval|
|
|
94
|
+
key == :headers ? oldval.merge(newval) : newval
|
|
95
|
+
end
|
|
96
|
+
end
|
|
12
97
|
end
|
|
13
98
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ferrum'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
module BrowserAuthenticator
|
|
7
|
+
# Characters that, if present in a cookie name or value, would corrupt the
|
|
8
|
+
# serialized Cookie header. Per RFC 6265 these are forbidden in cookie-octets,
|
|
9
|
+
# but a noncompliant IdP could still emit them.
|
|
10
|
+
COOKIE_DELIMITERS = /[;,\s]/
|
|
11
|
+
|
|
12
|
+
def self.authenticate(login_url)
|
|
13
|
+
unless $stdin.tty?
|
|
14
|
+
raise WPScan::Error::BrowserFailed,
|
|
15
|
+
'SAML authentication needs an interactive terminal to wait for login, but stdin is not a TTY. ' \
|
|
16
|
+
'Run wpscan from a real shell when using --expect-saml.'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
cookies = run_login_session(login_url)
|
|
20
|
+
|
|
21
|
+
raise WPScan::Error::SAMLAuthenticationFailed if cookies.nil? || cookies.empty?
|
|
22
|
+
|
|
23
|
+
serialize_cookies(cookies)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Drives the interactive browser session and returns the resulting cookie jar.
|
|
27
|
+
# Translates Ferrum failures into BrowserFailed with a context-specific message.
|
|
28
|
+
def self.run_login_session(login_url)
|
|
29
|
+
browser = Ferrum::Browser.new(headless: false)
|
|
30
|
+
|
|
31
|
+
puts 'SAML authentication needed. Log in via the browser window that just opened, then press enter.'
|
|
32
|
+
browser.goto(login_url)
|
|
33
|
+
gets # Waits for user input
|
|
34
|
+
|
|
35
|
+
# Attempt an innocuous command to check if the browser is still responsive
|
|
36
|
+
browser.current_url
|
|
37
|
+
|
|
38
|
+
browser.cookies.all
|
|
39
|
+
rescue Ferrum::BinaryNotFoundError, Ferrum::EmptyPathError => e
|
|
40
|
+
raise WPScan::Error::BrowserFailed, chrome_not_found_message(e)
|
|
41
|
+
rescue Ferrum::Error => e
|
|
42
|
+
raise WPScan::Error::BrowserFailed,
|
|
43
|
+
'The browser was closed or failed before SAML authentication could be completed ' \
|
|
44
|
+
"(#{e.class}: #{e.message})."
|
|
45
|
+
ensure
|
|
46
|
+
browser.quit if browser&.process
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.chrome_not_found_message(error)
|
|
50
|
+
'--expect-saml requires Chrome or Chromium to be installed and available on PATH ' \
|
|
51
|
+
'(install Chrome / Chromium, or point Ferrum at a binary via BROWSER_PATH). ' \
|
|
52
|
+
"Underlying error: #{error.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.serialize_cookies(cookies)
|
|
56
|
+
cookies.map do |_name, cookie|
|
|
57
|
+
raise WPScan::Error::SAMLAuthenticationFailed if cookie.name.match?(COOKIE_DELIMITERS) ||
|
|
58
|
+
cookie.value.to_s.match?(COOKIE_DELIMITERS)
|
|
59
|
+
|
|
60
|
+
"#{cookie.name}=#{cookie.value}"
|
|
61
|
+
end.join('; ')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Cache
|
|
5
|
+
# Cache Implementation using files
|
|
6
|
+
class FileStore
|
|
7
|
+
attr_reader :storage_path, :serializer
|
|
8
|
+
|
|
9
|
+
# The serializer must have the 2 methods #load and #dump
|
|
10
|
+
# (Marshal and YAML have them)
|
|
11
|
+
# YAML is Human Readable, contrary to Marshal which store in a binary format
|
|
12
|
+
# Marshal does not need any "require"
|
|
13
|
+
#
|
|
14
|
+
# @param [ String ] storage_path
|
|
15
|
+
# @param [ Constant ] serializer
|
|
16
|
+
def initialize(storage_path, serializer = Marshal)
|
|
17
|
+
@storage_path = File.expand_path(storage_path)
|
|
18
|
+
@serializer = serializer
|
|
19
|
+
|
|
20
|
+
FileUtils.mkdir_p(@storage_path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# TODO: rename this to clear ?
|
|
24
|
+
def clean
|
|
25
|
+
Dir[File.join(storage_path, '*')].each do |f|
|
|
26
|
+
File.delete(f) unless File.symlink?(f)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [ String ] key
|
|
31
|
+
#
|
|
32
|
+
# @return [ Mixed ]
|
|
33
|
+
def read_entry(key)
|
|
34
|
+
return if expired_entry?(key)
|
|
35
|
+
|
|
36
|
+
serializer.load(File.read(entry_path(key)))
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param [ String ] key
|
|
42
|
+
# @param [ Mixed ] data_to_store
|
|
43
|
+
# @param [ Integer ] cache_ttl
|
|
44
|
+
def write_entry(key, data_to_store, cache_ttl)
|
|
45
|
+
return unless cache_ttl.to_i.positive?
|
|
46
|
+
|
|
47
|
+
File.write(entry_path(key), serializer.dump(data_to_store))
|
|
48
|
+
File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [ String ] key
|
|
52
|
+
#
|
|
53
|
+
# @return [ String ] The file path associated to the key
|
|
54
|
+
def entry_path(key)
|
|
55
|
+
File.join(storage_path, key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param [ String ] key
|
|
59
|
+
#
|
|
60
|
+
# @return [ String ] The expiration file path associated to the key
|
|
61
|
+
def entry_expiration_path(key)
|
|
62
|
+
"#{entry_path(key)}.expiration"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @param [ String ] key
|
|
68
|
+
#
|
|
69
|
+
# @return [ Boolean ]
|
|
70
|
+
def expired_entry?(key)
|
|
71
|
+
File.read(entry_expiration_path(key)).to_i <= Time.now.to_i
|
|
72
|
+
rescue StandardError
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'wpscan/cache/file_store'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
module Cache
|
|
7
|
+
# Cache implementation for Typhoeus
|
|
8
|
+
class Typhoeus < FileStore
|
|
9
|
+
# @param [ Typhoeus::Request ] request
|
|
10
|
+
#
|
|
11
|
+
# @return [ Typhoeus::Response ]
|
|
12
|
+
def get(request)
|
|
13
|
+
read_entry(request.hash.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [ Typhoeus::Request ] request
|
|
17
|
+
# @param [ Typhoeus::Response ] response
|
|
18
|
+
def set(request, response)
|
|
19
|
+
return if response.timed_out? || response.code&.zero?
|
|
20
|
+
|
|
21
|
+
write_entry(request.hash.to_s, response, request.cache_ttl)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/wpscan/controller.rb
CHANGED
|
@@ -1,10 +1,106 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
# Needed to load at least the Core controller
|
|
5
|
-
# Otherwise, the following error will be raised:
|
|
6
|
-
# `initialize': uninitialized constant WPScan::Controller::Core (NameError)
|
|
7
4
|
module Controller
|
|
8
|
-
|
|
5
|
+
# Base Controller
|
|
6
|
+
class Base
|
|
7
|
+
include OptParseValidator
|
|
8
|
+
|
|
9
|
+
# @return [ Array<OptParseValidator::OptBase> ]
|
|
10
|
+
def cli_options; end
|
|
11
|
+
|
|
12
|
+
def before_scan; end
|
|
13
|
+
|
|
14
|
+
def run; end
|
|
15
|
+
|
|
16
|
+
def after_scan; end
|
|
17
|
+
|
|
18
|
+
def ==(other)
|
|
19
|
+
self.class == other.class
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Reset all the class attributes. Currently only used in specs.
|
|
23
|
+
def self.reset
|
|
24
|
+
@@target = nil
|
|
25
|
+
@@datastore = nil
|
|
26
|
+
@@formatter = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [ Target ]
|
|
30
|
+
def target
|
|
31
|
+
@@target ||= WPScan::Target.new(WPScan::ParsedCli.url, WPScan::ParsedCli.options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param [ OptParseValidator::OptParser ] parser
|
|
35
|
+
def self.option_parser=(parser)
|
|
36
|
+
@@option_parser = parser
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [ OptParseValidator::OptParser ]
|
|
40
|
+
def option_parser
|
|
41
|
+
@@option_parser
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [ Hash ]
|
|
45
|
+
def datastore
|
|
46
|
+
@@datastore ||= {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [ Formatter::Base ]
|
|
50
|
+
def formatter
|
|
51
|
+
@@formatter ||= WPScan::Formatter.load(WPScan::ParsedCli.format, datastore[:views])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @see Formatter#output
|
|
55
|
+
#
|
|
56
|
+
# @return [ Void ]
|
|
57
|
+
def output(tpl, vars = {})
|
|
58
|
+
formatter.output(*tpl_params(tpl, vars))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @see Formatter#render
|
|
62
|
+
#
|
|
63
|
+
# @return [ String ]
|
|
64
|
+
def render(tpl, vars = {})
|
|
65
|
+
formatter.render(*tpl_params(tpl, vars))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [ Boolean ]
|
|
69
|
+
def user_interaction?
|
|
70
|
+
formatter.user_interaction? && !WPScan::ParsedCli.output
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [ Pathname ]
|
|
74
|
+
def tmp_directory
|
|
75
|
+
return Pathname.new(ENV['TMPDIR']).join(WPScan.app_name) if ENV['TMPDIR']
|
|
76
|
+
|
|
77
|
+
WPScan.user_cache_dir
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
# @param [ String ] tpl
|
|
83
|
+
# @param [ Hash ] vars
|
|
84
|
+
#
|
|
85
|
+
# @return [ Array<String> ]
|
|
86
|
+
def tpl_params(tpl, vars)
|
|
87
|
+
[
|
|
88
|
+
tpl,
|
|
89
|
+
instance_variable_values.merge(vars),
|
|
90
|
+
self.class.name.demodulize.underscore
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [ Hash ] All instance variable keys (and their values) plus the verbose value.
|
|
95
|
+
def instance_variable_values
|
|
96
|
+
h = { verbose: WPScan::ParsedCli.verbose }
|
|
97
|
+
instance_variables.each do |a|
|
|
98
|
+
s = a.to_s
|
|
99
|
+
n = s[1..s.size]
|
|
100
|
+
h[n.to_sym] = instance_variable_get(a)
|
|
101
|
+
end
|
|
102
|
+
h
|
|
103
|
+
end
|
|
104
|
+
end
|
|
9
105
|
end
|
|
10
106
|
end
|
data/lib/wpscan/controllers.rb
CHANGED
|
@@ -1,10 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
#
|
|
5
|
-
class Controllers <
|
|
4
|
+
# Controllers container. Summary width is 45 (wpscan-specific; upstream default was 40).
|
|
5
|
+
class Controllers < Array
|
|
6
|
+
attr_reader :option_parser, :running
|
|
7
|
+
|
|
8
|
+
# @param [ OptParseValidator::OptParser ] option_parser
|
|
6
9
|
def initialize(option_parser = OptParseValidator::OptParser.new(nil, 45))
|
|
7
|
-
|
|
10
|
+
@option_parser = option_parser
|
|
11
|
+
|
|
12
|
+
register_config_files
|
|
13
|
+
|
|
14
|
+
option_parser.config_files.result_key = 'cli_options'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Registers the potential option-file paths with the option_parser.
|
|
18
|
+
def register_config_files
|
|
19
|
+
# XDG Base Directory support for configuration
|
|
20
|
+
# https://specifications.freedesktop.org/basedir/latest/
|
|
21
|
+
xdg = ENV.fetch('XDG_CONFIG_HOME', nil)
|
|
22
|
+
xdg = Pathname.new(Dir.home).join('.config') if xdg.nil? || xdg.empty?
|
|
23
|
+
app = WPScan.app_name
|
|
24
|
+
|
|
25
|
+
dirs = [[xdg, app], [Dir.home, ".#{app}"], [Dir.pwd, ".#{app}"]]
|
|
26
|
+
exts = option_parser.config_files.class.supported_extensions
|
|
27
|
+
|
|
28
|
+
dirs.product(exts).each do |(dir, sub), ext|
|
|
29
|
+
option_parser.config_files << Pathname.new(dir).join(sub, "scan.#{ext}").to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [ Controller::Base ] controller
|
|
34
|
+
#
|
|
35
|
+
# @return [ Controllers ] self
|
|
36
|
+
def <<(controller)
|
|
37
|
+
options = controller.cli_options
|
|
38
|
+
|
|
39
|
+
unless include?(controller)
|
|
40
|
+
option_parser.add(*options) if options
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Force the non-colored CLI formatter when ANSI escapes would be
|
|
47
|
+
# unwanted: writing to a file, piping to another process, or when the
|
|
48
|
+
# caller has set NO_COLOR (see https://no-color.org). Explicit
|
|
49
|
+
# --format choices are preserved.
|
|
50
|
+
def apply_no_colour_default
|
|
51
|
+
return if WPScan::ParsedCli.options[:format]
|
|
52
|
+
|
|
53
|
+
no_color = ENV.fetch('NO_COLOR', nil)
|
|
54
|
+
return unless WPScan::ParsedCli.output || !$stdout.tty? || (no_color && !no_color.empty?)
|
|
55
|
+
|
|
56
|
+
WPScan::ParsedCli.options[:format] = 'cli-no-colour'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run
|
|
60
|
+
WPScan::ParsedCli.options = option_parser.results
|
|
61
|
+
first.class.option_parser = option_parser # needed to output help on -h/--hh
|
|
62
|
+
|
|
63
|
+
apply_no_colour_default
|
|
64
|
+
redirect_output_to_file(WPScan::ParsedCli.output) if WPScan::ParsedCli.output
|
|
65
|
+
|
|
66
|
+
Timeout.timeout(WPScan::ParsedCli.max_scan_duration, WPScan::Error::MaxScanDurationReached) do
|
|
67
|
+
each(&:before_scan)
|
|
68
|
+
|
|
69
|
+
@running = true
|
|
70
|
+
|
|
71
|
+
each(&:run)
|
|
72
|
+
end
|
|
73
|
+
ensure
|
|
74
|
+
# The rescue prevents unfinished requests from raising, which would stop reverse_each from running.
|
|
75
|
+
# rubocop:disable Style/RescueModifier
|
|
76
|
+
WPScan::Browser.instance.hydra.abort rescue nil
|
|
77
|
+
# rubocop:enable Style/RescueModifier
|
|
78
|
+
|
|
79
|
+
# Reverse order: app/controllers/core#after_scan finishes the output and must be last.
|
|
80
|
+
# Guarantees stats are output even on error. after_scan runs only if scan was actually running
|
|
81
|
+
# (skipped on CLI error, -h/--hh/--version).
|
|
82
|
+
reverse_each(&:after_scan) if running
|
|
8
83
|
end
|
|
9
84
|
end
|
|
10
85
|
end
|
|
@@ -4,18 +4,14 @@ module WPScan
|
|
|
4
4
|
module DB
|
|
5
5
|
module DynamicFinders
|
|
6
6
|
class Base
|
|
7
|
-
# @return [
|
|
7
|
+
# @return [ Pathname ]
|
|
8
8
|
def self.df_file
|
|
9
|
-
@df_file ||= DB_DIR.join('dynamic_finders.yml')
|
|
9
|
+
@df_file ||= DB_DIR.join('dynamic_finders.yml')
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# @return [ Hash ]
|
|
13
13
|
def self.all_df_data
|
|
14
|
-
@all_df_data ||=
|
|
15
|
-
YAML.safe_load(File.read(df_file), permitted_classes: [Regexp])
|
|
16
|
-
else
|
|
17
|
-
YAML.safe_load(File.read(df_file), [Regexp])
|
|
18
|
-
end
|
|
14
|
+
@all_df_data ||= YAML.safe_load_file(df_file, permitted_classes: [Regexp])
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
# @return [ Array<Symbol> ]
|
|
@@ -66,7 +66,7 @@ module WPScan
|
|
|
66
66
|
# What about slugs such as js_composer which will be done as JsComposer, just like js-composer
|
|
67
67
|
constant_name = classify_slug(slug)
|
|
68
68
|
|
|
69
|
-
unless version_finder_module.
|
|
69
|
+
unless version_finder_module.const_defined?(constant_name, false)
|
|
70
70
|
version_finder_module.const_set(constant_name, Module.new)
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -92,7 +92,7 @@ module WPScan
|
|
|
92
92
|
|
|
93
93
|
next unless allowed_classes.include?(klass.to_sym)
|
|
94
94
|
|
|
95
|
-
created << if mod.
|
|
95
|
+
created << if mod.const_defined?(finder_class.to_sym, false)
|
|
96
96
|
mod.const_get(finder_class.to_sym)
|
|
97
97
|
else
|
|
98
98
|
version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
|
|
@@ -59,7 +59,7 @@ module WPScan
|
|
|
59
59
|
# So that, when new DF configs are put in the .yml
|
|
60
60
|
# users with old version of WPScan will still be able to scan blogs
|
|
61
61
|
# when updating the DB but not the tool
|
|
62
|
-
next if version_finder_module.
|
|
62
|
+
next if version_finder_module.const_defined?(finder_class.to_sym, false) ||
|
|
63
63
|
!allowed_classes.include?(klass.to_sym)
|
|
64
64
|
|
|
65
65
|
version_finder_super_class(klass).create_child_class(version_finder_module, finder_class.to_sym, config)
|
|
@@ -33,9 +33,9 @@ module WPScan
|
|
|
33
33
|
unique_fingerprints
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# @return [
|
|
36
|
+
# @return [ Pathname ]
|
|
37
37
|
def self.wp_fingerprints_path
|
|
38
|
-
@wp_fingerprints_path ||= DB_DIR.join('wp_fingerprints.json')
|
|
38
|
+
@wp_fingerprints_path ||= DB_DIR.join('wp_fingerprints.json')
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# @return [ Hash ]
|