Blog

The mass CMS hacking campaign hitting WordPress, Joomla, and Craft CMS: 15 vulnerabilities, one outcome — a web shell

cms_webshell_campaign
CVE / Joomla / Linux / PHP / Security / Shell / Wordpress

The mass CMS hacking campaign hitting WordPress, Joomla, and Craft CMS: 15 vulnerabilities, one outcome — a web shell

You’ve got a dozen client sites sitting on one server — shared nginx, shared PHP-FPM pool, separate web roots. One of them is a WordPress site with a Ninja Forms contact form and a file upload add-on. You haven’t touched it in six months: it works, why mess with it. On July 9, 2026, Australia’s ACSC (the Australian Signals Directorate’s Cyber Security Centre) put out a critical warning: a global campaign is scanning for exactly these sites and dropping web shells through known CMS vulnerabilities. The list runs 15 entries deep, almost all CVSS 9.8–10.0 — WordPress plugins, the Joomla Content Editor, Craft CMS and MaxSite CMS and MetInfo CMS themselves, and even a Laravel payment package (pay-uz) that has nothing to do with WordPress but somehow made the same list.

This isn’t one flashy zero-day getting exploited. It’s a coordinated push across a whole front of known, long-patched bugs — from a file upload flaw in Ninja Forms File Uploads (CVE-2026-0740, CVSS 9.8) to a code injection bug in Craft CMS’s image transform feature (CVE-2025-32432, CVSS 10.0). ACSC is pointing to AI-assisted scanning and exploit selection as the reason attacks are moving faster — the same theme showed up in a recent joint Five Eyes statement. For small and mid-sized business sites, where plugin updates get pushed off for months at a time, “it works, don’t touch it” is exactly the crack these attackers are walking through.

WHAT A WEB SHELL CAMPAIGN ACTUALLY MEANS

A web shell is a file — usually .php — that an attacker drops onto a server through some vulnerable upload handler, then hits directly by URL. The web server runs it like any other page on the site, except the code inside takes orders from whoever planted it: run shell commands, read and write files, create new accounts, pull down more tools. Unlike a one-off defacement, a web shell is persistent access. Find and delete one file and nothing stops the attacker from just rescanning the site and doing it again, as long as the underlying bug is still open.

“Mass campaign” here means the bots aren’t picking targets on purpose. They’re crawling the internet by signature — plugin file paths, version numbers leaking through meta tags, how a server responds to a probe request. Once a bot fingerprints “this CMS, this plugin version,” it checks that against a known-vulnerable list, and if there’s a hit, a ready-made exploit fires automatically. Your site doesn’t get hit because someone targeted you. It gets hit because it showed up in the net.

THE REAL-WORLD ATTACK CHAIN

Take Craft CMS and CVE-2025-32432 — the clearest example on the list, because real attacks against it started back in February 2025, two months before the patch even existed, and Orange Cyberdefense documented the whole thing. Craft CMS has a built-in image transform feature: upload a 4.5MB photo, and the CMS generates a thumbnail-sized version on its own. That’s handled by an unauthenticated controller endpoint, actions/assets/generate-transform — open with no login required, because it needs to serve images to anyone browsing the public site.

The problem: in versions 4.x and 5.x, the asset ID — the identifier for a specific image file — only gets validated after the transform object is already built, not before. An attacker starts by brute-forcing numeric asset ID values against POST requests to that same endpoint until one hits — in 3.x that check happens earlier in the execution path, so there you need a working ID upfront, but the end result is identical. Once a valid ID turns up, the second request carries a handle parameter in its body — normally just a string, the name of a transform like “thumbnail.” In the live exploit, instead of a string, the server gets a nested object with a __class field telling it: instantiate this PHP class — GuzzleHttp\Psr7\FnStream, an internal HTTP library class that should never be created directly from someone else’s request body. The server complies, without ever checking whether the client is even allowed to say what code runs. Craft CMS’s own CVSS vector — a 10.0, with confidentiality and integrity fully compromised — files this under Code Injection; GitHub Security Advisory’s independent classification calls it CWE-470, unsafe use of user input to select a class for reflection. Either way, the outcome is the same: someone else’s HTTP request body decides what your PHP process executes.

In the specific incident Orange Cyberdefense investigated, the attack kicked off on February 14, 2025, and the attacker didn’t stop at a proof-of-concept phpinfo() call. According to Orange, there were two working exploitation paths for this bug; this attack used the following one: the attacker sent a request carrying a “return URL” parameter, and Craft CMS wrote that value into a PHP session file on disk, then echoed it straight back to the visitor as part of the same HTTP response — meaning attacker-controlled code was now sitting in a session file on the server. Step two chained in a separate bug in Yii, the framework Craft CMS runs on (CVE-2024-58136, an input validation flaw), to get the server to execute that session file as PHP. The result: a PHP-based file manager dropped into the web root, which the attacker then used to pull in more PHP files.

From there, the attacker has a fully functional web shell — enough to read database credentials out of config files, plant backdoors for persistence, and use the compromised box as a launchpad to scan the internal network, assuming the web server shares a segment with the rest of the company’s infrastructure. This particular campaign’s scale is documented with real numbers: Orange Cyberdefense scanned roughly 35,000 unique Craft CMS domains, found around 13,000 vulnerable installs, and flagged about 300 as already compromised — identified by telltale filemanager.php and autoload_classmap.php files sitting in the web root — as of the report’s publication in April 2025. Craft CMS itself confirmed active exploitation on April 17, 2025, a week after the patch shipped, and emailed every license holder urging an immediate update.

The same logic, with different technical specifics, runs through the other 14 entries on ACSC’s list. In Ninja Forms File Uploads (CVE-2026-0740), the root cause isn’t a missing file-type check as such — it’s that the NF_FU_AJAX_Controllers_Uploads::handle_upload function validates the source file’s extension but never checks the final filename at the point it gets moved into the target directory. An attacker manipulates the destination path and slips right past the extension allowlist, dropping a PHP file directly into a web-accessible upload folder. No authentication, no CAPTCHA required — the AJAX endpoint is public by design, since the upload form has to work for anonymous site visitors. And this isn’t a theoretical risk: Wordfence reports exploitation began the same day the bug was disclosed, with a mass attack wave hitting April 9–13, 2026 — Wordfence’s firewall alone blocked over 118,600 exploitation attempts against this one vulnerability.

And proof that ACSC’s list describes an active hunt, not a hypothetical one, showed up in independent research just weeks before the bulletin dropped. On June 11, 2026, SOCRadar researchers found an attacker’s server sitting wide open with no password — left exposed by mistake for three weeks straight. Inside: nearly 800MB of tooling, logs, and target lists covering 1.4 million domains, all running the exact same vulnerabilities ACSC would go on to list: Breeze (CVE-2026-3844), ThemeREX Addons, Simple File List, BerqWP, Ninja Forms File Uploads, WavePlayer, WPBookit, and Joomla JCE. A separate team, Ctrl-Alt-Intel, analyzed the same trove and counted 25,195 confirmed or likely compromises — the largest chunk coming from the Breeze bug, and specifically from sites with the non-default “Host Files Locally — Gravatars” setting turned on: over 45,000 targets scanned, more than 17,000 successfully breached. Both research teams tied the operation to a Chinese-speaking group with moderate-to-high confidence, citing Chinese-language strings in command history and the use of FOFA, a Chinese Shodan equivalent that requires a Chinese phone number to register for.

The Breeze mechanism is worth a closer look precisely because the vulnerable function has nothing to do with user uploads at all — it’s about avatar caching. The fetch_gravatar_from_remote function, in class-breeze-cache-cronjobs.php, downloads a Gravatar image by URL and writes it to disk with file_put_contents() — without ever confirming the downloaded content is actually an image. With “Host Files Locally — Gravatars” enabled (off by default, which is exactly why only some of those 45,000 scanned targets were affected), an attacker feeds it a PHP file disguised as a Gravatar URL, and Breeze itself — acting on the site’s behalf — downloads and saves that web shell straight into /uploads/breeze/gravatars/. No form upload needed, no authentication needed — just knowing the setting is turned on.

TIMELINE

The vulnerabilities behind this campaign span almost two years of exploit development, all converging into one current wave: CVE-2020-36847 in the Simple File List plugin is the oldest entry, dating back to 2020 (a separately filed CVE-2025-34085 was rejected by ACSC as a duplicate of the same bug). CVE-2024-9234 in GutenKit and Hunk Companion goes back to 2024 — ACSC calls its link to this campaign “likely” but not fully confirmed. CVE-2025-32432 in Craft CMS was already being exploited by hand on February 10, 2025, and by February 14, Orange Cyberdefense was called in to investigate one specific compromised server — the point where documentation of the bug really started. The official patch (versions 3.9.15, 4.14.15, and 5.6.17) didn’t land until April 10 — a full two months after the first attack. Most of the remaining entries — CVE-2025-7443 (BerqWP), CVE-2025-7852 (WPBookit), CVE-2025-12057 (WavePlayer), CVE-2025-13486 (ACF Extended), CVE-2025-6389 (Sneeit Framework), CVE-2025-12352 (Gravity Forms) — were disclosed at various points through 2025. Researcher Sélim Lanouar reported CVE-2026-0740 in Ninja Forms File Uploads to the Wordfence Bug Bounty Program on January 8, 2026, earning a $2,145 payout; public disclosure landed on April 6, 2026, and the first exploitation attempts started that same day. The fix went through three rounds: a partial patch in 3.3.25 on February 10, a wider extension blocklist in 3.3.26, and only version 3.3.27 on March 19, 2026 finally closed the bypass for good — meaning the real fix shipped almost three weeks before the vulnerability was even publicly disclosed.

One entry deserves special attention as the newest and most aggressively exploited item on the list — CVE-2026-48907 in the Joomla Content Editor (JCE), installed across a huge share of Joomla sites. The patch landed June 3, 2026, as version 2.9.99.5, but a public PoC hit GitHub just six days later on June 9 — and automated attacks against hundreds of sites started immediately. On June 16, 2026, CISA added the bug to its Known Exploited Vulnerabilities (KEV) catalog, confirming active exploitation, and gave U.S. federal agencies a three-day window to remediate — by June 19. Three days is one of the tightest deadlines CISA has ever set, which alone tells you how serious this one is: less than two weeks between the patch shipping and the bug landing in KEV.

On July 9, 2026, ACSC rolled this whole mismatched set — a five-year-old bug sitting next to one that’s six months old — into a single bulletin describing one ongoing campaign. That’s the point worth sitting with: a CVE’s age means nothing to an attacker. Unpatched, the door’s just as open in 2026 as it was in 2020.

WHY IT MATTERS

If you’re running one site or a handful on shared hosting or your own VPS, here’s the takeaway: none of the 15 bugs on this list are new or technically hard to exploit. Every one of them has a public patch. The problem was never that defense is impossible — it’s that updating plugins is the routine chore that keeps getting pushed off as long as the site “works.” This campaign’s attackers are betting specifically on the gap between when a patch ships and when it actually gets applied on any given server.

There’s a separate trap with premium plugins like Ninja Forms File Uploads: they don’t ship through the standard WordPress.org repository, they come through the vendor’s own distribution channel, and WordPress core’s auto-update simply doesn’t touch them. If you’ve got auto-update turned on for core and for free catalog plugins but forgot you’re running a paid add-on with its own separate update mechanism, that plugin sits quietly on a vulnerable version until you manually go check it.

For anyone managing not one site but dozens of client installs on shared infrastructure — hosting providers, agencies, freelancers with a client portfolio — the risk scales linearly with the number of sites. Contact and upload forms (Ninja Forms, Gravity Forms, Simple File List, WPBookit) are exactly the kind of component present on nearly every commercial site, not some obscure edge case you can write off. A web shell on one client site sharing a server is a potential launchpad for attacking neighboring web roots through the same PHP-FPM pool or shared mount points, if isolation between sites isn’t set up carefully.

HOW TO PREVENT IT HAPPENING AGAIN

Patching is the bare minimum — but the reason this campaign is mass-scale in the first place is that patching alone has never been enough. What actually reduces the attack surface at the server architecture level:

Make web root directories read-only wherever nothing should be writing to them during normal operation. Most CMS platforms only need write access to specific subdirectories — wp-content/uploads, storage, cache folders. The rest of the CMS file tree — core engine, plugin code, themes — can be mounted read-only for the user PHP-FPM runs as. A WordPress core file getting modified by the web server is, on its own, a compromise signal worth alerting on.

Block PHP execution in upload directories at the web server level — don’t rely solely on a file-type check inside the plugin. In nginx, that’s a location block for /wp-content/uploads/ that serves static files but never hands requests off to PHP-FPM. Even if an attacker gets past the extension allowlist in the plugin itself and drops a .php file into the uploads folder, the server won’t execute it — it’ll serve it as plain text or refuse the request outright.

Control what child processes your web server worker is allowed to spawn. A web shell almost always calls system(), exec(), or something similar at some point to run a shell command. If you can lock this down through a seccomp profile in a container or an AppArmor/SELinux policy on the PHP-FPM process, any unexpected child process — a /bin/sh or curl spawned from php-fpm — is a signal worth alerting on, even if the exploit itself already succeeded.

Segment public-facing sites away from your internal corporate network. Even if a web shell does land, it shouldn’t have routable access to internal company resources — unrelated databases, file shares, other systems’ admin panels. nftables rules restricting outbound traffic from web servers to only what’s necessary is a basic control, and one that gets skipped more often than it should.

WHAT TO DO

Start with checking for signs of compromise, not with updating. If a vulnerable plugin’s been sitting there for months, there’s a real chance it’s already been scanned — and possibly exploited. Check your upload directory and the plugin’s own directory for files you didn’t put there: any .php file with a random-looking name, or a modification date that doesn’t line up with anything you actually did, is worth investigating. Executable code has no business being in uploads in the first place.

Comb through your web server’s access logs for GET and POST requests to unusual paths. For Craft CMS specifically, the official advisory points straight to POST requests hitting actions/assets/generate-transform with the string __class in the body — Craft CMS itself notes that finding this in your logs confirms at least a scan, not necessarily a successful breach. A clean result here doesn’t clear the other 14 CVEs on the list if you’re running the matching plugins. For Joomla with JCE, look for a different set of tells: unexpected new editor profiles in the database (the JCE profiles table) and POST requests to index.php?option=com_jce carrying parameters that suggest a profile import or creation, coming from external IPs with no prior admin login. If you’re running Breeze Cache, first check whether “Host Files Locally — Gravatars” is turned on (off by default); if it is, regardless of plugin version, check /wp-content/uploads/breeze/gravatars/ for anything with a .php, .phtml, or .phar extension — that folder should hold nothing but images.

If you find a web shell, don’t just delete the file and call the incident closed. ACSC’s guidance is direct here: treat any server with a discovered web shell as fully compromised. Isolate it from the network, check auth logs for admin accounts you didn’t create, look for new SSH keys or cron jobs that shouldn’t be there, and review network logs — including at the perimeter firewall — for outbound connections from that server to unfamiliar external addresses, which could point to data exfiltration or C2 traffic.

Only update the vulnerable components once you’ve confirmed compromise one way or the other. If you’re running Craft CMS specifically, update to 3.9.15, 4.14.15, or 5.6.17 depending on your major version branch. From your project root, update just the Craft package rather than every dependency at once:

composer update craftcms/cms

After that, run migrations — Craft stores part of its configuration in the database, and skipping this step can leave the site behaving strangely even after the code itself is updated. The official docs recommend running this non-interactively so the command doesn’t sit there waiting on a prompt:

php craft up --interactive=0

To confirm the update actually took, this command shows the installed package version:

composer show craftcms/cms

For Ninja Forms File Uploads, you need version 3.3.27 or later. It’s a paid add-on distributed outside the WordPress.org catalog through the vendor’s own channel, so core auto-update won’t touch it. Check the installed version in the WordPress dashboard (Plugins → Ninja Forms — File Uploads) and update manually through whichever channel you originally licensed it from. If you can’t update right away, deactivate the plugin through the dashboard or rename its directory under wp-content/plugins/ so WordPress stops loading it until you’re ready to patch.

If you’re running Joomla with Joomla Content Editor installed, this is the most urgent update on the entire list of fifteen: you need version 2.9.99.5 or later, released June 3, 2026 (the developer recommends going straight to 2.9.99.6 from June 8, which adds an extra layer of hardening). Check your installed version under Components → JCE Editor → Control Panel in the Joomla admin panel. Given that this vulnerability is already in CISA’s KEV catalog with confirmed active exploitation, any delay here isn’t a theoretical risk — it’s a known fact of ongoing attacks right now. JCE’s own developer is explicit about one thing: the update closes the entry point, but it doesn’t remove anything an attacker may have already left behind if your site was vulnerable after June 9, 2026, when the working exploit went public. After updating, check JCE’s upload directories (typically /images/, /images/stories/, or whatever path your editor profile specifies) for any unfamiliar PHP files.

CONCLUSIONS

If you’re running a single site on your own VPS, spend an hour auditing your installed plugins against ACSC’s list and running the log checks above before you let it slide another week. Pay particular attention to Breeze Cache: even if you’re sure “Host Files Locally — Gravatars” was never turned on, check the setting directly rather than trusting your memory — this one bug produced more confirmed compromises than anything else in the documented WP-SHELLSTORM campaign. If you’re a hosting provider or run multiple client sites on shared infrastructure, start by isolating PHP-FPM pools between clients and blocking PHP execution in upload directories at the nginx level — it’s the one measure on this whole list that defends against all 15 vulnerabilities at once instead of just one. And if you’re a site owner paying an agency for support, this is the moment to ask directly whether the patches from ACSC’s July 9 bulletin have actually been applied, rather than assuming “everything updates automatically” covers it.

Leave your thought here

Your email address will not be published. Required fields are marked *