AI Summary
The Exploit Chain Is the Entry Point, Not the Incident. wp2shell refers to the chained WordPress Core vulnerabilities CVE-2026-63030 and CVE-2026-60137, which together allow an unauthenticated attacker to reach remote code execution against a stock WordPress installation.
In both cases a burst of POST requests hit the WordPress REST batch route and returned HTTP 207, followed within seconds by administrator creation.
The attacker used it to write a temporary PHP helper, which loaded the WordPress environment, located the existing webshell, copied it into the uploads directory, created a must-use plugin guardian, checked whether the attacker’s preferred administrator account still existed, recreated it, destroyed the session tokens belonging to other users, and deleted itself.
Basic summary
Two WordPress sites on InMotion Hosting accounts were compromised within a two-hour window in July 2026. One was cleaned four days later, then reinfected six days after that. This is what our teams found: the persistence layers attackers build after initial access, the specific indicators you can check on your own sites today, and why deleting rogue administrator accounts does not end a compromise.
Most of the coverage of wp2shell stopped at the front door. The chain gets an attacker in. What matters to anyone responsible for a WordPress site is what happens in the minutes and days afterward, and that part has been documented far less.
Our teams investigated two separate customer compromises in early August 2026. Both sites have been remediated and returned to service. Account names, domains, server identifiers, and directory paths have been removed here. The behavioral details have not, because those are the parts that help other site owners find the same problem on their own installations.
The Exploit Chain Is the Entry Point, Not the Incident
wp2shell refers to the chained WordPress Core vulnerabilities CVE-2026-63030 and CVE-2026-60137, which together allow an unauthenticated attacker to reach remote code execution against a stock WordPress installation. The first flaw is a route confusion problem in the REST batch endpoint. The second is a SQL injection in WP_Query. Chained, they let an anonymous HTTP request end in an attacker-created administrator account, with no plugins, no credentials, and no user interaction required.
The version boundaries matter for triage. Patchstack’s technical breakdown of the release notes that the SQL injection primitive reaches back to WordPress 6.8, while the batch handler confusion that turns it into an unauthenticated, remotely reachable attack was only introduced in 6.9. A site on the 6.8 branch carries the injection flaw but cannot be driven to full remote code execution through this path.
WordPress shipped emergency releases 6.9.5, 7.0.2, and 6.8.6 on July 17, 2026 and enabled forced automatic updates. CISA added the vulnerability to its Known Exploited Vulnerabilities catalog on July 21. Exploitation was reported in the wild within hours of disclosure, and dozens of working proof-of-concept implementations circulated within days.
Both sites we investigated were hit inside the first 48 hours after the patch, from unrelated networks, roughly two hours apart. That spacing is worth noting. Nobody was targeting these businesses. This was scan traffic finding whatever was still unpatched.
Neither investigation could conclusively prove the exact authorization flaw used in the first request. That limitation is structural rather than local. Standard Apache and NGINX access logs record the request line, status code, and User-Agent, and no widely used log format retains request bodies, cookie headers, authorization headers, or REST nonces. Those fields are precisely what would identify the mechanism, and they are not retained by any host running a conventional logging stack.
What the logs do preserve, and what our teams reconstructed from them, is the sequence. In both cases a burst of POST requests hit the WordPress REST batch route and returned HTTP 207, followed within seconds by administrator creation. On one site the requests carried a User-Agent string matching the published name of the exploit chain. On the same site, no successful login event was recorded for the original administrator account at the time actions were performed in its name, which fits unauthenticated exploitation rather than a stolen password.
The evidence is consistent with wp2shell. It does not meet the bar for a definitive attribution, and we are not making one. Stating that a specific CVE caused a specific compromise, without the request-level evidence to support it, is how incorrect root causes end up in the record and how the wrong remediation gets prioritized.
The First Payload Landed in Under Half a Minute
The sequence on the first site, reconstructed from access logs and WordPress activity records, ran like this:
A request hit the REST batch route and returned HTTP 207.
Two seconds later, an action was recorded under the site’s original administrator account, from the attacker’s IP, modifying an unusual internal post record.
One second after that, the same context created a new administrator account with a name designed to read like a service account.
Eight seconds later, the new account logged in successfully.
Eleven seconds later, it uploaded a plugin ZIP.
Three seconds later, the plugin was installed and activated.
One second later, the attacker deleted the ZIP from the Media Library.
Start to finish, about 24 seconds. The second site followed the same shape and completed the same sequence in roughly 28 seconds.
The plugin presented itself in the WordPress dashboard as a security utility with a plausible name and version number. Its directory name looked like a legitimate performance plugin with a random hex suffix appended. Anyone scanning the plugin list quickly would have read past it.
Thirteen seconds after installation, the attacker requested the plugin’s main PHP file directly and executed id && uname -a && hostname && pwd. It returned HTTP 200. From that point the attacker had shell command execution running as the cPanel account user.
The Reconnaissance Reveals the Hacker’s Campaign
What the attacker checked next says more about intent than any of the malware does. Reading those commands in order is how our team established what this campaign was for. In quick succession, the webshell was used to test:
The location and type of the system sendmail binary
Whether PHP’s mail() function was available
Whether proc_open() was available
The contents of PHP’s disabled functions list
Whether outbound SMTP on port 25 was reachable
DNS and MX record resolution
Whether the mail environment behaved like a trap, discard, or blackhole configuration
That last check is the interesting one. The attacker was specifically testing whether outbound mail would actually be delivered or silently swallowed by the host. This is spam infrastructure reconnaissance. No successful outbound campaign was confirmed from the evidence we reviewed, but the intent is not ambiguous.
The second site pointed in a different direction. It carried SEO cloaking loaders that served different content depending on User-Agent, referrer, language, IP address, host, and request URI, with the ability to rewrite robots.txt. That is search result poisoning, which is a separate monetization path from spam relay. Compromised WordPress sites are commonly repurposed for redirects, injected scripts, and credential harvesting aimed at visitors, not just for whatever the attacker wants from the server itself.
Persistence Was Built in Nine Independent Layers
Ninety seconds after gaining command execution, the attacker on the first site created an account cron job running every five minutes. Its purpose was to restore a malicious plugin file from a backup copy stored under the uploads directory if the primary file went missing.
That was layer one of nine. The complete set, recovered during the investigation:
Attacker-created administrator accounts, several of them, created from multiple IPs over the following days
Malicious standard plugins, three additional directories beyond the original, each containing working webshells
A must-use plugin guardian, which can restore deleted files and cannot be deactivated from the WordPress dashboard
PHP copies under wp-content/uploads, named to resemble WordPress core class files
Encoded payloads in the options table, stored under names that mimic legitimate site health transients
Loader blocks injected into wp-config.php, which read those database options, decoded the stored PHP, and wrote executable files to disk
Executable payloads in the account’s CageFS temporary directory, five of them
Additional cron jobs, five separate 30-minute entries performing self-tests and restoration checks
Logic to recreate the attacker’s preferred administrator account and destroy other users’ session tokens
Read that list again with cleanup in mind. Removing the plugin leaves the cron job. Removing the cron job leaves the database payload. Removing the database payload leaves the wp-config.php loader that would rebuild it. Removing the administrator account leaves the webshell that created it.
Only two of those nine layers live in places a conventional malware scanner looks. Finding the rest took reading decoded payload logic and correlating it against the crontab and the options table.
The second site added a variation worth knowing about: a must-use plugin disguised as a login throttling component that hooked wp_authenticate, captured the username and plaintext password from every login attempt, resolved its destination address through a blockchain smart contract rather than a hardcoded domain, and transmitted the credentials to a remote endpoint. It also removed itself from the must-use plugin listing so it would not appear in the dashboard.
Any password typed into that login form while the file was active is compromised. Not hashed. Not guessed. Read in the clear.
The Six Days Between Cleanup and Reinfection
On July 20, activity from a different IP deleted every attacker-created administrator account on the first site. All four of them. WordPress Core was updated to a patched release the same day.
That looks like a successful cleanup. The rogue accounts were gone, the vulnerability was closed, and the site was serving normally.
The original webshell was never removed.
On July 26, an IP that had not appeared before requested that surviving plugin file. The attacker used it to write a temporary PHP helper, which loaded the WordPress environment, located the existing webshell, copied it into the uploads directory, created a must-use plugin guardian, checked whether the attacker’s preferred administrator account still existed, recreated it, destroyed the session tokens belonging to other users, and deleted itself.
WordPress logged the result as a new user registration. It was not a registration. It was PHP running with full application privileges, doing exactly what the site’s own code is allowed to do.
The site was fully re-owned. Patching Core on July 20 closed the door the attacker used on July 18, which by then was a door the attacker had stopped using.
The Backups Were Already Compromised
Restoring from backup is the reflexive answer to a compromise. Both investigations found that reflex would have failed.
One account carried a backdoor file that arrived from a previous hosting provider. It was present in a migration backup imported into the account, with modification timestamps from August 2025 and a second account-level copy from October 2025. That malware predates the account’s history on our infrastructure by roughly eleven months, and it was found because the investigation examined imported backup trees rather than the live document root alone.
Whether the July 2026 campaign reused that older foothold is not something the available evidence proves, and we did not assume it did. What it does establish is that the backup archive was not clean, and had not been clean since before the site arrived.
The same account’s maintenance plugin rollback data contained a randomly named plugin directory holding attack-period code. The rollback material could not be used as a clean restoration source. Checking the rollback tree, rather than trusting it, is what caught that.
Restore points capture whatever was on the site at the time, including whatever was already hiding there. Restoring to a date before the known compromise is a guess about when the compromise started, and on both of these accounts that guess would have been wrong.
Automatic Updates Did Not Reach One of These Sites
WordPress.org forced automatic updates for this release, which is a step reserved for the most severe class of flaw. The first site was still on a vulnerable version 28 hours later.
During remediation, our team found an update management plugin configured to block Core, plugin, and theme updates, and deactivated it. That configuration is a plausible explanation for why the emergency update did not land, though the update logs were not conclusive enough to state it as fact.
For agencies, this is the operationally useful finding in the entire investigation. Update-blocking is a normal, defensible choice on a site where a Core release once broke a checkout flow. It is also a decision that quietly opts a site out of emergency security releases, and nobody revisits it. If your maintenance stack includes an update manager, someone needs to own the question of what happens when WordPress ships a forced security update.
How to Check Your Own Sites for These Indicators
The paths below are relative to your WordPress document root. None of them require server access beyond what a normal cPanel or SFTP account provides. Campaign filenames rotate, so treat the patterns as more durable than any single filename.
Plugin and Theme Directory Names
Both accounts carried directories using these naming conventions under wp-content/plugins:
That last pattern is the one that gets past a visual scan. One malicious plugin used a slug resembling a content delivery utility and displayed itself in the dashboard as Security Headers Manager 2.1.4, complete with a version number. Randomly named theme directories were used the same way.
Must-use Plugins
Files in wp-content/mu-plugins do not appear in the normal plugin list and cannot be deactivated from the dashboard. Malicious files found there included a credential stealer named to resemble a login throttling component, a recovery guardian named to resemble a site repair utility, and a pair of short PHP files loading a compressed payload from an accompanying .gz archive.
Open that directory and account for every file in it by name. On most sites the list should be short or empty.
Files Disguised as WordPress Core Classes
Helper files were written into plugin directories using names copied from core conventions:
class-wp-rest-compat-*.php
class-wp-http-compat-*.php
class-wp-cache-helper-*.php
class-wp-widget-core-*.php
Hidden loaders used a leading dot followed by twelve hex characters and a .php extension. Files matching these patterns are not part of WordPress and do not belong in a plugin directory. A JPEG and PHP polyglot named 1.php was also present on one account, along with an older backdoor named wp-mytrack.php that creates an administrator account when executed.
Database Indicators
Malicious payloads were stored in the options table under names built to sit next to legitimate site health entries, using the prefix _site_transient_health_ followed by 8 hex characters. Your table prefix will differ from the default. WordPress does create real site health transients, so match on the trailing hex string rather than the prefix alone.
Configuration Indicators
Blocks labelled WP_Core_Integrity were injected into wp-config.php. Their function was to read the database payloads above, decode them, and write executable PHP into the account’s temporary directory. Open wp-config.php and read it end to end. Anything after the “stop editing” comment deserves attention.
Recurring code strings observed across payloads included _nx_auth, _NX_PERSISTED, and feature=selftest.
Temporary Directory Payloads
Five executable payloads were found in one account’s temporary directory using PHP’s own temporary file naming convention: the characters php followed by six random alphanumerics. This is the same pattern PHP uses for normal file uploads, which is exactly why it blended in. Normal temporary upload files are removed when the request ends. Any file matching that pattern that has survived for days is worth examining.
Cron Entries
Check the account crontab, not just WordPress cron. One account carried a five-minute job that restored a deleted plugin file from a backup copy under uploads, plus five separate thirty-minute jobs performing self-tests. A legitimate WordPress cron entry calls wp-cron.php and nothing else. Any other recurring PHP or curl invocation against your own site should be accounted for.
User Accounts and Tokens
Sequential administrator IDs. One account carried 38 unauthorized administrators in a continuous block of user IDs. Sequential creation is a structural tell.
Username patterns. Observed prefixes included wpsvc_ and wp2_ followed by hex, plus accounts built from the site name with suffixes such as _dev, _editor, and _suporte.
Application passwords. Check Users, then each profile, then Application Passwords. Attacker-created tokens carried names including auto-bootstrap and bot-token. These survive a password change and do not require the login form.
Log Signatures
In access logs, look for POST requests to /?rest_route=/batch/v1 or /wp-json/batch/v1 returning HTTP 207, particularly in bursts. On one account the requests carried the User-Agent strings wp2shell and wp2shell-uploader. Administrator creation within seconds of such a burst is the sequence to look for.
Both accounts showed the same pattern: a rapid series of batch requests, then a request to wp-login.php, then a successful login, then a plugin upload through update.php with the action=upload-plugin parameter. That upload is a normal WordPress administrator action and will not look unusual on its own. It is unusual immediately after a batch request burst from the same IP.
Note that the User-Agent strings are trivially changed and later variants may not use them. The batch route requests returning HTTP 207 are the more durable signal. If your host rotates or compresses access logs on a short schedule, retrieve the archived logs covering mid to late July 2026 before they age out.
Server Response Check for uploads
Request a PHP file under wp-content/uploads directly, with cache-busting, and confirm the response. HTTP 403 is correct. HTTP 200 returned with content-type: application/octet-stream means the file is being served as readable source rather than executed.
Serving is the safer of the two failure modes, since the code does not run, but it still exposes whatever that file contains to anyone who requests the URL. Both accounts needed an explicit deny rule covering executable extensions under uploads, and both received one. We identified this by testing from outside the server with cache-busted requests using a normal browser User-Agent, Googlebot, and others, because some configurations respond differently by client. Testing from a single browser would have missed it.
Hardening Values to Confirm
wp-config.php permissions set to 0600
Any legitimate PHP under uploads set to 0600
DISALLOW_FILE_EDIT and DISALLOW_FILE_MODS defined as true
FORCE_SSL_ADMIN defined as true
No world-writable files, no files owned by another account, no unexpected symbolic links in the document root
What a Verified Cleanup Actually Involves
The remediation on both accounts followed the same structure. This is the shape of the work, and it is a reasonable benchmark for evaluating any cleanup, whether performed in-house or by a vendor.
Contain first. The first site was placed behind an HTTP 403 block during the investigation. Cleaning a site that is still reachable means racing restoration mechanisms that run every five minutes.
Preserve before deleting. Every malicious file, database payload, user record, crontab, and configuration file was archived and hashed before removal. Deleting malware without preserving it destroys the ability to answer questions later, including the question of whether the cleanup worked.
Remove every layer, then verify each one independently. Filesystem, database options, wp-config.php injections, cron entries, temporary directories, must-use plugins, uploads, and user records were each cleaned and separately confirmed.
Verify Core and package integrity against official checksums. Both sites had Core verified. On the first, a plugin checksum mismatch was investigated and the plugin reinstalled from a trusted package, and two themes with modified files were replaced from official packages rather than patched in place.
Rotate everything the attacker could read. With PHP command execution and database access, the attacker could read wp-config.php, the options table, and any credential stored in either. That means WordPress administrator passwords, cPanel, FTP and SFTP, SSH, database, SMTP, payment processor keys, and every third-party API token the site holds. Both reports listed rotation as required, not recommended.
Reduce and confirm the administrator list. One site went from 38 unauthorized administrators to three preexisting accounts. Every remaining account needs the site owner to confirm it is authorized, by name.
Destroy sessions and application passwords. Attacker-created WordPress application passwords, carrying names suggesting automation tokens, provided authenticated access without ever touching the login form. Most site owners have never opened that screen in their profile. It survives a password change.
Establish a baseline afterward. The first site’s post-remediation baseline recorded hashes for more than 36,000 executable and configuration files. Without a known-good baseline, the next investigation starts from zero again.
Get Your Hacked Website Fixed Quickly and Safely
Our experts quickly remove malware, recover lost files, and restore your WordPress site’s security – getting you back online fast and ready for business.
Fix My Hacked Website Now
What Agencies and Developers Should Change This Week
Three specific actions, in order of how much risk they remove:
Confirm the installed WordPress version on every site you manage, individually. Do not trust the update dashboard and do not assume the forced update applied. Security researchers have consistently noted that sites where automatic updates were disabled or unsuccessful may still be exposed. Check the version string, then check whether anything in the plugin stack is configured to block updates.
Audit administrator accounts and application passwords together. A rogue administrator is visible. An application password attached to a legitimate account is not, and it is the mechanism most likely to survive a rushed cleanup. Check both on every site, then check the must-use plugins directory, which does not appear in the normal plugin list at all.
Treat any confirmed compromise as a full credential rotation event. If an attacker had PHP execution on the account, every secret reachable from that account is exposed. Partial rotation leaves a working key.
Reinfection is the normal outcome of a partial cleanup, not an unusual one. The gap on the first site was six days, and during those six days the site looked clean, loaded normally, and passed a casual inspection.
What This Kind of Investigation Actually Takes
Both of these investigations were performed in-house by InMotion Hosting’s own teams, on infrastructure we own and operate across three data center regions. Nothing was outsourced to a scanning vendor, and no part of the analysis was handed to a third party.
That matters because of where the malware was hiding. A commercial malware scanner finds files. It does not decode payloads stored in the options table under names that mimic legitimate site health transients. It does not trace loader blocks injected into wp-config.php to the executable files they rebuild in the account’s temporary directory. It does not notice that five files matching PHP’s normal upload naming convention should have been deleted at the end of a request three weeks ago and were not.
Those findings came from people reading code and correlating log timestamps across archived access logs, WordPress activity records, and the account crontab. Every engineer on our support teams completes more than 280 hours of training before handling Tier 1 requests, and average support tenure runs past five years. The same teams are available 24/7, and the investigations behind this article were carried out by people you can reach by phone.
This is consistent with how we handle threats at the infrastructure layer as well. When a critical pre-authentication vulnerability in cPanel and WHM was disclosed in April 2026, our network operations team blocked exposure at the network edge across all three data center regions within hours, then patched the fleet server by server. WordPress Core updates land inside your application rather than on the server stack, which is why a compromise like this one calls for a different response and a different kind of investigation.
If you manage WordPress sites for clients and you are not certain a past cleanup was complete, that uncertainty is the finding. Bring us the account and we will look at every layer, not the file system alone.
Talk to our team or review our managed hosting services.
