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
@@ -30,6 +30,7 @@ module WPScan
30
30
 
31
31
  return unless [200, 401, 403].include?(res.code)
32
32
  return if target.homepage_or_404?(res)
33
+
33
34
  # TODO: add the check for --exclude-content once implemented ?
34
35
 
35
36
  target.mu_plugins = true
@@ -18,7 +18,7 @@ module WPScan
18
18
  end
19
19
 
20
20
  def passive_from_css_href(res, opts)
21
- target.in_scope_urls(res, '//style|//link') do |url|
21
+ target.in_scope_urls(res, '//style/@src|//link/@href') do |url|
22
22
  next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
23
23
 
24
24
  return create_theme(Regexp.last_match[1], url, opts)
@@ -36,7 +36,7 @@ module WPScan
36
36
  end
37
37
 
38
38
  def create_progress_bar(opts = {})
39
- super(opts.merge(title: ' Brute Forcing Attachment Ids -'))
39
+ super(opts.merge(title: ' Brute Forcing Attachment IDs -'))
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,3 @@
1
+ require_relative 'passwords/wp_login'
2
+ require_relative 'passwords/xml_rpc'
3
+ require_relative 'passwords/xml_rpc_multicall'
@@ -0,0 +1,22 @@
1
+ module WPScan
2
+ module Finders
3
+ module Passwords
4
+ # Password attack against the wp-login.php
5
+ class WpLogin < CMSScanner::Finders::Finder
6
+ include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
7
+
8
+ def login_request(username, password)
9
+ target.login_request(username, password)
10
+ end
11
+
12
+ def valid_credentials?(response)
13
+ response.code == 302
14
+ end
15
+
16
+ def errored_response?(response)
17
+ response.code != 200 && response.body !~ /login_error/i
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module WPScan
2
+ module Finders
3
+ module Passwords
4
+ # Password attack against the XMLRPC interface
5
+ class XMLRPC < CMSScanner::Finders::Finder
6
+ include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
7
+
8
+ def login_request(username, password)
9
+ target.method_call('wp.getUsersBlogs', [username, password])
10
+ end
11
+
12
+ def valid_credentials?(response)
13
+ response.code == 200 && response.body =~ /blogName/
14
+ end
15
+
16
+ def errored_response?(response)
17
+ response.code != 200 && response.body !~ /login_error/i
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,102 @@
1
+ module WPScan
2
+ module Finders
3
+ module Passwords
4
+ # Password attack against the XMLRPC interface with the multicall method
5
+ # WP < 4.4 is vulnerable to such attack
6
+ class XMLRPCMulticall < CMSScanner::Finders::Finder
7
+ # @param [ Array<User> ] users
8
+ # @param [ Array<String> ] passwords
9
+ #
10
+ # @return [ Typhoeus::Response ]
11
+ def do_multi_call(users, passwords)
12
+ methods = []
13
+
14
+ users.each do |user|
15
+ passwords.each do |password|
16
+ methods << ['wp.getUsersBlogs', user.username, password]
17
+ end
18
+ end
19
+
20
+ target.multi_call(methods).run
21
+ end
22
+
23
+ # @param [ Array<CMSScanner::User> ] users
24
+ # @param [ Array<String> ] passwords
25
+ # @param [ Hash ] opts
26
+ # @option opts [ Boolean ] :show_progression
27
+ # @option opts [ Integer ] :multicall_max_passwords
28
+ #
29
+ # @yield [ CMSScanner::User ] When a valid combination is found
30
+ #
31
+ # TODO: Make rubocop happy about metrics etc
32
+ #
33
+ # rubocop:disable all
34
+ def attack(users, passwords, opts = {})
35
+ wordlist_index = 0
36
+ max_passwords = opts[:multicall_max_passwords]
37
+ current_passwords_size = passwords_size(max_passwords, users.size)
38
+
39
+ create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
40
+ show_progression: opts[:show_progression])
41
+
42
+ loop do
43
+ current_users = users.select { |user| user.password.nil? }
44
+ current_passwords = passwords[wordlist_index, current_passwords_size]
45
+ wordlist_index += current_passwords_size
46
+
47
+ break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
48
+
49
+ res = do_multi_call(current_users, current_passwords)
50
+
51
+ progress_bar.increment
52
+
53
+ check_and_output_errors(res)
54
+
55
+ # Avoid to parse the response and iterate over all the structs in the document
56
+ # if there isn't any tag matching a valid combination
57
+ next unless res.body =~ /isAdmin/ # maybe a better one ?
58
+
59
+ Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|
60
+ next if struct.text =~ /faultCode/
61
+
62
+ user = current_users[index / current_passwords.size]
63
+ user.password = current_passwords[index % current_passwords.size]
64
+
65
+ yield user
66
+
67
+ # Updates the current_passwords_size and progress_bar#total
68
+ # given that less requests will be done due to a valid combination found.
69
+ current_passwords_size = passwords_size(max_passwords, current_users.size - 1)
70
+
71
+ if current_passwords_size == 0
72
+ progress_bar.log('All Found') # remove ?
73
+ progress_bar.stop
74
+ break
75
+ end
76
+
77
+ progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
78
+ end
79
+ end
80
+ # Maybe a progress_bar.stop ?
81
+ end
82
+ # rubocop:disable all
83
+
84
+ def passwords_size(max_passwords, users_size)
85
+ return 1 if max_passwords < users_size
86
+ return 0 if users_size == 0
87
+
88
+ max_passwords / users_size
89
+ end
90
+
91
+ # @param [ Typhoeus::Response ] res
92
+ def check_and_output_errors(res)
93
+ progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
94
+
95
+ progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
96
+
97
+ progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
data/app/finders/users.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative 'users/author_posts'
2
2
  require_relative 'users/wp_json_api'
3
3
  require_relative 'users/oembed_api'
4
+ require_relative 'users/rss_generator'
4
5
  require_relative 'users/author_id_brute_forcing'
5
6
  require_relative 'users/login_error_messages'
6
7
 
@@ -17,6 +18,7 @@ module WPScan
17
18
  Users::AuthorPosts.new(target) <<
18
19
  Users::WpJsonApi.new(target) <<
19
20
  Users::OembedApi.new(target) <<
21
+ Users::RSSGenerator.new(target) <<
20
22
  Users::AuthorIdBruteForcing.new(target) <<
21
23
  Users::LoginErrorMessages.new(target)
22
24
  end
@@ -18,7 +18,7 @@ module WPScan
18
18
 
19
19
  next unless username
20
20
 
21
- found << WPScan::User.new(
21
+ found << CMSScanner::User.new(
22
22
  username,
23
23
  id: id,
24
24
  found_by: format(found_by_msg, found_by),
@@ -44,7 +44,7 @@ module WPScan
44
44
  end
45
45
 
46
46
  def create_progress_bar(opts = {})
47
- super(opts.merge(title: ' Brute Forcing Author Ids -'))
47
+ super(opts.merge(title: ' Brute Forcing Author IDs -'))
48
48
  end
49
49
 
50
50
  def request_params
@@ -76,7 +76,7 @@ module WPScan
76
76
  # @return [ String, nil ] The username found
77
77
  def username_from_response(res)
78
78
  # Permalink enabled
79
- target.in_scope_urls(res, '//link|//a') do |url|
79
+ target.in_scope_urls(res, '//link/@href|//a/@href') do |url|
80
80
  username = username_from_author_url(url)
81
81
  return username if username
82
82
  end
@@ -10,7 +10,7 @@ module WPScan
10
10
  found_by_msg = 'Author Posts - %s (Passive Detection)'
11
11
 
12
12
  usernames(opts).reduce([]) do |a, e|
13
- a << WPScan::User.new(
13
+ a << CMSScanner::User.new(
14
14
  e[0],
15
15
  found_by: format(found_by_msg, e[1]),
16
16
  confidence: e[2]
@@ -43,7 +43,7 @@ module WPScan
43
43
  def potential_usernames(res)
44
44
  usernames = []
45
45
 
46
- target.in_scope_urls(res, '//a', %w[href]) do |url, node|
46
+ target.in_scope_urls(res, '//a/@href') do |url, node|
47
47
  uri = Addressable::URI.parse(url)
48
48
 
49
49
  if uri.path =~ %r{/author/([^/\b]+)/?\z}i
@@ -24,7 +24,7 @@ module WPScan
24
24
 
25
25
  next unless error =~ /The password you entered for the username|Incorrect Password/i
26
26
 
27
- found << WPScan::User.new(username, found_by: found_by, confidence: 100)
27
+ found << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
28
28
  end
29
29
 
30
30
  found
@@ -31,10 +31,10 @@ module WPScan
31
31
 
32
32
  return unless details
33
33
 
34
- found << WPScan::User.new(details[0],
35
- found_by: format(found_by_msg, details[1]),
36
- confidence: details[2],
37
- interesting_entries: [api_url])
34
+ found << CMSScanner::User.new(details[0],
35
+ found_by: format(found_by_msg, details[1]),
36
+ confidence: details[2],
37
+ interesting_entries: [api_url])
38
38
  rescue JSON::ParserError
39
39
  found
40
40
  end
@@ -0,0 +1,38 @@
1
+ module WPScan
2
+ module Finders
3
+ module Users
4
+ # Users disclosed from the dc:creator field in the RSS
5
+ # The names disclosed are display names, however depending on the configuration of the blog,
6
+ # they can be the same than usernames
7
+ class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
8
+ def process_urls(urls, _opts = {})
9
+ found = []
10
+
11
+ urls.each do |url|
12
+ res = Browser.get_and_follow_location(url)
13
+
14
+ next unless res.code == 200 && res.body =~ /<dc\:creator>/i
15
+
16
+ potential_usernames = []
17
+
18
+ begin
19
+ res.xml.xpath('//item/dc:creator').each do |node|
20
+ potential_usernames << node.text.to_s unless node.text.to_s.length > 40
21
+ end
22
+ rescue Nokogiri::XML::XPath::SyntaxError
23
+ next
24
+ end
25
+
26
+ potential_usernames.uniq.each do |potential_username|
27
+ found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50)
28
+ end
29
+
30
+ break
31
+ end
32
+
33
+ found
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -13,11 +13,11 @@ module WPScan
13
13
  found = []
14
14
 
15
15
  JSON.parse(Browser.get(api_url).body)&.each do |user|
16
- found << WPScan::User.new(user['slug'],
17
- id: user['id'],
18
- found_by: found_by,
19
- confidence: 100,
20
- interesting_entries: [api_url])
16
+ found << CMSScanner::User.new(user['slug'],
17
+ id: user['id'],
18
+ found_by: found_by,
19
+ confidence: 100,
20
+ interesting_entries: [api_url])
21
21
  end
22
22
 
23
23
  found
@@ -26,7 +26,7 @@ module WPScan
26
26
  end
27
27
 
28
28
  def passive_urls_xpath
29
- '//link[@rel="alternate" and @type="application/atom+xml"]'
29
+ '//link[@rel="alternate" and @type="application/atom+xml"]/@href'
30
30
  end
31
31
 
32
32
  def aggressive_urls(_opts = {})
@@ -26,7 +26,7 @@ module WPScan
26
26
  end
27
27
 
28
28
  def passive_urls_xpath
29
- '//a[contains(@href, "rdf")]'
29
+ '//a[contains(@href, "rdf")]/@href'
30
30
  end
31
31
 
32
32
  def aggressive_urls(_opts = {})
@@ -29,7 +29,7 @@ module WPScan
29
29
  end
30
30
 
31
31
  def passive_urls_xpath
32
- '//link[@rel="alternate" and @type="application/rss+xml"]'
32
+ '//link[@rel="alternate" and @type="application/rss+xml"]/@href'
33
33
  end
34
34
 
35
35
  def aggressive_urls(_opts = {})
data/app/models.rb CHANGED
@@ -4,7 +4,7 @@ require_relative 'models/xml_rpc'
4
4
  require_relative 'models/wp_item'
5
5
  require_relative 'models/timthumb'
6
6
  require_relative 'models/media'
7
- require_relative 'models/user'
8
7
  require_relative 'models/plugin'
9
8
  require_relative 'models/theme'
10
9
  require_relative 'models/config_backup'
10
+ require_relative 'models/db_export'
@@ -0,0 +1,5 @@
1
+ module WPScan
2
+ # DB Export
3
+ class DbExport < InterestingFinding
4
+ end
5
+ end
@@ -141,6 +141,7 @@ module WPScan
141
141
  # @return [ Boolean ]
142
142
  def directory_listing?(path = nil, params = {})
143
143
  return if detection_opts[:mode] == :passive
144
+
144
145
  super(path, params)
145
146
  end
146
147
 
@@ -150,6 +151,7 @@ module WPScan
150
151
  # @return [ Boolean ]
151
152
  def error_log?(path = 'error_log', params = {})
152
153
  return if detection_opts[:mode] == :passive
154
+
153
155
  super(path, params)
154
156
  end
155
157
  end
@@ -9,6 +9,6 @@ _______________________________________________________________
9
9
  WordPress Security Scanner by the WPScan Team
10
10
  Version <%= WPScan::VERSION %>
11
11
  Sponsored by Sucuri - https://sucuri.net
12
- @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
12
+ @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
13
13
  _______________________________________________________________
14
14
 
@@ -0,0 +1,11 @@
1
+
2
+ <% if @db_exports.empty? -%>
3
+ <%= notice_icon %> No DB Exports Found.
4
+ <% else -%>
5
+ <%= notice_icon %> Db Export(s) Identified:
6
+ <% @db_exports.each do |db_export| -%>
7
+
8
+ <%= info_icon %> <%= db_export %>
9
+ <%= render('@finding', item: db_export) -%>
10
+ <% end -%>
11
+ <% end %>
@@ -0,0 +1,10 @@
1
+ "db_exports": {
2
+ <% unless @db_exports.empty? -%>
3
+ <% last_index = @db_exports.size - 1 -%>
4
+ <% @db_exports.each_with_index do |db_export, index| -%>
5
+ <%= db_export.url.to_json %>: {
6
+ <%= render('@finding', item: db_export) -%>
7
+ }<% unless index == last_index -%>,<% end -%>
8
+ <% end -%>
9
+ <% end -%>
10
+ },
@@ -1,4 +1,4 @@
1
- "brute_force": {
1
+ "password_attack": {
2
2
  <% unless @users.empty? -%>
3
3
  <% last_index = @users.size - 1 -%>
4
4
  <% @users.each_with_index do |user, index| -%>
data/bin/wpscan CHANGED
@@ -9,7 +9,7 @@ WPScan::Scan.new do |s|
9
9
  WPScan::Controller::WpVersion.new <<
10
10
  WPScan::Controller::MainTheme.new <<
11
11
  WPScan::Controller::Enumeration.new <<
12
- WPScan::Controller::BruteForce.new <<
12
+ WPScan::Controller::PasswordAttack.new <<
13
13
  WPScan::Controller::Aliases.new
14
14
 
15
15
  s.run