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
@@ -3,7 +3,15 @@
3
3
  module WPScan
4
4
  module Controller
5
5
  # Enumeration Methods
6
- class Enumeration < CMSScanner::Controller::Base
6
+ class Enumeration < WPScan::Controller::Base
7
+ # @return [ Boolean ] Whether enumeration findings should be streamed
8
+ # as they are discovered rather than batched at end of step. Streaming
9
+ # requires both a streaming-capable formatter (cli, cli_no_color, jsonl)
10
+ # and the user not having opted out via --no-stream.
11
+ def stream_findings?
12
+ formatter.streams? && ParsedCli.stream != false
13
+ end
14
+
7
15
  # @param [ String ] type (plugins or themes)
8
16
  # @param [ Symbol ] detection_mode
9
17
  #
@@ -11,9 +19,10 @@ module WPScan
11
19
  def enum_message(type, detection_mode)
12
20
  return unless %w[plugins themes].include?(type)
13
21
 
14
- details = if ParsedCli.enumerate[:"vulnerable_#{type}"]
22
+ enumerate = ParsedCli.enumerate || {}
23
+ details = if enumerate[:"vulnerable_#{type}"]
15
24
  'Vulnerable'
16
- elsif ParsedCli.enumerate[:"all_#{type}"]
25
+ elsif enumerate[:"all_#{type}"]
17
26
  'All'
18
27
  else
19
28
  'Most Popular'
@@ -52,11 +61,59 @@ module WPScan
52
61
  }
53
62
  end
54
63
 
64
+ # Resolves collisions between --plugins-list/--themes-list and the
65
+ # corresponding --enumerate choices. The list options take precedence;
66
+ # colliding enumerate keys are removed from the supplied hash and a
67
+ # notice is emitted for each ignored choice.
68
+ #
69
+ # @param [ Hash ] enum The ParsedCli.enumerate hash (mutated in place)
70
+ def resolve_list_enumerate_collisions(enum)
71
+ {
72
+ plugins_list: %i[vulnerable_plugins all_plugins popular_plugins],
73
+ themes_list: %i[vulnerable_themes all_themes popular_themes]
74
+ }.each do |list_opt, enum_keys|
75
+ next unless ParsedCli.send(list_opt)
76
+
77
+ ignored = enum_keys.select { |k| enum.key?(k) }
78
+ next if ignored.empty?
79
+
80
+ ignored.each { |k| enum.delete(k) }
81
+
82
+ output(
83
+ '@notice',
84
+ msg: "--#{list_opt.to_s.tr('_', '-')} provided; " \
85
+ "ignoring colliding --enumerate choice(s): #{ignored.join(', ')}"
86
+ )
87
+ end
88
+ end
89
+
90
+ # Suppresses plugin/theme --enumerate choices and --plugins-list / --themes-list
91
+ # when --wp-auth was supplied, since AuthenticatedInventory already populated
92
+ # the target with authoritative data.
93
+ #
94
+ # @param [ Hash ] enum The enumeration hash, mutated in place.
95
+ def suppress_plugin_theme_choices_when_authenticated(enum)
96
+ return unless ParsedCli.wp_auth
97
+
98
+ suppressed = enum.keys & WP_AUTH_SUPPRESSED_CHOICES
99
+ suppressed.each { |k| enum.delete(k) }
100
+
101
+ lists_suppressed = %i[plugins_list themes_list].select { |opt| ParsedCli.send(opt) }
102
+ return if suppressed.empty? && lists_suppressed.empty?
103
+
104
+ ignored = (suppressed + lists_suppressed.map { |o| "--#{o.to_s.tr('_', '-')}" }).join(', ')
105
+ output('@notice',
106
+ msg: "--wp-auth provided; ignoring plugin/theme enumeration option(s): #{ignored} " \
107
+ '(authoritative inventory already retrieved via the WP REST API).')
108
+ end
109
+
55
110
  # @param [ Hash ] opts
56
111
  #
57
112
  # @return [ Boolean ] Wether or not to enumerate the plugins
58
113
  def enum_plugins?(opts)
59
- opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
114
+ return false if ParsedCli.wp_auth
115
+
116
+ ParsedCli.plugins_list || opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
60
117
  end
61
118
 
62
119
  def enum_plugins
@@ -67,20 +124,11 @@ module WPScan
67
124
  )
68
125
 
69
126
  output('@info', msg: enum_message('plugins', opts[:mode])) if user_interaction?
70
- # Enumerate the plugins & find their versions to avoid doing that when #version
71
- # is called in the view
72
- plugins = target.plugins(opts)
73
-
74
- if user_interaction? && !plugins.empty?
75
- output('@info',
76
- msg: "Checking Plugin Versions #{enum_detection_message(opts[:version_detection][:mode])}")
77
- end
78
-
79
- plugins.each(&:version)
80
127
 
81
- plugins.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_plugins]
82
-
83
- output('plugins', plugins: plugins)
128
+ enum_wp_items(
129
+ 'plugin', target_method: :plugins, opts: opts,
130
+ only_vulnerable: ParsedCli.enumerate&.dig(:vulnerable_plugins)
131
+ )
84
132
  end
85
133
 
86
134
  # @param [ Hash ] opts
@@ -103,7 +151,9 @@ module WPScan
103
151
  #
104
152
  # @return [ Boolean ] Wether or not to enumerate the themes
105
153
  def enum_themes?(opts)
106
- opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
154
+ return false if ParsedCli.wp_auth
155
+
156
+ ParsedCli.themes_list || opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
107
157
  end
108
158
 
109
159
  def enum_themes
@@ -114,20 +164,56 @@ module WPScan
114
164
  )
115
165
 
116
166
  output('@info', msg: enum_message('themes', opts[:mode])) if user_interaction?
117
- # Enumerate the themes & find their versions to avoid doing that when #version
118
- # is called in the view
119
- themes = target.themes(opts)
120
167
 
121
- if user_interaction? && !themes.empty?
122
- output('@info',
123
- msg: "Checking Theme Versions #{enum_detection_message(opts[:version_detection][:mode])}")
168
+ enum_wp_items(
169
+ 'theme', target_method: :themes, opts: opts,
170
+ only_vulnerable: ParsedCli.enumerate&.dig(:vulnerable_themes)
171
+ )
172
+ end
173
+
174
+ # Shared plugins/themes enumeration body. Streams per-item output when
175
+ # the active formatter supports it (and --no-stream wasn't passed),
176
+ # otherwise batches the result and renders the plural view.
177
+ #
178
+ # @param [ String ] singular 'plugin' or 'theme' (view name)
179
+ # @param [ Symbol ] target_method :plugins or :themes
180
+ # @param [ Hash ] opts Options forwarded to the target call
181
+ # @param [ Boolean ] only_vulnerable Filter to vulnerable items only
182
+ def enum_wp_items(singular, target_method:, opts:, only_vulnerable:)
183
+ stream = stream_findings?
184
+
185
+ items = target.send(target_method, opts) do |item|
186
+ stream_wp_item(item, singular: singular, only_vulnerable: only_vulnerable) if stream
124
187
  end
125
188
 
126
- themes.each(&:version)
189
+ finalize_wp_items_output(items, singular: singular, opts: opts, stream: stream,
190
+ only_vulnerable: only_vulnerable)
191
+ end
192
+
193
+ def finalize_wp_items_output(items, singular:, opts:, stream:, only_vulnerable:)
194
+ plural = "#{singular}s"
195
+
196
+ if !stream && user_interaction? && !items.empty?
197
+ mode_msg = enum_detection_message(opts[:version_detection][:mode])
198
+ output('@info', msg: "Checking #{singular.capitalize} Versions #{mode_msg}")
199
+ end
127
200
 
128
- themes.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_themes]
201
+ items.each(&:version) unless stream
202
+ items.select!(&:vulnerable?) if only_vulnerable
129
203
 
130
- output('themes', themes: themes)
204
+ if stream
205
+ summary = items.empty? ? "No #{plural} Found." : "#{items.size} #{singular}(s) Identified."
206
+ output('@notice', msg: summary)
207
+ else
208
+ output(plural, plural.to_sym => items)
209
+ end
210
+ end
211
+
212
+ def stream_wp_item(item, singular:, only_vulnerable:)
213
+ item.version
214
+ return if only_vulnerable && !item.vulnerable?
215
+
216
+ output(singular, singular.to_sym => item)
131
217
  end
132
218
 
133
219
  # @param [ Hash ] opts
@@ -147,33 +233,39 @@ module WPScan
147
233
  end
148
234
 
149
235
  def enum_timthumbs
150
- opts = default_opts('timthumbs').merge(list: ParsedCli.timthumbs_list)
236
+ opts = { list: ParsedCli.timthumbs_list, show_progression: user_interaction? }
151
237
 
152
- output('@info', msg: "Enumerating Timthumbs #{enum_detection_message(opts[:mode])}") if user_interaction?
238
+ output('@info', msg: 'Enumerating Timthumbs') if user_interaction?
153
239
  output('timthumbs', timthumbs: target.timthumbs(opts))
154
240
  end
155
241
 
156
242
  def enum_config_backups
157
- opts = default_opts('config_backups').merge(list: ParsedCli.config_backups_list)
243
+ opts = { list: ParsedCli.config_backups_list, show_progression: user_interaction? }
158
244
 
159
- output('@info', msg: "Enumerating Config Backups #{enum_detection_message(opts[:mode])}") if user_interaction?
245
+ output('@info', msg: 'Enumerating Config Backups') if user_interaction?
160
246
  output('config_backups', config_backups: target.config_backups(opts))
161
247
  end
162
248
 
163
249
  def enum_db_exports
164
- opts = default_opts('db_exports').merge(list: ParsedCli.db_exports_list)
250
+ opts = { list: ParsedCli.db_exports_list, show_progression: user_interaction? }
165
251
 
166
- output('@info', msg: "Enumerating DB Exports #{enum_detection_message(opts[:mode])}") if user_interaction?
252
+ output('@info', msg: 'Enumerating DB Exports') if user_interaction?
167
253
  output('db_exports', db_exports: target.db_exports(opts))
168
254
  end
169
255
 
256
+ def enum_backup_folders
257
+ opts = { list: ParsedCli.backup_folders_list, show_progression: user_interaction? }
258
+
259
+ output('@info', msg: 'Enumerating Backup Folders') if user_interaction?
260
+ output('backup_folders', backup_folders: target.backup_folders(opts))
261
+ end
262
+
170
263
  def enum_medias
171
- opts = default_opts('medias').merge(range: ParsedCli.enumerate[:medias])
264
+ opts = { range: ParsedCli.enumerate[:medias], show_progression: user_interaction? }
172
265
 
173
266
  if user_interaction?
174
267
  output('@info',
175
- msg: "Enumerating Medias #{enum_detection_message(opts[:mode])} "\
176
- '(Permalink setting must be set to "Plain" for those to be detected)')
268
+ msg: 'Enumerating Medias (Permalink setting must be set to "Plain" for those to be detected)')
177
269
  end
178
270
 
179
271
  output('medias', medias: target.medias(opts))
@@ -193,14 +285,29 @@ module WPScan
193
285
  )
194
286
 
195
287
  output('@info', msg: "Enumerating Users #{enum_detection_message(opts[:mode])}") if user_interaction?
196
- output('users', users: target.users(opts))
288
+
289
+ stream = stream_findings?
290
+ exclude = ParsedCli.exclude_usernames
291
+
292
+ users = target.users(opts) do |user|
293
+ next unless stream
294
+ next if exclude&.match?(user.username)
295
+
296
+ output('user', user: user)
297
+ end || []
298
+
299
+ if stream
300
+ output('@notice', msg: users.empty? ? 'No Users Found.' : "#{users.size} user(s) Identified.")
301
+ else
302
+ output('users', users: users)
303
+ end
197
304
  end
198
305
 
199
306
  # @return [ Range ] The user ids range to enumerate
200
307
  # If the --enumerate is used, the default value is handled by the Option
201
308
  # However, when using --passwords alone, the default has to be set by the code below
202
309
  def enum_users_range
203
- ParsedCli.enumerate[:users] || cli_enum_choices[0].choices[:u].validate(nil)
310
+ ParsedCli.enumerate&.dig(:users) || cli_enum_choices[0].choices[:u].validate(nil)
204
311
  end
205
312
  end
206
313
  end
@@ -6,15 +6,38 @@ require_relative 'enumeration/enum_methods'
6
6
  module WPScan
7
7
  module Controller
8
8
  # Enumeration Controller
9
- class Enumeration < CMSScanner::Controller::Base
9
+ class Enumeration < WPScan::Controller::Base
10
+ def before_scan
11
+ enum = ParsedCli.enumerate || {}
12
+
13
+ # Plugin/theme enumeration is bypassed when --wp-auth is set, so the API token
14
+ # requirement for `vp`/`vt` is irrelevant in that case.
15
+ return if ParsedCli.wp_auth
16
+
17
+ # Check if vulnerable plugin/theme enumeration is requested without an API token
18
+ return unless (enum[:vulnerable_plugins] || enum[:vulnerable_themes]) && DB::VulnApi.token.nil?
19
+
20
+ raise Error::ApiTokenRequiredForVulnerableEnumeration
21
+ end
22
+
23
+ # Plugin/theme enumeration choices skipped when --wp-auth is supplied
24
+ # (authoritative inventory already fetched by AuthenticatedInventory).
25
+ WP_AUTH_SUPPRESSED_CHOICES = %i[
26
+ vulnerable_plugins all_plugins popular_plugins
27
+ vulnerable_themes all_themes popular_themes
28
+ ].freeze
29
+
10
30
  def run
11
31
  enum = ParsedCli.enumerate || {}
12
32
 
33
+ resolve_list_enumerate_collisions(enum)
34
+ suppress_plugin_theme_choices_when_authenticated(enum)
35
+
13
36
  enum_plugins if enum_plugins?(enum)
14
37
  enum_themes if enum_themes?(enum)
15
38
 
16
- %i[timthumbs config_backups db_exports medias].each do |key|
17
- send("enum_#{key}".to_sym) if enum.key?(key)
39
+ %i[timthumbs config_backups db_exports backup_folders medias].each do |key|
40
+ send(:"enum_#{key}") if enum.key?(key)
18
41
  end
19
42
 
20
43
  enum_users if enum_users?(enum)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Controller
5
+ # InterestingFindings Controller
6
+ class InterestingFindings < Base
7
+ def cli_options
8
+ [
9
+ OptChoice.new(
10
+ ['--interesting-findings-detection MODE',
11
+ 'Use the supplied mode for the interesting findings detection. '],
12
+ choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
13
+ )
14
+ ]
15
+ end
16
+
17
+ def run
18
+ mode = WPScan::ParsedCli.interesting_findings_detection || WPScan::ParsedCli.detection_mode
19
+ findings = target.interesting_findings(mode: mode)
20
+
21
+ output('findings', findings: findings) unless findings.empty?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,7 +3,7 @@
3
3
  module WPScan
4
4
  module Controller
5
5
  # Main Theme Controller
6
- class MainTheme < CMSScanner::Controller::Base
6
+ class MainTheme < WPScan::Controller::Base
7
7
  def cli_options
8
8
  [
9
9
  OptChoice.new(
@@ -3,7 +3,7 @@
3
3
  module WPScan
4
4
  module Controller
5
5
  # Password Attack Controller
6
- class PasswordAttack < CMSScanner::Controller::Base
6
+ class PasswordAttack < WPScan::Controller::Base
7
7
  def cli_options
8
8
  [
9
9
  OptFilePath.new(
@@ -21,14 +21,22 @@ module WPScan
21
21
  'Multicall will only work against WP < 4.4'],
22
22
  choices: %w[wp-login xmlrpc xmlrpc-multicall],
23
23
  normalize: %i[downcase underscore to_sym]),
24
- OptString.new(['--login-uri URI', 'The URI of the login page if different from /wp-login.php'])
24
+ OptString.new(['--login-uri URI', 'The URI of the login page if different from /wp-login.php']),
25
+ OptInteger.new(['--wordlist-skip N',
26
+ 'Skip the first N passwords in the wordlist (resume from line N+1)'],
27
+ default: 0),
28
+ OptInteger.new(['--max-retries N',
29
+ 'Maximum retry attempts for failed requests due to network/proxy errors'],
30
+ default: 0)
25
31
  ]
26
32
  end
27
33
 
28
34
  def attack_opts
29
35
  @attack_opts ||= {
30
36
  show_progression: user_interaction?,
31
- multicall_max_passwords: ParsedCli.multicall_max_passwords
37
+ multicall_max_passwords: ParsedCli.multicall_max_passwords,
38
+ wordlist_skip: ParsedCli.wordlist_skip,
39
+ max_retries: ParsedCli.max_retries
32
40
  }
33
41
  end
34
42
 
@@ -56,7 +64,7 @@ module WPScan
56
64
  end
57
65
  end
58
66
 
59
- # @return [ CMSScanner::Finders::Finder ] The finder used to perform the attack
67
+ # @return [ WPScan::Finders::Finder ] The finder used to perform the attack
60
68
  def attacker
61
69
  @attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
62
70
  end
@@ -66,7 +74,7 @@ module WPScan
66
74
  @xmlrpc ||= target.xmlrpc
67
75
  end
68
76
 
69
- # @return [ CMSScanner::Finders::Finder ]
77
+ # @return [ WPScan::Finders::Finder ]
70
78
  def attacker_from_cli_options
71
79
  return unless ParsedCli.password_attack
72
80
 
@@ -99,7 +107,7 @@ module WPScan
99
107
  end
100
108
  end
101
109
 
102
- # @return [ CMSScanner::Finders::Finder ]
110
+ # @return [ WPScan::Finders::Finder ]
103
111
  def attacker_from_automatic_detection
104
112
  if xmlrpc_get_users_blogs_enabled?
105
113
  wp_version = target.wp_version
@@ -3,7 +3,7 @@
3
3
  module WPScan
4
4
  module Controller
5
5
  # Controller to handle the API token
6
- class VulnApi < CMSScanner::Controller::Base
6
+ class VulnApi < WPScan::Controller::Base
7
7
  ENV_KEY = 'WPSCAN_API_TOKEN'
8
8
 
9
9
  def cli_options
@@ -11,6 +11,12 @@ module WPScan
11
11
  OptString.new(
12
12
  ['--api-token TOKEN',
13
13
  'The WPScan API Token to display vulnerability data, available at https://wpscan.com/profile']
14
+ ),
15
+ OptBoolean.new(
16
+ ['--proxy-target-only',
17
+ 'When used with --proxy, the proxy is only applied to requests made to the target, ' \
18
+ 'not to requests made to the WPScan API or database repository (data.wpscan.org). ' \
19
+ 'Has no effect unless --proxy is also set.']
14
20
  )
15
21
  ]
16
22
  end
@@ -18,13 +24,13 @@ module WPScan
18
24
  def before_scan
19
25
  return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
20
26
 
21
- DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
27
+ DB::VulnApi.token = ParsedCli.api_token || ENV.fetch(ENV_KEY, nil)
22
28
 
23
29
  api_status = DB::VulnApi.status
24
30
 
25
31
  raise Error::InvalidApiToken if api_status['status'] == 'forbidden'
26
32
  raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
27
- raise api_status['http_error'] if api_status['http_error']
33
+ raise Error::ApiConnectionError, api_status['http_error'] if api_status['http_error']
28
34
  end
29
35
 
30
36
  def after_scan
@@ -3,7 +3,7 @@
3
3
  module WPScan
4
4
  module Controller
5
5
  # Wp Version Controller
6
- class WpVersion < CMSScanner::Controller::Base
6
+ class WpVersion < WPScan::Controller::Base
7
7
  def cli_options
8
8
  [
9
9
  OptBoolean.new(['--wp-version-all', 'Check all the version locations'], advanced: true),
data/app/controllers.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'controllers/core'
4
4
  require_relative 'controllers/vuln_api'
5
5
  require_relative 'controllers/custom_directories'
6
6
  require_relative 'controllers/wp_version'
7
+ require_relative 'controllers/authenticated_inventory'
7
8
  require_relative 'controllers/main_theme'
8
9
  require_relative 'controllers/enumeration'
9
10
  require_relative 'controllers/password_attack'
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module BackupFolders
6
+ # Backup Folders finder
7
+ class KnownLocations < Finders::Finder
8
+ include WPScan::Finders::Finder::Enumerator
9
+
10
+ # @return [ Array<Integer> ]
11
+ def valid_response_codes
12
+ @valid_response_codes ||= [200].freeze
13
+ end
14
+
15
+ # @param [ Hash ] opts
16
+ # @option opts [ String ] :list
17
+ # @option opts [ Boolean ] :show_progression
18
+ #
19
+ # @return [ Array<BackupFolder> ]
20
+ def aggressive(opts = {})
21
+ found = []
22
+
23
+ enumerate(potential_urls(opts), opts.merge(check_full_response: valid_response_codes)) do |res|
24
+ next if target.homepage_or_404?(res)
25
+
26
+ # Only report if directory listing is enabled (makes finding actionable)
27
+ next unless target.directory_listing?(res.request.url)
28
+
29
+ found << Model::BackupFolder.new(
30
+ res.request.url,
31
+ confidence: 100, # Directory listing enabled - definite finding
32
+ found_by: DIRECT_ACCESS,
33
+ interesting_entries: target.directory_listing_entries(res.request.url)
34
+ )
35
+ end
36
+
37
+ found
38
+ end
39
+
40
+ # @param [ Hash ] opts
41
+ # @option opts [ String ] :list Mandatory
42
+ #
43
+ # @return [ Hash ]
44
+ def potential_urls(opts = {})
45
+ urls = {}
46
+ content_base = target.content_dir || 'wp-content'
47
+
48
+ File.open(opts[:list]) do |f|
49
+ f.each_with_index do |line, index|
50
+ path = line.chomp.strip
51
+ next if path.empty? || path.start_with?('#')
52
+
53
+ urls[target.url("#{content_base}/#{path}")] = index
54
+ end
55
+ end
56
+
57
+ urls
58
+ end
59
+
60
+ def create_progress_bar(opts = {})
61
+ super(opts.merge(title: ' Checking Backup Folders -'))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'backup_folders/known_locations'
4
+
5
+ module WPScan
6
+ module Finders
7
+ module BackupFolders
8
+ # Backup Folders Finder
9
+ class Base
10
+ include SameTypeFinder
11
+
12
+ # @param [ WPScan::Target ] target
13
+ def initialize(target)
14
+ finders << BackupFolders::KnownLocations.new(target)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module ConfigBackups
6
6
  # Config Backup finder
7
- class KnownFilenames < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::Enumerator
7
+ class KnownFilenames < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::Enumerator
9
9
 
10
10
  # @param [ Hash ] opts
11
11
  # @option opts [ String ] :list
@@ -31,8 +31,10 @@ module WPScan
31
31
  def potential_urls(opts = {})
32
32
  urls = {}
33
33
 
34
- File.open(opts[:list]).each_with_index do |file, index|
35
- urls[target.url(file.chomp)] = index
34
+ File.open(opts[:list]) do |f|
35
+ f.each_with_index do |file, index|
36
+ urls[target.url(file.chomp)] = index
37
+ end
36
38
  end
37
39
 
38
40
  urls
@@ -7,7 +7,7 @@ module WPScan
7
7
  module ConfigBackups
8
8
  # Config Backup Finder
9
9
  class Base
10
- include CMSScanner::Finders::SameTypeFinder
10
+ include WPScan::Finders::SameTypeFinder
11
11
 
12
12
  # @param [ WPScan::Target ] target
13
13
  def initialize(target)
@@ -4,14 +4,14 @@ module WPScan
4
4
  module Finders
5
5
  module DbExports
6
6
  # DB Exports finder
7
- class KnownLocations < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::Enumerator
7
+ class KnownLocations < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::Enumerator
9
9
 
10
10
  def valid_response_codes
11
11
  @valid_response_codes ||= [200, 206].freeze
12
12
  end
13
13
 
14
- SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE|ALTER) (?:TABLE|DATABASE)|INSERT INTO/.freeze
14
+ SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE|ALTER) (?:TABLE|DATABASE)|INSERT INTO/
15
15
 
16
16
  # @param [ Hash ] opts
17
17
  # @option opts [ String ] :list
@@ -46,22 +46,24 @@ module WPScan
46
46
  urls = {}
47
47
  index = 0
48
48
 
49
- File.open(opts[:list]).each do |path|
50
- path.chomp!
49
+ File.open(opts[:list]) do |f|
50
+ f.each do |path|
51
+ path.chomp!
51
52
 
52
- if path.include?('{domain_name}')
53
- urls[target.url(path.gsub('{domain_name}', domain_name))] = index
53
+ if path.include?('{domain_name}')
54
+ urls[target.url(path.gsub('{domain_name}', domain_name))] = index
54
55
 
55
- if domain_name != domain_name_with_sub
56
- urls[target.url(path.gsub('{domain_name}', domain_name_with_sub))] = index + 1
56
+ if domain_name != domain_name_with_sub
57
+ urls[target.url(path.gsub('{domain_name}', domain_name_with_sub))] = index + 1
57
58
 
58
- index += 1
59
+ index += 1
60
+ end
61
+ else
62
+ urls[target.url(path)] = index
59
63
  end
60
- else
61
- urls[target.url(path)] = index
62
- end
63
64
 
64
- index += 1
65
+ index += 1
66
+ end
65
67
  end
66
68
 
67
69
  urls
@@ -7,7 +7,7 @@ module WPScan
7
7
  module DbExports
8
8
  # DB Exports Finder
9
9
  class Base
10
- include CMSScanner::Finders::SameTypeFinder
10
+ include WPScan::Finders::SameTypeFinder
11
11
 
12
12
  # @param [ WPScan::Target ] target
13
13
  def initialize(target)
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # BackupDB finder
7
- class BackupDB < CMSScanner::Finders::Finder
7
+ class BackupDB < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'wp-content/backup-db/'
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # debug.log finder
7
- class DebugLog < CMSScanner::Finders::Finder
7
+ class DebugLog < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'wp-content/debug.log'