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
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # DuplicatorInstallerLog finder
7
- class DuplicatorInstallerLog < CMSScanner::Finders::Finder
7
+ class DuplicatorInstallerLog < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'installer-log.txt'
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Emergency Password Reset Script finder
7
- class EmergencyPwdResetScript < CMSScanner::Finders::Finder
7
+ class EmergencyPwdResetScript < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'emergency.php'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module InterestingFindings
6
+ # FantasticoFileslist finder
7
+ class FantasticoFileslist < Finder
8
+ # @return [ InterestingFinding ]
9
+ def aggressive(_opts = {})
10
+ path = 'fantastico_fileslist.txt'
11
+ res = target.head_and_get(path)
12
+
13
+ return if res.body.strip.empty?
14
+ return unless res.headers && res.headers['Content-Type']&.start_with?('text/plain')
15
+
16
+ WPScan::Model::FantasticoFileslist.new(target.url(path), confidence: 70, found_by: found_by)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Full Path Disclosure finder
7
- class FullPathDisclosure < CMSScanner::Finders::Finder
7
+ class FullPathDisclosure < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'wp-includes/rss-functions.php'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module InterestingFindings
6
+ # Interesting Headers finder
7
+ class Headers < Finder
8
+ # @return [ InterestingFinding ]
9
+ def passive(_opts = {})
10
+ r = WPScan::Model::Headers.new(target.homepage_url, confidence: 100, found_by: found_by)
11
+
12
+ r.interesting_entries.empty? ? nil : r
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Must Use Plugins Directory checker
7
- class MuPlugins < CMSScanner::Finders::Finder
7
+ class MuPlugins < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def passive(_opts = {})
10
10
  pattern = %r{#{target.content_dir}/mu-plugins/}i
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Multisite checker
7
- class Multisite < CMSScanner::Finders::Finder
7
+ class Multisite < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  url = target.url('wp-signup.php')
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # See https://github.com/wpscanteam/wpscan/issues/1593
7
- class PHPDisabled < CMSScanner::Finders::Finder
8
- PATTERN = /\$wp_version =/.freeze
7
+ class PHPDisabled < WPScan::Finders::Finder
8
+ PATTERN = /\$wp_version =/
9
9
 
10
10
  # @return [ InterestingFinding ]
11
11
  def aggressive(_opts = {})
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Readme.html finder
7
- class Readme < CMSScanner::Finders::Finder
7
+ class Readme < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  potential_files.each do |path|
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Registration Enabled checker
7
- class Registration < CMSScanner::Finders::Finder
7
+ class Registration < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def passive(_opts = {})
10
10
  # Maybe check in the homepage if there is the registration url ?
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module InterestingFindings
6
+ # Robots.txt finder
7
+ class RobotsTxt < Finder
8
+ # @return [ InterestingFinding ]
9
+ def aggressive(_opts = {})
10
+ path = 'robots.txt'
11
+ res = target.head_and_get(path)
12
+
13
+ return unless res&.code == 200 && res.body =~ /(?:user-agent|(?:dis)?allow):/i
14
+
15
+ WPScan::Model::RobotsTxt.new(target.url(path), confidence: 100, found_by: found_by)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module InterestingFindings
6
+ # SearchReplaceDB2 finder
7
+ class SearchReplaceDB2 < Finder
8
+ # @return [ InterestingFinding ]
9
+ def aggressive(_opts = {})
10
+ path = 'searchreplacedb2.php'
11
+
12
+ return unless /by interconnect/i.match?(target.head_and_get(path).body)
13
+
14
+ WPScan::Model::SearchReplaceDB2.new(target.url(path), confidence: 100, found_by: found_by)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # Tmm DB Migrate finder
7
- class TmmDbMigrate < CMSScanner::Finders::Finder
7
+ class TmmDbMigrate < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # UploadDirectoryListing finder
7
- class UploadDirectoryListing < CMSScanner::Finders::Finder
7
+ class UploadDirectoryListing < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  path = 'wp-content/uploads/'
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # UploadSQLDump finder
7
- class UploadSQLDump < CMSScanner::Finders::Finder
8
- SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/.freeze
7
+ class UploadSQLDump < WPScan::Finders::Finder
8
+ SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/
9
9
 
10
10
  # @return [ InterestingFinding ]
11
11
  def aggressive(_opts = {})
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module InterestingFindings
6
6
  # wp-cron.php finder
7
- class WPCron < CMSScanner::Finders::Finder
7
+ class WPCron < WPScan::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def aggressive(_opts = {})
10
10
  res = Browser.get(wp_cron_url)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module InterestingFindings
6
+ # XML RPC finder
7
+ class XMLRPC < Finder
8
+ # @return [ Array<String> ] The potential urls to the XMl RPC file
9
+ def potential_urls
10
+ @potential_urls ||= []
11
+ end
12
+
13
+ # @return [ Array<XMLRPC> ]
14
+ def passive(opts = {})
15
+ [passive_headers(opts), passive_body(opts)].compact
16
+ end
17
+
18
+ # @return [ XMLRPC ]
19
+ def passive_headers(_opts = {})
20
+ url = target.homepage_res.headers['X-Pingback']
21
+
22
+ return unless target.in_scope?(url)
23
+
24
+ potential_urls << url
25
+
26
+ WPScan::Model::XMLRPC.new(url, confidence: 30, found_by: 'Headers (Passive Detection)')
27
+ end
28
+
29
+ # @return [ XMLRPC ]
30
+ def passive_body(_opts = {})
31
+ target.homepage_res.html.css('link[rel="pingback"]').each do |tag|
32
+ url = tag.attribute('href').to_s
33
+
34
+ next unless target.in_scope?(url)
35
+
36
+ potential_urls << url
37
+
38
+ return WPScan::Model::XMLRPC.new(url, confidence: 30, found_by: 'Link Tag (Passive Detection)')
39
+ end
40
+ nil
41
+ end
42
+
43
+ # @return [ XMLRPC ]
44
+ def aggressive(_opts = {})
45
+ potential_urls << target.url('xmlrpc.php')
46
+
47
+ potential_urls.uniq.each do |potential_url|
48
+ next unless target.in_scope?(potential_url)
49
+
50
+ res = WPScan::Browser.post(potential_url, body: Digest::MD5.hexdigest(rand(999_999).to_s[0..5]))
51
+
52
+ next unless /<methodResponse>/i.match?(res&.body)
53
+
54
+ return WPScan::Model::XMLRPC.new(potential_url, confidence: 100, found_by: DIRECT_ACCESS)
55
+ end
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'interesting_findings/headers'
4
+ require_relative 'interesting_findings/robots_txt'
5
+ require_relative 'interesting_findings/fantastico_fileslist'
6
+ require_relative 'interesting_findings/search_replace_db_2'
7
+ require_relative 'interesting_findings/xml_rpc'
3
8
  require_relative 'interesting_findings/readme'
4
9
  require_relative 'interesting_findings/wp_cron'
5
10
  require_relative 'interesting_findings/multisite'
@@ -18,18 +23,22 @@ require_relative 'interesting_findings/emergency_pwd_reset_script'
18
23
  module WPScan
19
24
  module Finders
20
25
  module InterestingFindings
21
- # Interesting Files Finder
22
- class Base < CMSScanner::Finders::InterestingFindings::Base
26
+ # Interesting Files Finder (base + WordPress-specific finders).
27
+ class Base
28
+ include IndependentFinder
29
+
23
30
  # @param [ WPScan::Target ] target
24
31
  def initialize(target)
25
- super(target)
32
+ %w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC].each do |f|
33
+ finders << WPScan::Finders::InterestingFindings.const_get(f).new(target)
34
+ end
26
35
 
27
36
  %w[
28
37
  Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
29
38
  Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
30
39
  UploadSQLDump EmergencyPwdResetScript WPCron PHPDisabled
31
40
  ].each do |f|
32
- finders << InterestingFindings.const_get(f).new(target)
41
+ finders << WPScan::Finders::InterestingFindings.const_get(f).new(target)
33
42
  end
34
43
  end
35
44
  end
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module MainTheme
6
6
  # From the CSS style in the homepage
7
- class CssStyleInHomepage < CMSScanner::Finders::Finder
7
+ class CssStyleInHomepage < WPScan::Finders::Finder
8
8
  include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here
9
9
 
10
10
  def create_theme(slug, style_url, opts)
@@ -4,22 +4,18 @@ module WPScan
4
4
  module Finders
5
5
  module MainTheme
6
6
  # URLs In Homepage Finder
7
- class UrlsInHomepage < CMSScanner::Finders::Finder
7
+ class UrlsInHomepage < WPScan::Finders::Finder
8
8
  include WpItems::UrlsInPage
9
9
 
10
10
  # @param [ Hash ] opts
11
11
  #
12
12
  # @return [ Array<Theme> ]
13
13
  def passive(opts = {})
14
- found = []
15
-
16
14
  slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false)
17
15
 
18
- slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
19
- found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
16
+ slugs.tally.map do |slug, occurences|
17
+ Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
20
18
  end
21
-
22
- found
23
19
  end
24
20
 
25
21
  # @return [ Typhoeus::Response ]
@@ -4,10 +4,10 @@ module WPScan
4
4
  module Finders
5
5
  module MainTheme
6
6
  # From the WooFramework meta generators
7
- class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
8
- THEME_PATTERN = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?"\s+/?>}.freeze
9
- FRAMEWORK_PATTERN = %r{<meta name="generator" content="WooFramework\s?([^"]+)?"\s+/?>}.freeze
10
- PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
7
+ class WooFrameworkMetaGenerator < WPScan::Finders::Finder
8
+ THEME_PATTERN = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?"\s+/?>}
9
+ FRAMEWORK_PATTERN = %r{<meta name="generator" content="WooFramework\s?([^"]+)?"\s+/?>}
10
+ PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i
11
11
 
12
12
  def passive(opts = {})
13
13
  return unless target.homepage_res.body =~ PATTERN || target.error_404_res.body =~ PATTERN
@@ -11,7 +11,7 @@ module WPScan
11
11
  module MainTheme
12
12
  # Main Theme Finder
13
13
  class Base
14
- include CMSScanner::Finders::UniqueFinder
14
+ include WPScan::Finders::UniqueFinder
15
15
 
16
16
  # @param [ WPScan::Target ] target
17
17
  def initialize(target)
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module Medias
6
6
  # Medias Finder, see https://github.com/wpscanteam/wpscan/issues/172
7
- class AttachmentBruteForcing < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::Enumerator
7
+ class AttachmentBruteForcing < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::Enumerator
9
9
 
10
10
  # @param [ Hash ] opts
11
11
  # @option opts [ Range ] :range Mandatory
@@ -7,7 +7,7 @@ module WPScan
7
7
  module Medias
8
8
  # Medias Finder
9
9
  class Base
10
- include CMSScanner::Finders::SameTypeFinder
10
+ include WPScan::Finders::SameTypeFinder
11
11
 
12
12
  # @param [ WPScan::Target ] target
13
13
  def initialize(target)
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module Passwords
6
6
  # Password attack against the wp-login.php
7
- class WpLogin < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
7
+ class WpLogin < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::BreadthFirstDictionaryAttack
9
9
 
10
10
  def login_request(username, password)
11
11
  target.login_request(username, password)
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module Passwords
6
6
  # Password attack against the XMLRPC interface
7
- class XMLRPC < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
7
+ class XMLRPC < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::BreadthFirstDictionaryAttack
9
9
 
10
10
  def login_request(username, password)
11
11
  target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
@@ -5,7 +5,7 @@ module WPScan
5
5
  module Passwords
6
6
  # Password attack against the XMLRPC interface with the multicall method
7
7
  # WP < 4.4 is vulnerable to such attack
8
- class XMLRPCMulticall < CMSScanner::Finders::Finder
8
+ class XMLRPCMulticall < WPScan::Finders::Finder
9
9
  # @param [ Array<User> ] users
10
10
  # @param [ Array<String> ] passwords
11
11
  #
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module PluginVersion
6
6
  # Plugin Version Finder from the readme.txt file
7
- class Readme < CMSScanner::Finders::Finder
7
+ class Readme < WPScan::Finders::Finder
8
8
  # @return [ Version ]
9
9
  def aggressive(_opts = {})
10
10
  found_by_msg = 'Readme - %s (Aggressive Detection)'
@@ -7,7 +7,7 @@ module WPScan
7
7
  module PluginVersion
8
8
  # Plugin Version Finder
9
9
  class Base
10
- include CMSScanner::Finders::UniqueFinder
10
+ include WPScan::Finders::UniqueFinder
11
11
 
12
12
  # @param [ Model::Plugin ] plugin
13
13
  def initialize(plugin)
@@ -4,8 +4,8 @@ module WPScan
4
4
  module Finders
5
5
  module Plugins
6
6
  # Known Locations Plugins Finder
7
- class KnownLocations < CMSScanner::Finders::Finder
8
- include CMSScanner::Finders::Finder::Enumerator
7
+ class KnownLocations < WPScan::Finders::Finder
8
+ include WPScan::Finders::Finder::Enumerator
9
9
 
10
10
  # @return [ Array<Integer> ]
11
11
  def valid_response_codes
@@ -14,22 +14,32 @@ module WPScan
14
14
 
15
15
  # @param [ Hash ] opts
16
16
  # @option opts [ String ] :list
17
+ # @option opts [ Findings ] :found Shared findings collection passed by
18
+ # BaseFinders#run_finder. We append directly into it as each plugin
19
+ # is detected so that Findings#on_append fires during the hydra run,
20
+ # enabling streaming output. Falls back to a local array when called
21
+ # outside the framework (e.g. directly from specs).
17
22
  #
18
- # @return [ Array<Plugin> ]
23
+ # @return [ Array<Plugin> ] Items appended this call (empty when
24
+ # already streamed into opts[:found] to avoid double-appending).
19
25
  def aggressive(opts = {})
20
- found = []
26
+ shared = opts[:found]
27
+ local = shared ? nil : []
28
+ count = 0
21
29
 
22
30
  enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
23
31
  finding_opts = opts.merge(found_by: found_by,
24
32
  confidence: 80,
25
33
  interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
26
34
 
27
- found << Model::Plugin.new(slug, target, finding_opts)
35
+ plugin = Model::Plugin.new(slug, target, finding_opts)
36
+ (shared || local) << plugin
37
+ count += 1
28
38
 
29
- raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
39
+ raise Error::PluginsThresholdReached if opts[:threshold].positive? && count >= opts[:threshold]
30
40
  end
31
41
 
32
- found
42
+ local || []
33
43
  end
34
44
 
35
45
  # @param [ Hash ] opts
@@ -5,20 +5,16 @@ module WPScan
5
5
  module Plugins
6
6
  # URLs In Homepage Finder
7
7
  # Typically, the items detected from URLs like /wp-content/plugins/<slug>/
8
- class UrlsInHomepage < CMSScanner::Finders::Finder
8
+ class UrlsInHomepage < WPScan::Finders::Finder
9
9
  include WpItems::UrlsInPage
10
10
 
11
11
  # @param [ Hash ] opts
12
12
  #
13
13
  # @return [ Array<Plugin> ]
14
14
  def passive(opts = {})
15
- found = []
16
-
17
- (items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
18
- found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
15
+ (items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.map do |slug|
16
+ Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
19
17
  end
20
-
21
- found
22
18
  end
23
19
 
24
20
  # @return [ Typhoeus::Response ]
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module Plugins
6
+ # Authenticated plugin inventory via the WordPress REST API
7
+ # endpoint /wp-json/wp/v2/plugins (WP >= 5.5).
8
+ #
9
+ # Requires admin credentials with the install_plugins / activate_plugins
10
+ # capability. An Application Password (WP >= 5.6) is the recommended
11
+ # secret as it bypasses 2FA and is colon-free by construction.
12
+ class WpJsonApi < WPScan::Finders::Finder
13
+ FOUND_BY = 'WP REST API (Authenticated)'
14
+
15
+ # @param [ Hash ] opts
16
+ # @option opts [ String ] :userpwd "user:password" credentials for Basic Auth
17
+ #
18
+ # @return [ Array<Model::Plugin> ]
19
+ def aggressive(opts = {})
20
+ response = Browser.get(api_url, userpwd: opts[:userpwd], headers: { 'Accept' => 'application/json' })
21
+
22
+ raise Error::WpAuthFailed.new(response.code, api_url) if [401, 403].include?(response.code)
23
+ raise Error::WpAuthEndpointUnavailable.new(response.code, api_url) unless response.code == 200
24
+
25
+ plugins_from_response(response)
26
+ rescue JSON::ParserError, TypeError
27
+ []
28
+ end
29
+
30
+ # @param [ Typhoeus::Response ] response
31
+ #
32
+ # @return [ Array<Model::Plugin> ]
33
+ def plugins_from_response(response)
34
+ json = JSON.parse(response.body)
35
+ return [] unless json.is_a?(Enumerable)
36
+
37
+ json.filter_map { |entry| build_plugin(entry, response.effective_url) }
38
+ end
39
+
40
+ # @return [ String ] The REST API URL for the plugins endpoint
41
+ def api_url
42
+ @api_url ||= target.url('wp-json/wp/v2/plugins')
43
+ end
44
+
45
+ private
46
+
47
+ # @param [ Hash ] entry
48
+ # @param [ String ] effective_url
49
+ #
50
+ # @return [ Model::Plugin, nil ]
51
+ def build_plugin(entry, effective_url)
52
+ slug = slug_from_entry(entry)
53
+ return nil if slug.nil? || slug.empty?
54
+
55
+ plugin = Model::Plugin.new(
56
+ slug,
57
+ target,
58
+ confidence: 100,
59
+ found_by: FOUND_BY,
60
+ interesting_entries: [effective_url]
61
+ )
62
+
63
+ version = entry['version']
64
+ plugin.instance_variable_set(
65
+ :@version,
66
+ version && !version.to_s.empty? ? Model::Version.new(version, confidence: 100, found_by: FOUND_BY) : false
67
+ )
68
+
69
+ plugin
70
+ end
71
+
72
+ # @param [ Hash ] entry
73
+ #
74
+ # @return [ String, nil ]
75
+ # WP REST returns "plugin" as e.g. "akismet/akismet" or just "hello" for the legacy single-file plugin.
76
+ def slug_from_entry(entry)
77
+ raw = entry['plugin'] || entry['textdomain']
78
+ return nil unless raw.is_a?(String)
79
+
80
+ raw.split('/').first
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -11,13 +11,14 @@ require_relative 'plugins/body_pattern'
11
11
  require_relative 'plugins/javascript_var'
12
12
  require_relative 'plugins/query_parameter'
13
13
  require_relative 'plugins/config_parser' # Not loaded below as not implemented
14
+ require_relative 'plugins/wp_json_api' # Authenticated, used by AuthenticatedInventory controller
14
15
 
15
16
  module WPScan
16
17
  module Finders
17
18
  module Plugins
18
19
  # Plugins Finder
19
20
  class Base
20
- include CMSScanner::Finders::SameTypeFinder
21
+ include WPScan::Finders::SameTypeFinder
21
22
 
22
23
  # @param [ WPScan::Target ] target
23
24
  def initialize(target)
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module ThemeVersion
6
6
  # Theme Version Finder from the style.css file
7
- class Style < CMSScanner::Finders::Finder
7
+ class Style < WPScan::Finders::Finder
8
8
  # @param [ Hash ] opts
9
9
  #
10
10
  # @return [ Version ]
@@ -4,7 +4,7 @@ module WPScan
4
4
  module Finders
5
5
  module ThemeVersion
6
6
  # Theme Version Finder from the WooFramework generators
7
- class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
7
+ class WooFrameworkMetaGenerator < WPScan::Finders::Finder
8
8
  # @param [ Hash ] opts
9
9
  #
10
10
  # @return [ Version ]
@@ -8,7 +8,7 @@ module WPScan
8
8
  module ThemeVersion
9
9
  # Theme Version Finder
10
10
  class Base
11
- include CMSScanner::Finders::UniqueFinder
11
+ include WPScan::Finders::UniqueFinder
12
12
 
13
13
  # @param [ Model::Theme ] theme
14
14
  def initialize(theme)