wpscan 3.2.1 → 3.3.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -70
  3. data/app/controllers.rb +1 -1
  4. data/app/controllers/enumeration.rb +1 -1
  5. data/app/controllers/enumeration/cli_options.rb +32 -15
  6. data/app/controllers/enumeration/enum_methods.rb +7 -0
  7. data/app/controllers/password_attack.rb +108 -0
  8. data/app/finders.rb +2 -0
  9. data/app/finders/config_backups/known_filenames.rb +1 -1
  10. data/app/finders/db_exports.rb +17 -0
  11. data/app/finders/db_exports/known_locations.rb +49 -0
  12. data/app/finders/interesting_findings/mu_plugins.rb +1 -0
  13. data/app/finders/main_theme/css_style.rb +1 -1
  14. data/app/finders/medias/attachment_brute_forcing.rb +1 -1
  15. data/app/finders/passwords.rb +3 -0
  16. data/app/finders/passwords/wp_login.rb +22 -0
  17. data/app/finders/passwords/xml_rpc.rb +22 -0
  18. data/app/finders/passwords/xml_rpc_multicall.rb +102 -0
  19. data/app/finders/users.rb +2 -0
  20. data/app/finders/users/author_id_brute_forcing.rb +3 -3
  21. data/app/finders/users/author_posts.rb +2 -2
  22. data/app/finders/users/login_error_messages.rb +1 -1
  23. data/app/finders/users/oembed_api.rb +4 -4
  24. data/app/finders/users/rss_generator.rb +38 -0
  25. data/app/finders/users/wp_json_api.rb +5 -5
  26. data/app/finders/wp_version/atom_generator.rb +1 -1
  27. data/app/finders/wp_version/rdf_generator.rb +1 -1
  28. data/app/finders/wp_version/rss_generator.rb +1 -1
  29. data/app/models.rb +1 -1
  30. data/app/models/db_export.rb +5 -0
  31. data/app/models/wp_item.rb +2 -0
  32. data/app/views/cli/core/banner.erb +1 -1
  33. data/app/views/cli/enumeration/db_exports.erb +11 -0
  34. data/app/views/cli/{brute_force → password_attack}/users.erb +0 -0
  35. data/app/views/json/enumeration/db_exports.erb +10 -0
  36. data/app/views/json/{brute_force → password_attack}/users.erb +1 -1
  37. data/bin/wpscan +1 -1
  38. data/lib/wpscan/browser.rb +1 -1
  39. data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
  40. data/lib/wpscan/db/dynamic_finders/wordpress.rb +2 -2
  41. data/lib/wpscan/db/fingerprints.rb +1 -1
  42. data/lib/wpscan/db/updater.rb +4 -1
  43. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +2 -1
  44. data/lib/wpscan/finders/dynamic_finder/wp_item_version.rb +2 -1
  45. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +5 -4
  46. data/lib/wpscan/target.rb +13 -0
  47. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +1 -1
  48. data/lib/wpscan/version.rb +1 -1
  49. metadata +29 -22
  50. data/app/controllers/brute_force.rb +0 -116
  51. data/app/models/user.rb +0 -31
  52. data/app/views/cli/brute_force/error.erb +0 -1
  53. data/app/views/cli/brute_force/found.erb +0 -2
@@ -10,7 +10,7 @@ module WPScan
10
10
 
11
11
  # @return [ String ]
12
12
  def default_user_agent
13
- "WPScan v#{VERSION} (http://wpscan.org/)"
13
+ "WPScan v#{VERSION} (https://wpscan.org/)"
14
14
  end
15
15
  end
16
16
  end
@@ -28,7 +28,7 @@ module WPScan
28
28
  end
29
29
 
30
30
  fs.each do |finder_name, config|
31
- klass = config['class'] ? config['class'] : finder_name
31
+ klass = config['class'] || finder_name
32
32
 
33
33
  next unless klass.to_sym == finder_class
34
34
 
@@ -79,7 +79,7 @@ module WPScan
79
79
  mod = maybe_create_modudle(slug)
80
80
 
81
81
  finders.each do |finder_class, config|
82
- klass = config['class'] ? config['class'] : finder_class
82
+ klass = config['class'] || finder_class
83
83
 
84
84
  # Instead of raising exceptions, skip unallowed/already defined finders
85
85
  # So that, when new DF configs are put in the .yml
@@ -34,7 +34,7 @@ module WPScan
34
34
  end
35
35
 
36
36
  finders.each do |finder_name, config|
37
- klass = config['class'] ? config['class'] : finder_name
37
+ klass = config['class'] || finder_name
38
38
 
39
39
  next unless klass.to_sym == finder_class
40
40
 
@@ -51,7 +51,7 @@ module WPScan
51
51
 
52
52
  def self.create_versions_finders
53
53
  versions_finders_configs.each do |finder_class, config|
54
- klass = config['class'] ? config['class'] : finder_class
54
+ klass = config['class'] || finder_class
55
55
 
56
56
  # Instead of raising exceptions, skip unallowed/already defined finders
57
57
  # So that, when new DF configs are put in the .yml
@@ -33,7 +33,7 @@ module WPScan
33
33
 
34
34
  # @return [ String ]
35
35
  def self.wp_fingerprints_path
36
- @wp_unique_fingerprints_path ||= File.join(DB_DIR, 'wp_fingerprints.json')
36
+ @wp_fingerprints_path ||= File.join(DB_DIR, 'wp_fingerprints.json')
37
37
  end
38
38
 
39
39
  # @return [ Hash ]
@@ -7,7 +7,7 @@ module WPScan
7
7
  FILES = %w[
8
8
  plugins.json themes.json wordpresses.json
9
9
  timthumbs-v3.txt user-agents.txt config_backups.txt
10
- dynamic_finders.yml wp_fingerprints.json LICENSE
10
+ db_exports.txt dynamic_finders.yml wp_fingerprints.json LICENSE
11
11
  ].freeze
12
12
 
13
13
  OLD_FILES = %w[wordpress.db dynamic_finders_01.yml].freeze
@@ -82,6 +82,7 @@ module WPScan
82
82
 
83
83
  res = Browser.get(url, request_params)
84
84
  raise DownloadError, res if res.timed_out? || res.code != 200
85
+
85
86
  res.body.chomp
86
87
  end
87
88
 
@@ -99,11 +100,13 @@ module WPScan
99
100
 
100
101
  def create_backup(filename)
101
102
  return unless File.exist?(local_file_path(filename))
103
+
102
104
  FileUtils.cp(local_file_path(filename), backup_file_path(filename))
103
105
  end
104
106
 
105
107
  def restore_backup(filename)
106
108
  return unless File.exist?(backup_file_path(filename))
109
+
107
110
  FileUtils.cp(backup_file_path(filename), local_file_path(filename))
108
111
  end
109
112
 
@@ -37,6 +37,7 @@ module WPScan
37
37
  uri = Addressable::URI.parse(url)
38
38
 
39
39
  next unless uri.path =~ path_pattern && uri.query&.match(self.class::PATTERN)
40
+
40
41
  version = Regexp.last_match[:v].to_s
41
42
 
42
43
  found[version] ||= []
@@ -48,7 +49,7 @@ module WPScan
48
49
 
49
50
  # @return [ String ]
50
51
  def xpath
51
- @xpath ||= self.class::XPATH || '//link[@href]|//script[@src]'
52
+ @xpath ||= self.class::XPATH || '//link[@href]/@href|//script[@src]/@src'
52
53
  end
53
54
 
54
55
  # @return [ Regexp ]
@@ -30,7 +30,8 @@ module WPScan
30
30
 
31
31
  def xpath
32
32
  @xpath ||= self.class::XPATH ||
33
- "//link[contains(@href,'#{target.slug}')]|//script[contains(@src,'#{target.slug}')]"
33
+ "//link[contains(@href,'#{target.slug}')]/@href" \
34
+ "|//script[contains(@src,'#{target.slug}')]/@src"
34
35
  end
35
36
  end
36
37
 
@@ -37,13 +37,14 @@ module WPScan
37
37
 
38
38
  class WpItemQueryParameter < QueryParameter
39
39
  def xpath
40
- @xpath ||= self.class::XPATH ||
41
- "//link[contains(@href,'#{target.plugins_dir}') or contains(@href,'#{target.themes_dir}')]|" \
42
- "//script[contains(@src,'#{target.plugins_dir}') or contains(@src,'#{target.themes_dir}')]"
40
+ @xpath ||=
41
+ self.class::XPATH ||
42
+ "//link[contains(@href,'#{target.plugins_dir}') or contains(@href,'#{target.themes_dir}')]/@href" \
43
+ "|//script[contains(@src,'#{target.plugins_dir}') or contains(@src,'#{target.themes_dir}')]/@src"
43
44
  end
44
45
 
45
46
  def path_pattern
46
- @pattern ||= %r{
47
+ @path_pattern ||= %r{
47
48
  (?:#{Regexp.escape(target.plugins_dir)}|#{Regexp.escape(target.themes_dir)})/
48
49
  [^/]+/
49
50
  .*\.(?:css|js)\z
data/lib/wpscan/target.rb CHANGED
@@ -12,12 +12,18 @@ module WPScan
12
12
  end
13
13
 
14
14
  return true unless [*@config_backups].empty?
15
+ return true unless [*@db_exports].empty?
15
16
 
16
17
  [*@users].each { |u| return true if u.password }
17
18
 
18
19
  false
19
20
  end
20
21
 
22
+ # @return [ XMLRPC, nil ]
23
+ def xmlrpc
24
+ @xmlrpc ||= interesting_findings&.select { |f| f.is_a?(WPScan::XMLRPC) }&.first
25
+ end
26
+
21
27
  # @param [ Hash ] opts
22
28
  #
23
29
  # @return [ WpVersion, false ] The WpVersion found or false if not detected
@@ -64,6 +70,13 @@ module WPScan
64
70
  @config_backups ||= Finders::ConfigBackups::Base.find(self, opts)
65
71
  end
66
72
 
73
+ # @param [ Hash ] opts
74
+ #
75
+ # @return [ Array<DBExport> ]
76
+ def db_exports(opts = {})
77
+ @db_exports ||= Finders::DbExports::Base.find(self, opts)
78
+ end
79
+
67
80
  # @param [ Hash ] opts
68
81
  #
69
82
  # @return [ Array<Media> ]
@@ -15,7 +15,7 @@ module WPScan
15
15
  def content_dir
16
16
  unless @content_dir
17
17
  escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
18
- pattern = %r{#{escaped_url}(.+?)\/(?:themes|plugins|uploads)\/}i
18
+ pattern = %r{#{escaped_url}(.+?)\/(?:themes|plugins|uploads|cache)\/}i
19
19
 
20
20
  in_scope_urls(homepage_res) do |url|
21
21
  return @content_dir = Regexp.last_match[1] if url.match(pattern)
@@ -1,4 +1,4 @@
1
1
  # Version
2
2
  module WPScan
3
- VERSION = '3.2.1'.freeze
3
+ VERSION = '3.3.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wpscan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-04 00:00:00.000000000 Z
11
+ date: 2018-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cms_scanner
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.39.1
19
+ version: 0.0.40
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.39.1
26
+ version: 0.0.40
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.1'
33
+ version: '5.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '5.1'
40
+ version: '5.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: yajl-ruby
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -86,28 +86,28 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '12.0'
89
+ version: '12.3'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '12.0'
96
+ version: '12.3'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rspec
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 3.7.0
103
+ version: 3.8.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 3.7.0
110
+ version: 3.8.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rspec-its
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -128,42 +128,42 @@ dependencies:
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.52.0
131
+ version: 0.59.2
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.52.0
138
+ version: 0.59.2
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: simplecov
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 0.14.0
145
+ version: 0.16.1
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 0.14.0
152
+ version: 0.16.1
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: webmock
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 3.3.0
159
+ version: 3.4.2
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 3.3.0
166
+ version: 3.4.2
167
167
  description: WPScan is a black box WordPress vulnerability scanner.
168
168
  email:
169
169
  - team@wpscan.org
@@ -177,17 +177,19 @@ files:
177
177
  - app/app.rb
178
178
  - app/controllers.rb
179
179
  - app/controllers/aliases.rb
180
- - app/controllers/brute_force.rb
181
180
  - app/controllers/core.rb
182
181
  - app/controllers/custom_directories.rb
183
182
  - app/controllers/enumeration.rb
184
183
  - app/controllers/enumeration/cli_options.rb
185
184
  - app/controllers/enumeration/enum_methods.rb
186
185
  - app/controllers/main_theme.rb
186
+ - app/controllers/password_attack.rb
187
187
  - app/controllers/wp_version.rb
188
188
  - app/finders.rb
189
189
  - app/finders/config_backups.rb
190
190
  - app/finders/config_backups/known_filenames.rb
191
+ - app/finders/db_exports.rb
192
+ - app/finders/db_exports/known_locations.rb
191
193
  - app/finders/interesting_findings.rb
192
194
  - app/finders/interesting_findings/backup_db.rb
193
195
  - app/finders/interesting_findings/debug_log.rb
@@ -207,6 +209,10 @@ files:
207
209
  - app/finders/main_theme/woo_framework_meta_generator.rb
208
210
  - app/finders/medias.rb
209
211
  - app/finders/medias/attachment_brute_forcing.rb
212
+ - app/finders/passwords.rb
213
+ - app/finders/passwords/wp_login.rb
214
+ - app/finders/passwords/xml_rpc.rb
215
+ - app/finders/passwords/xml_rpc_multicall.rb
210
216
  - app/finders/plugin_version.rb
211
217
  - app/finders/plugin_version/readme.rb
212
218
  - app/finders/plugins.rb
@@ -234,6 +240,7 @@ files:
234
240
  - app/finders/users/author_posts.rb
235
241
  - app/finders/users/login_error_messages.rb
236
242
  - app/finders/users/oembed_api.rb
243
+ - app/finders/users/rss_generator.rb
237
244
  - app/finders/users/wp_json_api.rb
238
245
  - app/finders/wp_items.rb
239
246
  - app/finders/wp_items/urls_in_homepage.rb
@@ -245,24 +252,22 @@ files:
245
252
  - app/finders/wp_version/unique_fingerprinting.rb
246
253
  - app/models.rb
247
254
  - app/models/config_backup.rb
255
+ - app/models/db_export.rb
248
256
  - app/models/interesting_finding.rb
249
257
  - app/models/media.rb
250
258
  - app/models/plugin.rb
251
259
  - app/models/theme.rb
252
260
  - app/models/timthumb.rb
253
- - app/models/user.rb
254
261
  - app/models/wp_item.rb
255
262
  - app/models/wp_version.rb
256
263
  - app/models/xml_rpc.rb
257
- - app/views/cli/brute_force/error.erb
258
- - app/views/cli/brute_force/found.erb
259
- - app/views/cli/brute_force/users.erb
260
264
  - app/views/cli/core/banner.erb
261
265
  - app/views/cli/core/db_update_finished.erb
262
266
  - app/views/cli/core/db_update_started.erb
263
267
  - app/views/cli/core/not_fully_configured.erb
264
268
  - app/views/cli/core/version.erb
265
269
  - app/views/cli/enumeration/config_backups.erb
270
+ - app/views/cli/enumeration/db_exports.erb
266
271
  - app/views/cli/enumeration/medias.erb
267
272
  - app/views/cli/enumeration/plugins.erb
268
273
  - app/views/cli/enumeration/themes.erb
@@ -272,18 +277,19 @@ files:
272
277
  - app/views/cli/info.erb
273
278
  - app/views/cli/main_theme/theme.erb
274
279
  - app/views/cli/notice.erb
280
+ - app/views/cli/password_attack/users.erb
275
281
  - app/views/cli/theme.erb
276
282
  - app/views/cli/usage.erb
277
283
  - app/views/cli/vulnerability.erb
278
284
  - app/views/cli/wp_item.erb
279
285
  - app/views/cli/wp_version/version.erb
280
- - app/views/json/brute_force/users.erb
281
286
  - app/views/json/core/banner.erb
282
287
  - app/views/json/core/db_update_finished.erb
283
288
  - app/views/json/core/db_update_started.erb
284
289
  - app/views/json/core/not_fully_configured.erb
285
290
  - app/views/json/core/version.erb
286
291
  - app/views/json/enumeration/config_backups.erb
292
+ - app/views/json/enumeration/db_exports.erb
287
293
  - app/views/json/enumeration/medias.erb
288
294
  - app/views/json/enumeration/plugins.erb
289
295
  - app/views/json/enumeration/themes.erb
@@ -291,6 +297,7 @@ files:
291
297
  - app/views/json/enumeration/users.erb
292
298
  - app/views/json/finding.erb
293
299
  - app/views/json/main_theme/theme.erb
300
+ - app/views/json/password_attack/users.erb
294
301
  - app/views/json/theme.erb
295
302
  - app/views/json/wp_item.erb
296
303
  - app/views/json/wp_version/version.erb
@@ -1,116 +0,0 @@
1
- module WPScan
2
- module Controller
3
- # Brute Force Controller
4
- class BruteForce < CMSScanner::Controller::Base
5
- def cli_options
6
- [
7
- OptFilePath.new(
8
- ['--passwords FILE-PATH', '-P',
9
- 'List of passwords to use during the brute forcing.',
10
- 'If no --username/s option supplied, user enumeration will be run'],
11
- exists: true
12
- ),
13
- OptSmartList.new(['--usernames LIST', '-U', 'List of usernames to use during the brute forcing'])
14
- ]
15
- end
16
-
17
- def run
18
- return unless parsed_options[:passwords]
19
-
20
- begin
21
- found = []
22
-
23
- brute_force(users, passwords(parsed_options[:passwords])) do |user|
24
- found << user
25
-
26
- output('found', user: user) if user_interaction?
27
- end
28
- ensure
29
- output('users', users: found)
30
- end
31
- end
32
-
33
- # @return [ Array<Users> ] The users to brute force
34
- def users
35
- return target.users unless parsed_options[:usernames]
36
-
37
- parsed_options[:usernames].reduce([]) do |acc, elem|
38
- acc << User.new(elem.chomp)
39
- end
40
- end
41
-
42
- # the iteration should be on the passwords to be more efficient
43
- # however, it's not that simple expecially when a combination is found:
44
- # - the estimated number of requests (for the progressbar) has to be updated.
45
- # - the user found has to be deleted from the loop
46
- #
47
- # @param [ Array<User> ] users
48
- # @param [ Array<String> ] passwords
49
- #
50
- # @yield [ User ] when a valid combination is found
51
- def brute_force(users, passwords)
52
- hydra = Browser.instance.hydra
53
-
54
- users.each do |user|
55
- bar = progress_bar(passwords.size, user.username) if user_interaction?
56
-
57
- passwords.each do |password|
58
- request = target.login_request(user.username, password)
59
-
60
- request.on_complete do |res|
61
- bar.progress += 1 if user_interaction?
62
-
63
- if res.code == 302
64
- user.password = password
65
- hydra.abort
66
-
67
- yield user
68
- next
69
- elsif user_interaction? && res.code != 200
70
- # Errors not displayed when using formats other than cli/cli-no-colour
71
- output_error(res)
72
- end
73
- end
74
-
75
- hydra.queue(request)
76
- end
77
- hydra.run
78
- end
79
- end
80
-
81
- def progress_bar(size, username)
82
- ProgressBar.create(
83
- format: '%t %a <%B> (%c / %C) %P%% %e',
84
- title: "Brute Forcing #{username} -",
85
- total: size
86
- )
87
- end
88
-
89
- # @param [ String ] wordlist_path
90
- #
91
- # @return [ Array<String> ]
92
- def passwords(wordlist_path)
93
- @passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
94
- acc << elem.chomp
95
- end
96
- end
97
-
98
- # @param [ Typhoeus::Response ] response
99
- def output_error(response)
100
- return if response.body =~ /login_error/i
101
-
102
- error = if response.timed_out?
103
- 'Request timed out.'
104
- elsif response.code.zero?
105
- "No response from remote server. WAF/IPS? (#{response.return_message})"
106
- elsif response.code.to_s =~ /^50/
107
- 'Server error, try reducing the number of threads.'
108
- else
109
- "Unknown response received Code: #{response.code}\n Body: #{response.body}"
110
- end
111
-
112
- output('error', msg: error)
113
- end
114
- end
115
- end
116
- end