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.
- checksums.yaml +4 -4
- data/README.md +73 -70
- data/app/controllers.rb +1 -1
- data/app/controllers/enumeration.rb +1 -1
- data/app/controllers/enumeration/cli_options.rb +32 -15
- data/app/controllers/enumeration/enum_methods.rb +7 -0
- data/app/controllers/password_attack.rb +108 -0
- data/app/finders.rb +2 -0
- data/app/finders/config_backups/known_filenames.rb +1 -1
- data/app/finders/db_exports.rb +17 -0
- data/app/finders/db_exports/known_locations.rb +49 -0
- data/app/finders/interesting_findings/mu_plugins.rb +1 -0
- data/app/finders/main_theme/css_style.rb +1 -1
- data/app/finders/medias/attachment_brute_forcing.rb +1 -1
- data/app/finders/passwords.rb +3 -0
- data/app/finders/passwords/wp_login.rb +22 -0
- data/app/finders/passwords/xml_rpc.rb +22 -0
- data/app/finders/passwords/xml_rpc_multicall.rb +102 -0
- data/app/finders/users.rb +2 -0
- data/app/finders/users/author_id_brute_forcing.rb +3 -3
- data/app/finders/users/author_posts.rb +2 -2
- data/app/finders/users/login_error_messages.rb +1 -1
- data/app/finders/users/oembed_api.rb +4 -4
- data/app/finders/users/rss_generator.rb +38 -0
- data/app/finders/users/wp_json_api.rb +5 -5
- data/app/finders/wp_version/atom_generator.rb +1 -1
- data/app/finders/wp_version/rdf_generator.rb +1 -1
- data/app/finders/wp_version/rss_generator.rb +1 -1
- data/app/models.rb +1 -1
- data/app/models/db_export.rb +5 -0
- data/app/models/wp_item.rb +2 -0
- data/app/views/cli/core/banner.erb +1 -1
- data/app/views/cli/enumeration/db_exports.erb +11 -0
- data/app/views/cli/{brute_force → password_attack}/users.erb +0 -0
- data/app/views/json/enumeration/db_exports.erb +10 -0
- data/app/views/json/{brute_force → password_attack}/users.erb +1 -1
- data/bin/wpscan +1 -1
- data/lib/wpscan/browser.rb +1 -1
- data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
- data/lib/wpscan/db/dynamic_finders/wordpress.rb +2 -2
- data/lib/wpscan/db/fingerprints.rb +1 -1
- data/lib/wpscan/db/updater.rb +4 -1
- data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +2 -1
- data/lib/wpscan/finders/dynamic_finder/wp_item_version.rb +2 -1
- data/lib/wpscan/finders/dynamic_finder/wp_version.rb +5 -4
- data/lib/wpscan/target.rb +13 -0
- data/lib/wpscan/target/platform/wordpress/custom_directories.rb +1 -1
- data/lib/wpscan/version.rb +1 -1
- metadata +29 -22
- data/app/controllers/brute_force.rb +0 -116
- data/app/models/user.rb +0 -31
- data/app/views/cli/brute_force/error.erb +0 -1
- data/app/views/cli/brute_force/found.erb +0 -2
@@ -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)
|
@@ -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 <<
|
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
|
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 <<
|
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'
|
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 <<
|
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 <<
|
35
|
-
|
36
|
-
|
37
|
-
|
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 <<
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
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'
|
data/app/models/wp_item.rb
CHANGED
@@ -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
|
-
|
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 %>
|
File without changes
|
@@ -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
|
+
},
|
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::
|
12
|
+
WPScan::Controller::PasswordAttack.new <<
|
13
13
|
WPScan::Controller::Aliases.new
|
14
14
|
|
15
15
|
s.run
|