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.
Files changed (253) 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_items/urls_in_page.rb +1 -1
  78. data/app/finders/wp_version/atom_generator.rb +1 -1
  79. data/app/finders/wp_version/rdf_generator.rb +1 -1
  80. data/app/finders/wp_version/readme.rb +1 -1
  81. data/app/finders/wp_version/rss_generator.rb +1 -1
  82. data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
  83. data/app/finders/wp_version.rb +1 -1
  84. data/app/finders.rb +1 -0
  85. data/app/formatters/cli.rb +79 -0
  86. data/app/formatters/cli_no_color.rb +9 -0
  87. data/app/formatters/cli_no_colour.rb +17 -0
  88. data/app/formatters/json.rb +14 -0
  89. data/app/formatters/jsonl.rb +29 -0
  90. data/app/formatters/sarif.rb +311 -0
  91. data/app/models/backup_folder.rb +39 -0
  92. data/app/models/fantastico_fileslist.rb +34 -0
  93. data/app/models/headers.rb +44 -0
  94. data/app/models/interesting_finding.rb +41 -2
  95. data/app/models/plugin.rb +8 -2
  96. data/app/models/robots_txt.rb +31 -0
  97. data/app/models/search_replace_db_2.rb +17 -0
  98. data/app/models/theme.rb +9 -2
  99. data/app/models/timthumb.rb +4 -4
  100. data/app/models/user.rb +35 -0
  101. data/app/models/version.rb +49 -0
  102. data/app/models/wp_item/wordpress_org_data.rb +55 -0
  103. data/app/models/wp_item.rb +116 -14
  104. data/app/models/wp_version.rb +5 -3
  105. data/app/models/xml_rpc.rb +73 -3
  106. data/app/models.rb +2 -1
  107. data/app/user_agents.txt +46 -0
  108. data/app/views/cli/core/banner.erb +3 -3
  109. data/app/views/cli/core/finished.erb +15 -0
  110. data/app/views/cli/core/help.erb +4 -0
  111. data/app/views/cli/core/started.erb +11 -0
  112. data/app/views/cli/enumeration/backup_folders.erb +11 -0
  113. data/app/views/cli/enumeration/plugin.erb +13 -0
  114. data/app/views/cli/enumeration/plugins.erb +1 -12
  115. data/app/views/cli/enumeration/theme.erb +4 -0
  116. data/app/views/cli/enumeration/themes.erb +1 -3
  117. data/app/views/cli/enumeration/user.erb +4 -0
  118. data/app/views/cli/enumeration/users.erb +1 -3
  119. data/app/views/cli/finding.erb +1 -1
  120. data/app/views/cli/interesting_findings/_array.erb +10 -0
  121. data/app/views/cli/interesting_findings/findings.erb +23 -0
  122. data/app/views/cli/scan_aborted.erb +5 -0
  123. data/app/views/cli/update_aborted.erb +5 -0
  124. data/app/views/cli/vuln_api/status.erb +2 -0
  125. data/app/views/cli/vulnerability.erb +6 -0
  126. data/app/views/cli/wp_item.erb +4 -1
  127. data/app/views/json/core/banner.erb +2 -8
  128. data/app/views/json/core/finished.erb +13 -0
  129. data/app/views/json/core/help.erb +4 -0
  130. data/app/views/json/core/started.erb +10 -0
  131. data/app/views/json/enumeration/backup_folders.erb +11 -0
  132. data/app/views/json/enumeration/plugin.erb +15 -0
  133. data/app/views/json/enumeration/theme.erb +5 -0
  134. data/app/views/json/enumeration/user.erb +6 -0
  135. data/app/views/json/finding.erb +8 -2
  136. data/app/views/json/interesting_findings/findings.erb +24 -0
  137. data/app/views/json/notice.erb +1 -0
  138. data/app/views/json/scan_aborted.erb +5 -0
  139. data/app/views/json/update_aborted.erb +5 -0
  140. data/app/views/json/vuln_api/status.erb +2 -0
  141. data/app/views/json/wp_item.erb +4 -1
  142. data/bin/wpscan +1 -0
  143. data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
  144. data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
  145. data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
  146. data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
  147. data/lib/opt_parse_validator/errors.rb +9 -0
  148. data/lib/opt_parse_validator/hacks.rb +19 -0
  149. data/lib/opt_parse_validator/opts/alias.rb +28 -0
  150. data/lib/opt_parse_validator/opts/array.rb +34 -0
  151. data/lib/opt_parse_validator/opts/base.rb +142 -0
  152. data/lib/opt_parse_validator/opts/boolean.rb +19 -0
  153. data/lib/opt_parse_validator/opts/choice.rb +43 -0
  154. data/lib/opt_parse_validator/opts/credentials.rb +15 -0
  155. data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
  156. data/lib/opt_parse_validator/opts/file_path.rb +34 -0
  157. data/lib/opt_parse_validator/opts/headers.rb +33 -0
  158. data/lib/opt_parse_validator/opts/integer.rb +15 -0
  159. data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
  160. data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
  161. data/lib/opt_parse_validator/opts/path.rb +78 -0
  162. data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
  163. data/lib/opt_parse_validator/opts/proxy.rb +7 -0
  164. data/lib/opt_parse_validator/opts/regexp.rb +14 -0
  165. data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
  166. data/lib/opt_parse_validator/opts/string.rb +8 -0
  167. data/lib/opt_parse_validator/opts/uri.rb +41 -0
  168. data/lib/opt_parse_validator/opts/url.rb +11 -0
  169. data/lib/opt_parse_validator/opts.rb +9 -0
  170. data/lib/opt_parse_validator/version.rb +6 -0
  171. data/lib/opt_parse_validator.rb +161 -0
  172. data/lib/wpscan/browser/actions.rb +48 -0
  173. data/lib/wpscan/browser/options.rb +92 -0
  174. data/lib/wpscan/browser.rb +87 -2
  175. data/lib/wpscan/browser_authenticator.rb +64 -0
  176. data/lib/wpscan/cache/file_store.rb +77 -0
  177. data/lib/wpscan/cache/typhoeus.rb +25 -0
  178. data/lib/wpscan/controller.rb +100 -4
  179. data/lib/wpscan/controllers.rb +78 -3
  180. data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
  181. data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
  182. data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
  183. data/lib/wpscan/db/fingerprints.rb +2 -2
  184. data/lib/wpscan/db/updater.rb +23 -13
  185. data/lib/wpscan/db/vuln_api.rb +25 -9
  186. data/lib/wpscan/db/wp_item.rb +2 -2
  187. data/lib/wpscan/errors/enumeration.rb +4 -4
  188. data/lib/wpscan/errors/http.rb +82 -3
  189. data/lib/wpscan/errors/saml.rb +28 -0
  190. data/lib/wpscan/errors/scan.rb +14 -0
  191. data/lib/wpscan/errors/update.rb +11 -3
  192. data/lib/wpscan/errors/vuln_api.rb +24 -0
  193. data/lib/wpscan/errors/wordpress.rb +2 -2
  194. data/lib/wpscan/errors/wp_auth.rb +37 -0
  195. data/lib/wpscan/errors.rb +4 -3
  196. data/lib/wpscan/exit_code.rb +25 -0
  197. data/lib/wpscan/finders/base_finders.rb +45 -0
  198. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
  199. data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
  200. data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
  201. data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
  202. data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
  203. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
  204. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  205. data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
  206. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  207. data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
  208. data/lib/wpscan/finders/finder/enumerator.rb +77 -0
  209. data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
  210. data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
  211. data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
  212. data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
  213. data/lib/wpscan/finders/finder.rb +78 -0
  214. data/lib/wpscan/finders/finding.rb +54 -0
  215. data/lib/wpscan/finders/findings.rb +33 -0
  216. data/lib/wpscan/finders/independent_finder.rb +33 -0
  217. data/lib/wpscan/finders/independent_finders.rb +26 -0
  218. data/lib/wpscan/finders/same_type_finder.rb +19 -0
  219. data/lib/wpscan/finders/same_type_finders.rb +28 -0
  220. data/lib/wpscan/finders/unique_finder.rb +19 -0
  221. data/lib/wpscan/finders/unique_finders.rb +47 -0
  222. data/lib/wpscan/finders.rb +11 -12
  223. data/lib/wpscan/formatter/buffer.rb +17 -0
  224. data/lib/wpscan/formatter.rb +152 -0
  225. data/lib/wpscan/helper.rb +7 -1
  226. data/lib/wpscan/http_status_tracking.rb +128 -0
  227. data/lib/wpscan/numeric.rb +13 -0
  228. data/lib/wpscan/parsed_cli.rb +31 -2
  229. data/lib/wpscan/progressbar_null_output.rb +23 -0
  230. data/lib/wpscan/public_suffix/domain.rb +44 -0
  231. data/lib/wpscan/references.rb +118 -4
  232. data/lib/wpscan/scan.rb +130 -0
  233. data/lib/wpscan/target/hashes.rb +45 -0
  234. data/lib/wpscan/target/platform/php.rb +124 -0
  235. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  236. data/lib/wpscan/target/platform/wordpress.rb +7 -8
  237. data/lib/wpscan/target/platform.rb +3 -0
  238. data/lib/wpscan/target/scope.rb +103 -0
  239. data/lib/wpscan/target/server/apache.rb +27 -0
  240. data/lib/wpscan/target/server/generic.rb +72 -0
  241. data/lib/wpscan/target/server/iis.rb +29 -0
  242. data/lib/wpscan/target/server/nginx.rb +27 -0
  243. data/lib/wpscan/target/server.rb +6 -0
  244. data/lib/wpscan/target.rb +129 -9
  245. data/lib/wpscan/typhoeus/hydra.rb +12 -0
  246. data/lib/wpscan/typhoeus/response.rb +24 -1
  247. data/lib/wpscan/version.rb +1 -1
  248. data/lib/wpscan/vulnerability.rb +157 -5
  249. data/lib/wpscan/vulnerability_filter.rb +68 -0
  250. data/lib/wpscan/vulnerable.rb +13 -1
  251. data/lib/wpscan/web_site.rb +152 -0
  252. data/lib/wpscan.rb +126 -29
  253. 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.1'
6
6
  end
@@ -1,10 +1,46 @@
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, :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
- fixed_in: json_data['fixed_in'],
24
- introduced_in: json_data['introduced_in'],
25
- cvss: json_data['cvss']&.symbolize_keys
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
@@ -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