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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # This class is designed to handle independent results
6
+ # which are not related with each others
7
+ # e.g: interesting files
8
+ class IndependentFinders < BaseFinders
9
+ # @param [ Hash ] opts
10
+ # @option opts [ Symbol ] mode :mixed, :passive or :aggressive
11
+ #
12
+ # @return [ Findings ]
13
+ def run(opts = {})
14
+ methods = symbols_from_mode(opts[:mode])
15
+
16
+ each do |finder|
17
+ methods.each do |symbol|
18
+ run_finder(finder, symbol, opts)
19
+ end
20
+ end
21
+
22
+ filter_findings
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # Same Type Finder
6
+ module SameTypeFinder
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include IndependentFinder
10
+
11
+ # @return [ Array ]
12
+ def finders
13
+ @finders ||= WPScan::Finders::SameTypeFinders.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # This class is designed to handle same type results, such as enumeration of plugins,
6
+ # themes etc.
7
+ class SameTypeFinders < BaseFinders
8
+ # @param [ Hash ] opts
9
+ # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
10
+ # @option opts [ Boolean ] :sort Wether or not to sort the findings
11
+ #
12
+ # @return [ Findings ]
13
+ def run(opts = {}, &block)
14
+ findings.on_append = block if block
15
+
16
+ symbols_from_mode(opts[:mode]).each do |symbol|
17
+ each do |finder|
18
+ run_finder(finder, symbol, opts)
19
+ end
20
+ end
21
+
22
+ findings.sort! if opts[:sort]
23
+
24
+ filter_findings
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # Unique Finder
6
+ module UniqueFinder
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include IndependentFinder
10
+
11
+ # @return [ Array ]
12
+ def finders
13
+ @finders ||= WPScan::Finders::UniqueFinders.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # This class is designed to return a unique result such as a version
6
+ # Note: Finders contained can return multiple results but the #run will only
7
+ # returned the best finding
8
+ class UniqueFinders < BaseFinders
9
+ # @param [ Hash ] opts
10
+ # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
11
+ # @option opts [ Int ] :confidence_threshold If a finding's confidence reaches this value,
12
+ # it will be returned as the best finding.
13
+ # Default is 100.
14
+ # If <= 0, all finders will be ran.
15
+ #
16
+ # @return [ Object, false ] The best finding or false if none
17
+ def run(opts = {})
18
+ opts[:confidence_threshold] ||= 100
19
+
20
+ symbols_from_mode(opts[:mode]).each do |symbol|
21
+ each do |finder|
22
+ run_finder(finder, symbol, opts)
23
+
24
+ next if opts[:confidence_threshold] <= 0
25
+
26
+ findings.each { |f| return f if f.confidence >= opts[:confidence_threshold] }
27
+ end
28
+ end
29
+
30
+ filter_findings
31
+ end
32
+
33
+ protected
34
+
35
+ # @return [ Object, false ] The best finding or false if none
36
+ def filter_findings
37
+ # results are sorted by confidence ASC
38
+ findings.sort_by!(&:confidence)
39
+
40
+ # If all findings have the same confidence, false is returned
41
+ return false if findings.size > 1 && findings.first.confidence == findings.last.confidence
42
+
43
+ findings.last || false
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'wpscan/finders/finder'
4
+ require 'wpscan/finders/finding'
5
+ require 'wpscan/finders/findings'
6
+ require 'wpscan/finders/base_finders'
7
+ require 'wpscan/finders/independent_finders'
8
+ require 'wpscan/finders/independent_finder'
9
+ require 'wpscan/finders/unique_finders'
10
+ require 'wpscan/finders/unique_finder'
11
+ require 'wpscan/finders/same_type_finders'
12
+ require 'wpscan/finders/same_type_finder'
13
+
3
14
  require 'wpscan/finders/finder/wp_version/smart_url_checker'
4
15
 
5
16
  require 'wpscan/finders/dynamic_finder/finder'
@@ -14,15 +25,3 @@ require 'wpscan/finders/dynamic_finder/version/query_parameter'
14
25
  require 'wpscan/finders/dynamic_finder/version/config_parser'
15
26
  require 'wpscan/finders/dynamic_finder/wp_item_version'
16
27
  require 'wpscan/finders/dynamic_finder/wp_version'
17
-
18
- module WPScan
19
- # Custom Finders
20
- module Finders
21
- include CMSScanner::Finders
22
-
23
- # Custom InterestingFindings
24
- module InterestingFindings
25
- include CMSScanner::Finders::InterestingFindings
26
- end
27
- end
28
- end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Formatter
5
+ # Module used to output the rendered views into a buffer
6
+ # and beautify it a the end of the scan
7
+ module Buffer
8
+ def output(tpl, vars = {}, controller_name = nil)
9
+ buffer << render(tpl, vars, controller_name).encode('UTF-8', invalid: :replace, undef: :replace)
10
+ end
11
+
12
+ def buffer
13
+ @buffer ||= +''
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/formatter/buffer'
4
+
5
+ module WPScan
6
+ # Formatter
7
+ module Formatter
8
+ # Module to be able to do Formatter.load() & Formatter.availables
9
+ # and do that as well when the Formatter is included in another module
10
+ module ClassMethods
11
+ # @param [ String ] format
12
+ # @param [ Array<String> ] custom_views
13
+ #
14
+ # @return [ Formatter::Base ]
15
+ def load(format = nil, custom_views = nil)
16
+ format ||= 'cli'
17
+ custom_views ||= []
18
+
19
+ f = const_get(format.tr('-', '_').camelize).new
20
+ custom_views.each { |v| f.views_directories << v }
21
+ f
22
+ end
23
+
24
+ # @return [ Array<String> ] The list of the available formatters (except the Base one)
25
+ # @note: the #load method above should then be used to create the associated formatter
26
+ def availables
27
+ formatters = WPScan::Formatter.constants.select do |const|
28
+ name = WPScan::Formatter.const_get(const)
29
+ name.is_a?(Class) && name != WPScan::Formatter::Base
30
+ end
31
+
32
+ formatters.map { |sym| sym.to_s.underscore.dasherize }
33
+ end
34
+ end
35
+
36
+ extend ClassMethods
37
+
38
+ def self.included(base)
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ # This module should be implemented in the code which uses this Framework to
43
+ # be able to override/implements instance methods for all the Formatters
44
+ # w/o having to include/write the methods in each formatters.
45
+ #
46
+ # Example: to override the #views_directories (see the wpscan-v3/lib/wpscan/formatter.rb)
47
+ module InstanceMethods
48
+ end
49
+
50
+ # Base Formatter
51
+ class Base
52
+ attr_reader :controller_name
53
+
54
+ def initialize
55
+ # Can't put this at the top level of the class, due to the WPScan::
56
+ extend WPScan::Formatter::InstanceMethods
57
+ end
58
+
59
+ # @return [ String ] The underscored name of the class
60
+ def format
61
+ self.class.name.demodulize.underscore
62
+ end
63
+
64
+ # @return [ Boolean ]
65
+ def user_interaction?
66
+ format == 'cli'
67
+ end
68
+
69
+ # @return [ Boolean ]
70
+ # Whether this formatter can render findings incrementally as they
71
+ # arrive (cli, jsonl), or needs to receive the full result set first
72
+ # (json, sarif — they emit a single well-formed document at end-of-scan).
73
+ def streams?
74
+ false
75
+ end
76
+
77
+ # @return [ String ] The underscored format to use as a base
78
+ def base_format; end
79
+
80
+ # @return [ Array<String> ]
81
+ def formats
82
+ [format, base_format].compact
83
+ end
84
+
85
+ # This is called after the scan
86
+ # and used in some formatters (e.g JSON)
87
+ # to indent results
88
+ def beautify; end
89
+
90
+ # @see #render
91
+ def output(tpl, vars = {}, controller_name = nil)
92
+ puts render(tpl, vars, controller_name)
93
+ end
94
+
95
+ ERB_SUPPORTS_KVARGS = ::ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
96
+
97
+ # @param [ String ] tpl
98
+ # @param [ Hash ] vars
99
+ # @param [ String ] controller_name
100
+ def render(tpl, vars = {}, controller_name = nil)
101
+ template_vars(vars)
102
+ @controller_name = controller_name if controller_name
103
+
104
+ # '-' disables new lines when -%> is used.
105
+ # See http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/ERB.html
106
+ ERB.new(File.read(view_path(tpl)), trim_mode: '-').result(binding)
107
+ end
108
+
109
+ # @param [ Hash ] vars
110
+ #
111
+ # @return [ Void ]
112
+ def template_vars(vars)
113
+ vars.each do |key, value|
114
+ instance_variable_set("@#{key}", value) unless key == :views_directories
115
+ end
116
+ end
117
+
118
+ # @param [ String ] tpl
119
+ #
120
+ # @return [ String ] The path of the view
121
+ def view_path(tpl)
122
+ if tpl[0, 1] == '@' # Global Template
123
+ tpl = tpl.delete('@')
124
+ else
125
+ raise 'The controller_name can not be nil' unless controller_name
126
+
127
+ tpl = "#{controller_name}/#{tpl}"
128
+ end
129
+
130
+ raise "Wrong tpl format: '#{tpl}'" unless %r{\A[\w/_]+\z}.match?(tpl)
131
+
132
+ views_directories.reverse_each do |dir|
133
+ formats.each do |format|
134
+ potential_file = File.join(dir, format, "#{tpl}.erb")
135
+
136
+ return potential_file if File.exist?(potential_file)
137
+ end
138
+ end
139
+
140
+ raise "View not found for #{format}/#{tpl}"
141
+ end
142
+
143
+ # @return [ Array<String> ] The directories to look into for views
144
+ def views_directories
145
+ @views_directories ||= [
146
+ APP_DIR, WPScan::APP_DIR,
147
+ File.join(Dir.home, ".#{WPScan.app_name}"), File.join(Dir.pwd, ".#{WPScan.app_name}")
148
+ ].uniq.reduce([]) { |acc, elem| acc << Pathname.new(elem).join('views').to_s }
149
+ end
150
+ end
151
+ end
152
+ end
data/lib/wpscan/helper.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @param [ String ] file The file path
4
+ def redirect_output_to_file(file)
5
+ $stdout.reopen(file, 'w')
6
+ $stdout.sync = true
7
+ end
8
+
3
9
  def read_json_file(file)
4
10
  JSON.parse(File.read(file))
5
11
  rescue StandardError => e
@@ -13,7 +19,7 @@ end
13
19
  #
14
20
  # @return [ Symbol ]
15
21
  def classify_slug(slug)
16
- classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
22
+ classified = slug.to_s.gsub(/[^a-z\d-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
17
23
  classified = "D_#{classified}" if /\d/.match?(classified[0])
18
24
 
19
25
  # Special case for slugs with all non-latin characters.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # Module for HTTP status code tracking and error detection
5
+ module HttpStatusTracking
6
+ # Tracking for HTTP status codes
7
+ def status_codes
8
+ @@status_codes_mutex ||= Mutex.new
9
+ @@status_codes ||= Hash.new(0)
10
+ end
11
+
12
+ # Reset status codes (mainly for testing)
13
+ def reset_status_codes
14
+ @@status_codes_mutex ||= Mutex.new
15
+ @@status_codes_mutex.synchronize do
16
+ @@status_codes = Hash.new(0)
17
+ end
18
+ end
19
+
20
+ # Thread-safe increment of status code count
21
+ def increment_status_code(code)
22
+ @@status_codes_mutex ||= Mutex.new
23
+ @@status_codes_mutex.synchronize do
24
+ status_codes[code] += 1
25
+ end
26
+ end
27
+
28
+ # Thread-safe set of status code count (mainly for testing)
29
+ def set_status_code(code, count)
30
+ @@status_codes_mutex ||= Mutex.new
31
+ @@status_codes_mutex.synchronize do
32
+ status_codes[code] = count
33
+ end
34
+ end
35
+
36
+ # Get top N status codes by frequency
37
+ def top_status_codes(limit = 5)
38
+ @@status_codes_mutex ||= Mutex.new
39
+ @@status_codes_mutex.synchronize do
40
+ return {} if status_codes.empty?
41
+
42
+ status_codes.sort_by { |_code, count| -count }.first(limit).to_h
43
+ end
44
+ end
45
+
46
+ # Helper to count specific error types
47
+ def error_counts
48
+ @@status_codes_mutex ||= Mutex.new
49
+ @@status_codes_mutex.synchronize do
50
+ {
51
+ failed: status_codes[0] || 0,
52
+ rate_limit: status_codes[429] || 0,
53
+ server_errors: status_codes.select { |code, _| code >= 500 }.values.sum,
54
+ client_errors: status_codes.select { |code, _count| code >= 400 && code < 500 && code != 404 }.values.sum
55
+ }
56
+ end
57
+ end
58
+
59
+ # Format status codes for display (converts code 0 to "failed")
60
+ def format_status_codes(codes_hash)
61
+ codes_hash.to_h do |code, count|
62
+ label = code.zero? ? 'failed' : code.to_s
63
+ [label, count]
64
+ end
65
+ end
66
+
67
+ # Get all applicable warning messages based on error types
68
+ def error_warning_messages
69
+ return [] if total_requests.zero?
70
+
71
+ messages = []
72
+ counts = error_counts
73
+
74
+ check_specific_error_conditions(messages, counts)
75
+ check_generic_error_rate(messages, counts)
76
+
77
+ messages
78
+ end
79
+
80
+ # Determine if warning should be shown for concerning error codes
81
+ def concerning_error_codes?
82
+ return false if total_requests.zero?
83
+
84
+ counts = error_counts
85
+ # Total errors excluding 404s but including failed requests (code 0)
86
+ total_errors = counts[:client_errors] + counts[:server_errors] + counts[:failed]
87
+ error_percentage = total_errors.to_f / total_requests
88
+
89
+ # Warning conditions
90
+ error_percentage > 0.2 ||
91
+ counts[:rate_limit] > 10 ||
92
+ counts[:server_errors] > 10 ||
93
+ counts[:failed] > 10
94
+ end
95
+
96
+ private
97
+
98
+ def check_specific_error_conditions(messages, counts)
99
+ if counts[:failed] > 10
100
+ messages << 'Too many failed requests (no response) could indicate network issues, ' \
101
+ 'WAF/IPS blocking, or an unavailable target'
102
+ end
103
+
104
+ if counts[:rate_limit] > 10
105
+ messages << 'Rate limiting detected (429 responses). Consider using --throttle or reducing --max-threads'
106
+ end
107
+
108
+ if counts[:server_errors] > 10
109
+ messages << 'Too many server errors (5xx). The target may be experiencing issues or blocking requests'
110
+ end
111
+
112
+ # Check for other 4xx client errors (excluding 404 and 429 which are handled separately)
113
+ other_client_errors = counts[:client_errors] - counts[:rate_limit]
114
+ return unless other_client_errors > 10
115
+
116
+ messages << 'Too many client errors (4xx). This could indicate access restrictions, ' \
117
+ 'authentication issues, or WAF blocking'
118
+ end
119
+
120
+ def check_generic_error_rate(messages, counts)
121
+ error_percentage = (counts[:client_errors] + counts[:server_errors] + counts[:failed]).to_f / total_requests
122
+ return unless error_percentage > 0.2 && messages.empty?
123
+
124
+ messages << 'Too many errors detected. This could indicate network issues, rate limiting, ' \
125
+ 'or security protection (e.g. WAF)'
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hack of the Numeric class
4
+ class Numeric
5
+ # @return [ String ] A human readable string of the value
6
+ def bytes_to_human
7
+ units = %w[B KB MB GB TB]
8
+ e = abs.zero? ? abs : (Math.log(abs) / Math.log(1024)).floor
9
+ s = format('%<s>.3f', s: (abs.to_f / (1024**e)))
10
+
11
+ s.sub(/\.?0*$/, " #{units[e]}")
12
+ end
13
+ end
@@ -1,7 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- # To be able to use ParsedCli directly, rather than having to access it via WPscan::ParsedCli
5
- class ParsedCli < CMSScanner::ParsedCli
4
+ # Holds the parsed CLI options and exposes them via class methods (e.g. #verbose?)
5
+ # rather than from the raw hash. Similar to an OpenStruct but class-wise, with extra
6
+ # logic to push browser-relevant options into the Browser singleton.
7
+ class ParsedCli
8
+ # @return [ Hash ]
9
+ def self.options
10
+ @options ||= {}
11
+ end
12
+
13
+ # Sets the CLI options and propagates them to the Browser.
14
+ # @param [ Hash ] options
15
+ def self.options=(options)
16
+ @options = options.dup || {}
17
+
18
+ WPScan::Browser.reset
19
+ WPScan::Browser.instance(@options)
20
+ end
21
+
22
+ # @return [ Boolean ]
23
+ def self.verbose?
24
+ options[:verbose] ? true : false
25
+ end
26
+
27
+ # Unknown methods return nil — expected behaviour for option lookups.
28
+ # rubocop:disable Style/MissingRespondToMissing
29
+ def self.method_missing(method_name, *_args, &)
30
+ super if method_name == :new
31
+
32
+ options[method_name.to_sym]
33
+ end
34
+ # rubocop:enable Style/MissingRespondToMissing
6
35
  end
7
36
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby-progressbar/outputs/null'
4
+
5
+ module WPScan
6
+ # Adds the feature to log message sent to #log
7
+ # So they can be retrieved at some point, like after a password attack with a JSON output
8
+ # which won't display the progressbar but still call #log for errors etc
9
+ class ProgressBarNullOutput < ::ProgressBar::Outputs::Null
10
+ # @retutn [ Array<String> ]
11
+ def logs
12
+ @logs ||= []
13
+ end
14
+
15
+ # Override of parent method
16
+ # @return [ Array<String> ] return the logs when no string provided
17
+ def log(string = nil)
18
+ return logs if string.nil?
19
+
20
+ logs << string unless logs.include?(string)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PublicSuffix
4
+ # Monkey Patch to include the match logic
5
+ class Domain
6
+ # For Sanity
7
+ def ==(other)
8
+ name == other.name
9
+ end
10
+
11
+ # @return [ Boolean ]
12
+ #
13
+ # rubocop:disable Naming/PredicateMethod
14
+ def match(pattern)
15
+ pattern = PublicSuffix.parse(pattern) unless pattern.is_a?(PublicSuffix::Domain)
16
+
17
+ return name == pattern.name unless pattern.trd
18
+ return false unless tld == pattern.tld && sld == pattern.sld
19
+
20
+ matching_pattern?(pattern)
21
+ end
22
+ # rubocop:enable Naming/PredicateMethod
23
+
24
+ protected
25
+
26
+ # @rturn [ Boolean ]
27
+ def matching_pattern?(pattern)
28
+ pattern_trds = pattern.trd.split('.')
29
+ domain_trds = trd.split('.')
30
+
31
+ case pattern_trds.first
32
+ when '*'
33
+ pattern_trds[1..] == domain_trds[1..]
34
+ when '**'
35
+ pa = pattern_trds[1..]
36
+ pa_size = pa.size
37
+
38
+ domain_trds[domain_trds.size - pa_size, pa_size] == pa
39
+ else
40
+ name == pattern.name
41
+ end
42
+ end
43
+ end
44
+ end