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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # Options available in the Browser
5
+ class Browser
6
+ OPTIONS = %i[
7
+ cache_ttl
8
+ cookie_jar
9
+ cookie_string
10
+ connect_timeout
11
+ disable_tls_checks
12
+ headers
13
+ http_auth
14
+ max_threads
15
+ proxy
16
+ proxy_auth
17
+ random_user_agent
18
+ request_timeout
19
+ throttle
20
+ url
21
+ user_agent
22
+ user_agents_list
23
+ vhost
24
+ ].freeze
25
+
26
+ attr_accessor(*OPTIONS)
27
+
28
+ # @return [ String ]
29
+ def default_user_agent
30
+ "#{NS} v#{WPScan::VERSION}"
31
+ end
32
+
33
+ # @return [ Typhoeus::Hydra ]
34
+ def hydra
35
+ @hydra ||= Typhoeus::Hydra.new(max_concurrency: max_threads || 1)
36
+ end
37
+
38
+ # @param [ Hash ] options
39
+ def load_options(options = {})
40
+ OPTIONS.each do |sym|
41
+ send("#{sym}=", options[sym]) if options.key?(sym)
42
+ end
43
+ end
44
+
45
+ # Set the threads attribute and update hydra accordinly
46
+ # If the throttle attribute is > 0, max_threads will be forced to 1
47
+ #
48
+ # @param [ Integer ] number
49
+ def max_threads=(number)
50
+ @max_threads = number.to_i.positive? && throttle.zero? ? number.to_i : 1
51
+
52
+ hydra.max_concurrency = @max_threads
53
+ end
54
+
55
+ # @return [ String ] The user agent
56
+ def user_agent
57
+ @user_agent ||= random_user_agent ? user_agents.sample : default_user_agent
58
+ end
59
+
60
+ # @return [ Array<String> ]
61
+ def user_agents
62
+ return @user_agents if @user_agents
63
+
64
+ @user_agents = []
65
+
66
+ # The user_agents_list is managed by the CLI options, with the default being
67
+ # APP_DIR/user_agents.txt
68
+ File.open(user_agents_list) do |f|
69
+ f.each do |line|
70
+ next if line == "\n" || line[0, 1] == '#'
71
+
72
+ @user_agents << line.chomp
73
+ end
74
+ end
75
+
76
+ @user_agents
77
+ end
78
+
79
+ # @param [ value ] The throttle time in milliseconds
80
+ #
81
+ # if value > 0, the max_threads will be set to 1
82
+ def throttle=(value)
83
+ @throttle = value.to_i.abs / 1000.0
84
+
85
+ self.max_threads = 1 if @throttle.positive?
86
+ end
87
+
88
+ def trottle!
89
+ sleep(throttle) if throttle.positive?
90
+ end
91
+ end
92
+ end
@@ -1,13 +1,98 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'wpscan/browser/actions'
4
+ require 'wpscan/browser/options'
5
+
3
6
  module WPScan
4
- # Custom Browser
5
- class Browser < CMSScanner::Browser
7
+ # Singleton used to perform HTTP/HTTPS requests to the target.
8
+ class Browser
6
9
  extend Actions
7
10
 
11
+ def initialize(parsed_options = {})
12
+ self.throttle = 0
13
+
14
+ load_options(parsed_options.dup)
15
+ end
16
+
17
+ private_class_method :new
18
+
19
+ # @param [ Hash ] parsed_options
20
+ #
21
+ # @return [ Browser ] The instance
22
+ def self.instance(parsed_options = {})
23
+ @@instance ||= new(parsed_options)
24
+ end
25
+
26
+ def self.reset
27
+ @@instance = nil
28
+ end
29
+
8
30
  # @return [ String ]
9
31
  def default_user_agent
10
32
  @default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.com/wordpress-security-scanner)"
11
33
  end
34
+
35
+ # @param [ String ] url
36
+ # @param [ Hash ] params
37
+ #
38
+ # @return [ Typhoeus::Request ]
39
+ def forge_request(url, params = {})
40
+ Typhoeus::Request.new(url, request_params(params))
41
+ end
42
+
43
+ # @return [ Hash ] The request params used to connect to the target as well as other systems (e.g. API).
44
+ def default_connect_request_params
45
+ params = {}
46
+
47
+ if disable_tls_checks
48
+ # See http://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
49
+ params[:ssl_verifypeer] = false
50
+ params[:ssl_verifyhost] = 0
51
+ # TLSv1.0 and plus, allows to use a protocol potentially lower than the OS default
52
+ params[:sslversion] = :tlsv1
53
+ end
54
+
55
+ {
56
+ connecttimeout: :connect_timeout, cache_ttl: :cache_ttl,
57
+ proxy: :proxy, timeout: :request_timeout
58
+ }.each do |typhoeus_opt, browser_opt|
59
+ attr_value = public_send(browser_opt)
60
+ params[typhoeus_opt] = attr_value unless attr_value.nil?
61
+ end
62
+
63
+ params
64
+ end
65
+
66
+ # @return [ Hash ]
67
+ # The params are not cached (using @params ||= for example) so they are set accordingly if updated
68
+ # by a controller / other piece of code.
69
+ def default_request_params
70
+ params = default_connect_request_params.merge(
71
+ headers: { 'User-Agent' => user_agent, 'Referer' => url }.merge(headers || {}),
72
+ accept_encoding: 'gzip, deflate',
73
+ method: :get
74
+ )
75
+
76
+ { cookiejar: :cookie_jar, cookiefile: :cookie_jar, cookie: :cookie_string }.each do |typhoeus_opt, browser_opt|
77
+ attr_value = public_send(browser_opt)
78
+ params[typhoeus_opt] = attr_value unless attr_value.nil?
79
+ end
80
+
81
+ params[:proxyuserpwd] = "#{proxy_auth[:username]}:#{proxy_auth[:password]}" if proxy_auth
82
+ params[:userpwd] = "#{http_auth[:username]}:#{http_auth[:password]}" if http_auth
83
+
84
+ params[:headers]['Host'] = vhost if vhost
85
+
86
+ params
87
+ end
88
+
89
+ # @param [ Hash ] params
90
+ #
91
+ # @return [ Hash ]
92
+ def request_params(params = {})
93
+ default_request_params.merge(params) do |key, oldval, newval|
94
+ key == :headers ? oldval.merge(newval) : newval
95
+ end
96
+ end
12
97
  end
13
98
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ferrum'
4
+
5
+ module WPScan
6
+ module BrowserAuthenticator
7
+ # Characters that, if present in a cookie name or value, would corrupt the
8
+ # serialized Cookie header. Per RFC 6265 these are forbidden in cookie-octets,
9
+ # but a noncompliant IdP could still emit them.
10
+ COOKIE_DELIMITERS = /[;,\s]/
11
+
12
+ def self.authenticate(login_url)
13
+ unless $stdin.tty?
14
+ raise WPScan::Error::BrowserFailed,
15
+ 'SAML authentication needs an interactive terminal to wait for login, but stdin is not a TTY. ' \
16
+ 'Run wpscan from a real shell when using --expect-saml.'
17
+ end
18
+
19
+ cookies = run_login_session(login_url)
20
+
21
+ raise WPScan::Error::SAMLAuthenticationFailed if cookies.nil? || cookies.empty?
22
+
23
+ serialize_cookies(cookies)
24
+ end
25
+
26
+ # Drives the interactive browser session and returns the resulting cookie jar.
27
+ # Translates Ferrum failures into BrowserFailed with a context-specific message.
28
+ def self.run_login_session(login_url)
29
+ browser = Ferrum::Browser.new(headless: false)
30
+
31
+ puts 'SAML authentication needed. Log in via the browser window that just opened, then press enter.'
32
+ browser.goto(login_url)
33
+ gets # Waits for user input
34
+
35
+ # Attempt an innocuous command to check if the browser is still responsive
36
+ browser.current_url
37
+
38
+ browser.cookies.all
39
+ rescue Ferrum::BinaryNotFoundError, Ferrum::EmptyPathError => e
40
+ raise WPScan::Error::BrowserFailed, chrome_not_found_message(e)
41
+ rescue Ferrum::Error => e
42
+ raise WPScan::Error::BrowserFailed,
43
+ 'The browser was closed or failed before SAML authentication could be completed ' \
44
+ "(#{e.class}: #{e.message})."
45
+ ensure
46
+ browser.quit if browser&.process
47
+ end
48
+
49
+ def self.chrome_not_found_message(error)
50
+ '--expect-saml requires Chrome or Chromium to be installed and available on PATH ' \
51
+ '(install Chrome / Chromium, or point Ferrum at a binary via BROWSER_PATH). ' \
52
+ "Underlying error: #{error.message}"
53
+ end
54
+
55
+ def self.serialize_cookies(cookies)
56
+ cookies.map do |_name, cookie|
57
+ raise WPScan::Error::SAMLAuthenticationFailed if cookie.name.match?(COOKIE_DELIMITERS) ||
58
+ cookie.value.to_s.match?(COOKIE_DELIMITERS)
59
+
60
+ "#{cookie.name}=#{cookie.value}"
61
+ end.join('; ')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Cache
5
+ # Cache Implementation using files
6
+ class FileStore
7
+ attr_reader :storage_path, :serializer
8
+
9
+ # The serializer must have the 2 methods #load and #dump
10
+ # (Marshal and YAML have them)
11
+ # YAML is Human Readable, contrary to Marshal which store in a binary format
12
+ # Marshal does not need any "require"
13
+ #
14
+ # @param [ String ] storage_path
15
+ # @param [ Constant ] serializer
16
+ def initialize(storage_path, serializer = Marshal)
17
+ @storage_path = File.expand_path(storage_path)
18
+ @serializer = serializer
19
+
20
+ FileUtils.mkdir_p(@storage_path)
21
+ end
22
+
23
+ # TODO: rename this to clear ?
24
+ def clean
25
+ Dir[File.join(storage_path, '*')].each do |f|
26
+ File.delete(f) unless File.symlink?(f)
27
+ end
28
+ end
29
+
30
+ # @param [ String ] key
31
+ #
32
+ # @return [ Mixed ]
33
+ def read_entry(key)
34
+ return if expired_entry?(key)
35
+
36
+ serializer.load(File.read(entry_path(key)))
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ # @param [ String ] key
42
+ # @param [ Mixed ] data_to_store
43
+ # @param [ Integer ] cache_ttl
44
+ def write_entry(key, data_to_store, cache_ttl)
45
+ return unless cache_ttl.to_i.positive?
46
+
47
+ File.write(entry_path(key), serializer.dump(data_to_store))
48
+ File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl)
49
+ end
50
+
51
+ # @param [ String ] key
52
+ #
53
+ # @return [ String ] The file path associated to the key
54
+ def entry_path(key)
55
+ File.join(storage_path, key)
56
+ end
57
+
58
+ # @param [ String ] key
59
+ #
60
+ # @return [ String ] The expiration file path associated to the key
61
+ def entry_expiration_path(key)
62
+ "#{entry_path(key)}.expiration"
63
+ end
64
+
65
+ private
66
+
67
+ # @param [ String ] key
68
+ #
69
+ # @return [ Boolean ]
70
+ def expired_entry?(key)
71
+ File.read(entry_expiration_path(key)).to_i <= Time.now.to_i
72
+ rescue StandardError
73
+ true
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/cache/file_store'
4
+
5
+ module WPScan
6
+ module Cache
7
+ # Cache implementation for Typhoeus
8
+ class Typhoeus < FileStore
9
+ # @param [ Typhoeus::Request ] request
10
+ #
11
+ # @return [ Typhoeus::Response ]
12
+ def get(request)
13
+ read_entry(request.hash.to_s)
14
+ end
15
+
16
+ # @param [ Typhoeus::Request ] request
17
+ # @param [ Typhoeus::Response ] response
18
+ def set(request, response)
19
+ return if response.timed_out? || response.code&.zero?
20
+
21
+ write_entry(request.hash.to_s, response, request.cache_ttl)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,10 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- # Needed to load at least the Core controller
5
- # Otherwise, the following error will be raised:
6
- # `initialize': uninitialized constant WPScan::Controller::Core (NameError)
7
4
  module Controller
8
- include CMSScanner::Controller
5
+ # Base Controller
6
+ class Base
7
+ include OptParseValidator
8
+
9
+ # @return [ Array<OptParseValidator::OptBase> ]
10
+ def cli_options; end
11
+
12
+ def before_scan; end
13
+
14
+ def run; end
15
+
16
+ def after_scan; end
17
+
18
+ def ==(other)
19
+ self.class == other.class
20
+ end
21
+
22
+ # Reset all the class attributes. Currently only used in specs.
23
+ def self.reset
24
+ @@target = nil
25
+ @@datastore = nil
26
+ @@formatter = nil
27
+ end
28
+
29
+ # @return [ Target ]
30
+ def target
31
+ @@target ||= WPScan::Target.new(WPScan::ParsedCli.url, WPScan::ParsedCli.options)
32
+ end
33
+
34
+ # @param [ OptParseValidator::OptParser ] parser
35
+ def self.option_parser=(parser)
36
+ @@option_parser = parser
37
+ end
38
+
39
+ # @return [ OptParseValidator::OptParser ]
40
+ def option_parser
41
+ @@option_parser
42
+ end
43
+
44
+ # @return [ Hash ]
45
+ def datastore
46
+ @@datastore ||= {}
47
+ end
48
+
49
+ # @return [ Formatter::Base ]
50
+ def formatter
51
+ @@formatter ||= WPScan::Formatter.load(WPScan::ParsedCli.format, datastore[:views])
52
+ end
53
+
54
+ # @see Formatter#output
55
+ #
56
+ # @return [ Void ]
57
+ def output(tpl, vars = {})
58
+ formatter.output(*tpl_params(tpl, vars))
59
+ end
60
+
61
+ # @see Formatter#render
62
+ #
63
+ # @return [ String ]
64
+ def render(tpl, vars = {})
65
+ formatter.render(*tpl_params(tpl, vars))
66
+ end
67
+
68
+ # @return [ Boolean ]
69
+ def user_interaction?
70
+ formatter.user_interaction? && !WPScan::ParsedCli.output
71
+ end
72
+
73
+ # @return [ Pathname ]
74
+ def tmp_directory
75
+ return Pathname.new(ENV['TMPDIR']).join(WPScan.app_name) if ENV['TMPDIR']
76
+
77
+ WPScan.user_cache_dir
78
+ end
79
+
80
+ protected
81
+
82
+ # @param [ String ] tpl
83
+ # @param [ Hash ] vars
84
+ #
85
+ # @return [ Array<String> ]
86
+ def tpl_params(tpl, vars)
87
+ [
88
+ tpl,
89
+ instance_variable_values.merge(vars),
90
+ self.class.name.demodulize.underscore
91
+ ]
92
+ end
93
+
94
+ # @return [ Hash ] All instance variable keys (and their values) plus the verbose value.
95
+ def instance_variable_values
96
+ h = { verbose: WPScan::ParsedCli.verbose }
97
+ instance_variables.each do |a|
98
+ s = a.to_s
99
+ n = s[1..s.size]
100
+ h[n.to_sym] = instance_variable_get(a)
101
+ end
102
+ h
103
+ end
104
+ end
9
105
  end
10
106
  end
@@ -1,10 +1,85 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- # Override to set the OptParser's summary width to 45 (instead of 40 from the CMSScanner)
5
- class Controllers < CMSScanner::Controllers
4
+ # Controllers container. Summary width is 45 (wpscan-specific; upstream default was 40).
5
+ class Controllers < Array
6
+ attr_reader :option_parser, :running
7
+
8
+ # @param [ OptParseValidator::OptParser ] option_parser
6
9
  def initialize(option_parser = OptParseValidator::OptParser.new(nil, 45))
7
- super(option_parser)
10
+ @option_parser = option_parser
11
+
12
+ register_config_files
13
+
14
+ option_parser.config_files.result_key = 'cli_options'
15
+ end
16
+
17
+ # Registers the potential option-file paths with the option_parser.
18
+ def register_config_files
19
+ # XDG Base Directory support for configuration
20
+ # https://specifications.freedesktop.org/basedir/latest/
21
+ xdg = ENV.fetch('XDG_CONFIG_HOME', nil)
22
+ xdg = Pathname.new(Dir.home).join('.config') if xdg.nil? || xdg.empty?
23
+ app = WPScan.app_name
24
+
25
+ dirs = [[xdg, app], [Dir.home, ".#{app}"], [Dir.pwd, ".#{app}"]]
26
+ exts = option_parser.config_files.class.supported_extensions
27
+
28
+ dirs.product(exts).each do |(dir, sub), ext|
29
+ option_parser.config_files << Pathname.new(dir).join(sub, "scan.#{ext}").to_s
30
+ end
31
+ end
32
+
33
+ # @param [ Controller::Base ] controller
34
+ #
35
+ # @return [ Controllers ] self
36
+ def <<(controller)
37
+ options = controller.cli_options
38
+
39
+ unless include?(controller)
40
+ option_parser.add(*options) if options
41
+ super
42
+ end
43
+ self
44
+ end
45
+
46
+ # Force the non-colored CLI formatter when ANSI escapes would be
47
+ # unwanted: writing to a file, piping to another process, or when the
48
+ # caller has set NO_COLOR (see https://no-color.org). Explicit
49
+ # --format choices are preserved.
50
+ def apply_no_colour_default
51
+ return if WPScan::ParsedCli.options[:format]
52
+
53
+ no_color = ENV.fetch('NO_COLOR', nil)
54
+ return unless WPScan::ParsedCli.output || !$stdout.tty? || (no_color && !no_color.empty?)
55
+
56
+ WPScan::ParsedCli.options[:format] = 'cli-no-colour'
57
+ end
58
+
59
+ def run
60
+ WPScan::ParsedCli.options = option_parser.results
61
+ first.class.option_parser = option_parser # needed to output help on -h/--hh
62
+
63
+ apply_no_colour_default
64
+ redirect_output_to_file(WPScan::ParsedCli.output) if WPScan::ParsedCli.output
65
+
66
+ Timeout.timeout(WPScan::ParsedCli.max_scan_duration, WPScan::Error::MaxScanDurationReached) do
67
+ each(&:before_scan)
68
+
69
+ @running = true
70
+
71
+ each(&:run)
72
+ end
73
+ ensure
74
+ # The rescue prevents unfinished requests from raising, which would stop reverse_each from running.
75
+ # rubocop:disable Style/RescueModifier
76
+ WPScan::Browser.instance.hydra.abort rescue nil
77
+ # rubocop:enable Style/RescueModifier
78
+
79
+ # Reverse order: app/controllers/core#after_scan finishes the output and must be last.
80
+ # Guarantees stats are output even on error. after_scan runs only if scan was actually running
81
+ # (skipped on CLI error, -h/--hh/--version).
82
+ reverse_each(&:after_scan) if running
8
83
  end
9
84
  end
10
85
  end
@@ -4,18 +4,14 @@ module WPScan
4
4
  module DB
5
5
  module DynamicFinders
6
6
  class Base
7
- # @return [ String ]
7
+ # @return [ Pathname ]
8
8
  def self.df_file
9
- @df_file ||= DB_DIR.join('dynamic_finders.yml').to_s
9
+ @df_file ||= DB_DIR.join('dynamic_finders.yml')
10
10
  end
11
11
 
12
12
  # @return [ Hash ]
13
13
  def self.all_df_data
14
- @all_df_data ||= if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('4.0.0')
15
- YAML.safe_load(File.read(df_file), permitted_classes: [Regexp])
16
- else
17
- YAML.safe_load(File.read(df_file), [Regexp])
18
- end
14
+ @all_df_data ||= YAML.safe_load_file(df_file, permitted_classes: [Regexp])
19
15
  end
20
16
 
21
17
  # @return [ Array<Symbol> ]
@@ -66,7 +66,7 @@ module WPScan
66
66
  # What about slugs such as js_composer which will be done as JsComposer, just like js-composer
67
67
  constant_name = classify_slug(slug)
68
68
 
69
- unless version_finder_module.constants.include?(constant_name)
69
+ unless version_finder_module.const_defined?(constant_name, false)
70
70
  version_finder_module.const_set(constant_name, Module.new)
71
71
  end
72
72
 
@@ -92,7 +92,7 @@ module WPScan
92
92
 
93
93
  next unless allowed_classes.include?(klass.to_sym)
94
94
 
95
- created << if mod.constants.include?(finder_class.to_sym)
95
+ created << if mod.const_defined?(finder_class.to_sym, false)
96
96
  mod.const_get(finder_class.to_sym)
97
97
  else
98
98
  version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
@@ -59,7 +59,7 @@ module WPScan
59
59
  # So that, when new DF configs are put in the .yml
60
60
  # users with old version of WPScan will still be able to scan blogs
61
61
  # when updating the DB but not the tool
62
- next if version_finder_module.constants.include?(finder_class.to_sym) ||
62
+ next if version_finder_module.const_defined?(finder_class.to_sym, false) ||
63
63
  !allowed_classes.include?(klass.to_sym)
64
64
 
65
65
  version_finder_super_class(klass).create_child_class(version_finder_module, finder_class.to_sym, config)
@@ -33,9 +33,9 @@ module WPScan
33
33
  unique_fingerprints
34
34
  end
35
35
 
36
- # @return [ String ]
36
+ # @return [ Pathname ]
37
37
  def self.wp_fingerprints_path
38
- @wp_fingerprints_path ||= DB_DIR.join('wp_fingerprints.json').to_s
38
+ @wp_fingerprints_path ||= DB_DIR.join('wp_fingerprints.json')
39
39
  end
40
40
 
41
41
  # @return [ Hash ]