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,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module WPScan
6
+ module Formatter
7
+ # SARIF v2.1.0 Formatter.
8
+ #
9
+ # Emits scan results in SARIF v2.1.0 format so they can be consumed by
10
+ # static-analysis aggregators such as GitHub Code Scanning. WPScan is a
11
+ # DAST tool, so findings don't have a source file + line; we follow the
12
+ # mapping discussed on issue #1879:
13
+ #
14
+ # * `result.locations[].physicalLocation.artifactLocation.uri` carries
15
+ # the URL where the finding was observed. SARIF explicitly allows
16
+ # `uri` to be an absolute URL — see SARIF v2.1.0 §3.4.3
17
+ # (https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317419).
18
+ # * `result.locations[].logicalLocations[]` carries the WordPress
19
+ # component identity (core / plugin <slug> / theme <slug>) decoupled
20
+ # from any URL — see SARIF v2.1.0 §3.33
21
+ # (https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317719).
22
+ #
23
+ # GitHub's guidance on which SARIF fields surface in Code Scanning is
24
+ # at https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning.
25
+ #
26
+ # This formatter inherits from {Json} and reuses the JSON ERB views: the
27
+ # full scan is buffered as JSON during the run, then transformed into a
28
+ # SARIF document in {#beautify}. Reusing the JSON layer keeps the SARIF
29
+ # mapping in one place and lets new JSON fields flow through automatically.
30
+ class Sarif < Json
31
+ SARIF_VERSION = '2.1.0'
32
+ SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json'
33
+ INFO_URI = 'https://wpscan.com/wordpress-security-scanner'
34
+
35
+ # Make ERB lookups fall back to the json/* views.
36
+ def base_format
37
+ 'json'
38
+ end
39
+
40
+ def beautify
41
+ data = JSON.parse("{#{buffer.chomp.chomp(',')}}")
42
+ puts JSON.pretty_generate(sarif_document(data))
43
+ end
44
+
45
+ private
46
+
47
+ def sarif_document(data)
48
+ rules = {}
49
+ results = []
50
+
51
+ collect_vulnerability_results(data, rules, results)
52
+ collect_interesting_finding_results(data, rules, results)
53
+
54
+ {
55
+ '$schema' => SARIF_SCHEMA,
56
+ 'version' => SARIF_VERSION,
57
+ 'runs' => [build_run(data, rules, results)]
58
+ }
59
+ end
60
+
61
+ def build_run(data, rules, results)
62
+ {
63
+ 'tool' => {
64
+ 'driver' => {
65
+ 'name' => 'WPScan',
66
+ 'version' => WPScan::VERSION,
67
+ 'informationUri' => INFO_URI,
68
+ 'rules' => rules.values
69
+ }
70
+ },
71
+ 'invocations' => [invocation(data)].compact,
72
+ 'results' => results,
73
+ 'properties' => run_properties(data)
74
+ }.compact
75
+ end
76
+
77
+ def collect_vulnerability_results(data, rules, results)
78
+ components(data).each do |component|
79
+ Array(component[:vulnerabilities]).each do |vuln|
80
+ rule_id = rule_id_for(vuln)
81
+ rules[rule_id] ||= rule_for(vuln, rule_id)
82
+ results << vulnerability_result(vuln, rule_id, component)
83
+ end
84
+ end
85
+ end
86
+
87
+ def collect_interesting_finding_results(data, rules, results)
88
+ Array(data['interesting_findings']).each do |finding|
89
+ rule_id = "wpscan.interesting-finding.#{finding['type'] || 'unknown'}"
90
+ rules[rule_id] ||= interesting_finding_rule(rule_id, finding)
91
+ results << interesting_finding_result(finding, rule_id)
92
+ end
93
+ end
94
+
95
+ # @return [ Array<Hash> ] one entry per WordPress component observed
96
+ def components(data)
97
+ list = []
98
+ list << core_component(data) if data['version']
99
+ list << theme_component(data['main_theme']) if data['main_theme']
100
+ (data['themes'] || {}).each_value { |theme| list << theme_component(theme) }
101
+ (data['plugins'] || {}).each { |slug, plugin| list << plugin_component(slug, plugin) }
102
+ list
103
+ end
104
+
105
+ def core_component(data)
106
+ {
107
+ kind: 'core',
108
+ slug: nil,
109
+ version: data['version']['number'],
110
+ url: data['effective_url'] || data['target_url'],
111
+ vulnerabilities: data['version']['vulnerabilities']
112
+ }
113
+ end
114
+
115
+ def plugin_component(slug, plugin)
116
+ {
117
+ kind: 'plugin',
118
+ slug: slug,
119
+ version: plugin.dig('version', 'number'),
120
+ url: plugin['location'],
121
+ vulnerabilities: plugin['vulnerabilities']
122
+ }
123
+ end
124
+
125
+ def theme_component(theme)
126
+ {
127
+ kind: 'theme',
128
+ slug: theme['slug'],
129
+ version: theme.dig('version', 'number'),
130
+ url: theme['location'],
131
+ vulnerabilities: theme['vulnerabilities']
132
+ }
133
+ end
134
+
135
+ def rule_id_for(vuln)
136
+ cve = Array(vuln.dig('references', 'cve')).first
137
+ return "CVE-#{cve}" if cve
138
+
139
+ wpvulndb = Array(vuln.dig('references', 'wpvulndb')).first
140
+ return "WPVULNDB-#{wpvulndb}" if wpvulndb
141
+
142
+ # Stable fallback so we don't emit duplicate rule entries for the same title.
143
+ "wpscan.vuln.#{Digest::SHA1.hexdigest(vuln['title'].to_s)[0, 12]}"
144
+ end
145
+
146
+ def rule_for(vuln, rule_id)
147
+ title = vuln['title'].to_s
148
+ {
149
+ 'id' => rule_id,
150
+ 'name' => rule_id.gsub(/[^A-Za-z0-9_]/, '_'),
151
+ 'shortDescription' => { 'text' => title },
152
+ 'fullDescription' => { 'text' => title },
153
+ 'helpUri' => help_uri_for(vuln),
154
+ 'defaultConfiguration' => { 'level' => level_for(vuln) },
155
+ 'messageStrings' => {
156
+ # {0} = component label (e.g. "Plugin 'foo' 1.2.3"), {1} = fix status.
157
+ # Title is baked in here so the rule owns its phrasing and the result
158
+ # message stays small (SARIF2002). Dynamic args are single-quoted per
159
+ # SARIF2015 and the message terminates with a period per SARIF2001.
160
+ 'default' => { 'text' => "'{0}': #{title} ('{1}')." }
161
+ },
162
+ 'properties' => rule_properties(vuln)
163
+ }.compact
164
+ end
165
+
166
+ def vulnerability_result(vuln, rule_id, component)
167
+ {
168
+ 'ruleId' => rule_id,
169
+ 'level' => level_for(vuln),
170
+ 'message' => {
171
+ 'id' => 'default',
172
+ 'arguments' => [component_label(component), fix_status(vuln)]
173
+ },
174
+ 'locations' => [location_for(component[:url], logical_location(component))]
175
+ }
176
+ end
177
+
178
+ def interesting_finding_rule(rule_id, finding)
179
+ {
180
+ 'id' => rule_id,
181
+ 'name' => rule_id.gsub(/[^A-Za-z0-9_]/, '_'),
182
+ 'shortDescription' => { 'text' => "Interesting finding: #{finding['type']}" },
183
+ # Non-vulnerability findings (enumeration results, headers, robots.txt entries,
184
+ # etc.) are emitted at `note` level so they don't drown out real vulnerabilities
185
+ # in Code Scanning's UI — see GitHub's SARIF support guidance.
186
+ 'defaultConfiguration' => { 'level' => 'note' },
187
+ 'messageStrings' => {
188
+ # {0} = finding header (to_s), {1} = interesting entries joined,
189
+ # {2} = found-by strategy, {3} = confidence percentage.
190
+ 'default' => {
191
+ 'text' => "'{0}' — entries: '{1}'; found by: '{2}'; confidence: '{3}'."
192
+ }
193
+ },
194
+ 'helpUri' => INFO_URI
195
+ }
196
+ end
197
+
198
+ def interesting_finding_result(finding, rule_id)
199
+ location = location_for(finding['url'], nil)
200
+ entries = Array(finding['interesting_entries']).join(' | ')
201
+ {
202
+ 'ruleId' => rule_id,
203
+ 'level' => 'note',
204
+ 'message' => {
205
+ 'id' => 'default',
206
+ 'arguments' => [
207
+ finding['to_s'].to_s,
208
+ entries,
209
+ finding['found_by'].to_s,
210
+ finding['confidence'].to_s
211
+ ]
212
+ },
213
+ 'locations' => location ? [location] : []
214
+ }
215
+ end
216
+
217
+ def location_for(url, logical)
218
+ physical = physical_location(url)
219
+ return nil if physical.nil? && logical.nil?
220
+
221
+ loc = {}
222
+ loc['physicalLocation'] = physical if physical
223
+ loc['logicalLocations'] = [logical] if logical
224
+ loc
225
+ end
226
+
227
+ def physical_location(url)
228
+ return nil if url.nil? || url.to_s.empty?
229
+
230
+ { 'artifactLocation' => { 'uri' => url.to_s } }
231
+ end
232
+
233
+ def logical_location(component)
234
+ fqn = case component[:kind]
235
+ when 'core' then "wordpress.core@#{component[:version] || 'unknown'}"
236
+ when 'theme' then "wordpress.theme.#{component[:slug]}@#{component[:version] || 'unknown'}"
237
+ when 'plugin' then "wordpress.plugin.#{component[:slug]}@#{component[:version] || 'unknown'}"
238
+ end
239
+
240
+ {
241
+ 'name' => component[:slug] || component[:kind],
242
+ 'fullyQualifiedName' => fqn,
243
+ 'kind' => 'module'
244
+ }
245
+ end
246
+
247
+ # Map CVSS v3 base score to a SARIF level. Vulnerabilities without a
248
+ # score default to `error` since WPScan only flags entries the WPScan
249
+ # vulnerability database considers exploitable.
250
+ def level_for(vuln)
251
+ score = vuln.dig('cvss', 'score').to_f
252
+ return 'error' if score >= 7.0
253
+ return 'warning' if score >= 4.0
254
+ return 'note' if score.positive?
255
+
256
+ 'error'
257
+ end
258
+
259
+ def component_label(component)
260
+ case component[:kind]
261
+ when 'core' then "WordPress core #{component[:version]}"
262
+ when 'theme' then "Theme '#{component[:slug]}' #{component[:version]}"
263
+ when 'plugin' then "Plugin '#{component[:slug]}' #{component[:version]}"
264
+ end
265
+ end
266
+
267
+ def fix_status(vuln)
268
+ vuln['fixed_in'] ? "fixed in #{vuln['fixed_in']}" : 'no fix available'
269
+ end
270
+
271
+ def help_uri_for(vuln)
272
+ wpvulndb = Array(vuln.dig('references', 'wpvulndb')).first
273
+ return "https://wpscan.com/vulnerability/#{wpvulndb}" if wpvulndb
274
+
275
+ Array(vuln.dig('references', 'url')).first
276
+ end
277
+
278
+ def rule_properties(vuln)
279
+ props = {}
280
+ props['cvss'] = vuln['cvss'] if vuln['cvss']
281
+ props['references'] = vuln['references'] if vuln['references'] && !vuln['references'].empty?
282
+ props['fixed_in'] = vuln['fixed_in'] if vuln['fixed_in']
283
+ props['poc'] = vuln['poc'] if vuln['poc']
284
+ props.empty? ? nil : props
285
+ end
286
+
287
+ def invocation(data)
288
+ inv = { 'executionSuccessful' => true }
289
+ inv['commandLine'] = "wpscan #{data['command_line']}" if data['command_line']
290
+ inv['hostname'] = data['hostname'] if data['hostname']
291
+ inv['startTimeUtc'] = to_iso8601(data['start_time']) if data['start_time']
292
+ inv['endTimeUtc'] = to_iso8601(data['stop_time']) if data['stop_time']
293
+ inv
294
+ end
295
+
296
+ def run_properties(data)
297
+ props = {}
298
+ props['targetUrl'] = data['target_url'] if data['target_url']
299
+ props['effectiveUrl'] = data['effective_url'] if data['effective_url']
300
+ props['targetIp'] = data['target_ip'] if data['target_ip']
301
+ props.empty? ? nil : props
302
+ end
303
+
304
+ def to_iso8601(epoch)
305
+ return nil if epoch.nil? || epoch.to_i.zero?
306
+
307
+ Time.at(epoch.to_i).utc.strftime('%Y-%m-%dT%H:%M:%S.000Z')
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # BackupFolder
6
+ class BackupFolder < InterestingFinding
7
+ MAX_ENTRIES_DISPLAY = 10
8
+
9
+ # @return [ String ]
10
+ def to_s
11
+ msg = "Backup folder found: #{url}"
12
+ if interesting_entries&.any?
13
+ total = @interesting_entries.size
14
+ msg += " (#{total} #{total == 1 ? 'entry' : 'entries'})"
15
+ end
16
+ msg
17
+ end
18
+
19
+ # @return [ Symbol ]
20
+ def severity
21
+ return :high if interesting_entries&.any?
22
+
23
+ :medium
24
+ end
25
+
26
+ # Limit displayed entries to avoid overwhelming output
27
+ # @return [ Array<String> ]
28
+ def interesting_entries
29
+ return [] unless @interesting_entries
30
+
31
+ entries = @interesting_entries.first(MAX_ENTRIES_DISPLAY)
32
+ if @interesting_entries.size > MAX_ENTRIES_DISPLAY
33
+ entries << "... and #{@interesting_entries.size - MAX_ENTRIES_DISPLAY} more"
34
+ end
35
+ entries
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # Fantastico is a commercial script library that automates the installation of web applications to a website.
6
+ # Fantastico scripts are executed from the administration area of a website control panel such as cPanel.
7
+ # It creates a file named fantastico_fileslist.txt that is publicly available and contains a list of all the
8
+ # files from the current directory. The contents of this file may expose sensitive information to an attacker.
9
+ class FantasticoFileslist < InterestingFinding
10
+ # @return [ String ]
11
+ def to_s
12
+ @to_s ||= "Fantastico list found: #{url}"
13
+ end
14
+
15
+ # @return [ Array<String> ] The interesting files/dirs detected
16
+ def interesting_entries
17
+ results = []
18
+
19
+ entries.each do |entry|
20
+ next unless /(?:admin|\.log|\.sql|\.db)/i.match?(entry)
21
+
22
+ results << entry
23
+ end
24
+ results
25
+ end
26
+
27
+ def references
28
+ @references ||= {
29
+ url: ['https://web.archive.org/web/20140518040021/http://www.acunetix.com/vulnerabilities/fantastico-fileslist/']
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # Interesting Headers
6
+ class Headers < InterestingFinding
7
+ # @return [ Hash ] The headers
8
+ def entries
9
+ res = WPScan::Browser.get(url)
10
+ return [] unless res&.headers
11
+
12
+ res.headers
13
+ end
14
+
15
+ # @return [ Array<String> ] The interesting headers detected
16
+ def interesting_entries
17
+ results = []
18
+
19
+ entries.each do |header, value|
20
+ next if known_headers.include?(header.downcase)
21
+
22
+ results << "#{header}: #{Array(value).join(', ')}"
23
+ end
24
+ results
25
+ end
26
+
27
+ # @return [ Array<String> ] Downcased known headers
28
+ def known_headers
29
+ %w[
30
+ age accept-ranges cache-control content-encoding content-length content-type connection date
31
+ etag expires keep-alive location last-modified link pragma set-cookie strict-transport-security
32
+ transfer-encoding vary x-cache x-content-security-policy x-content-type-options
33
+ x-frame-options x-language x-permitted-cross-domain-policies x-pingback x-varnish
34
+ x-webkit-csp x-xss-protection
35
+ ]
36
+ end
37
+
38
+ # @return [ String ]
39
+ def to_s
40
+ @to_s ||= 'Headers'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -2,9 +2,48 @@
2
2
 
3
3
  module WPScan
4
4
  module Model
5
- # Custom class to include the WPScan::References module
6
- class InterestingFinding < CMSScanner::Model::InterestingFinding
5
+ # Interesting Finding base class.
6
+ class InterestingFinding
7
+ include Finders::Finding
7
8
  include References
9
+
10
+ attr_reader :url
11
+ attr_writer :to_s
12
+
13
+ # @param [ String ] url
14
+ # @param [ Hash ] opts
15
+ # :to_s (override the to_s method)
16
+ # See Finders::Finding for other available options
17
+ def initialize(url, opts = {})
18
+ @url = url
19
+ @to_s = opts[:to_s]
20
+
21
+ parse_finding_options(opts)
22
+ end
23
+
24
+ # @return [ Array<String> ]
25
+ def entries
26
+ res = WPScan::Browser.get(url)
27
+
28
+ return [] unless res && res.headers['Content-Type'] =~ %r{\Atext/plain;}i
29
+
30
+ res.body.split("\n").reject { |s| s.strip.empty? }
31
+ end
32
+
33
+ # @return [ String ]
34
+ def to_s
35
+ @to_s || url
36
+ end
37
+
38
+ # @return [ String ]
39
+ def type
40
+ @type ||= self.class.to_s.demodulize.underscore
41
+ end
42
+
43
+ # @return [ Boolean ]
44
+ def ==(other)
45
+ self.class == other.class && to_s == other.to_s
46
+ end
8
47
  end
9
48
 
10
49
  class BackupDB < InterestingFinding
data/app/models/plugin.rb CHANGED
@@ -6,7 +6,7 @@ module WPScan
6
6
  class Plugin < WpItem
7
7
  # See WpItem
8
8
  def initialize(slug, blog, opts = {})
9
- super(slug, blog, opts)
9
+ super
10
10
 
11
11
  # To be used by #head_and_get
12
12
  # If custom wp-content, it will be replaced by blog#url
@@ -36,9 +36,15 @@ module WPScan
36
36
  @version
37
37
  end
38
38
 
39
+ # @return [ String ]
40
+ def wordpress_org_api_url
41
+ encoded_slug = Addressable::URI.encode_component(slug, Addressable::URI::CharacterClasses::UNRESERVED)
42
+ "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=#{encoded_slug}"
43
+ end
44
+
39
45
  # @return [ Array<String> ]
40
46
  def potential_readme_filenames
41
- @potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
47
+ @potential_readme_filenames ||= Array(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)
42
48
  end
43
49
  end
44
50
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # Robots.txt
6
+ class RobotsTxt < InterestingFinding
7
+ # @return [ String ]
8
+ def to_s
9
+ @to_s ||= "robots.txt found: #{url}"
10
+ end
11
+
12
+ # @todo Better detection, currently everything not empty or / is returned
13
+ #
14
+ # @return [ Array<String> ] The interesting Allow/Disallow rules detected
15
+ def interesting_entries
16
+ results = []
17
+
18
+ entries.each do |entry|
19
+ next unless entry =~ /\A(?:dis)?allow:\s*(.+)\z/i
20
+
21
+ match = Regexp.last_match(1)
22
+ next if match == '/'
23
+
24
+ results << match
25
+ end
26
+
27
+ results.uniq
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # SearchReplaceDB2
6
+ class SearchReplaceDB2 < InterestingFinding
7
+ # @return [ String ]
8
+ def to_s
9
+ @to_s ||= "Search Replace DB script found: #{url}"
10
+ end
11
+
12
+ def references
13
+ @references ||= { url: ['https://interconnectit.com/products/search-and-replace-for-wordpress-databases/'] }
14
+ end
15
+ end
16
+ end
17
+ end
data/app/models/theme.rb CHANGED
@@ -9,7 +9,7 @@ module WPScan
9
9
 
10
10
  # See WpItem
11
11
  def initialize(slug, blog, opts = {})
12
- super(slug, blog, opts)
12
+ super
13
13
 
14
14
  # To be used by #head_and_get
15
15
  # If custom wp-content, it will be replaced by blog#url
@@ -42,6 +42,13 @@ module WPScan
42
42
  @version
43
43
  end
44
44
 
45
+ # @return [ String ]
46
+ def wordpress_org_api_url
47
+ encoded_slug = Addressable::URI.encode_component(slug, Addressable::URI::CharacterClasses::UNRESERVED)
48
+ 'https://api.wordpress.org/themes/info/1.2/?action=theme_information' \
49
+ "&request[slug]=#{encoded_slug}&request[fields][active_installs]=1&request[fields][last_updated]=1"
50
+ end
51
+
45
52
  # @return [ Theme ]
46
53
  def parent_theme
47
54
  return unless template
@@ -107,7 +114,7 @@ module WPScan
107
114
  end
108
115
 
109
116
  def ==(other)
110
- super(other) && style_url == other.style_url
117
+ super && style_url == other.style_url
111
118
  end
112
119
  end
113
120
  end
@@ -12,7 +12,7 @@ module WPScan
12
12
  # @param [ Hash ] opts
13
13
  # @option opts [ Symbol ] :mode The mode to use to detect the version
14
14
  def initialize(url, opts = {})
15
- super(url, opts)
15
+ super
16
16
 
17
17
  @version_detection_opts = opts[:version_detection] || {}
18
18
  end
@@ -63,7 +63,7 @@ module WPScan
63
63
  def webshot_enabled?
64
64
  res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
65
65
 
66
- !/WEBSHOT_ENABLED == true/.match?(res.body)
66
+ !res.body.include?('WEBSHOT_ENABLED == true')
67
67
  end
68
68
 
69
69
  # @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # User
6
+ class User
7
+ include Finders::Finding
8
+
9
+ attr_accessor :password
10
+ attr_reader :id, :username
11
+
12
+ # @param [ String ] username
13
+ # @param [ Hash ] opts
14
+ # @option opts [ Integer ] :id
15
+ # @option opts [ String ] :password
16
+ def initialize(username, opts = {})
17
+ @username = username
18
+ @password = opts[:password]
19
+ @id = opts[:id]
20
+
21
+ parse_finding_options(opts)
22
+ end
23
+
24
+ def ==(other)
25
+ return false unless self.class == other.class
26
+
27
+ username == other.username && password == other.password
28
+ end
29
+
30
+ def to_s
31
+ username
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Model
5
+ # Version
6
+ class Version
7
+ include Finders::Finding
8
+
9
+ attr_reader :number
10
+
11
+ def initialize(number, opts = {})
12
+ @number = number.to_s
13
+ @number = "0#{number}" if @number[0, 1] == '.'
14
+
15
+ parse_finding_options(opts)
16
+ end
17
+
18
+ # @param [ Version, String ] other
19
+ # rubocop:disable Style/NumericPredicate
20
+ def ==(other)
21
+ (self <=> other) == 0
22
+ end
23
+ # rubocop:enable all
24
+
25
+ # @param [ Version, String ] other
26
+ def <(other)
27
+ (self <=> other) == -1
28
+ end
29
+
30
+ # @param [ Version, String ] other
31
+ def >(other)
32
+ (self <=> other) == 1
33
+ end
34
+
35
+ # @param [ Version, String ] other
36
+ def <=>(other)
37
+ other = self.class.new(other) unless other.is_a?(self.class) # handle potential '.1' version
38
+
39
+ Gem::Version.new(number) <=> Gem::Version.new(other.number)
40
+ rescue ArgumentError
41
+ false
42
+ end
43
+
44
+ def to_s
45
+ number
46
+ end
47
+ end
48
+ end
49
+ end