wpscan 3.2.1 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
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