wpscan 3.8.28 → 4.0.1
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_items/urls_in_page.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 +4 -4
- 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 +116 -14
- data/app/models/wp_version.rb +5 -3
- 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 +25 -9
- 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 +130 -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 +157 -5
- 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,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
class Target < WebSite
|
|
5
|
+
module Server
|
|
6
|
+
# Generic Server methods
|
|
7
|
+
module Generic
|
|
8
|
+
# @param [ String ] path
|
|
9
|
+
# @param [ Hash ] params The request params
|
|
10
|
+
#
|
|
11
|
+
# @return [ Symbol ] The detected remote server (:Apache, :IIS, :Nginx)
|
|
12
|
+
def server(path = nil, params = {})
|
|
13
|
+
headers = headers(path, params)
|
|
14
|
+
|
|
15
|
+
return unless headers
|
|
16
|
+
|
|
17
|
+
case headers[:server]
|
|
18
|
+
when /\Aapache/i
|
|
19
|
+
:Apache
|
|
20
|
+
when /\AMicrosoft-IIS/i
|
|
21
|
+
:IIS
|
|
22
|
+
when /\Anginx/
|
|
23
|
+
:Nginx
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param [ String ] path
|
|
28
|
+
# @param [ Hash ] params The request params
|
|
29
|
+
#
|
|
30
|
+
# @return [ Hash ] The headers
|
|
31
|
+
def headers(path = nil, params = {})
|
|
32
|
+
# The HEAD method might be rejected by some servers ... maybe switch to GET ?
|
|
33
|
+
WPScan::Browser.head(url(path), params).headers
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param [ String ] path
|
|
37
|
+
# @param [ Hash ] params The request params
|
|
38
|
+
#
|
|
39
|
+
# @return [ Boolean ] true if url(path) has the directory
|
|
40
|
+
# listing enabled, false otherwise
|
|
41
|
+
def directory_listing?(path = nil, params = {})
|
|
42
|
+
res = WPScan::Browser.get(url(path), params)
|
|
43
|
+
|
|
44
|
+
res.code == 200 && res.body.include?('<h1>Index of')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param [ String ] path
|
|
48
|
+
# @param [ Hash ] params The request params
|
|
49
|
+
# @param [ String ] selector
|
|
50
|
+
# @param [ Regexp ] ignore
|
|
51
|
+
#
|
|
52
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
|
53
|
+
# or an empty array if none
|
|
54
|
+
def directory_listing_entries(path = nil, params = {}, selector = 'pre a', ignore = /parent directory/i)
|
|
55
|
+
return [] unless directory_listing?(path, params)
|
|
56
|
+
|
|
57
|
+
found = []
|
|
58
|
+
|
|
59
|
+
WPScan::Browser.get(url(path), params).html.css(selector).each do |node|
|
|
60
|
+
entry = node.text.to_s
|
|
61
|
+
|
|
62
|
+
next if entry&.match?(ignore)
|
|
63
|
+
|
|
64
|
+
found << entry
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
found
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
class Target < WebSite
|
|
5
|
+
module Server
|
|
6
|
+
# Some IIS specific implementation
|
|
7
|
+
module IIS
|
|
8
|
+
# @param [ String ] path
|
|
9
|
+
# @param [ Hash ] params The request params
|
|
10
|
+
#
|
|
11
|
+
# @return [ Symbol ] :IIS
|
|
12
|
+
def server(_path = nil, _params = {})
|
|
13
|
+
:IIS
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [ String ] path
|
|
17
|
+
# @param [ Hash ] params The request params
|
|
18
|
+
#
|
|
19
|
+
# @return [ Boolean ] true if url(path) has the directory
|
|
20
|
+
# listing enabled, false otherwise
|
|
21
|
+
def directory_listing?(path = nil, params = {})
|
|
22
|
+
res = WPScan::Browser.get(url(path), params)
|
|
23
|
+
|
|
24
|
+
res.code == 200 && res.body =~ %r{<H1>#{uri.host} - /} ? true : false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
class Target < WebSite
|
|
5
|
+
module Server
|
|
6
|
+
# Some Nginx specific implementation
|
|
7
|
+
module Nginx
|
|
8
|
+
# @param [ String ] path
|
|
9
|
+
# @param [ Hash ] params The request params
|
|
10
|
+
#
|
|
11
|
+
# @return [ Symbol ] :Nginx
|
|
12
|
+
def server(_path = nil, _params = {})
|
|
13
|
+
:Nginx
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [ String ] path
|
|
17
|
+
# @param [ Hash ] params The request params
|
|
18
|
+
#
|
|
19
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
|
20
|
+
# or an empty array if none
|
|
21
|
+
def directory_listing_entries(path = nil, params = {})
|
|
22
|
+
super(path, params, 'pre a', /\A\.\./i)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/wpscan/target.rb
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'wpscan/web_site'
|
|
4
|
+
require 'wpscan/target/platform'
|
|
5
|
+
require 'wpscan/target/server'
|
|
6
|
+
require 'wpscan/target/scope'
|
|
7
|
+
require 'wpscan/target/hashes'
|
|
3
8
|
require 'wpscan/target/platform/wordpress'
|
|
4
9
|
|
|
5
10
|
module WPScan
|
|
6
|
-
#
|
|
7
|
-
class Target <
|
|
11
|
+
# Target to Scan (WordPress).
|
|
12
|
+
class Target < WebSite
|
|
13
|
+
include Server::Generic
|
|
8
14
|
include Platform::WordPress
|
|
9
15
|
|
|
16
|
+
# @param [ String ] url
|
|
17
|
+
# @param [ Hash ] opts
|
|
18
|
+
# @option opts [ Array<PublicSuffix::Domain, String> ] :scope
|
|
19
|
+
def initialize(url, opts = {})
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
scope << uri.host
|
|
23
|
+
Array(opts[:scope]).each { |s| scope << s }
|
|
24
|
+
end
|
|
25
|
+
|
|
10
26
|
# @return [ Hash ]
|
|
11
27
|
def head_or_get_request_params
|
|
12
28
|
@head_or_get_request_params ||= if Browser.head(url).code == 405
|
|
@@ -30,9 +46,100 @@ module WPScan
|
|
|
30
46
|
false
|
|
31
47
|
end
|
|
32
48
|
|
|
49
|
+
# @param [ Hash ] opts
|
|
50
|
+
#
|
|
51
|
+
# @return [ Findings ]
|
|
52
|
+
def interesting_findings(opts = {})
|
|
53
|
+
@interesting_findings ||= WPScan::Finders::InterestingFindings::Base.find(self, opts)
|
|
54
|
+
end
|
|
55
|
+
|
|
33
56
|
# @return [ XMLRPC, nil ]
|
|
34
57
|
def xmlrpc
|
|
35
|
-
@xmlrpc ||= interesting_findings&.
|
|
58
|
+
@xmlrpc ||= interesting_findings&.grep(Model::XMLRPC)&.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [ Regexp ] The pattern related to the target url, also matches escaped /, such as
|
|
62
|
+
# in JSON JS data: http:\/\/t.com\/
|
|
63
|
+
def url_pattern
|
|
64
|
+
@url_pattern ||= Regexp.new(Regexp.escape(url).gsub(/https?/i, 'https?').gsub('/', '\\\\\?/'), Regexp::IGNORECASE)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param [ String ] xpath
|
|
68
|
+
# @param [ Regexp ] pattern
|
|
69
|
+
# @param [ Typhoeus::Response, String ] page
|
|
70
|
+
#
|
|
71
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
|
|
72
|
+
# @yield [ MatchData, Nokogiri::XML::Element ]
|
|
73
|
+
def xpath_pattern_from_page(xpath, pattern, page = nil)
|
|
74
|
+
page = WPScan::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
|
|
75
|
+
matches = []
|
|
76
|
+
|
|
77
|
+
page.html.xpath(xpath).each do |node|
|
|
78
|
+
next unless node.text.strip =~ pattern
|
|
79
|
+
|
|
80
|
+
yield Regexp.last_match, node if block_given?
|
|
81
|
+
|
|
82
|
+
matches << [Regexp.last_match, node]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
matches
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param [ Regexp ] pattern
|
|
89
|
+
# @param [ Typhoeus::Response, String ] page
|
|
90
|
+
#
|
|
91
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Comment>> ]
|
|
92
|
+
# @yield [ MatchData, Nokogiri::XML::Comment ]
|
|
93
|
+
def comments_from_page(pattern, page = nil)
|
|
94
|
+
xpath_pattern_from_page('//comment()', pattern, page) do |match, node|
|
|
95
|
+
yield match, node if block_given?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param [ Regexp ] pattern
|
|
100
|
+
# @param [ Typhoeus::Response, String ] page
|
|
101
|
+
#
|
|
102
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
|
|
103
|
+
# @yield [ MatchData, Nokogiri::XML::Element ]
|
|
104
|
+
def javascripts_from_page(pattern, page = nil)
|
|
105
|
+
xpath_pattern_from_page('//script', pattern, page) do |match, node|
|
|
106
|
+
yield match, node if block_given?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param [ Typhoeus::Response, String ] page
|
|
111
|
+
# @param [ String ] xpath
|
|
112
|
+
#
|
|
113
|
+
# @yield [ Addressable::URI, Nokogiri::XML::Element ] The url and its associated tag
|
|
114
|
+
#
|
|
115
|
+
# @return [ Array<Addressable::URI> ] The absolute URIs detected in the response's body from the HTML tags
|
|
116
|
+
#
|
|
117
|
+
# @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
|
|
118
|
+
# time consuming when there are a lof of uris to check
|
|
119
|
+
def uris_from_page(page = nil, xpath = '//@href|//@src|//@data-src')
|
|
120
|
+
page = WPScan::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
|
|
121
|
+
found = []
|
|
122
|
+
|
|
123
|
+
page.html.xpath(xpath).each do |node|
|
|
124
|
+
attr_value = node.text.to_s
|
|
125
|
+
|
|
126
|
+
next unless attr_value && !attr_value.empty?
|
|
127
|
+
|
|
128
|
+
node_uri = begin
|
|
129
|
+
uri.join(attr_value.strip)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
# Skip potential malformed URLs etc.
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
next unless node_uri.host
|
|
136
|
+
|
|
137
|
+
yield node_uri, node.parent if block_given? && !found.include?(node_uri)
|
|
138
|
+
|
|
139
|
+
found << node_uri
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
found.uniq
|
|
36
143
|
end
|
|
37
144
|
|
|
38
145
|
# @param [ Hash ] opts
|
|
@@ -54,17 +161,21 @@ module WPScan
|
|
|
54
161
|
end
|
|
55
162
|
|
|
56
163
|
# @param [ Hash ] opts
|
|
164
|
+
# @yield [ Plugin ] Optional block called as each plugin is first
|
|
165
|
+
# discovered (used to stream findings).
|
|
57
166
|
#
|
|
58
167
|
# @return [ Array<Plugin> ]
|
|
59
|
-
def plugins(opts = {})
|
|
60
|
-
@plugins ||= Finders::Plugins::Base.find(self, opts)
|
|
168
|
+
def plugins(opts = {}, &)
|
|
169
|
+
@plugins ||= Finders::Plugins::Base.find(self, opts, &)
|
|
61
170
|
end
|
|
62
171
|
|
|
63
172
|
# @param [ Hash ] opts
|
|
173
|
+
# @yield [ Theme ] Optional block called as each theme is first
|
|
174
|
+
# discovered (used to stream findings).
|
|
64
175
|
#
|
|
65
176
|
# @return [ Array<Theme> ]
|
|
66
|
-
def themes(opts = {})
|
|
67
|
-
@themes ||= Finders::Themes::Base.find(self, opts)
|
|
177
|
+
def themes(opts = {}, &)
|
|
178
|
+
@themes ||= Finders::Themes::Base.find(self, opts, &)
|
|
68
179
|
end
|
|
69
180
|
|
|
70
181
|
# @param [ Hash ] opts
|
|
@@ -88,6 +199,13 @@ module WPScan
|
|
|
88
199
|
@db_exports ||= Finders::DbExports::Base.find(self, opts)
|
|
89
200
|
end
|
|
90
201
|
|
|
202
|
+
# @param [ Hash ] opts
|
|
203
|
+
#
|
|
204
|
+
# @return [ Array<BackupFolder> ]
|
|
205
|
+
def backup_folders(opts = {})
|
|
206
|
+
@backup_folders ||= Finders::BackupFolders::Base.find(self, opts)
|
|
207
|
+
end
|
|
208
|
+
|
|
91
209
|
# @param [ Hash ] opts
|
|
92
210
|
#
|
|
93
211
|
# @return [ Array<Media> ]
|
|
@@ -96,10 +214,12 @@ module WPScan
|
|
|
96
214
|
end
|
|
97
215
|
|
|
98
216
|
# @param [ Hash ] opts
|
|
217
|
+
# @yield [ User ] Optional block called as each user is first
|
|
218
|
+
# discovered (used to stream findings).
|
|
99
219
|
#
|
|
100
220
|
# @return [ Array<User> ]
|
|
101
|
-
def users(opts = {})
|
|
102
|
-
@users ||= Finders::Users::Base.find(self, opts)
|
|
221
|
+
def users(opts = {}, &)
|
|
222
|
+
@users ||= Finders::Users::Base.find(self, opts, &)
|
|
103
223
|
end
|
|
104
224
|
end
|
|
105
225
|
end
|
|
@@ -3,10 +3,33 @@
|
|
|
3
3
|
module Typhoeus
|
|
4
4
|
# Custom Response class
|
|
5
5
|
class Response
|
|
6
|
-
# @
|
|
6
|
+
# @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::HTML
|
|
7
|
+
def html
|
|
8
|
+
@html ||= Nokogiri::HTML(body.encode('UTF-8', invalid: :replace, undef: :replace))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::XML
|
|
12
|
+
def xml
|
|
13
|
+
@xml ||= Nokogiri::XML(body.encode('UTF-8', invalid: :replace, undef: :replace))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Override of the original to ensure an integer is returned
|
|
17
|
+
# @return [ Integer ]
|
|
18
|
+
def request_size
|
|
19
|
+
super || 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [ Integer ]
|
|
23
|
+
def size
|
|
24
|
+
(body.nil? ? 0 : body.size) + (response_headers.nil? ? 0 : response_headers.size)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @note Ignores requests done to the /status endpoint of the WPScan Vuln API.
|
|
7
28
|
#
|
|
8
29
|
# @return [ Boolean ]
|
|
9
30
|
def from_vuln_api?
|
|
31
|
+
return false unless effective_url
|
|
32
|
+
|
|
10
33
|
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) &&
|
|
11
34
|
!effective_url.start_with?(WPScan::DB::VulnApi.uri.join('status').to_s)
|
|
12
35
|
end
|
data/lib/wpscan/version.rb
CHANGED
data/lib/wpscan/vulnerability.rb
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
#
|
|
5
|
-
class Vulnerability
|
|
4
|
+
# Vulnerability model.
|
|
5
|
+
class Vulnerability
|
|
6
6
|
include References
|
|
7
7
|
|
|
8
|
+
attr_reader :title, :type, :affected_versions, :cvss, :poc, :uuid
|
|
9
|
+
|
|
10
|
+
# Version of the scanned item (plugin/theme/WordPress), used to resolve
|
|
11
|
+
# which backported range to surface as the scalar fixed_in / introduced_in.
|
|
12
|
+
# Left nil/false when the version could not be detected.
|
|
13
|
+
attr_accessor :detected_version
|
|
14
|
+
|
|
15
|
+
# @param [ String ] title
|
|
16
|
+
# @param [ Hash ] references
|
|
17
|
+
# @option references [ Array<String>, String ] :cve
|
|
18
|
+
# @option references [ Array<String>, String ] :secunia
|
|
19
|
+
# @option references [ Array<String>, String ] :osvdb
|
|
20
|
+
# @option references [ Array<String>, String ] :exploitdb
|
|
21
|
+
# @option references [ Array<String> ] :url URL(s) to related advisories etc
|
|
22
|
+
# @option references [ Array<String>, String ] :metasploit The related metasploit module(s)
|
|
23
|
+
# @option references [ Array<String> ] :youtube
|
|
24
|
+
# @param [ String ] type
|
|
25
|
+
# @param [ Array<Hash> ] affected_versions One entry per backported fix range,
|
|
26
|
+
# each with :fixed_in and :introduced_in keys (either may be nil).
|
|
27
|
+
# @param [ HashSymbol ] cvss
|
|
28
|
+
# @option cvss [ String ] :score
|
|
29
|
+
# @option cvss [ String ] :vector
|
|
30
|
+
# rubocop:disable Metrics/ParameterLists
|
|
31
|
+
def initialize(title, references: {}, type: nil, affected_versions: nil, cvss: nil, poc: nil, uuid: nil)
|
|
32
|
+
# rubocop:enable Metrics/ParameterLists
|
|
33
|
+
@title = title
|
|
34
|
+
@type = type
|
|
35
|
+
@affected_versions = normalize_affected_versions(affected_versions)
|
|
36
|
+
@cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
|
|
37
|
+
@poc = poc
|
|
38
|
+
@uuid = uuid
|
|
39
|
+
@detected_version = nil
|
|
40
|
+
|
|
41
|
+
self.references = references
|
|
42
|
+
end
|
|
43
|
+
|
|
8
44
|
# @param [ Hash ] json_data
|
|
9
45
|
# @return [ Vulnerability ]
|
|
10
46
|
def self.load_from_json(json_data)
|
|
@@ -20,10 +56,126 @@ module WPScan
|
|
|
20
56
|
json_data['title'],
|
|
21
57
|
references: references,
|
|
22
58
|
type: json_data['vuln_type'],
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
affected_versions: json_data['affected_versions'],
|
|
60
|
+
cvss: json_data['cvss']&.symbolize_keys,
|
|
61
|
+
poc: json_data['poc'],
|
|
62
|
+
uuid: json_data['id'].to_s # The 'id' field IS the UUID in WPScan API
|
|
26
63
|
)
|
|
27
64
|
end
|
|
65
|
+
|
|
66
|
+
# Whether this vulnerability covers a given version. True when no
|
|
67
|
+
# affected_versions are declared (advisory with no fix metadata) or when at
|
|
68
|
+
# least one backported range covers the version.
|
|
69
|
+
#
|
|
70
|
+
# @param [ #< ] version
|
|
71
|
+
#
|
|
72
|
+
# @return [ Boolean ]
|
|
73
|
+
def affects?(version)
|
|
74
|
+
return true if affected_versions.empty?
|
|
75
|
+
|
|
76
|
+
affected_versions.any? { |range| self.class.range_covers?(range, version) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Backward-compatible scalar: the fixed_in of the range relevant to the
|
|
80
|
+
# detected version (the branch the scanned site is actually on). nil when
|
|
81
|
+
# there is no fix metadata.
|
|
82
|
+
#
|
|
83
|
+
# @return [ String, nil ]
|
|
84
|
+
def fixed_in
|
|
85
|
+
relevant_range && relevant_range[:fixed_in]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Backward-compatible scalar counterpart of {#fixed_in}.
|
|
89
|
+
#
|
|
90
|
+
# @return [ String, nil ]
|
|
91
|
+
def introduced_in
|
|
92
|
+
relevant_range && relevant_range[:introduced_in]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The single affected-version range whose fixed_in / introduced_in we
|
|
96
|
+
# surface in the output. Picks the range covering the detected version so a
|
|
97
|
+
# site on an older but still-supported branch is matched against the right
|
|
98
|
+
# fix. Falls back to the highest (most recent) range when the version is
|
|
99
|
+
# unknown or no range covers it, mirroring the pre-v4 single-value output.
|
|
100
|
+
#
|
|
101
|
+
# @return [ Hash{Symbol => String, nil}, nil ]
|
|
102
|
+
def relevant_range
|
|
103
|
+
return nil if affected_versions.empty?
|
|
104
|
+
|
|
105
|
+
if detected_version
|
|
106
|
+
covering = affected_versions.find { |range| self.class.range_covers?(range, detected_version) }
|
|
107
|
+
return covering if covering
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
highest_range
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Whether a single range covers the version. A range matches when
|
|
114
|
+
# introduced_in is missing or <= version AND fixed_in is missing or >
|
|
115
|
+
# version.
|
|
116
|
+
#
|
|
117
|
+
# @param [ Hash{Symbol => String, nil} ] range
|
|
118
|
+
# @param [ #< ] version
|
|
119
|
+
#
|
|
120
|
+
# @return [ Boolean ]
|
|
121
|
+
def self.range_covers?(range, version)
|
|
122
|
+
introduced = range[:introduced_in]
|
|
123
|
+
fixed = range[:fixed_in]
|
|
124
|
+
|
|
125
|
+
return false if introduced && !introduced.to_s.empty? && version < introduced
|
|
126
|
+
return true if fixed.nil? || fixed.to_s.empty?
|
|
127
|
+
|
|
128
|
+
version < fixed
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param [ Vulnerability ] other
|
|
132
|
+
#
|
|
133
|
+
# @return [ Boolean ]
|
|
134
|
+
def ==(other)
|
|
135
|
+
title == other.title &&
|
|
136
|
+
type == other.type &&
|
|
137
|
+
references == other.references &&
|
|
138
|
+
affected_versions == other.affected_versions &&
|
|
139
|
+
cvss == other.cvss &&
|
|
140
|
+
poc == other.poc &&
|
|
141
|
+
uuid == other.uuid
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# The range with the greatest fixed_in. An unpatched range (no fixed_in)
|
|
147
|
+
# outranks every patched one. Used as the fallback for {#relevant_range}.
|
|
148
|
+
#
|
|
149
|
+
# @return [ Hash{Symbol => String, nil} ]
|
|
150
|
+
def highest_range
|
|
151
|
+
affected_versions.max_by { |range| version_key(range[:fixed_in]) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Comparable sort key for a fixed_in value. Returns a [tier, Gem::Version]
|
|
155
|
+
# tuple so a missing fixed_in (unpatched) always sorts highest, and patched
|
|
156
|
+
# ranges sort by their numeric version.
|
|
157
|
+
#
|
|
158
|
+
# @return [ Array ]
|
|
159
|
+
def version_key(fixed_in)
|
|
160
|
+
return [1, Gem::Version.new('0')] if fixed_in.nil? || fixed_in.to_s.empty?
|
|
161
|
+
|
|
162
|
+
[0, Gem::Version.new(fixed_in.to_s[/\A[\d.]+/] || '0')]
|
|
163
|
+
rescue ArgumentError
|
|
164
|
+
[0, Gem::Version.new('0')]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Coerces the v4 affected_versions payload into an array of symbolised range
|
|
168
|
+
# hashes. Accepts either an array of hashes (with string or symbol keys) or
|
|
169
|
+
# nil; anything else is dropped so a malformed entry can't break parsing.
|
|
170
|
+
#
|
|
171
|
+
# @return [ Array<Hash{Symbol => String, nil}> ]
|
|
172
|
+
def normalize_affected_versions(value)
|
|
173
|
+
Array(value).filter_map do |range|
|
|
174
|
+
next unless range.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
range = range.transform_keys(&:to_sym)
|
|
177
|
+
{ fixed_in: range[:fixed_in], introduced_in: range[:introduced_in] }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
28
180
|
end
|
|
29
181
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Filter for excluding specific vulnerabilities by UUID
|
|
5
|
+
class VulnerabilityFilter
|
|
6
|
+
attr_reader :excluded_uuids, :excluded_count
|
|
7
|
+
|
|
8
|
+
# @param [ Array<String>, String, nil ] uuids UUID identifiers to exclude
|
|
9
|
+
def initialize(uuids = nil)
|
|
10
|
+
@excluded_uuids = normalize_uuids(uuids)
|
|
11
|
+
@excluded_count = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Filter vulnerabilities, removing those with excluded UUIDs
|
|
15
|
+
#
|
|
16
|
+
# @param [ Array<Vulnerability> ] vulnerabilities
|
|
17
|
+
# @return [ Array<Vulnerability> ] Filtered vulnerabilities
|
|
18
|
+
def filter(vulnerabilities)
|
|
19
|
+
return vulnerabilities if excluded_uuids.empty?
|
|
20
|
+
|
|
21
|
+
vulnerabilities.reject do |vuln|
|
|
22
|
+
should_exclude?(vuln).tap do |excluded|
|
|
23
|
+
@excluded_count += 1 if excluded
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if a vulnerability should be excluded
|
|
29
|
+
#
|
|
30
|
+
# @param [ Vulnerability ] vulnerability
|
|
31
|
+
# @return [ Boolean ]
|
|
32
|
+
def should_exclude?(vulnerability)
|
|
33
|
+
return false if excluded_uuids.empty?
|
|
34
|
+
return false unless vulnerability.uuid
|
|
35
|
+
|
|
36
|
+
# Check UUID match (case-insensitive)
|
|
37
|
+
excluded_uuids.include?(vulnerability.uuid.downcase)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Reset the excluded count
|
|
41
|
+
def reset_count!
|
|
42
|
+
@excluded_count = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if any UUIDs are being excluded
|
|
46
|
+
#
|
|
47
|
+
# @return [ Boolean ]
|
|
48
|
+
def excluding?
|
|
49
|
+
!excluded_uuids.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Normalize UUIDs
|
|
55
|
+
#
|
|
56
|
+
# @param [ Array<String>, String, nil ] uuids
|
|
57
|
+
# @return [ Array<String> ] Normalized UUIDs
|
|
58
|
+
def normalize_uuids(uuids)
|
|
59
|
+
return [] if uuids.nil?
|
|
60
|
+
|
|
61
|
+
# Convert string to array if needed
|
|
62
|
+
uuids = uuids.is_a?(String) ? uuids.split(',') : Array(uuids)
|
|
63
|
+
|
|
64
|
+
# Normalize each UUID (trim whitespace, lowercase, remove duplicates)
|
|
65
|
+
uuids.map { |u| u.to_s.strip.downcase }.reject(&:empty?).uniq
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/wpscan/vulnerable.rb
CHANGED
|
@@ -4,9 +4,21 @@ module WPScan
|
|
|
4
4
|
# Module to include in vulnerable WP item such as WpVersion.
|
|
5
5
|
# the vulnerabilities method should be implemented
|
|
6
6
|
module Vulnerable
|
|
7
|
+
# @return [ VulnerabilityFilter, nil ]
|
|
8
|
+
def vulnerability_filter
|
|
9
|
+
@vulnerability_filter ||= VulnerabilityFilter.new(ParsedCli.exclude_vulns) if ParsedCli.exclude_vulns
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [ Array<Vulnerability> ] Filtered vulnerabilities
|
|
13
|
+
def filtered_vulnerabilities
|
|
14
|
+
return vulnerabilities unless vulnerability_filter
|
|
15
|
+
|
|
16
|
+
vulnerability_filter.filter(vulnerabilities)
|
|
17
|
+
end
|
|
18
|
+
|
|
7
19
|
# @return [ Boolean ]
|
|
8
20
|
def vulnerable?
|
|
9
|
-
!
|
|
21
|
+
!filtered_vulnerabilities.empty?
|
|
10
22
|
end
|
|
11
23
|
end
|
|
12
24
|
end
|