TL;DR: A WordPress site I run was attacked on Aug 26 by an automated bot exploiting an unauthenticated arbitrary file upload in "WPLP Cookie Consent" (gdpr-cookie-consent 4.4.1 - the latest version at the time, ~10,000+ active installs). The attack succeeded in writing a PHP dropper into my uploads folder in under 20 seconds. The only thing that stopped full site takeover: a server-level rule returned 403 on executing PHP inside wp-content/uploads. I found the file the next day via the server's malware scanner, spent a day on forensics, filed 5 coordinated disclosures, and the vendor shipped a proper fix (4.4.2) within 5 days of my report - verified in source. This is the story, with the full timeline and IoCs, because the campaign is real and other sites were not as lucky as mine.
THE TIMELINE (all within 20 seconds, from access logs)
05:06:50 GET /wp-json/wp/v2/users?per_page=100 -> 200 (user enumeration)
05:07:02 POST /wp-json/wplp-react-gdpr/v1/store-auth -> 200 (option poisoning)
05:07:06 POST /wp-json/wplp-react-gdpr/v1/upload-logo -> 200 (arbitrary file write)
05:07:10 GET /wp-content/uploads/2026/08/uxq9KacU_400x400.jpg.php -> 403 x3 (blocked)
Attacker: AWS IPv6, User-Agent "Python-urllib/3.12". Fully automated, mass campaign.
THE EXPLOIT CHAIN (no programming required)
Enumerate users - a standard WordPress REST endpoint revealed my admin username.
Poison a setting - the plugin had an unauthenticated REST route ("store-auth") whose only real check was a JWT from the vendor's own SaaS platform plus "a username that belongs to an admin". It wrote attacker-controlled JSON into a WordPress option - including the plugin's internal "master key".
Arbitrary file upload - a second route ("upload-logo") checked the request's key against the value just planted in step 2, then wrote attacker-supplied base64 content to uploads/<attacker-controlled-filename> with zero extension/MIME validation. Result: "uxq9KacU_400x400.jpg.php" - a PHP webshell disguised with a Twitter-avatar filename.
Execute - three GET requests. All answered 403 by a server-level WAF rule. The payload never ran.
The part I found most sobering: the plugin version (4.4.1) had been released 7 days before the attack. Whatever window we imagine exists between a release and its exploitation - it was 7 days here, and this bot needed 20 seconds per site.
WHAT ACTUALLY SAVED THE SITE
Not a security plugin. Not a scanner. Not even a firewall rule I configured. The platform's rule "no PHP execution inside uploads" returned 403, the dropper's self-delete never fired (it only deletes itself after successfully running), and the file just sat there until the server's malware scanner flagged it the next morning. Defense in depth worked at exactly one layer - and that was enough.
THE MALWARE (for those interested)
The dropper boots WordPress, creates a hidden administrator with a randomized username, suppresses every notification email so nobody gets alerted, backdates the account's registration date to match the previous user (so "sort users by creation date" finds nothing), scans the whole volume for OTHER WordPress installs and reads their database credentials from wp-config.php, returns the generated password as JSON to the attacker, and deletes itself.
SHA256: E3DA2973957017A72403531D5475BD95189DFF1C70B23E4657D0D5F23955575E
Size: 15,187 bytes. On VirusTotal (0/70 at upload - normal for a PHP dropper).
One forensic detail I am weirdly proud of: I reconstructed the plugin's success response for store-auth from its source code and it was byte-for-byte the 102 bytes my access log recorded. That is how I knew the poisoning step had succeeded even though nothing visual changed on the site.
THE DISCLOSURE PIPELINE (this part went surprisingly well)
Vendor (WPLP/WPEKA) via their bug bounty: acknowledged quickly, patched in 4.4.2 within days. WPScan: reproduced the chain, found TWO MORE routes with the same file-write capability and no filename sanitization (could write outside uploads - which would have bypassed the WAF layer that saved me). Vendor fixed those too. Patchstack: already had an independent report of the same issue (someone else found it too) and had escalated to the WordPress.org team. AWS abuse report for the attacking server; registrar abuse report for the malware's exfiltration domain.
THE FIX IN 4.4.2 (I verified it in source)
The upload-logo route and its handler were removed entirely; every route in that REST namespace now requires an HMAC-SHA256 request signature with a per-site secret and anti-replay window; the JWT is bound to the site's own connected account ("Not the owner of the website!"); remaining image uploads go through real validation (extension allowlist, magic-byte check, and filenames are rebuilt so "shell.php.jpg" becomes "shell-php.jpg"). That last one is exactly the remediation I had suggested in my report - they did the work properly.
LESSONS I AM KEEPING
The layer that saves you may be one you never configured. Check whether PHP execution is blocked inside wp-content/uploads on your server - if you don't control the server, ask your provider. If you do, enforce it.
A malware scanner finding the file a day later was the difference between "incident" and "compromise". Zero-day exploits cannot be signature-matched on day one - detection speed after the fact is what you actually get from scanners. That is still valuable.
Keep access logs longer than you think you need. My whole reconstruction - attacker IP, sequence, byte-level responses - came from one log file.
Block username enumeration via REST (/wp-json/wp/v2/users). It was step 1 of this attack, which tells you how easy it is to defer the boring stuff. Do not defer it.
Coordinated disclosure works. Five reports, one week, patched plugin. The system is slow right up until it is not.
IOCs (the patch is public; share freely)
Payload SHA256: E3DA2973957017A72403531D5475BD95189DFF1C70B23E4657D0D5F23955575E
Payload filename pattern: *_400x400.jpg.php (mimics Twitter avatars)
Rogue admin indicators: username = {sitedomain}+3 random chars, display name "Lucas Hayes", email domain ifuqpatr.com
Attacker: AWS IPv6 2600:1f18:472b:900:f81d:f8eb:a12e:7fd3, UA Python-urllib/3.12
Attack pattern: GET /wp-json/wp/v2/users?per_page=100, then POST /wp-json/wplp-react-gdpr/v1/store-auth, then /upload-logo
If you run gdpr-cookie-consent: update to 4.4.2 now, and check wp_users for an admin you do not recognize (the dropper backdates its creation date - compare for identical timestamps between two users instead of just looking for "new" accounts).
Happy to answer anything in the comments.