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
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- # References module (which should be included along with the CMSScanner::References)
5
- # to allow the use of the wpvulndb reference.
4
+ # References related to a vulnerability / finding.
6
5
  module References
7
6
  extend ActiveSupport::Concern
8
7
 
@@ -10,22 +9,137 @@ module WPScan
10
9
  module ClassMethods
11
10
  # @return [ Array<Symbol> ]
12
11
  def references_keys
13
- @references_keys ||= super << :wpvulndb
12
+ @references_keys ||= %i[cve exploitdb url metasploit packetstorm securityfocus youtube wpvulndb]
14
13
  end
15
14
  end
16
15
 
16
+ # @param [ Hash ] refs
17
+ def references=(refs)
18
+ @references = {}
19
+
20
+ self.class.references_keys.each do |key|
21
+ next unless refs.key?(key)
22
+
23
+ @references[key] = if key == :youtube
24
+ Array(refs[:youtube]).map { |id| youtube_url(id) }
25
+ else
26
+ Array(refs[key]).map(&:to_s)
27
+ end
28
+ end
29
+ end
30
+
31
+ # @return [ Hash ]
32
+ def references
33
+ @references ||= {}
34
+ end
35
+
36
+ # @return [ Array<String> ] All the references URLs
17
37
  def references_urls
18
- wpvulndb_urls + super
38
+ wpvulndb_urls + cve_urls + exploitdb_urls + urls + msf_urls +
39
+ packetstorm_urls + securityfocus_urls + youtube_urls
40
+ end
41
+
42
+ # @return [ Array<String> ] The CVEs
43
+ def cves
44
+ references[:cve] || []
45
+ end
46
+
47
+ # @return [ Array<String> ]
48
+ def cve_urls
49
+ cves.reduce([]) { |acc, elem| acc << cve_url(elem) }
50
+ end
51
+
52
+ # @return [ String ] The URL to the CVE
53
+ def cve_url(cve)
54
+ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{cve}"
55
+ end
56
+
57
+ # @return [ Array<String> ] The ExploitDB ID
58
+ def exploitdb_ids
59
+ references[:exploitdb] || []
60
+ end
61
+
62
+ # @return [ Array<String> ]
63
+ def exploitdb_urls
64
+ exploitdb_ids.reduce([]) { |acc, elem| acc << exploitdb_url(elem) }
65
+ end
66
+
67
+ # @return [ String ]
68
+ def exploitdb_url(id)
69
+ "https://www.exploit-db.com/exploits/#{id}/"
70
+ end
71
+
72
+ # @return [ Array<String> ]
73
+ def urls
74
+ references[:url] || []
75
+ end
76
+
77
+ # @return [ Array<String> ] The metasploit modules
78
+ def msf_modules
79
+ references[:metasploit] || []
80
+ end
81
+
82
+ # @return [ Array<String> ]
83
+ def msf_urls
84
+ msf_modules.reduce([]) { |acc, elem| acc << msf_url(elem) }
85
+ end
86
+
87
+ # @return [ String ] The URL to the metasploit module page
88
+ def msf_url(mod)
89
+ "https://www.rapid7.com/db/modules/#{mod.sub(%r{^/}, '')}/"
90
+ end
91
+
92
+ # @return [ Array<String> ] The Packetstormsecurity IDs
93
+ def packetstorm_ids
94
+ @packetstorm_ids ||= references[:packetstorm] || []
95
+ end
96
+
97
+ # @return [ Array<String> ]
98
+ def packetstorm_urls
99
+ packetstorm_ids.reduce([]) { |acc, elem| acc << packetstorm_url(elem) }
100
+ end
101
+
102
+ # @return [ String ]
103
+ def packetstorm_url(id)
104
+ "https://packetstormsecurity.com/files/#{id}/"
105
+ end
106
+
107
+ # @return [ Array<String> ] The Security Focus IDs
108
+ def securityfocus_ids
109
+ references[:securityfocus] || []
110
+ end
111
+
112
+ # @return [ Array<String> ]
113
+ def securityfocus_urls
114
+ securityfocus_ids.reduce([]) { |acc, elem| acc << securityfocus_url(elem) }
115
+ end
116
+
117
+ # @return [ String ]
118
+ def securityfocus_url(id)
119
+ "https://www.securityfocus.com/bid/#{id}/"
120
+ end
121
+
122
+ # @return [ Array<String> ]
123
+ def youtube_urls
124
+ references[:youtube] || []
125
+ end
126
+
127
+ # @return [ String ]
128
+ def youtube_url(id)
129
+ "https://www.youtube.com/watch?v=#{id}"
19
130
  end
20
131
 
132
+ # @return [ Array<String> ] wpvulndb (now WPScan) reference IDs
21
133
  def wpvulndb_ids
22
134
  references[:wpvulndb] || []
23
135
  end
24
136
 
137
+ # @return [ Array<String> ]
25
138
  def wpvulndb_urls
26
139
  wpvulndb_ids.reduce([]) { |acc, elem| acc << wpvulndb_url(elem) }
27
140
  end
28
141
 
142
+ # @return [ String ]
29
143
  def wpvulndb_url(id)
30
144
  "https://wpscan.com/vulnerability/#{id}"
31
145
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # Scan
5
+ class Scan
6
+ attr_reader :run_error
7
+
8
+ def initialize
9
+ WPScan.start_memory = GetProcessMem.new.bytes
10
+
11
+ # Capture the original command line arguments with sensitive data masked
12
+ WPScan.command_line = mask_sensitive_arguments(ARGV)
13
+
14
+ controllers << WPScan::Controller::Core.new
15
+
16
+ exit_hook
17
+
18
+ yield self if block_given?
19
+ end
20
+
21
+ # Masks sensitive arguments in the command line to prevent exposing secrets
22
+ # @param [ Array<String> ] args The command line arguments
23
+ # @return [ String ] The sanitized command line string
24
+ def mask_sensitive_arguments(args)
25
+ # List of sensitive arguments that contain actual secrets (not file paths)
26
+ # File paths like --passwords and --cookie-jar are not masked as they're
27
+ # not secrets themselves, just references to files
28
+ sensitive_args = %w[
29
+ --api-token
30
+ --http-auth
31
+ --proxy-auth
32
+ --cookie-string
33
+ --wp-auth
34
+ ]
35
+
36
+ masked_args = args.dup
37
+ args.each_with_index do |arg, index|
38
+ # Check if this argument is sensitive
39
+ if sensitive_args.include?(arg)
40
+ # Mask the next argument (the value)
41
+ masked_args[index + 1] = '[REDACTED]' if index + 1 < args.length
42
+ elsif arg.start_with?('--') && arg.include?('=')
43
+ # Handle --arg=value format
44
+ arg_name = arg.split('=').first
45
+ masked_args[index] = "#{arg_name}=[REDACTED]" if sensitive_args.include?(arg_name)
46
+ end
47
+ end
48
+
49
+ masked_args.join(' ')
50
+ end
51
+
52
+ # @return [ Controllers ]
53
+ def controllers
54
+ @controllers ||= WPScan::Controllers.new
55
+ end
56
+
57
+ def run
58
+ controllers.run
59
+ rescue OptParseValidator::NoRequiredOption => e
60
+ @run_error = e
61
+
62
+ formatter.output('@usage', msg: e.message)
63
+ rescue NoMemoryError, ScriptError, SecurityError, SignalException, StandardError, SystemStackError => e
64
+ @run_error = e
65
+
66
+ output_params = {
67
+ reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message,
68
+ trace: e.backtrace,
69
+ verbose: WPScan::ParsedCli.verbose || run_error_exit_code == WPScan::ExitCode::EXCEPTION
70
+ }
71
+
72
+ output_params[:url] = controllers.first.target.url if WPScan::ParsedCli.url
73
+
74
+ formatter.output(aborted_view, output_params)
75
+ ensure
76
+ formatter.beautify
77
+ end
78
+
79
+ # Used for convenience
80
+ # @See Formatter
81
+ def formatter
82
+ controllers.first.formatter
83
+ end
84
+
85
+ # @return [ String ] The global view to render when the run is aborted
86
+ def aborted_view
87
+ core = controllers.first
88
+ core.respond_to?(:updating_db?) && core.updating_db? ? '@update_aborted' : '@scan_aborted'
89
+ end
90
+
91
+ # @return [ Hash ]
92
+ def datastore
93
+ controllers.first.datastore
94
+ end
95
+
96
+ # Hook to be able to have an exit code returned
97
+ # depending on the findings / errors
98
+ # :nocov:
99
+ def exit_hook
100
+ # Avoid hooking the exit when rspec is running, otherwise it will always return 0
101
+ # and Travis won't detect failed builds. Couldn't find a better way, even though
102
+ # some people managed to https://github.com/rspec/rspec-core/pull/410
103
+ return if defined?(RSpec)
104
+
105
+ at_exit do
106
+ exit(run_error_exit_code) if run_error
107
+
108
+ # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given
109
+ exit(WPScan::ExitCode::VULNERABLE) if WPScan::ParsedCli.url && controllers.first.target.vulnerable?
110
+ exit(WPScan::ExitCode::OK)
111
+ end
112
+ end
113
+ # :nocov:
114
+
115
+ # @return [ Integer ] The exit code related to the run_error
116
+ def run_error_exit_code
117
+ return WPScan::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) ||
118
+ run_error.is_a?(OptionParser::ParseError)
119
+
120
+ return WPScan::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt)
121
+
122
+ return WPScan::ExitCode::ERROR if run_error.is_a?(WPScan::Error::Standard)
123
+
124
+ WPScan::ExitCode::EXCEPTION
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # Scope system logic
5
+ class Target < WebSite
6
+ # @note Comments are deleted to avoid cache generation details
7
+ #
8
+ # @param [ Typhoeus::Response, String ] page
9
+ #
10
+ # @return [ String ] The md5sum of the page
11
+ def self.page_hash(page)
12
+ page = WPScan::Browser.get(page, followlocation: true, maxredirs: 10) unless page.is_a?(Typhoeus::Response)
13
+
14
+ # Removes comments and script tags before computing the hash
15
+ # to remove any potential cached stuff
16
+ html = Nokogiri::HTML(page.body)
17
+ html.xpath('//script|//comment()').each(&:remove)
18
+
19
+ Digest::MD5.hexdigest(html)
20
+ end
21
+
22
+ # @return [ String ] The hash of the homepage
23
+ def homepage_hash
24
+ @homepage_hash ||= self.class.page_hash(url)
25
+ end
26
+
27
+ # @note This is used to detect potential custom 404 responding with a 200
28
+ # @return [ String ] The hash of a 404
29
+ def error_404_hash
30
+ @error_404_hash ||= self.class.page_hash(error_404_res)
31
+ end
32
+
33
+ # @param [ Typhoeus::Response, String ] page
34
+ # @return [ Boolean ] Wether or not the page is a the homepage or a 404 based on its md5sum
35
+ def homepage_or_404?(page)
36
+ homepage_and_404_hashes.include?(self.class.page_hash(page))
37
+ end
38
+
39
+ protected
40
+
41
+ def homepage_and_404_hashes
42
+ @homepage_and_404_hashes ||= [homepage_hash, error_404_hash].freeze
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ class Target < WebSite
5
+ module Platform
6
+ # Some PHP specific implementation
7
+ module PHP
8
+ DEBUG_LOG_PATTERN = /(?:\[\d{2}-[a-zA-Z]{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s[A-Z]{3}\]|
9
+ PHP\s(?:Fatal|Warning|Strict|Error|Notice):)/x
10
+ FPD_PATTERN = /Fatal error:.+? in (.+?) on/
11
+ ERROR_LOG_PATTERN = /PHP Fatal error/i
12
+
13
+ # @param [ String ] path
14
+ # @param [ Regexp ] pattern
15
+ # @param [ Hash ] params The request params
16
+ #
17
+ # @return [ Boolean ]
18
+ def log_file?(path, pattern, params = {})
19
+ max_mib = WPScan::ParsedCli.max_log_file_size.to_i
20
+ max_size = max_mib.positive? ? max_mib * 1024 * 1024 : nil
21
+
22
+ head_res = WPScan::Browser.forge_request(url(path), head_or_get_params).run
23
+ return false unless head_res.code == 200
24
+
25
+ if max_size
26
+ content_length = head_res.headers&.[]('Content-Length').to_i
27
+ return false if content_length > max_size
28
+ end
29
+
30
+ body = stream_capped_body(path, params, max_size)
31
+ return false if body.nil?
32
+
33
+ body.match?(pattern) || false
34
+ end
35
+
36
+ # Performs a streaming GET of `path` and returns the body as a String, capped at
37
+ # `max_size` bytes. Returns nil if the transfer was aborted because it exceeded the cap.
38
+ #
39
+ # Only the first 700 bytes of the file are needed to match log-file patterns. Servers may
40
+ # ignore the Range header, and libcurl's maxfilesize is a no-op when the response has no
41
+ # Content-Length (chunked transfer encoding), so an on_body callback is used to abort the
42
+ # transfer once the accumulated body exceeds the configured limit. Streaming via on_body
43
+ # also keeps Typhoeus::Response#body empty, so the response cache cannot Marshal-dump a
44
+ # huge payload.
45
+ #
46
+ # @param [ String ] path
47
+ # @param [ Hash ] params The base request params
48
+ # @param [ Integer, nil ] max_size Size cap in bytes, or nil to disable
49
+ #
50
+ # @return [ String, nil ]
51
+ def stream_capped_body(path, params, max_size)
52
+ get_params = params.merge(
53
+ headers: { 'Range' => 'bytes=0-700' },
54
+ method: :get,
55
+ cache_ttl: 0
56
+ )
57
+ get_params[:maxfilesize] = max_size if max_size
58
+
59
+ req = WPScan::Browser.forge_request(url(path), get_params)
60
+ body = +''
61
+ aborted = install_body_cap(req, body, max_size)
62
+ req.run
63
+
64
+ aborted.call ? nil : body
65
+ end
66
+
67
+ # Installs an on_body callback on `req` that appends chunks to `body`. If `max_size` is
68
+ # set, the transfer is aborted once the buffer exceeds it. Returns a callable that
69
+ # reports whether the transfer was aborted.
70
+ def install_body_cap(req, body, max_size)
71
+ aborted = false
72
+
73
+ if max_size
74
+ req.on_body do |chunk|
75
+ body << chunk
76
+ next if body.bytesize <= max_size
77
+
78
+ aborted = true
79
+ :abort
80
+ end
81
+ else
82
+ req.on_body { |chunk| body << chunk }
83
+ end
84
+
85
+ -> { aborted }
86
+ end
87
+
88
+ # @param [ String ] path
89
+ # @param [ Hash ] params The request params
90
+ #
91
+ # @return [ Boolean ] true if url(path) is a debug log, false otherwise
92
+ def debug_log?(path, params = {})
93
+ log_file?(path, DEBUG_LOG_PATTERN, params)
94
+ end
95
+
96
+ # @param [ String ] path
97
+ # @param [ Hash ] params The request params
98
+ #
99
+ # @return [ Boolean ] Wether or not url(path) is an error log file
100
+ def error_log?(path, params = {})
101
+ log_file?(path, ERROR_LOG_PATTERN, params)
102
+ end
103
+
104
+ # @param [ String ] path
105
+ # @param [ Hash ] params The request params
106
+ #
107
+ # @return [ Boolean ] true if url(path) contains a FPD, false otherwise
108
+ def full_path_disclosure?(path = nil, params = {})
109
+ !full_path_disclosure_entries(path, params).empty?
110
+ end
111
+
112
+ # @param [ String ] path
113
+ # @param [ Hash ] params The request params
114
+ #
115
+ # @return [ Array<String> ] The FPD found, or an empty array if none
116
+ def full_path_disclosure_entries(path = nil, params = {})
117
+ res = WPScan::Browser.get(url(path), params)
118
+
119
+ res.body.scan(FPD_PATTERN).flatten
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WPScan
4
- class Target < CMSScanner::Target
4
+ class Target
5
5
  module Platform
6
6
  # wp-content & plugins directory implementation
7
7
  module WordPress
@@ -16,7 +16,7 @@ module WPScan
16
16
  # @return [ String ] The wp-content directory
17
17
  def content_dir
18
18
  unless @content_dir
19
- # scope_url_pattern is from CMSScanner::Target
19
+ # scope_url_pattern is from WPScan::Target
20
20
  pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i
21
21
 
22
22
  [homepage_res, error_404_res].each do |page_res|
@@ -103,7 +103,7 @@ module WPScan
103
103
  def sub_dir
104
104
  return @sub_dir unless @sub_dir.nil?
105
105
 
106
- # url_pattern is from CMSScanner::Target
106
+ # url_pattern is from WPScan::Target
107
107
  pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
108
108
  xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'
109
109
 
@@ -5,16 +5,16 @@
5
5
  end
6
6
 
7
7
  module WPScan
8
- class Target < CMSScanner::Target
8
+ class Target
9
9
  module Platform
10
10
  # Some WordPress specific implementation
11
11
  module WordPress
12
- include CMSScanner::Target::Platform::PHP
12
+ include WPScan::Target::Platform::PHP
13
13
 
14
- WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i.freeze
15
- WORDPRESS_HOSTED_PATTERN = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
16
- WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i.freeze
17
- WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i.freeze
14
+ WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i
15
+ WORDPRESS_HOSTED_PATTERN = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i
16
+ WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i
17
+ WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i
18
18
 
19
19
  # These methods are used in the associated interesting_findings finders
20
20
  # to keep the boolean state of the finding rather than re-check the whole thing again
@@ -87,8 +87,7 @@ module WPScan
87
87
  # Force recheck of the homepage when retying wordpress?
88
88
  # No need to clear the cache, as the request (which will contain the cookies)
89
89
  # will be different
90
- @homepage_res = nil
91
- @homepage_url = nil
90
+ reset_homepage_cache!
92
91
 
93
92
  break
94
93
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wpscan/target/platform/php'
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ # Scope system logic
5
+ class Target < WebSite
6
+ # @return [ Array<PublicSuffix::Domain, String> ]
7
+ def scope
8
+ @scope ||= Scope.new
9
+ end
10
+
11
+ # @param [ String, Addressable::URI ] url An absolute URL or URI
12
+ #
13
+ # @return [ Boolean ] true if the url given is in scope
14
+ def in_scope?(url_or_uri)
15
+ url_or_uri = Addressable::URI.parse(url_or_uri.strip) unless url_or_uri.is_a?(Addressable::URI)
16
+
17
+ scope.include?(url_or_uri.host)
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ # @param [ Typhoeus::Response ] res
23
+ # @param [ String ] xpath
24
+ #
25
+ # @yield [ Addressable::URI, Nokogiri::XML::Element ] The in scope url and its associated tag
26
+ #
27
+ # @return [ Array<Addressable::URI> ] The in scope absolute URIs detected in the response's body
28
+ #
29
+ # @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
30
+ # time consuming when there are a lof of uris to check
31
+ def in_scope_uris(res, xpath = '//@href|//@src|//@data-src')
32
+ found = []
33
+
34
+ uris_from_page(res, xpath) do |uri, tag|
35
+ next unless in_scope?(uri)
36
+
37
+ yield uri, tag if block_given?
38
+
39
+ found << uri
40
+ end
41
+
42
+ found
43
+ end
44
+
45
+ # Similar to Target#url_pattern but considering the in scope domains as well
46
+ #
47
+ # @return [ Regexp ] The pattern related to the target url and in scope domains,
48
+ # it also matches escaped /, such as in JSON JS data: http:\/\/t.com\/
49
+ # rubocop:disable Metrics/AbcSize
50
+ def scope_url_pattern
51
+ return @scope_url_pattern if @scope_url_pattern
52
+
53
+ domains = [uri.host + uri.path]
54
+
55
+ domains += if scope.domains.empty?
56
+ Array(scope.invalid_domains[1..])
57
+ else
58
+ Array(scope.domains[1..]).map(&:to_s) + scope.invalid_domains
59
+ end
60
+
61
+ domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') }
62
+
63
+ domains[0].gsub!(Regexp.escape(uri.host), "#{Regexp.escape(uri.host)}(?::\\d+)?") if uri.port
64
+
65
+ @scope_url_pattern = %r{https?:\\?/\\?/(?:#{domains.join('|')})\\?/?}i
66
+ end
67
+ # rubocop:enable Metrics/AbcSize
68
+
69
+ # Scope Implementation
70
+ class Scope
71
+ # @return [ Array<PublicSuffix::Domain> ] The valid domains in scope
72
+ def domains
73
+ @domains ||= []
74
+ end
75
+
76
+ # @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc)
77
+ def invalid_domains
78
+ @invalid_domains ||= []
79
+ end
80
+
81
+ def <<(element)
82
+ if PublicSuffix.valid?(element, ignore_private: true)
83
+ domains << PublicSuffix.parse(element, ignore_private: true)
84
+ else
85
+ invalid_domains << element
86
+ end
87
+ end
88
+
89
+ # @return [ Boolean ] Wether or not the host is in the scope
90
+ def include?(host)
91
+ if PublicSuffix.valid?(host, ignore_private: true)
92
+ domain = PublicSuffix.parse(host, ignore_private: true)
93
+
94
+ domains.each { |d| return true if domain.match(d) }
95
+ else
96
+ invalid_domains.each { |d| return true if host == d }
97
+ end
98
+
99
+ false
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ class Target < WebSite
5
+ module Server
6
+ # Some Apche specific implementation
7
+ module Apache
8
+ # @param [ String ] path
9
+ # @param [ Hash ] params The request params
10
+ #
11
+ # @return [ Symbol ] :Apache
12
+ def server(_path = nil, _params = {})
13
+ :Apache
14
+ end
15
+
16
+ # @param [ String ] path
17
+ # @param [ Hash ] params The request params
18
+ #
19
+ # @return [ Array<String> ] The first level of directories/files listed,
20
+ # or an empty array if none
21
+ def directory_listing_entries(path = nil, params = {})
22
+ super(path, params, 'td a')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end