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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ class Finder
6
+ # Module to provide an easy way to perform password attacks
7
+ module BreadthFirstDictionaryAttack
8
+ # Tracks progress for password attack
9
+ class ProgressTracker
10
+ attr_reader :progress_bar
11
+
12
+ def initialize(progress_bar:, total_passwords:)
13
+ @progress_bar = progress_bar
14
+ @total_passwords = total_passwords
15
+ @user_requests_count = Hash.new(0)
16
+ end
17
+
18
+ def record_request(username)
19
+ @user_requests_count[username] += 1
20
+ end
21
+
22
+ def requests_for_user(username)
23
+ @user_requests_count[username]
24
+ end
25
+
26
+ def update_title(username, password, retry_count, max_retries)
27
+ retry_info = retry_count.positive? ? " (retry #{retry_count}/#{max_retries})" : ''
28
+ @progress_bar.title = "Trying #{username} / #{password}#{retry_info}"
29
+ end
30
+
31
+ def increment
32
+ @progress_bar.increment unless @progress_bar.progress == @progress_bar.total
33
+ end
34
+
35
+ def adjust_total_on_success(username)
36
+ @progress_bar.total -= @total_passwords - @user_requests_count[username]
37
+ rescue ProgressBar::InvalidProgressError
38
+ # Due to Typhoeus threads, progress bar might be in invalid state
39
+ end
40
+
41
+ def log(message)
42
+ @progress_bar.log(message)
43
+ end
44
+
45
+ def stop
46
+ @progress_bar.stop
47
+ end
48
+ end
49
+
50
+ # Configuration bundle for LoginAttempt
51
+ LoginConfig = Struct.new(:tracker, :request_builder, :credentials_validator,
52
+ :error_checker, :error_handler, :hydra, :opts,
53
+ keyword_init: true)
54
+
55
+ # Handles a single login attempt with retry support
56
+ class LoginAttempt
57
+ attr_reader :user, :password, :max_retries
58
+ attr_accessor :retry_count
59
+
60
+ def initialize(user:, password:, max_retries:, config:)
61
+ @user = user
62
+ @password = password
63
+ @max_retries = max_retries
64
+ @retry_count = 0
65
+ @config = config
66
+ end
67
+
68
+ def execute(&block)
69
+ request = @config.request_builder.call(@user.username, @password)
70
+ @config.tracker.record_request(@user.username) if @retry_count.zero?
71
+
72
+ request.on_complete do |response|
73
+ handle_response(response, &block)
74
+ end
75
+
76
+ @config.hydra.queue(request)
77
+ end
78
+
79
+ private
80
+
81
+ def handle_response(response, &)
82
+ @config.tracker.update_title(@user.username, @password, @retry_count, @max_retries)
83
+
84
+ if @config.credentials_validator.call(response)
85
+ handle_success(&)
86
+ elsif @config.error_checker.call(response)
87
+ handle_error(response, &)
88
+ else
89
+ handle_invalid_credentials
90
+ end
91
+ end
92
+
93
+ def handle_success
94
+ @config.tracker.increment
95
+ @user.password = @password
96
+ @config.tracker.adjust_total_on_success(@user.username)
97
+
98
+ yield @user if block_given?
99
+ end
100
+
101
+ def handle_error(response, &)
102
+ if @user.password
103
+ @config.tracker.increment
104
+ return
105
+ end
106
+
107
+ if should_retry?
108
+ retry_attempt(&)
109
+ else
110
+ finalize_failed_attempt(response)
111
+ end
112
+ end
113
+
114
+ def should_retry?
115
+ @retry_count < @max_retries
116
+ end
117
+
118
+ def retry_attempt(&)
119
+ @retry_count += 1
120
+
121
+ if @config.opts[:show_progression] && WPScan::ParsedCli.verbose?
122
+ @config.tracker.log("[RETRY #{@retry_count}/#{@max_retries}] #{@user.username} / #{@password}")
123
+ end
124
+
125
+ execute(&)
126
+ end
127
+
128
+ def finalize_failed_attempt(response)
129
+ @config.error_handler.call(response)
130
+ @config.tracker.increment
131
+ end
132
+
133
+ def handle_invalid_credentials
134
+ @config.tracker.increment
135
+ end
136
+ end
137
+
138
+ # @param [ Array<WPScan::Model::User> ] users
139
+ # @param [ String ] wordlist_path
140
+ # @param [ Hash ] opts
141
+ # @option opts [ Boolean ] :show_progression
142
+ # @option opts [ Integer ] :wordlist_skip Number of passwords to skip from the beginning
143
+ # @option opts [ Integer ] :max_retries Maximum retry attempts for failed requests
144
+ #
145
+ # @yield [ WPScan::User ] When a valid combination is found
146
+ #
147
+ # Due to Typhoeus threads shenanigans, in rare cases the progress-bar might
148
+ # be incorrectly updated, hence the 'rescue ProgressBar::InvalidProgressError'
149
+ #
150
+ # TODO: Make rubocop happy about metrics etc
151
+ #
152
+ # rubocop:disable all
153
+ def attack(users, wordlist_path, opts = {})
154
+ wordlist = File.open(wordlist_path)
155
+ skip_count = opts[:wordlist_skip] || 0
156
+ max_retries = opts[:max_retries] || 0
157
+
158
+ # Calculate total, accounting for skipped passwords
159
+ total_passwords = wordlist.count
160
+ effective_passwords = [total_passwords - skip_count, 0].max
161
+
162
+ create_progress_bar(total: users.size * effective_passwords, show_progression: opts[:show_progression])
163
+ tracker = ProgressTracker.new(progress_bar: progress_bar, total_passwords: effective_passwords)
164
+ config = create_login_config(tracker, opts)
165
+
166
+ # Show skip progress if skipping
167
+ if skip_count.positive? && opts[:show_progression]
168
+ tracker.log("[INFO] Skipping first #{skip_count} password(s) from wordlist...")
169
+ end
170
+
171
+ queue_count = 0
172
+
173
+ File.foreach(wordlist, chomp: true).lazy.drop(skip_count).each do |password|
174
+ remaining_users = users.select { |u| u.password.nil? }
175
+ break if remaining_users.empty?
176
+
177
+ remaining_users.each do |user|
178
+ attempt = LoginAttempt.new(user: user, password: password, max_retries: max_retries, config: config)
179
+ attempt.execute { |found_user| yield found_user if block_given? }
180
+ queue_count += 1
181
+
182
+ if queue_count >= hydra.max_concurrency
183
+ hydra.run
184
+ queue_count = 0
185
+ end
186
+ end
187
+ end
188
+
189
+ hydra.run
190
+ tracker.stop
191
+ end
192
+ # rubocop:enable all
193
+
194
+ # Create login configuration with all dependencies
195
+ #
196
+ # @param [ ProgressTracker ] tracker
197
+ # @param [ Hash ] opts
198
+ #
199
+ # @return [ LoginConfig ]
200
+ #
201
+ def create_login_config(tracker, opts)
202
+ LoginConfig.new(
203
+ tracker: tracker,
204
+ request_builder: method(:login_request).to_proc,
205
+ credentials_validator: method(:valid_credentials?).to_proc,
206
+ error_checker: method(:errored_response?).to_proc,
207
+ error_handler: method(:output_error).to_proc,
208
+ hydra: hydra,
209
+ opts: opts
210
+ )
211
+ end
212
+
213
+ # @param [ String ] username
214
+ # param [ String ] password
215
+ #
216
+ # @return [ Typhoeus::Request ]
217
+ def login_request(username, password)
218
+ # To Implement in the finder related to the attack
219
+ end
220
+
221
+ # @param [ Typhoeus::Response ] response
222
+ #
223
+ # @return [ Boolean ] Whether or not credentials related to the request are valid
224
+ def valid_credentials?(response)
225
+ # To Implement in the finder related to the attack
226
+ end
227
+
228
+ # @param [ Typhoeus::Response ] response
229
+ #
230
+ # @return [ Boolean ] Whether or not something wrong happened
231
+ # other than wrong credentials
232
+ def errored_response?(response)
233
+ # To Implement in the finder related to the attack
234
+ end
235
+
236
+ protected
237
+
238
+ # @param [ Typhoeus::Response ] response
239
+ def output_error(response)
240
+ error = if response.timed_out?
241
+ 'Request timed out.'
242
+ elsif response.code.zero?
243
+ "No response from remote server. WAF/IPS? (#{response.return_message})"
244
+ elsif response.code.to_s.start_with?('50')
245
+ 'Server error, try reducing the number of threads.'
246
+ elsif WPScan::ParsedCli.verbose?
247
+ "Unknown response received Code: #{response.code}\nBody: #{response.body}"
248
+ else
249
+ "Unknown response received Code: #{response.code}"
250
+ end
251
+
252
+ progress_bar.log("Error: #{error}")
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ class Finder
6
+ # Module to provide an easy way to enumerate items such as plugins, themes etc
7
+ module Enumerator
8
+ # @return [ Hash ]
9
+ def head_or_get_request_params
10
+ # Disabling the cache, as it causes a 'stack level too deep' exception
11
+ # with a large number of requests.
12
+ # See https://github.com/typhoeus/typhoeus/issues/408
13
+ @head_or_get_request_params ||= target.head_or_get_params.merge(cache_ttl: 0)
14
+ end
15
+
16
+ # @return [ Array<Integer> ]
17
+ def valid_response_codes
18
+ @valid_response_codes ||= [200]
19
+ end
20
+
21
+ # @param [ Hash ] The target urls
22
+ # @param [ Hash ] opts
23
+ # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
24
+ # @option opts [ Regexp ] :exclude_content
25
+ # @option opts [ Boolean, Array, String ] :check_full_response
26
+ #
27
+ # @yield [ Typhoeus::Response, String ]
28
+ def enumerate(urls, opts = {})
29
+ create_progress_bar(opts.merge(total: urls.size))
30
+
31
+ urls.each do |url, id|
32
+ request = browser.forge_request(url, head_or_get_request_params)
33
+
34
+ request.on_complete do |head_res|
35
+ progress_bar.increment
36
+
37
+ next unless valid_response_codes.include?(head_res.code)
38
+
39
+ next if opts[:exclude_content] && head_res.response_headers&.match(opts[:exclude_content])
40
+
41
+ head_or_full_res = maybe_get_full_response(head_res, opts)
42
+
43
+ yield head_or_full_res, id if head_or_full_res
44
+ end
45
+
46
+ hydra.queue(request)
47
+ end
48
+
49
+ hydra.run
50
+ end
51
+
52
+ # @param [ Typhoeus::Response ] head_res
53
+ # @param [ Hash ] opts
54
+ #
55
+ # @return [ Typhoeus::Response, nil ]
56
+ def maybe_get_full_response(head_res, opts)
57
+ return head_res unless opts[:check_full_response] == true ||
58
+ Array(opts[:check_full_response]).include?(head_res.code)
59
+
60
+ full_res = WPScan::Browser.get(head_res.effective_url, full_request_params)
61
+
62
+ return unless valid_response_codes.include?(full_res.code)
63
+
64
+ return if target.homepage_or_404?(full_res) ||
65
+ (opts[:exclude_content] && full_res.body&.match(opts[:exclude_content]))
66
+
67
+ full_res
68
+ end
69
+
70
+ # @return [ Hash ]
71
+ def full_request_params
72
+ @full_request_params ||= {}
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ class Finder
6
+ # Module to provide an easy way to fingerprint things such as versions
7
+ module Fingerprinter
8
+ include Enumerator
9
+
10
+ # @param [ Hash ] fingerprints The fingerprints
11
+ # Format should be like the following:
12
+ # {
13
+ # file_path_1: {
14
+ # md5_hash_1: version_1,
15
+ # md5_hash_2: [version_2]
16
+ # },
17
+ # file_path_2: {
18
+ # md5_hash_3: [version_1, version_2],
19
+ # md5_hash_4: version_3
20
+ # }
21
+ # }
22
+ # Note that the version can either be an array or a string
23
+ #
24
+ # @param [ Hash ] opts
25
+ # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
26
+ #
27
+ # @yield [ Mixed, String, String ] version/s, url, hash The version associated to the
28
+ # fingerprint of the url
29
+ def fingerprint(fingerprints, opts = {})
30
+ enum_opts = opts.merge(check_full_response: 200)
31
+
32
+ enumerate(fingerprints.transform_keys { |k| target.url(k) }, enum_opts) do |res, fingerprint|
33
+ md5sum = hexdigest(res.body)
34
+
35
+ next unless fingerprint.key?(md5sum)
36
+
37
+ yield fingerprint[md5sum], res.effective_url, md5sum
38
+ end
39
+ end
40
+
41
+ # @return [ String ] The hashed value for the given body
42
+ def hexdigest(body)
43
+ Digest::MD5.hexdigest(body)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ class Finder
6
+ module SmartURLChecker
7
+ # Findings
8
+ class Findings < Array
9
+ def <<(finding)
10
+ return self unless finding
11
+
12
+ each do |f|
13
+ next unless f == finding && f.found_by == finding.found_by
14
+
15
+ # This makes sure entries added are unique
16
+ # and prevent pages redirecting to the same one to be added twice
17
+ entries_to_add = finding.interesting_entries - f.interesting_entries
18
+ return self if entries_to_add.empty?
19
+
20
+ entries_to_add.each { |entry| f.interesting_entries << entry }
21
+
22
+ f.confidence += finding.confidence
23
+
24
+ return self
25
+ end
26
+
27
+ super
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/finders/finder/smart_url_checker/findings'
4
+
5
+ module WPScan
6
+ module Finders
7
+ class Finder
8
+ # Smart URL Checker
9
+ # Typically used when some URLs are potentially in the homepage. If they are found
10
+ # in it, they will be checked in the #passive (like a browser/client would do when loading the page).
11
+ # Otherwise they will be checked in the #aggressive
12
+ module SmartURLChecker
13
+ # @param [ Array<String> ] urls
14
+ # @param [ Hash ] opts
15
+ #
16
+ # @return []
17
+ def process_urls(_urls, _opts = {})
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # @param [ Hash ] opts
22
+ #
23
+ # @return [ Array<Finding> ]
24
+ def passive(opts = {})
25
+ process_urls(passive_urls(opts), opts)
26
+ end
27
+
28
+ # @param [ Hash ] opts
29
+ #
30
+ # @return [ Array<String> ]
31
+ def passive_urls(_opts = {})
32
+ target.in_scope_uris(target.homepage_res, passive_urls_xpath).map(&:to_s)
33
+ end
34
+
35
+ # @return [ String ]
36
+ def passive_urls_xpath
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # @param [ Hash ] opts
41
+ #
42
+ # @return [ Array<Finding> ]
43
+ def aggressive(opts = {})
44
+ # To avoid scanning the same twice
45
+ urls = aggressive_urls(opts)
46
+ urls -= passive_urls(opts) if opts[:mode] == :mixed
47
+
48
+ process_urls(urls, opts)
49
+ end
50
+
51
+ # @param [ Hash ] opts
52
+ #
53
+ # @return [ Array<String> ]
54
+ def aggressive_urls(_opts = {})
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -6,7 +6,7 @@ module WPScan
6
6
  module WpVersion
7
7
  # SmartURLChecker specific for the WP Version
8
8
  module SmartURLChecker
9
- include CMSScanner::Finders::Finder::SmartURLChecker
9
+ include WPScan::Finders::Finder::SmartURLChecker
10
10
 
11
11
  def create_version(number, opts = {})
12
12
  Model::WpVersion.new(
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/finders/finder/smart_url_checker'
4
+ require 'wpscan/finders/finder/enumerator'
5
+ require 'wpscan/finders/finder/fingerprinter'
6
+ require 'wpscan/finders/finder/breadth_first_dictionary_attack'
7
+
8
+ module WPScan
9
+ module Finders
10
+ # Finder
11
+ class Finder
12
+ # Constants for common found_by
13
+ DIRECT_ACCESS = 'Direct Access (Aggressive Detection)'
14
+
15
+ attr_accessor :target, :progress_bar
16
+
17
+ def initialize(target)
18
+ @target = target
19
+ end
20
+
21
+ # @return [ String ] The titleized name of the finder
22
+ def titleize
23
+ # Put a _ char before any digits except those at the end, which will be replaced by a space
24
+ # Otherwise, class such as Error404Page are returned as Error404 Page instead of Error 404 page
25
+ # The keep_id_suffix is to concevert classes such as CssId to Css Id instead of Css
26
+
27
+ @titleize ||= self.class.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
28
+ end
29
+
30
+ # @param [ Hash ] _opts
31
+ def passive(_opts = {}); end
32
+
33
+ # @param [ Hash ] _opts
34
+ def aggressive(_opts = {}); end
35
+
36
+ # @param [ Hash ] opts See https://github.com/jfelchner/ruby-progressbar/wiki/Options
37
+ # @option opts [ Boolean ] :show_progression
38
+ #
39
+ # @return [ ProgressBar::Base ]
40
+ def create_progress_bar(opts = {})
41
+ bar_opts = { format: '%t %a <%B> (%c / %C) %P%% %e' }
42
+ bar_opts[:output] = ProgressBarNullOutput unless opts[:show_progression]
43
+
44
+ @progress_bar = ::ProgressBar.create(bar_opts.merge(opts))
45
+ end
46
+
47
+ # @return [ Browser ]
48
+ def browser
49
+ @browser ||= WPScan::Browser.instance
50
+ end
51
+
52
+ # @return [ Typhoeus::Hydra ]
53
+ def hydra
54
+ @hydra ||= browser.hydra
55
+ end
56
+
57
+ # @param [String, Class ] klass
58
+ # @return [ String ]
59
+ def found_by(klass = self.class)
60
+ labels = %w[aggressive passive]
61
+
62
+ caller_locations.each do |call|
63
+ label = call.label
64
+ # Since ruby 3.4, the label contains the full name, including module and class
65
+ # rather than just the method like in ruby < 3.4
66
+ label = label[/#(.*)/i, 1] if label.include?('#')
67
+
68
+ next unless labels.include? label
69
+
70
+ title = klass.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
71
+
72
+ return "#{title} (#{label.capitalize} Detection)"
73
+ end
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # Finding
6
+ module Finding
7
+ # Fix for "Double/Dynamic Inclusion Problem"
8
+ def self.included(base)
9
+ base.include References
10
+ super
11
+ end
12
+
13
+ FINDING_OPTS = %i[confidence confirmed_by references found_by interesting_entries].freeze
14
+
15
+ attr_accessor(*FINDING_OPTS)
16
+
17
+ # @return [ Array ]
18
+ def confirmed_by
19
+ @confirmed_by ||= []
20
+ end
21
+
22
+ # Should be overriden in child classes
23
+ # @return [ Array ]
24
+ def interesting_entries
25
+ @interesting_entries ||= []
26
+ end
27
+
28
+ # @return [ Integer ]
29
+ def confidence
30
+ @confidence ||= 0
31
+ end
32
+
33
+ # @param [ Integer ] value
34
+ def confidence=(value)
35
+ @confidence = [value, 100].min
36
+ end
37
+
38
+ # @param [ Hash ] opts
39
+ def parse_finding_options(opts = {})
40
+ FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) }
41
+ end
42
+
43
+ # TODO: maybe also check for interesting_entries and confirmed_by ?
44
+ # So far this is used in specs only
45
+ def eql?(other)
46
+ self == other && confidence == other.confidence && found_by == other.found_by
47
+ end
48
+
49
+ def <=>(other)
50
+ to_s.downcase <=> other.to_s.downcase
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # Findings container
6
+ class Findings < Array
7
+ # Optional callable invoked with each newly-appended finding (not invoked
8
+ # when a duplicate is merged into an existing one via confirmed_by). Used
9
+ # by enumeration controllers to stream findings as they are discovered.
10
+ attr_accessor :on_append
11
+
12
+ # Override to include the confirmed_by logic
13
+ #
14
+ # @param [ Finding ] finding
15
+ def <<(finding)
16
+ return self unless finding
17
+
18
+ each do |found|
19
+ next unless found == finding
20
+
21
+ found.confirmed_by << finding
22
+ found.confidence += finding.confidence
23
+
24
+ return self
25
+ end
26
+
27
+ super
28
+ @on_append&.call(finding)
29
+ self
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ # Independent Finder
6
+ module IndependentFinder
7
+ extend ActiveSupport::Concern
8
+
9
+ # See ActiveSupport::Concern
10
+ module ClassMethods
11
+ def find(target, opts = {}, &)
12
+ new(target).find(opts, &)
13
+ end
14
+ end
15
+
16
+ # @param [ Hash ] opts
17
+ # @option opts [ Symbol ] mode (:mixed, :passive, :aggressive)
18
+ # @yield [ Finding ] Optional block called for each finding the moment
19
+ # it is first appended to the result set (used to
20
+ # stream enumeration findings as they are discovered).
21
+ #
22
+ # @return [ Findings ]
23
+ def find(opts = {}, &)
24
+ finders.run(opts, &)
25
+ end
26
+
27
+ # @return [ Array ]
28
+ def finders
29
+ @finders ||= WPScan::Finders::IndependentFinders.new
30
+ end
31
+ end
32
+ end
33
+ end