Detecting PHP2Shell CVEs with Graylog

Detecting PHP2Shell CVEs With Graylog

PHP shipped emergency patches on October 21, 2026, for two unauthenticated remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, known together as PHP2Shell. Both are rated 9.2 under CVSS 4.0. A working exploit leaked ahead of the coordinated disclosure, and Sansec expected mass exploitation within fifteen minutes of the release.

Patch asap. Then work out whether anyone got to you before you did. The searches in this post cover the four places an attempt shows up in your logs.

What PHP2Shell is

The bug is a use-after-free in PHP’s session upload progress handler, in php_session_rfc1867_callback. PHP caches two pointers while it tracks a multipart upload; a second request forces the backing hash table to be destroyed, and the freed memory can then be reclaimed with attacker-controlled data. The next use of the dangling pointer reaches zend_call_function, which can be aimed at system().

Two details make this worse than a typical PHP CVE:

  • Default configuration is enough. session.upload_progress.enabled is on in stock php.ini. No plugin, no application code, no authentication.
  • It fires before your application runs. The parsing happens in PHP core, so WAF rules scoped to application routes and framework-level input filtering never see it.

Every installation that serves web requests is affected, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed.

Branch Status Action
8.5 Fixed in 8.5.12 Update
8.4 Fixed in 8.4.27 Update
8.3 Fixed in 8.3.36 Update
8.2 Fixed in 8.2.35 Update, and plan the move off 8.2 before December 31, 2026
8.1 and older, 7.x, 5.x No official release Source backports exist for 7.4, 8.0 and 8.1; otherwise isolate or upgrade

Reporting puts the affected range at every version from PHP 5.4.0 onward, and Palo Alto’s Cortex Xpanse counted over 22.6 million potentially exposed instances at the time of publication.

The four signals an attempt leaves behind

Exploitation needs an offset specific to your PHP build. Attackers scanning at scale do not have it, so most attempts crash the worker rather than succeed. That is good news for detection. A wrong offset or a lost heap race segfaults the PHP worker, and behind nginx that surfaces as a 5xx, usually a 502. A correct offset returns 200 and runs the payload.

Signal Log source What it means
Upload-progress field in a multipart POST WAF, ModSecurity, any body-logging proxy An attempt, or a legitimate progress bar
5xx on a multipart POST Web server access logs Attempt that crashed the worker
SIGSEGV or SIGABRT in a worker PHP-FPM, lsphp, Apache, nginx error logs Crash, correlate with the POST above
Shell or recon tool spawned by the PHP process auditd, Sysmon for Linux Successful execution

The first signal is the highest-fidelity one, but it needs request body logging, which most shops do not have on every host. The second and third need nothing but the logs you already ship. Take them together: a 502 on a multipart POST plus a segfault in the same second is a near-certain attempt, and either one alone is worth a look today.

The searches

These are Lucene query strings. Field names follow the common Graylog conventions. Swap in whatever your extractors, pipeline rules or Illuminate content packs actually produce.

  1. Attempt, with the default field name. Needs request body or WAF logs.

message:*PHP_SESSION_UPLOAD_PROGRESS* OR request_body:*PHP_SESSION_UPLOAD_PROGRESS*

  1. Failed attempt in access logs. A multipart POST that crashed the worker.

http_method:POST AND http_response_code:(500 OR 502 OR 503 OR 504)

AND (http_content_type:multipart* OR message:*multipart\/form-data*)

  1. Worker crashes and the OOM path. Error logs from PHP-FPM, LiteSpeed, Apache and nginx.

message:(“exited on signal 11” OR “exited on signal 6” OR SIGSEGV OR SIGABRT

OR “Segmentation fault” OR “exit signal Segmentation”

OR “upstream prematurely closed connection” OR “Allowed memory size of”)

  1. Successful execution. Host process telemetry from auditd or Sysmon for Linux.

parent_process_name:(php-fpm* OR php-cgi* OR lsphp* OR httpd OR apache2 OR frankenphp)

AND process_name:(sh OR bash OR dash OR curl OR wget OR nc OR ncat

OR python* OR perl OR whoami OR uname OR id)

Search 4 is worth adding even if you already run the SigmaHQ web shell rules. Those watch httpd, apache2, nginx, lighttpd and Java app servers as parents. Under PHP-FPM, however, the process that spawns the command is php-fpm, which the stock rule does not cover.

Leading wildcards, as in searches 1 and 2, are slow across large indices. Scope them to your web server streams rather than running them against everything.

Wiring them into event definitions

All four are Filter & Aggregation definitions. Search within the last 5 minutes, execute every minute.

  1. Attempt (search 1). No aggregation. Alert on every hit while you are still patching, then move to a threshold once you know your baseline.
  2. Failed attempt (search 2). Group by the client IP field and alert when the count reaches 3. A single 502 on a multipart POST is normal background noise; three from one source in five minutes is not.
  3. Worker crashes (search 3). Group by host and alert at 3 or more. Crashes cluster, and a per-event alert will bury you.
  4. Successful execution (search 4). No aggregation, highest priority. One hit is an incident.

Set definitions 1 and 4 to high priority and route them to whatever your team actually watches. Definitions 2 and 3 are better as a dashboard widget plus a lower-priority notification. They will fire during ordinary outages too.

If you run a correlation-capable Graylog tier, the strongest single rule is definition 2 followed by definition 3 on the same host within 10 seconds. That pairing is specific enough to page on.

Tuning, and the limits of each search

Legitimate traffic uses this field. Applications with a real upload progress bar send PHP_SESSION_UPLOAD_PROGRESS as designed. Run definition 1 in detection mode for a day before you page on it, and consider scoping it to routes that never accept uploads. That variant is close to zero-noise.

The field name is configurable. session.upload_progress.name can be changed, so the default name is a reliable indicator only for the default configuration. Check what your own builds use before you trust its absence.

String matching can be evaded. PHP’s multipart parser is considerably looser than a line-based matcher. It accepts folded headers, lines with no colon glued to the previous header, Content-Disposition without form-data, missing spaces and junk after the closing quote. A detection that insists on a clean name=”PHP_SESSION_UPLOAD_PROGRESS” on one line will miss a deliberately malformed body. This is why search 1 uses a bare substring match and why searches 2 and 3 matter independently. They key on the crash, not on the request.

mod_php may show you nothing. Without a proxy in front, a crashed mod_php worker often just drops the connection, so there may be no status code to match at all. On those hosts, search 3 is your primary signal.

A 200 is not an all-clear. On the crash-signal searches, a 200 on the same request pattern is the success case, not the benign one. If search 1 fires and the response was 200, treat it as a confirmed hit and go straight to search 4.

Detection is not the fix

Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Everything below is for the gap between now and when that finishes rolling out.

  • Disable the feature. Set session.upload_progress.enabled=0 in php.ini, .user.ini or .htaccess, and restart every worker. PHP’s advisory notes this blocks the confirmed session path of CVE-2026-109619, not every variant.
  • Block the field at the edge. A ModSecurity rule on ARGS_NAMES matching PHP_SESSION_UPLOAD_PROGRESS rejects the request before PHP parses it. Run it in detection mode first if any of your applications use progress bars.
  • Block multipart POSTs where you do not need them. Heavy-handed, but effective on routes that never take uploads.
  • Inventory everything. The NCSC’s guidance is not to limit discovery to outdated versions. Find every PHP installation, including appliances and vendor products that bundle PHP as a component.

Do not block on the PHPSESSID cookie. It breaks every valid session you have and does not stop the attack.

Hunt backwards, not just forwards

The proof of concept leaked before the coordinated disclosure, which means the window of exposure opened before the patch did. Run all four searches across your full retention period, not just from now on. If the crash searches light up on a date before today, you have an incident rather than an alert. Special note: Graylog does not use PHP in it’s application stack.

Additional Resources

 

Categories

Get the Monthly Tech Blog Roundup

Subscribe to the latest in log management, security, and all things Graylog blog delivered to your inbox once a month.