Features Guarantee Pricing Agencies Blog Log in Get the free scanner

WP2Shell Attack Analysis

WP2Shell

Most WP2Shell coverage answers the same question: “How does the exploit work?” But we are more interested in what happened next. “Did the patch hold up across 220 real WordPress sites, or were attackers still finding a way in?”

To find the answer, our WP Guard team ran our own checker script against a representative sample of our managed fleet of WP Websites after the vulnerability WP2Shell was disclosed on July 17, 2026. We then filtered the log noise, manually verified every flagged account, and monitored how the exploit evolved over the following 11 days.

The results below come directly from our analysis, and you can reproduce them using the checker script published alongside this article. You can also use this guide to:

  • Run the same checker against your own fleet and get a real pass/fail without a guess
  • Correct your own attempt counts if you’ve been using the broad indicators of compromise (IOCs) list circulating in advisories
  • Decide whether HTTP 207 responses in your logs warrant further investigation

Let’s get started.

Key Takeaways

  • 88.6% of our fleet patched the same day WP2Shell was disclosed. Attackers made their first confirmed attempt the following day.
  • Attackers targeted only 11 of our 220 sites before we patched them. Zero were compromised.
  • Raw log scans recorded 135,502 apparent “attack” hits. After filtering unrelated traffic, we identified 12,145 genuine attempts, which shows that raw counts overstated activity by roughly 10x.
  • Attackers changed the exploit tooling four times over the following 11 days and adopted a browser-like user agent by day four.
  • Wiz’s public research identifies a 207 Multi-Status response as a high-fidelity indicator of compromise. But our fleet data tells a different story, and we explain why in the following sections.

What Was Actually at Risk

WP2Shell is a two-chain vulnerability, CVE-2026-63030 and CVE-2026-60137, and exposure depended entirely on which version a site was running.

WordPress versionExposureFixed in
6.8.0–6.8.5SQL injection only, no full chain6.8.6
6.9.0–6.9.4Full pre-auth remote code execution6.9.5
7.0.0–7.0.1Full pre-auth remote code execution7.0.2

Every site in our sample set fell somewhere on this table. If your own site does too, our findings apply directly. If it’s already on 6.8.6, 6.9.5, or 7.0.2 and later, this specific chain never reached it.

Dataset: The Fleet We Checked

We checked every WordPress installation across our managed fleet: 30 servers hosting 220 sites. One server was unreachable, so we excluded it from the analysis. Every installation ran Ubuntu and nginx under SpinupWP with WordPress core auto-updates enabled.

This is the scope of what we checked, in full:

MetricValue
Servers targeted30
Servers with usable data29
Servers with no data at all1 (SSH/SCP failure)
WordPress installs checked220
Hosting stackUbuntu / nginx / SpinupWP
Core auto-updatesOn, fleet-wide
Log window analyzedJuly 18–28, 2026 (11 days post-disclosure)

To protect client privacy, we don’t name any client sites in this article. Every domain is anonymized, and every screenshot has hostnames and paths redacted. Our analysis focuses on the observed patterns rather than individual websites.

Here’s the same check, run server-by-server across the fleet. The data below comes straight from wp2shell-check.sh and the script we’re publishing further down, with hostnames and IPs redacted for this article.

Fleet-wide server checker results

Note: This visual was generated by the WP Guard team from the script’s real output, then we reformatted it for readability. However, running wp2shell-check .sh yourself will print this same data as plain text in your terminal, not as a styled table.

The log evidence above covers the 11 days following the WP2Shell disclosure, from July 18 to July 28, 2026. Most hosts retain older logs, but log rotation prevents us from confirming earlier exposure with the same level of certainty. We’ll highlight that limitation again wherever it affects our findings.

Keep in Mind: We analyzed a single hosting environment with WordPress core auto-updates enabled, so the patch-speed figures below apply only to that setup.

Finding One: The Patch Outran the Exploit Wave

The patch came first, and the data shows it.

Across our fleet:

  1. July 17, 2026 (disclosure day): 195 of 220 sites (88.6%) were already running a patched WordPress core.
  2. Within 48 hours: Nine more sites patched and raised coverage to 92.7%.
  3. July 21: Four days after disclosure, 218 of 220 sites had been patched.
  4. July 27: The final two sites patched, including one development/staging installation.

The first confirmed exploitation attempt reached the fleet on July 18, one day after disclosure. By then, we had already patched most of the fleet before a single confirmed attack arrived.

Here’s the patch curve against the exploit wave, day by day:

WordPress patch outpaced exploit attempts

The attacks didn’t stop after disclosure.

Eleven days later, our fleet was still absorbing 1,200 to 1,800 confirmed exploitation attempts each day. And the attack volume remained steady throughout the final week of this dataset, well beyond the initial news cycle.

Despite that sustained activity:

  1. Only 11 of 220 sites (5%) received attack attempts before patching.
  2. Those 11 sites received 260 confirmed attempts during their actual exposure windows.
  3. None of those attempts resulted in a compromise.

We explain how we verified the outcome of point three.

The thing is, these results come from a single managed fleet with WordPress core auto-updates already enabled. So they don’t represent every WordPress deployment.

They do, however, demonstrate what automatic updates achieved in this environment. In our fleet, allowing the update to install automatically closed almost every exposure window before attackers reached the site.

Now, Check Your Own Timing

You don’t need our fleet data to answer this question for your own site. The starting point is already public: the first confirmed exploitation attempt against our fleet arrived on July 18, 2026. Now, you can compare your site’s patch timing against that date.

Generally, every patched WordPress installation records its patch time in a single file: wp-includes/version.php. WordPress rewrites this file as soon as the update finishes, and the file’s modification timestamp marks the patch time.

Once you compare that timestamp to July 18, you’ll know whether your site was patched before attackers arrived or during the attack wave.

The script below performs that timestamp check automatically for a single site or an entire list, with no log parsing required:

bash
#!/usr/bin/env bash
# wp2shell-patch-timing.sh — did your patch land before the exploit wave?
# Wave start (first confirmed attempts, per this dataset): 2026-07-18

WAVE_START="2026-07-18 00:00:00"
WAVE_START_EPOCH=$(date -d "$WAVE_START" +%s)

for path in "$@"; do
  patched_at=$(stat -c %y "$path/wp-includes/version.php" 2>/dev/null | cut -d. -f1)
  [ -z "$patched_at" ] && { echo "$path: could not read patch time"; continue; }
  patched_epoch=$(date -d "$patched_at" +%s)

  if [ "$patched_epoch" -lt "$WAVE_START_EPOCH" ]; then
    hours_ahead=$(( (WAVE_START_EPOCH - patched_epoch) / 3600 ))
    echo "$path: patched $patched_at — ${hours_ahead}h before the wave started. Outran it."
  else
    hours_behind=$(( (patched_epoch - WAVE_START_EPOCH) / 3600 ))
    echo "$path: patched $patched_at — ${hours_behind}h after the wave started. Check logs for pre-patch attempts."
  fi
done

Run it against one site, or pass every install path on your server at once:

./wp2shell-patch-timing.sh /var/www/site1 /var/www/site2 /var/www/site3

Note: This checks a file timestamp rather than a changelog. For that reason, if your site went through a migration, restore, or rsync since patching, that timestamp may not reflect the real patch date. So if the result looks wrong, check your update logs directly instead of trusting the file alone.

A result showing your site was patched before July 18 doesn’t mean attackers never scanned it. Instead, it may indicate WordPress had already applied the patch before the attack wave reached your site.

Pro Tip: If the result shows your site patched after July 18, review your logs for activity during the exposure window.

And finding three will explain how to do that.

Finding Two: Attempt Counts Inflated by 10x

Our first count returned 135,502 apparent WP2Shell attack hits across the fleet. But that figure turned out to be heavily inflated, and understanding why is more valuable than the raw number itself.

We grepped our access logs for every indicator circulating in public advisories: the batch endpoint paths, the leakix scanner signature, known malicious user agents, and the author__not_in parameter. That search returned matches going back to 2023, three years before this CVE existed. A number that old can’t possibly be measuring an attack that started in July 2026.

We searched our access logs for every indicator published in public advisories, including the batch endpoint paths, the leakix scanner signature, known malicious user agents, and the author__not_in parameter.

The search returned matches dating back to 2023, three years before WP2Shell was disclosed. Those earlier entries couldn’t represent WP2Shell activity, so the raw count clearly included unrelated traffic.

Two sources accounted for almost all of the inflation:

  1. leakix scanner traffic
  2. Legitimate batch endpoint requests

The breakdown below shows how those sources compare with the 12,145 confirmed exploitation attempts.

CategoryHitsWhat it actually is
leakix scanner traffic75,892A general internet research scanner that’s crawled these sites for years, unrelated to this CVE
/wp-json/batch/v1 traffic47,548A legitimate WordPress core endpoint since version 5.6, used constantly by the block editor for routine bulk saves
Real wp2shell attempts12,145Wp2shell-branded user agents, the CVE signature, or a genuine author__not_in payload
Raw, unfiltered total135,502What a naive log search returns without removing the two rows above

Once you stack those two sources against the number, the gap is obvious at a glance:

Raw attack counts inflated ten times

After removing unrelated traffic and filtering for unambiguous WP2Shell exploitation attempts, the apparent attack count dropped from 135,502 to 12,145. That’s a 10x gap between what a quick log search shows and what actually happened.

Further, we validated the filter by checking the timeline. The earliest entry in the filtered dataset appeared on July 18, 2026, one day after disclosure, with no earlier matches. In contrast, the unfiltered dataset stretched back to 2023, well before the vulnerability existed. That pattern indicates background noise rather than a campaign tied to the July 2026 disclosure.

Check Your Own Logs

If you’ve counted WP2Shell attempts in your own access logs and the number seemed unusually high, there’s a good chance it includes leakix scans and legitimate /batch/v1 requests.

The script below applies the same broad-versus-strict filtering we used in our analysis, which helps you separate confirmed exploitation attempts from unrelated background traffic:

bash
#!/usr/bin/env bash
# wp2shell-ioc-filter.sh — separate real wp2shell attempts from noise
# Usage: cat access.log | ./wp2shell-ioc-filter.sh

IOC_STRICT='wp2shell|cve-2026-63030|rezwp2shell|author__not_in'
IOC_BROAD='rest_route=/batch/v1|/wp-json/batch/v1|leakix'

echo "Reading from stdin..."
LOGDATA=$(cat)

STRICT_COUNT=$(echo "$LOGDATA" | grep -aiE "$IOC_STRICT" | wc -l)
BROAD_COUNT=$(echo "$LOGDATA" | grep -aicE "$IOC_BROAD")
TOTAL=$((STRICT_COUNT + BROAD_COUNT))

echo ""
echo "Real wp2shell attempts (strict):  $STRICT_COUNT"
echo "Noise (batch/v1 + leakix):        $BROAD_COUNT"
echo "Naive total if unfiltered:        $TOTAL"
echo ""
if [ "$STRICT_COUNT" -gt 0 ]; then
  INFLATION=$(echo "scale=1; $TOTAL / $STRICT_COUNT" | bc 2>/dev/null || echo "N/A")
  echo "Inflation factor: ${INFLATION}x"
fi

Run it against any access log:

cat /var/log/nginx/access.log | ./wp2shell-ioc-filter.sh

Note: This filter reflects the tooling and request patterns we observed during this campaign. Attackers have already changed their signatures at least once, as Finding Three shows, so you may need to update the pattern list as the campaign evolves.

But you can treat it as a starting point for your own analysis rather than a permanent detection rule.

Suggestion: When you read a report that quotes a CVE attempt count, including this one, it’s worth asking how the authors produced it. It’s because counts based on a raw grep of public IOCs are likely to include unrelated traffic and can substantially overstate the level of attack activity.

Finding Three: Exploit Tooling Changed 4 Times in 11 Days

Other researchers have already shown that WP2Shell attackers vary their user agents. For example, ELLIO documented a single attacker cycling through 42 different user-agent strings in a single day, including several designed to resemble ordinary browser traffic.

But our fleet data answers a different question. Instead of capturing one attacker’s behavior during a single session, it shows: “When each version of the tooling appeared across the campaign?” And that timeline lets us track how the exploit evolved over the first 11 days after disclosure.

The table below follows those changes in the order we first observed them.

User agentAttemptsFirst seenLast seen
wp2shell8,143Jul 18Jul 28
wp2shell/4.01,672Jul 21Jul 28
Mozilla/5.0 (compatible; wp2shell-check/1.0)1,037Jul 20Jul 28
wp2shell-rce/1.0221Jul 19Jul 27
wp2shell-checker218Jul 20Jul 24
wp2shell-evolved-PoC/2.0204Jul 19Jul 23
cve-2026-63030/1.0127Jul 19Jul 26
rezwp2shell53Jul 19Jul 26

Note: These eight variants were the busiest of the 24 distinct user agents we tracked, accounting for 11,675 of the 12,145 strict matches. The remaining 16 (each seen far less often) are listed in full in the companion dataset on GitHub.

The user-agent timeline shows how quickly the tooling evolved:

  • Day 1: wp2shell and wp2shell/2.0 both appeared.
  • Day 2: -evolved-PoC/2.0 appeared.
  • Day 4: wp2shell/4.0 replaced the earlier versions.
  • Days 4-11: wp2shell/4.0 remained the dominant version.
  • Mid-campaign: The tooling adopted a Mozilla/5.0 (compatible; …) prefix to make exploit traffic resemble ordinary browser requests.

Interestingly, none of these user-agent strings is new. Several already appear in public IOC lists. What’s new is the timeline. And mapping the strings against first-seen dates across the same fleet shows that the tooling evolved throughout the campaign rather than remaining static (new versions continued to appear for eleven days after disclosure).

Check Your Own Logs Against This List

The user-agent list changed rapidly during the first eleven days after disclosure. The script below checks your access logs against all eight variants we tracked to help you identify which ones, if any, reached your site.

bash
#!/usr/bin/env bash
# wp2shell-ua-check.sh — check your logs against known wp2shell UA variants
# Usage: cat access.log | ./wp2shell-ua-check.sh

declare -A UAS=(
  ["wp2shell"]="bare, first seen Jul 18"
  ["wp2shell/4.0"]="versioned, first seen Jul 21, still active"
  ["wp2shell-check/1.0"]="disguised behind Mozilla prefix, first seen Jul 20"
  ["wp2shell-rce/1.0"]="first seen Jul 19"
  ["wp2shell-checker"]="first seen Jul 20"
  ["wp2shell-evolved-PoC/2.0"]="first seen Jul 19"
  ["cve-2026-63030"]="CVE-labeled, first seen Jul 19"
  ["rezwp2shell"]="first seen Jul 19"
)

LOGDATA=$(cat)
echo "Checking against 8 known wp2shell UA variants..."
echo ""
for ua in "${!UAS[@]}"; do
  count=$(echo "$LOGDATA" | grep -aic "$ua")
  [ "$count" -gt 0 ] && echo "FOUND: \"$ua\" — $count hits (${UAS[$ua]})"
done
echo ""
echo "Note: this list reflects known variants as of this dataset's collection date."
echo "Attackers have already updated tooling once; expect more variants over time."

However, this list will age the same way the exploit tooling did. So treat a match as confirmation that your site was targeted by one of the tracked variants. Not as a complete inventory of every variant that may have circulated by the time you’re reading this.

Where We Differ From Wiz on 207 Responses

Wiz’s public research on wp2shell, published on July 20, 2026, identifies HTTP 200 and 207 Multi-Status responses from the batch endpoint as relatively high-fidelity indicators of successful exploitation. Bitdefender’s technical advisory says nearly the same thing, describing 207 as a high-fidelity indicator of exploitation attempts.

Our fleet data supports part of that conclusion but adds an important qualification.

Where They’re Right

On a site that’s genuinely still vulnerable, with no other explanation for the traffic, a 207 from the batch endpoint can be exactly the signal Wiz and Bitdefender describe. Their research reflects real compromises on real unpatched sites.

And a published incident report catches them too. A Wordfence alert recorded the creation of a new administrator account named wpenginebot on a still-vulnerable installation, two days before the site was patched.

Where It Breaks Down

Across our fleet, patched sites with no evidence of WP2Shell exploitation also logged 207 Multi-Status responses from the same endpoint. That’s because the WordPress block editor uses HTTP 207 during legitimate bulk save operations. WordPress has used this status code for valid multi-request operations since version 5.6, independent of any exploitation attempt.

The difference lies in how anyone measures the environment. For example, on a confirmed vulnerable site with no alternative explanation for the request, an HTTP 207 can be a useful indicator. But across a mixed fleet of patched and unpatched sites, the same status code no longer distinguishes malicious requests from normal activity.

What distinguishes the two situations is the evidence surrounding the request:

  • The request payload
  • The user agent
  • Outcome-based checks

The shortcoming is that an HTTP status code shows only that the endpoint responded. It does not reveal what the request contained or whether the request led to a successful compromise.

So if you’re investigating your own site, treat an HTTP 207 response as a reason to investigate, especially if the site was unpatched during the exposure window. However, it’s not evidence of compromise on its own, particularly if the site remained patched throughout the campaign.

What “Zero Compromised” Actually Means

Zero compromised means none of the 220 sites showed verified evidence of a successful WP2Shell compromise. It’s also the claim others are most likely to challenge, so it deserves the closest analysis.

Across all 220 sites, wp core verify-checksums reported zero modified core files. It indicates not one installation contained a core file that differed from the official WordPress release. And that’s the important single piece of evidence in this study because it measures an outcome rather than interpreting log data.

However, core file integrity was only one part of the investigation. We also examined two other categories that commonly indicate post-exploitation activity:

  1. New admin accounts
  2. PHP files sitting inside wp-content/uploads (a location that should never contain executable code)

The initial review flagged seven administrator accounts created after disclosure and 26 PHP files under uploads. On their own, both findings warranted further investigation.

WordPress checker script showing pass verdicts

The tool below shows exactly how we flagged those indicators across the fleet before any manual review. And an INVESTIGATE verdict means a potential compromise indicator was present, not that a compromise was confirmed.

Admin account flag resolved as benign

Manual investigation resolved each flagged indicator. One administrator account, for example, belonged to an internal developer and used the company’s own email domain. It had been created the day before disclosure during routine staging work, not by an attacker.

Our team repeated the same process for all 33 flagged indicators across the fleet, including all seven administrator accounts and all 26 PHP files under uploads. The administrator accounts belonged to internal staff or, in one case, a client’s own team member.

The PHP files told a similar story. We traced them to documented plugin behavior, including

  • Breakdance and WPML Twig caches
  • a Redux Framework extension
  • Sucuri log files
  • WPIDE backup files

All of which legitimately write PHP files to that directory.

Remember, zero compromised doesn’t mean nothing appeared suspicious. Instead, we investigated all suspicious indicators, and none proved to be evidence of an attacker.

One limitation is worth stating clearly. The SQL injection stage of the exploit chain can read database contents without modifying files on disk. Zero file-level compromise indicators therefore provide strong evidence against successful compromise, but they cannot prove that no data was read during the exposure window.

For that reason, the 11 sites that were attacked while still unpatched are undergoing precautionary credential rotation regardless of the file integrity results.

The Companion Tools

Everything in this piece came from two scripts, and we’re publishing both so you can run the same checks yourself.

  1. wp2shell-check.sh is the fleet checker. Once you point it at a single WordPress installation or a list of sites, it performs the version check, core checksum verification, administrator account audit, and log-based IOC filtering in a single pass. The script is read-only and makes no changes to the systems it examines.
  2. nginx-wp2shell-mitigation.conf is a temporary mitigation configuration for sites that cannot be patched immediately. It blocks known scanner user agents, rate-limits requests to the batch endpoint, and includes a commented-out emergency rule for situations where stronger temporary protection is necessary. As the configuration notes, these measures reduce exposure but do not replace installing the official WordPress security update.

We considered attaching both files directly to this post. But WordPress’s Media Library blocks script and config uploads by default, and we checked before assuming:

WordPress upload restrictions blocking script files

We could have expanded our upload whitelist to allow .sh files, but we chose not to. Why? Well, allowing shell script uploads on a WordPress security company’s own website, especially in an article about a WordPress remote code execution vulnerability. And it is the kind of configuration we would flag during a client security review.

Instead, both scripts are available in a GitHub repository alongside:

  • The mitigation configuration,
  • A README explaining the broad-versus-strict IOC methodology,
  • The project license, and
  • A redacted sample output CSV

GitHub Link: Click here

If the scripts don’t behave as expected in your environment, particularly around log path detection or the WP-CLI fallback, please open a GitHub issue instead of maintaining a local workaround. This way, reporting edge cases helps improve the tools for everyone running the same checks.

One Patch Down, More Coming

WordPress patched WP2Shell in its July 17, 2026 security release, but it won’t be the last WordPress core CVE that demands this level of attention.

Our analysis found no confirmed compromises across 220 WordPress sites, despite attackers targeting 11 of them before they were patched. Those results came from a repeatable process:

  1. Applying the security update quickly
  2. Separating genuine exploitation attempts from background traffic
  3. Verifying every suspicious indicator with the same repeatable process

You can follow the same process for upcoming…. vulnerabilities.

But for now, we’ve published the scripts we used throughout this analysis. So you can compare your results with ours, validate the findings against your own environment, and be ready before the next critical WordPress advisory arrives.

Protect your sites with WP Guard

Start free with the scanner plugin, upgrade when you are ready for the guarantee.