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.
Files changed (252) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +104 -30
  3. data/app/app.rb +26 -0
  4. data/app/controllers/aliases.rb +2 -2
  5. data/app/controllers/authenticated_inventory.rb +43 -0
  6. data/app/controllers/core/cli_options.rb +151 -0
  7. data/app/controllers/core.rb +200 -25
  8. data/app/controllers/custom_directories.rb +1 -1
  9. data/app/controllers/enumeration/cli_options.rb +21 -31
  10. data/app/controllers/enumeration/enum_methods.rb +145 -38
  11. data/app/controllers/enumeration.rb +26 -3
  12. data/app/controllers/interesting_findings.rb +25 -0
  13. data/app/controllers/main_theme.rb +1 -1
  14. data/app/controllers/password_attack.rb +14 -6
  15. data/app/controllers/vuln_api.rb +9 -3
  16. data/app/controllers/wp_version.rb +1 -1
  17. data/app/controllers.rb +1 -0
  18. data/app/finders/backup_folders/known_locations.rb +66 -0
  19. data/app/finders/backup_folders.rb +19 -0
  20. data/app/finders/config_backups/known_filenames.rb +6 -4
  21. data/app/finders/config_backups.rb +1 -1
  22. data/app/finders/db_exports/known_locations.rb +16 -14
  23. data/app/finders/db_exports.rb +1 -1
  24. data/app/finders/interesting_findings/backup_db.rb +1 -1
  25. data/app/finders/interesting_findings/debug_log.rb +1 -1
  26. data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
  27. data/app/finders/interesting_findings/emergency_pwd_reset_script.rb +1 -1
  28. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  29. data/app/finders/interesting_findings/full_path_disclosure.rb +1 -1
  30. data/app/finders/interesting_findings/headers.rb +17 -0
  31. data/app/finders/interesting_findings/mu_plugins.rb +1 -1
  32. data/app/finders/interesting_findings/multisite.rb +1 -1
  33. data/app/finders/interesting_findings/php_disabled.rb +2 -2
  34. data/app/finders/interesting_findings/readme.rb +1 -1
  35. data/app/finders/interesting_findings/registration.rb +1 -1
  36. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  37. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  38. data/app/finders/interesting_findings/tmm_db_migrate.rb +1 -1
  39. data/app/finders/interesting_findings/upload_directory_listing.rb +1 -1
  40. data/app/finders/interesting_findings/upload_sql_dump.rb +2 -2
  41. data/app/finders/interesting_findings/wp_cron.rb +1 -1
  42. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  43. data/app/finders/interesting_findings.rb +13 -4
  44. data/app/finders/main_theme/css_style_in_homepage.rb +1 -1
  45. data/app/finders/main_theme/urls_in_homepage.rb +3 -7
  46. data/app/finders/main_theme/woo_framework_meta_generator.rb +4 -4
  47. data/app/finders/main_theme.rb +1 -1
  48. data/app/finders/medias/attachment_brute_forcing.rb +2 -2
  49. data/app/finders/medias.rb +1 -1
  50. data/app/finders/passwords/wp_login.rb +2 -2
  51. data/app/finders/passwords/xml_rpc.rb +2 -2
  52. data/app/finders/passwords/xml_rpc_multicall.rb +1 -1
  53. data/app/finders/plugin_version/readme.rb +1 -1
  54. data/app/finders/plugin_version.rb +1 -1
  55. data/app/finders/plugins/known_locations.rb +17 -7
  56. data/app/finders/plugins/urls_in_homepage.rb +3 -7
  57. data/app/finders/plugins/wp_json_api.rb +85 -0
  58. data/app/finders/plugins.rb +2 -1
  59. data/app/finders/theme_version/style.rb +1 -1
  60. data/app/finders/theme_version/woo_framework_meta_generator.rb +1 -1
  61. data/app/finders/theme_version.rb +1 -1
  62. data/app/finders/themes/known_locations.rb +12 -6
  63. data/app/finders/themes/urls_in_homepage.rb +3 -7
  64. data/app/finders/themes/wp_json_api.rb +74 -0
  65. data/app/finders/themes.rb +2 -1
  66. data/app/finders/timthumb_version/bad_request.rb +1 -1
  67. data/app/finders/timthumb_version.rb +1 -1
  68. data/app/finders/timthumbs/known_locations.rb +6 -4
  69. data/app/finders/timthumbs.rb +1 -1
  70. data/app/finders/users/author_id_brute_forcing.rb +11 -7
  71. data/app/finders/users/author_posts.rb +1 -1
  72. data/app/finders/users/author_sitemap.rb +1 -1
  73. data/app/finders/users/login_error_messages.rb +1 -1
  74. data/app/finders/users/oembed_api.rb +3 -1
  75. data/app/finders/users/wp_json_api.rb +11 -7
  76. data/app/finders/users.rb +1 -1
  77. data/app/finders/wp_version/atom_generator.rb +1 -1
  78. data/app/finders/wp_version/rdf_generator.rb +1 -1
  79. data/app/finders/wp_version/readme.rb +1 -1
  80. data/app/finders/wp_version/rss_generator.rb +1 -1
  81. data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
  82. data/app/finders/wp_version.rb +1 -1
  83. data/app/finders.rb +1 -0
  84. data/app/formatters/cli.rb +79 -0
  85. data/app/formatters/cli_no_color.rb +9 -0
  86. data/app/formatters/cli_no_colour.rb +17 -0
  87. data/app/formatters/json.rb +14 -0
  88. data/app/formatters/jsonl.rb +29 -0
  89. data/app/formatters/sarif.rb +311 -0
  90. data/app/models/backup_folder.rb +39 -0
  91. data/app/models/fantastico_fileslist.rb +34 -0
  92. data/app/models/headers.rb +44 -0
  93. data/app/models/interesting_finding.rb +41 -2
  94. data/app/models/plugin.rb +8 -2
  95. data/app/models/robots_txt.rb +31 -0
  96. data/app/models/search_replace_db_2.rb +17 -0
  97. data/app/models/theme.rb +9 -2
  98. data/app/models/timthumb.rb +2 -2
  99. data/app/models/user.rb +35 -0
  100. data/app/models/version.rb +49 -0
  101. data/app/models/wp_item/wordpress_org_data.rb +55 -0
  102. data/app/models/wp_item.rb +109 -9
  103. data/app/models/wp_version.rb +2 -2
  104. data/app/models/xml_rpc.rb +73 -3
  105. data/app/models.rb +2 -1
  106. data/app/user_agents.txt +46 -0
  107. data/app/views/cli/core/banner.erb +3 -3
  108. data/app/views/cli/core/finished.erb +15 -0
  109. data/app/views/cli/core/help.erb +4 -0
  110. data/app/views/cli/core/started.erb +11 -0
  111. data/app/views/cli/enumeration/backup_folders.erb +11 -0
  112. data/app/views/cli/enumeration/plugin.erb +13 -0
  113. data/app/views/cli/enumeration/plugins.erb +1 -12
  114. data/app/views/cli/enumeration/theme.erb +4 -0
  115. data/app/views/cli/enumeration/themes.erb +1 -3
  116. data/app/views/cli/enumeration/user.erb +4 -0
  117. data/app/views/cli/enumeration/users.erb +1 -3
  118. data/app/views/cli/finding.erb +1 -1
  119. data/app/views/cli/interesting_findings/_array.erb +10 -0
  120. data/app/views/cli/interesting_findings/findings.erb +23 -0
  121. data/app/views/cli/scan_aborted.erb +5 -0
  122. data/app/views/cli/update_aborted.erb +5 -0
  123. data/app/views/cli/vuln_api/status.erb +2 -0
  124. data/app/views/cli/vulnerability.erb +6 -0
  125. data/app/views/cli/wp_item.erb +4 -1
  126. data/app/views/json/core/banner.erb +2 -8
  127. data/app/views/json/core/finished.erb +13 -0
  128. data/app/views/json/core/help.erb +4 -0
  129. data/app/views/json/core/started.erb +10 -0
  130. data/app/views/json/enumeration/backup_folders.erb +11 -0
  131. data/app/views/json/enumeration/plugin.erb +15 -0
  132. data/app/views/json/enumeration/theme.erb +5 -0
  133. data/app/views/json/enumeration/user.erb +6 -0
  134. data/app/views/json/finding.erb +8 -2
  135. data/app/views/json/interesting_findings/findings.erb +24 -0
  136. data/app/views/json/notice.erb +1 -0
  137. data/app/views/json/scan_aborted.erb +5 -0
  138. data/app/views/json/update_aborted.erb +5 -0
  139. data/app/views/json/vuln_api/status.erb +2 -0
  140. data/app/views/json/wp_item.erb +4 -1
  141. data/bin/wpscan +1 -0
  142. data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
  143. data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
  144. data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
  145. data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
  146. data/lib/opt_parse_validator/errors.rb +9 -0
  147. data/lib/opt_parse_validator/hacks.rb +19 -0
  148. data/lib/opt_parse_validator/opts/alias.rb +28 -0
  149. data/lib/opt_parse_validator/opts/array.rb +34 -0
  150. data/lib/opt_parse_validator/opts/base.rb +142 -0
  151. data/lib/opt_parse_validator/opts/boolean.rb +19 -0
  152. data/lib/opt_parse_validator/opts/choice.rb +43 -0
  153. data/lib/opt_parse_validator/opts/credentials.rb +15 -0
  154. data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
  155. data/lib/opt_parse_validator/opts/file_path.rb +34 -0
  156. data/lib/opt_parse_validator/opts/headers.rb +33 -0
  157. data/lib/opt_parse_validator/opts/integer.rb +15 -0
  158. data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
  159. data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
  160. data/lib/opt_parse_validator/opts/path.rb +78 -0
  161. data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
  162. data/lib/opt_parse_validator/opts/proxy.rb +7 -0
  163. data/lib/opt_parse_validator/opts/regexp.rb +14 -0
  164. data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
  165. data/lib/opt_parse_validator/opts/string.rb +8 -0
  166. data/lib/opt_parse_validator/opts/uri.rb +41 -0
  167. data/lib/opt_parse_validator/opts/url.rb +11 -0
  168. data/lib/opt_parse_validator/opts.rb +9 -0
  169. data/lib/opt_parse_validator/version.rb +6 -0
  170. data/lib/opt_parse_validator.rb +161 -0
  171. data/lib/wpscan/browser/actions.rb +48 -0
  172. data/lib/wpscan/browser/options.rb +92 -0
  173. data/lib/wpscan/browser.rb +87 -2
  174. data/lib/wpscan/browser_authenticator.rb +64 -0
  175. data/lib/wpscan/cache/file_store.rb +77 -0
  176. data/lib/wpscan/cache/typhoeus.rb +25 -0
  177. data/lib/wpscan/controller.rb +100 -4
  178. data/lib/wpscan/controllers.rb +78 -3
  179. data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
  180. data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
  181. data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
  182. data/lib/wpscan/db/fingerprints.rb +2 -2
  183. data/lib/wpscan/db/updater.rb +23 -13
  184. data/lib/wpscan/db/vuln_api.rb +19 -7
  185. data/lib/wpscan/db/wp_item.rb +2 -2
  186. data/lib/wpscan/errors/enumeration.rb +4 -4
  187. data/lib/wpscan/errors/http.rb +82 -3
  188. data/lib/wpscan/errors/saml.rb +28 -0
  189. data/lib/wpscan/errors/scan.rb +14 -0
  190. data/lib/wpscan/errors/update.rb +11 -3
  191. data/lib/wpscan/errors/vuln_api.rb +24 -0
  192. data/lib/wpscan/errors/wordpress.rb +2 -2
  193. data/lib/wpscan/errors/wp_auth.rb +37 -0
  194. data/lib/wpscan/errors.rb +4 -3
  195. data/lib/wpscan/exit_code.rb +25 -0
  196. data/lib/wpscan/finders/base_finders.rb +45 -0
  197. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
  198. data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
  199. data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
  200. data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
  201. data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
  202. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
  203. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  204. data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
  205. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  206. data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
  207. data/lib/wpscan/finders/finder/enumerator.rb +77 -0
  208. data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
  209. data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
  210. data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
  211. data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
  212. data/lib/wpscan/finders/finder.rb +78 -0
  213. data/lib/wpscan/finders/finding.rb +54 -0
  214. data/lib/wpscan/finders/findings.rb +33 -0
  215. data/lib/wpscan/finders/independent_finder.rb +33 -0
  216. data/lib/wpscan/finders/independent_finders.rb +26 -0
  217. data/lib/wpscan/finders/same_type_finder.rb +19 -0
  218. data/lib/wpscan/finders/same_type_finders.rb +28 -0
  219. data/lib/wpscan/finders/unique_finder.rb +19 -0
  220. data/lib/wpscan/finders/unique_finders.rb +47 -0
  221. data/lib/wpscan/finders.rb +11 -12
  222. data/lib/wpscan/formatter/buffer.rb +17 -0
  223. data/lib/wpscan/formatter.rb +152 -0
  224. data/lib/wpscan/helper.rb +7 -1
  225. data/lib/wpscan/http_status_tracking.rb +128 -0
  226. data/lib/wpscan/numeric.rb +13 -0
  227. data/lib/wpscan/parsed_cli.rb +31 -2
  228. data/lib/wpscan/progressbar_null_output.rb +23 -0
  229. data/lib/wpscan/public_suffix/domain.rb +44 -0
  230. data/lib/wpscan/references.rb +118 -4
  231. data/lib/wpscan/scan.rb +127 -0
  232. data/lib/wpscan/target/hashes.rb +45 -0
  233. data/lib/wpscan/target/platform/php.rb +124 -0
  234. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  235. data/lib/wpscan/target/platform/wordpress.rb +7 -8
  236. data/lib/wpscan/target/platform.rb +3 -0
  237. data/lib/wpscan/target/scope.rb +103 -0
  238. data/lib/wpscan/target/server/apache.rb +27 -0
  239. data/lib/wpscan/target/server/generic.rb +72 -0
  240. data/lib/wpscan/target/server/iis.rb +29 -0
  241. data/lib/wpscan/target/server/nginx.rb +27 -0
  242. data/lib/wpscan/target/server.rb +6 -0
  243. data/lib/wpscan/target.rb +129 -9
  244. data/lib/wpscan/typhoeus/hydra.rb +12 -0
  245. data/lib/wpscan/typhoeus/response.rb +24 -1
  246. data/lib/wpscan/version.rb +1 -1
  247. data/lib/wpscan/vulnerability.rb +49 -3
  248. data/lib/wpscan/vulnerability_filter.rb +68 -0
  249. data/lib/wpscan/vulnerable.rb +13 -1
  250. data/lib/wpscan/web_site.rb +152 -0
  251. data/lib/wpscan.rb +126 -29
  252. 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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/target/server/generic'
4
+ require 'wpscan/target/server/apache'
5
+ require 'wpscan/target/server/iis'
6
+ require 'wpscan/target/server/nginx'
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
- # Includes the WordPress Platform
7
- class Target < CMSScanner::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&.select { |f| f.is_a?(Model::XMLRPC) }&.first
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typhoeus
4
+ # Ensure a clean abort of hydra
5
+ # See https://github.com/typhoeus/typhoeus/issues/439
6
+ class Hydra
7
+ def abort
8
+ super
9
+ run
10
+ end
11
+ end
12
+ end
@@ -3,10 +3,33 @@
3
3
  module Typhoeus
4
4
  # Custom Response class
5
5
  class Response
6
- # @note: Ignores requests done to the /status endpoint of the API
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version
4
4
  module WPScan
5
- VERSION = '3.8.28'
5
+ VERSION = '4.0.0'
6
6
  end
@@ -1,10 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- # Specific implementation
5
- class Vulnerability < CMSScanner::Vulnerability
4
+ # Vulnerability model.
5
+ class Vulnerability
6
6
  include References
7
7
 
8
+ attr_reader :title, :type, :fixed_in, :introduced_in, :cvss, :poc, :uuid
9
+
10
+ # @param [ String ] title
11
+ # @param [ Hash ] references
12
+ # @option references [ Array<String>, String ] :cve
13
+ # @option references [ Array<String>, String ] :secunia
14
+ # @option references [ Array<String>, String ] :osvdb
15
+ # @option references [ Array<String>, String ] :exploitdb
16
+ # @option references [ Array<String> ] :url URL(s) to related advisories etc
17
+ # @option references [ Array<String>, String ] :metasploit The related metasploit module(s)
18
+ # @option references [ Array<String> ] :youtube
19
+ # @param [ String ] type
20
+ # @param [ String ] fixed_in
21
+ # @param [ String ] introduced_in
22
+ # @param [ HashSymbol ] cvss
23
+ # @option cvss [ String ] :score
24
+ # @option cvss [ String ] :vector
25
+ # rubocop:disable Metrics/ParameterLists
26
+ def initialize(title, references: {}, type: nil, fixed_in: nil, introduced_in: nil, cvss: nil, poc: nil, uuid: nil)
27
+ # rubocop:enable Metrics/ParameterLists
28
+ @title = title
29
+ @type = type
30
+ @fixed_in = fixed_in
31
+ @introduced_in = introduced_in
32
+ @cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
33
+ @poc = poc
34
+ @uuid = uuid
35
+
36
+ self.references = references
37
+ end
38
+
8
39
  # @param [ Hash ] json_data
9
40
  # @return [ Vulnerability ]
10
41
  def self.load_from_json(json_data)
@@ -22,8 +53,23 @@ module WPScan
22
53
  type: json_data['vuln_type'],
23
54
  fixed_in: json_data['fixed_in'],
24
55
  introduced_in: json_data['introduced_in'],
25
- cvss: json_data['cvss']&.symbolize_keys
56
+ cvss: json_data['cvss']&.symbolize_keys,
57
+ poc: json_data['poc'],
58
+ uuid: json_data['id'].to_s # The 'id' field IS the UUID in WPScan API
26
59
  )
27
60
  end
61
+
62
+ # @param [ Vulnerability ] other
63
+ #
64
+ # @return [ Boolean ]
65
+ def ==(other)
66
+ title == other.title &&
67
+ type == other.type &&
68
+ references == other.references &&
69
+ fixed_in == other.fixed_in &&
70
+ cvss == other.cvss &&
71
+ poc == other.poc &&
72
+ uuid == other.uuid
73
+ end
28
74
  end
29
75
  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
@@ -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
- !vulnerabilities.empty?
21
+ !filtered_vulnerabilities.empty?
10
22
  end
11
23
  end
12
24
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # WebSite Implementation
5
+ class WebSite
6
+ attr_reader :uri, :opts
7
+
8
+ # @param [ String ] site_url
9
+ # @param [ Hash ] opts
10
+ def initialize(site_url, opts = {})
11
+ self.url = site_url
12
+ @opts = opts
13
+ end
14
+
15
+ def url=(site_url)
16
+ new_url = site_url.dup
17
+
18
+ # Add a trailing slash to the URL
19
+ new_url << '/' if new_url[-1, 1] != '/'
20
+
21
+ # Use the validator to ensure the URL has a correct format
22
+ OptParseValidator::OptURL.new([]).validate(new_url)
23
+
24
+ @uri = Addressable::URI.parse(new_url).normalize
25
+ end
26
+
27
+ # @param [ String ] path Optional path to merge with the uri
28
+ #
29
+ # @return [ String ]
30
+ def url(path = nil)
31
+ return @uri.to_s unless path
32
+
33
+ @uri.join(Addressable::URI.encode(path).gsub('#', '%23')).to_s
34
+ end
35
+
36
+ # @return [ String ] The IP address of the target
37
+ def ip
38
+ @ip ||= IPSocket.getaddress(uri.host)
39
+ rescue SocketError
40
+ 'Unknown'
41
+ end
42
+
43
+ attr_writer :homepage_res
44
+
45
+ # @return [ Typhoeus::Response ]
46
+ #
47
+ # As webmock does not support redirects mocking, coverage is ignored
48
+ # :nocov:
49
+ def homepage_res
50
+ @homepage_res ||= WPScan::Browser.get_and_follow_location(url)
51
+ end
52
+ # :nocov:
53
+
54
+ # @return [ String ]
55
+ def homepage_url
56
+ @homepage_url ||= homepage_res.effective_url
57
+ end
58
+
59
+ # Discards the cached homepage response and URL so the next access refetches.
60
+ # Used after mutating cookies/auth state mid-scan.
61
+ def reset_homepage_cache!
62
+ @homepage_res = nil
63
+ @homepage_url = nil
64
+ end
65
+
66
+ # @return [ Typhoeus::Response ]
67
+ def error_404_res
68
+ @error_404_res ||= WPScan::Browser.get_and_follow_location(error_404_url)
69
+ end
70
+
71
+ # @return [ String ] The URL of an unlikely existant page
72
+ def error_404_url
73
+ @error_404_url ||= uri.join("#{Digest::MD5.hexdigest(rand(999_999).to_s)[0..6]}.html").to_s
74
+ end
75
+
76
+ # Checks if the remote website is up.
77
+ #
78
+ # @param [ String ] path
79
+ #
80
+ # @return [ Boolean ]
81
+ def online?(path = nil)
82
+ WPScan::Browser.get(url(path)).code.nonzero? ? true : false
83
+ end
84
+
85
+ # @param [ String ] path
86
+ #
87
+ # @return [ Boolean ]
88
+ def http_auth?(path = nil)
89
+ WPScan::Browser.get(url(path)).code == 401
90
+ end
91
+
92
+ # @param [ String ] path
93
+ #
94
+ # @return [ Boolean ]
95
+ def access_forbidden?(path = nil)
96
+ WPScan::Browser.get(url(path)).code == 403
97
+ end
98
+
99
+ # @param [ String ] path
100
+ #
101
+ # @return [ Boolean ]
102
+ def proxy_auth?(path = nil)
103
+ WPScan::Browser.get(url(path)).code == 407
104
+ end
105
+
106
+ # @param [ String ] url
107
+ #
108
+ # @return [ String ] The redirection url or nil
109
+ #
110
+ # As webmock does not support redirects mocking, coverage is ignored
111
+ # :nocov:
112
+ def redirection(url = nil)
113
+ url ||= @uri.to_s
114
+
115
+ return unless [301, 302].include?(WPScan::Browser.get(url).code)
116
+
117
+ res = WPScan::Browser.get(url, followlocation: true, maxredirs: 10)
118
+
119
+ res.effective_url == url ? nil : res.effective_url
120
+ end
121
+ # :nocov:
122
+
123
+ # @return [ Hash ] The Typhoeus params to use to perform head requests
124
+ def head_or_get_params
125
+ @head_or_get_params ||= if [0, 405, 501].include?(WPScan::Browser.head(homepage_url).code)
126
+ { method: :get, maxfilesize: 1 }
127
+ else
128
+ { method: :head }
129
+ end
130
+ end
131
+
132
+ # Perform a HEAD request to the path provided, then if its response code
133
+ # is in the array of codes given, a GET is done and the response returned. Otherwise the
134
+ # HEAD response is returned.
135
+ #
136
+ # @param [ String ] path
137
+ # @param [ Array<String> ] codes
138
+ # @param [ Hash ] params The requests params
139
+ # @option params [ Hash ] :head Request params for the HEAD
140
+ # @option params [ hash ] :get Request params for the GET
141
+ #
142
+ # @return [ Typhoeus::Response ]
143
+ def head_and_get(path, codes = [200], params = {})
144
+ url_to_get = url(path)
145
+ head_params = (params[:head] || {}).merge(head_or_get_params)
146
+
147
+ head_res = WPScan::Browser.forge_request(url_to_get, head_params).run
148
+
149
+ codes.include?(head_res.code) ? WPScan::Browser.get(url_to_get, params[:get] || {}) : head_res
150
+ end
151
+ end
152
+ end