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