PHP2Shell in your server logs: what reached four non-PHP sites, and what to grep for
In the access logs of four new sites that run no PHP, we saw no request of the shape PHP2Shell needs: of 3,977 requests for `.php` files between 19 and 22 October 2026, none was a multipart POST. The rate of `.php` requests did not change at the 14:00 UTC release on 21 October (median 38 per hour before, 37 after), and it did not jump when Cloudflare's WAF rules started at 03:02 UTC that day. Whatever surge other feeds show after disclosure is not visible here: the traffic is the usual reconnaissance for `phpinfo.php`, `install.php` and similar files. If you run PHP, the signals to check in your own logs are multipart POSTs to PHP URLs, bursts of HTTP 500, memory-limit errors and unexplained worker restarts, and the greps below find them.
By the Crawlerpeek editorial team · Updated · How we check
What is the timeline around the disclosure?
PHP2Shell is the name Wiz Research gave to CVE-2026-109618 and CVE-2026-109619, two unauthenticated remote code execution flaws in PHP (both CVSS v4.0 9.2). The public release was scheduled for 21 October 2026 at 14:00 UTC. Times below are UTC.
| When | What | Source |
|---|---|---|
| 17 Oct 2026 | An attempt dated 17 October is the earliest AWS describes seeing | AWS Security Blog |
| 20 Oct | The PHP project announces a critical release for 21 October. It was originally planned for 22 October and was moved up after leaks | php.net, PHP Foundation |
| 21 Oct, 03:02 | Cloudflare enables default WAF rules for free and paid plans | Cloudflare blog |
| 21 Oct, about 02:00 | AWS sees the earliest proof-of-concept traffic it reports | AWS Security Blog |
| 21 Oct, 14:00 | PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 released; GitHub advisories published that day | php.net, GitHub |
| 21 Oct | CISA alert: it had not observed exploitation at the time of publication | CISA |
| 21 Oct, 14:19 | watchTowr updates its write-up to report active exploitation attempts | The Hacker News |
Cloudflare's blog says attackers were already changing their payloads before the release, and gives no attempt counts. Counts appear only in a statement on X by Cloudflare's CEO, relayed by The Register; we could not verify them and do not repeat them. As of 22 October we found no confirmed successful compromise reported by any of these sources.
What reached four non-PHP sites, hour by hour?
The log is the one from our 48-hour guide, extended: the web-server logs of four new sites on one server, this one included, running Next.js with no PHP, GitLab or WordPress. This extract is 73 continuous clock hours from 19 October 2026 16:07 UTC to 22 October 2026 16:30 UTC, with 65,847 requests and no gaps. IP addresses were counted, never stored. 3,977 requests (6.0 percent) asked for a path ending in .php. None of the four sites has such files.
The release at 21 October 14:00 UTC splits the data into a "before" window of 46 hours and an "after" window of 27 hours (the last hour is partial). The windows differ in length, so we compare per-hour rates and medians, not totals. Distinct IP addresses cannot be added across hours or windows, so sources are shown per hour or per bucket.
| Measure (per clock hour) | Before the release (46 h) | After the release (27 h) |
|---|---|---|
| All requests, mean | 1,082 | 595 |
.php requests, mean | 52.1 | 58.5 |
.php requests, median | 38 | 37 |
.php requests, busiest hour | 260 (20 Oct 18:00) | 233 (22 Oct 09:00) |
Distinct .php sources, median | 18.5 | 21 |
Distinct .php sources, busiest hour | 29 | 39 (22 Oct 01:00) |
The next table groups the hourly counts into 6-hour buckets in UTC (00:00 to 05:59, 06:00 to 11:59, and so on). "Median sources per hour" is the median of the hourly counts of distinct IP addresses within the bucket.
| Bucket (UTC) | All requests | `.php` requests | `.php` share | Median sources per hour |
|---|---|---|---|---|
| 19 Oct 16:00 to 17:59 (2 h) | 6,424 | 19 | 0.3% | 5.5 |
| 19 Oct 18:00 to 23:59 | 10,455 | 359 | 3.4% | 10 |
| 20 Oct 00:00 to 05:59 | 3,279 | 266 | 8.1% | 19 |
| 20 Oct 06:00 to 11:59 | 2,488 | 200 | 8.0% | 20.5 |
| 20 Oct 12:00 to 17:59 | 8,269 | 295 | 3.6% | 17.5 |
| 20 Oct 18:00 to 23:59 | 8,770 | 656 | 7.5% | 18 |
| 21 Oct 00:00 to 05:59 (Cloudflare rules from 03:02) | 5,538 | 278 | 5.0% | 18.5 |
| 21 Oct 06:00 to 11:59 | 3,130 | 258 | 8.2% | 23.5 |
| 21 Oct 12:00 to 17:59 (release at 14:00) | 3,902 | 230 | 5.9% | 21.5 |
| 21 Oct 18:00 to 23:59 | 3,509 | 260 | 7.4% | 20 |
| 22 Oct 00:00 to 05:59 | 3,678 | 559 | 15.2% | 20 |
| 22 Oct 06:00 to 11:59 | 4,717 | 427 | 9.1% | 24.5 |
| 22 Oct 12:00 to 16:30 (5 h) | 1,688 | 170 | 10.1% | 21 |
What the data shows, in order of how much weight it can bear:
- The
.phprequest rate is flat across the release. The median is 38 per hour before and 37 after. The mean is a little higher after (58.5 against 52.1), driven by a few busy hours, and the busiest single hour was before the release, on 20 October. - Distinct sources per hour crept up (median 18.5 before, 21 after). Over whole windows, 379 distinct addresses asked for
.phppaths before the release and 262 after, and 148 appear in both. These window totals are not comparable because the windows differ in length. - Around Cloudflare's 03:02 UTC rule start on 21 October there was no jump: the hours 03:00 and 04:00 UTC had 56 and 32
.phprequests. The pre-release hours of 21 October ran between 23 and 100 per hour, with the highest at 05:00 and 06:00 UTC (99 and 100), eight hours before the release. The hour of the release itself (14:00 UTC) had 29. - The only visible rise comes on 22 October: 91 in the 01:00 hour, 173 and 188 at 03:00 and 04:00, 233 at 09:00. That is also when the
.phpshare peaks (15.2 percent in the 00:00 to 05:59 bucket), partly because total traffic per hour was lower than on 19 and 20 October. These are single hours inside a range the data had already shown before the release, and the paths in them look like the usual ones. We cannot tie them to PHP2Shell.
What did the `.php` requests ask for?
| Path | Before, per hour (46 h) | After, per hour (27 h) |
|---|---|---|
/wp-admin/install.php | 22.5 (1,033 total) | 26.9 (726) |
/wp-login.php | 3.3 (153) | 1.0 (27) |
/xmlrpc.php | 2.5 (114) | 1.9 (50) |
/phpinfo.php | 1.0 (44) | 1.1 (30) |
/info.php | 0.8 (37) | 0.6 (17) |
/test.php | 0.7 (31) | 0.7 (18) |
/wordpress/index.php | 1.3 (60) | not in top 20 |
/wp-config.php | not in top 20 | 0.6 (15) |
/phpversion.php, /server-info.php | not in top 20 | 0.4 each (12) |
Before the release 2,397 .php requests came in, after it 1,580. /wp-admin/install.php is the largest single path in both windows (43.1 percent of .php requests before, 45.9 percent after), and its hourly rate is similar. Debug-page probes (phpinfo.php and its many folder variants, info.php, test.php) are steady, and wp-login.php and xmlrpc.php were, if anything, less frequent per hour after. Config-style names such as wp-config.php entered the top 20 after the release, with small counts. Across the whole extract, 48 requests asked for the PHPUnit test runner file eval-stdin.php under eight different folder names, six each. That file is a known, years-old exposure in PHPUnit test setups left on a public server. It is not part of PHP2Shell, and our logs do not split it by period.
These are lookups for files that tell a scanner what a host runs. A request that asks whether phpinfo.php exists is reconnaissance. It is not an attack on the two CVEs, which the advisories describe as parsing flaws in how PHP handles a request body.
Did we see the request shape PHP2Shell needs?
No. The advisories and vendor write-ups describe the trigger as unusual multipart/form-data POST requests, with no specific endpoint required. Of the 3,977 .php requests, 3,901 were GET and 76 were POST. The logged content type tells the rest: 60 POSTs were application/x-www-form-urlencoded, the standard encoding of a login or contact form, and 16 had no content type. None was multipart/form-data.
That is a clean result for a narrow question and a weak one for a broad one. Our sites run no PHP, so even a perfect exploit request would have found nothing to run, and a scanner may skip a host that does not answer like PHP. Our logs record headers and paths, not request bodies. We can say: during the hours we have, nobody sent these hosts a PHP2Shell-shaped request. We cannot say how many were sent to PHP sites. AWS, watchTowr and Cloudflare report attempts against hosts that do run it.
What should you look for in your own logs?
The signals below are the ones Wiz, AWS and the PHP project publish for log review. None of them proves a compromise, and some also match harmless events. They tell you where to look. If you have not updated yet, do that first: the patch section of our 48-hour guide lists the fixed versions and how to check which PHP serves your site.
| Signal | Where it shows up | Needs extra logging? |
|---|---|---|
| Unusual multipart POSTs to any PHP URL | Access log, if the content type is logged | Yes, on nginx and Apache |
A POST body containing PHP_SESSION_UPLOAD_PROGRESS in a form upload | Only where bodies are logged, such as a WAF audit log | Yes |
| Memory-limit errors, repeated HTTP 500 | PHP error log, access log | No |
Oversized or malformed PHPSESSID in a response cookie | Only where response headers are logged | Yes |
| Unexplained PHP worker restarts | PHP-FPM or web-server error log | No |
Default access logs do not record the content type, so a POST-only grep shows more than you want to read, but it is the only one that works on an unchanged setup. This lists every POST to a path ending in .php in an nginx or Apache log in the combined format, then counts them per source address. A pretty URL or /index.php/route path needs the pattern \.php without the closing quote:
grep -E '"POST [^ "]*\.php' access.log
grep -E '"POST [^ "]*\.php' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | headTo see multipart bodies in the future, add the content type to the log format. In nginx, add "$content_type" to a log_format; in Apache, add "%{Content-Type}i" to a LogFormat. With the content type as the last quoted field, this finds multipart POSTs:
grep -E '"POST [^"]*" .*"multipart/form-data' access.log
Bursts of HTTP 500 are visible in the default format. This counts 500 responses per minute (the status is field 9 in the combined format) and prints the ten busiest minutes:
awk '$9 == 500 {print substr($4, 2, 17)}' access.log | sort | uniq -c | sort -rn | headCaddy writes JSON access logs, with the request headers included. These jq filters list multipart POSTs to PHP URLs and count 500 responses per minute:
jq -c 'select(.request.method == "POST"
and ((.request.headers["Content-Type"][0] // "") | startswith("multipart/form-data"))
and (.request.uri | test("\\.php")))' access.log
jq -r 'select(.status == 500) | .ts | floor | strftime("%Y-%m-%d %H:%M")' access.log | sort | uniq -c | sort -rn | headThe remaining signals live in other files. The first two lines search the PHP error log for the standard memory-limit message and the PHP-FPM log for a worker that died; paths differ by system (for example /var/log/php8.3-fpm.log on Debian). The third needs a log that stores request bodies, such as a WAF audit log:
grep -i 'allowed memory size' /var/log/php_errors.log grep -E 'exited on signal|exited with code [1-9]' /var/log/php8.3-fpm.log grep -l 'PHP_SESSION_UPLOAD_PROGRESS' /var/log/waf-audit.log
One match is a reason to look, not a verdict. A memory-limit error can come from a slow plugin and a restarted worker from a deploy. A cluster of them, shortly after a multipart POST from one address, deserves a closer look at that address and at what ran on the server afterwards. If you think the server was hit, treat it like any suspected code execution: take a copy of the logs and the file system before you change anything, and rotate secrets stored on that host.
What the data cannot show
- Coverage. 73 continuous hours, from 19 October 16:07 UTC to 22 October 16:30 UTC. Our logs start after AWS's 17 October attempt, so that day is not covered.
- Scope. Four small new sites on one server, three days, no PHP.
- Bodies. We log request paths, headers and status codes, not bodies. A multipart POST that our server did log would show up as such; we saw none.
- Attribution. We cannot say which scanner belongs to which campaign, or that any hour reflects PHP2Shell specifically.
Checked 22 October 2026. Sources: AWS Security Blog, Cloudflare, CISA alert, Wiz, php.net archive, PHP Foundation.
Follow-up questions
How do I check my access log for PHP2Shell?
Look for POST requests to PHP URLs, and for multipart POSTs if your log records the content type. Then check for bursts of HTTP 500, memory-limit errors in the PHP error log and unexplained PHP-FPM worker restarts, as in the greps above. A match is a reason to investigate, not proof. Updating PHP is the fix; the logs only tell you whether something tried.
Does a request for phpinfo.php or install.php mean someone exploited CVE-2026-109618?
No. Those are lookups for files that reveal what a host runs, and they have been sent to new hosts for years. In our logs, their rate per hour was the same before and after the release, and no request had the multipart shape the advisories describe.
Is CVE-2026-109618 being exploited, and is it in CISA's catalog?
AWS, watchTowr and Cloudflare report attempts, some before the 21 October release. CISA's alert of 21 October said it had not observed exploitation at the time of publication, and as far as we checked on 22 October the CVE was not in its Known Exploited Vulnerabilities catalog. That can change daily. We found no confirmed successful compromise.
Can I rely on my web application firewall instead of reading logs?
A WAF can help, but the PHP project lists no complete mitigation other than updating, and Cloudflare notes that attackers change their payloads. Use a WAF as an extra layer and update PHP.
Why did you only look at sites without PHP?
Those were the sites we had. It makes the result a picture of what scanners send to every new host, not of how often PHP servers were hit.
See how your own site does.Run the checker