
Suppression arbitraire de fichiers/dossiers non authentifiée dans Joomla Helix Ultimate (JoomShaper) <= 2.2.6 — CVE-2026-57830
Il s’agit d’un DoS confirmé + accès au système de fichiers inter-tenant, PAS d’une RCE. Une théorie d’escalade RCE (supprimer configuration.php pour réexposer l’installateur Joomla) a été testée en direct et réfutée — voir « Escalade RCE — testée et réfutée » ci-dessous. Ne présentez pas ceci comme une RCE dans un rapport sans d’abord dériver une véritable chaîne.
Mise à jour de la sévérité (2026-07-06, deuxième passage) : le paramètre path N’EST PAS confiné en toute sécurité à la racine web Joomla comme évalué initialement — le filtre d’entrée PATH de Joomla ne bloque pas un seul composant de traversée /../, donc ce bug atteint (lecture : liste complète de dossiers ; écriture : suppression de fichier ou vidage récursif de contenu de dossier) tout ce que l’utilisateur du serveur web peut lire sur le système de fichiers, pas seulement les fichiers dans l’installation Joomla. Sur toute configuration d’hébergement partagé où plusieurs sites/tenants résident en tant que répertoires frères sous le même utilisateur OS (très courant : cPanel « addon domains », abonnements Plesk partageant un utilisateur système, hébergement bon marché), un unique site propulsé par Helix-Ultimate permet à un visiteur anonyme de détruire tous les sites du même compte. Voir « La traversée de chemin échappe entièrement à JPATH_ROOT » ci-dessous pour la preuve en direct.
Composant : JoomShaper Helix Ultimate Framework (plg_system_helixultimate), fourni avec pratiquement tous les templates Joomla JoomShaper (basés sur Helix Ultimate).
Version testée : 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD au 2026-07, dernier push 2026-06-30)
Auteur : Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/src/Platform/Media.php expose deleteMedia(), getFolders() et createFolder() via le hook de dispatch com_ajax de Joomla dans helixultimate.php::onAfterRoute(). Ces trois méthodes appellent uniquement Session::checkToken() (une simple vérification CSRF, satisfaite par le propre jeton de session d’un visiteur anonyme — récupérable depuis le HTML de la page d’accueil du site) — aucune vérification authorise() / de connexion. Ceci est incohérent avec la méthode sœur uploadMedia() dans la même classe, qui exige correctement core.edit sur com_templates.
Comme il s’agit d’un plugin système, onAfterRoute() se déclenche sur chaque requête, indépendamment du template actif — le chemin de code vulnérable est accessible tant que le plugin est installé et activé (ce qui est le cas par défaut sur tout site utilisant un template JoomShaper basé sur Helix Ultimate).
plugins/system/helixultimate/helixultimate.php (~lignes 464-489) :
if ($this->app->isClient('site'))
{
$option = $this->app->input->get('option', '', 'STRING');
$helix = $this->app->input->get('helix', '', 'STRING');
$request = $this->app->input->get('request', '', 'STRING');
$action = $this->app->input->get('action', '', 'STRING');
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
switch ($action)
{
case 'upload-blog-image': Blog::upload_image(); break; // a vérification core.create/com_media
case 'remove-blog-image': Blog::remove_image(); break; // a vérification core.delete/com_media
case 'view-media': Media::getFolders(); break; // PAS de vérification authorise()
case 'delete-media': Media::deleteMedia(); break; // PAS de vérification authorise()
case 'upload-media': Media::uploadMedia(); break; // a vérification core.edit/com_templates
}
}
}
plugins/system/helixultimate/src/Platform/Media.php :
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← seulement CSRF, pas de authorise()
$path = $input->post->get('path', '/images', 'PATH');
$type = $input->post->get('type', 'file', 'STRING');
if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); }
else { Folder::delete(JPATH_ROOT . '/' . $path); } // récursif
}
$path passe par le filtre d’entrée PATH de Joomla (InputFilter::cleanPath()). Deux éléments indépendants rendent ceci dangereux :
path est résolu directement sous JPATH_ROOT, donc tout chemin absolu depuis la racine (/configuration.php, /administrator/..., /media/...) est déjà accessible.cleanPath() (^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$) autorise exactement une séquence de points/tirets/caractères alphanumériques placée juste après le bloc initial [A-Za-z0-9_/-]+ — et un / seul satisfait ce bloc initial, donc une chaîne comme /../sibling_dir correspond proprement : / (bloc 1), .. (la séquence pointillée autorisée), /sibling_dir (un segment final normal). Le filtre a été écrit pour rejeter un segment qui commence un nouveau composant /… par un point, mais n’a jamais anticipé un seul .. directement après le tout premier caractère de la chaîne. Enchaîner ../../.. ne survit pas (chaque segment /… suivant après la première séquence pointillée doit commencer par un caractère non point) — donc l’évasion est limitée à exactement un niveau de dossier au-dessus de JPATH_ROOT, mais tout ce qui se trouve en dessous de ce niveau (profondeur arbitraire) est alors accessible normalement, puisque les segments suivants sont simplement des composants de chemin sans point.configuration.php → panne totale et instantanée du site (erreur fatale « Aucune configuration »), une requête HTTP, zéro authentification.type=folder) → p. ex. /administrator, /components, /media → bien plus destructeur, détruit effectivement l’installation.view-media (Media::getFolders()) : liste tous les fichiers image, tous les noms de sous-dossiers et les chemins serveur absolus sous tout chemin relatif à la racine, sans authentification (utilisé ci-dessous comme signal de détection sûr).Testé sur une instance Docker jetable (Joomla 5.4.6 + plugin Helix Ultimate 2.2.6, installation fraîche, session navigateur totalement anonyme — pas de connexion, pas de cookie autre que celui que Joomla donne à tout visiteur) :
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/configuration.php" \
--data-urlencode "type=file" \
--data-urlencode "<csrf-token-de-la-page-daccueil>=1"
{"status":true, ...}
$ curl http://TARGET/
"Aucun fichier de configuration trouvé et aucun répertoire trouvé pour l'installation."
Créé un répertoire frère de la racine web Joomla (/var/www/canary_sibling, frère de /var/www/html), appartenant au même utilisateur que le serveur web (www-data) pour refléter une configuration réaliste d’hébergement partagé, peuplé d’un fichier, d’une image et d’un sous-dossier :
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "<csrf-token>=1"
{"status":true, "path":"/../canary_sibling",
"images":["/var/www/html/../canary_sibling/proof.png"],
"folders":["subdir"], ...}
Lecture/énumération complète d’un répertoire totalement en dehors de l’installation Joomla, y compris les chemins serveur absolus résolus, sans aucune authentification.
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "type=folder" \
--data-urlencode "<csrf-token>=1"
Résultat : tous les fichiers dans canary_sibling (le fichier simple, l’image et le sous-dossier) ont été supprimés. Seul le dossier canary_sibling lui-même (désormais vide) a survécu, et uniquement parce que dans ce lab il se trouvait directement sous /var/www, propriété de root — supprimer l’entrée du dossier vide nécessite une permission d’écriture sur son parent, que www-data n’a pas sur /var/www. Sur une configuration réelle d’hébergement partagé (par ex. /home/user/domains/siteA.com/public_html et /home/user/domains/siteB.com/public_html comme vrais frères, tous deux appartenant entièrement au même utilisateur du compte), cette dernière barrière n’existe pas et l’arborescence complète du site frère est supprimable.
La théorie évidente suivante était : sur un site où installation/ n’a jamais été supprimé après l’installation, supprimer configuration.php rendrait l’assistant d’installation accessible à nouveau, permettant à un attaquant de le compléter et de créer un nouveau Super Utilisateur. Ceci a été testé directement en laboratoire et ne tient pas :
installation/ a été recopié dans la racine web (simulant un site qui a oublié de le supprimer) — configuration.php était toujours présent et valide à ce stade.302 Found -> /installation/index.php pour chaque requête, y compris le POST de l’exploit vers option=com_ajax&helix=ultimate&...&action=delete-media. Le chemin de code vulnérable ne s’exécute jamais dans cet état — le noyau Joomla court-circuite tout en premier.installation/ est présent : le site est déjà complètement ouvert à une prise de contrôle par tout visiteur, indépendamment de cette vulnérabilité — c’est une mauvaise configuration Joomla préexistante et sans rapport, pas quelque chose que ce bug cause ou nécessite.installation/ est absent (l’état normal et sécurisé) : cette vulnérabilité peut uniquement supprimer des fichiers, elle ne peut pas créer le dossier installation/ — il n’y a aucun moyen de réexposer l’installateur à partir d’une primitive de suppression uniquement.Conclusion : aucune combinaison d’états ne transforme ceci en RCE. Le plafond d’impact confirmé et honnête est un DoS complet du site non authentifié, inconditionnel et garanti (plus la divulgation d’informations non authentifiée mentionnée ci-dessus). C’est déjà un résultat de sévérité Critique en soi et n’a pas besoin d’une revendication RCE gonflée.
createFolder() N’est PAS accessible sans authentification (brouillon précédent erroné)Une version antérieure de cet article prétendait que Media::createFolder() était également accessible sans authentification (comme troisième primitive avec suppression/lecture). C’était incorrect et a été corrigé après un passage complet sur le câblage de dispatch du plugin :
createFolder(), et les véritables points d’écriture de contenu de fichiers dans la base de code (Request.php : fwrite(), File::write() pour les fichiers de style de template/webfonts/cache CSS), se trouvent tous dans plugins/system/helixultimate/src/Platform/Request.php, dispatchés uniquement via Platform::handleRequests() <- onAfterRespond().onAfterRespond() exige explicitement $this->app->isClient('administrator'), et onAfterRoute() redirige séparément tout visiteur non connecté avant ce point. Ce chemin est véritablement authentifié en tant qu’administrateur — confirmé en lisant la condition de porte exacte, pas seulement l’absence d’un appel authorise() dans la méthode elle-même (contrairement aux deleteMedia()/getFolders() côté site, qui n’ont vraiment aucune porte).isClient('site')) dans onAfterRoute() ne câble que cinq actions : upload-blog-image, remove-blog-image, view-media (Media::getFolders), delete-media (Media::deleteMedia), upload-media (Media::uploadMedia, qui vérifie bien core.edit/com_templates). create-folder n’en fait pas partie.Ensemble de capacité non authentifiée confirmé, définitif : suppression (fichier ou dossier récursif) + lecture (liste de dossiers/images) uniquement. Aucune primitive d’écriture de contenu non authentifiée n’existe nulle part dans ce plugin. C’est précisément pourquoi aucune chaîne RCE n’a été trouvée même après avoir spécifiquement cherché une — RCE nécessite fondamentalement une primitive d’écriture, et cette classe de bug n’en possède pas.
helix_ultimate_detect.pyNon destructif. Utilise action=view-media (liste de dossiers/fichiers) comme signal de preuve — ne supprime jamais rien.
helix_ultimate_delete_poc.py (nom conservé pour continuité ; confirme seulement le DoS)Destructif. Nécessite --delete <path> explicite pour toucher quoi que ce soit. Le drapeau --rce supprime configuration.php et sonde /installation/ uniquement pour vérifier si ce dossier est déjà présent (auquel cas le site était indépendamment grand ouvert indépendamment de ce bug) — il ne représente pas une véritable escalade causée par cette vulnérabilité ; voir « Escalade RCE — testée et réfutée » ci-dessus. À utiliser uniquement avec une autorisation écrite.
Deux correctifs indépendants sont nécessaires, l’un ou l’autre suffirait déjà à stopper ceci :
uploadMedia() possède déjà à deleteMedia() et getFolders() dans src/Platform/Media.php — au minimum core.edit/core.delete sur com_templates (ou com_media, correspondant au modèle de Blog::remove_image()), avant toute opération sur le système de fichiers.Amin İsayev / Proxima Cyber Security — 2026. Usage éducatif / tests autorisés uniquement.