php2shell Detection
Zeek detections and Suricata rules for HTTP indicators associated with "php2shell" CVE-2026-109618 / CVE-2026-109619, potential RCE in PHP longjmp() memory handling. Findings identify suspicious traffic or potentially affected software.
Zeek
Requires Zeek 7.0.0 or later. Install with the Zeek Package Manager:
zkg install https://github.com/corelight/php2shell-detect
Load installed packages with @load packages in your Zeek configuration.
For a direct checkout, load /path/to/php2shell-detect/scripts.
The package uses standard Zeek HTTP, file-analysis, notice, and cluster APIs.
The package inspects POST field-name lengths, selected serialized-data structures, and repeated MIME headers. It also correlates related suspicious transactions using configured session cookies. Response headers provide separate PHP version inventory. Traffic must be visible to Zeek's HTTP analyzer; encrypted HTTP requires decryption before inspection.
Notices
All notice types are under PHP2SHELL:::
| Notice | Observation |
|---|---|
PHP_Crash_Sequence |
Related suspicious HTTP transactions within one session |
Multipart_Header_Flood |
Excessive repetition of recognized MIME headers |
Large_Form_Key |
Unusually long URL-encoded form field name |
Serialized_Integer_Overflow |
Unusually large serialized integer token |
Serialized_Integer_Collisions |
Recognized serialized integer-key pattern |
Serialized_String_Collisions |
Recognized serialized string-key pattern |
Serialized_Duplicate_Array |
Repeated serialized array-entry pattern |
Serialized_Object_Repetition |
Repeated serialized object pattern |
Potentially_Affected_PHP_Version |
Response header advertises a version within the configured affected range |
Results go to standard notice.log; the package creates no additional log
stream. The notice sub field supplies bounded context such as header name,
transaction depth, counts, and a short escaped POST-body sample. Samples are
limited to 64 raw bytes before escaping by default; set
notice_body_sample_bytes=0 to omit them. Cookies and their digests are used
only in memory and are not logged by this package.
By default, each distinct request indicator can emit a notice per transaction.
Repeated matches of the same indicator are suppressed. Set detect_only_once=T
to stop body inspection after the first request finding in each transaction.
Version inventory notices are independent of request findings.
Configuration
All settings use the PHP2SHELL:: namespace:
| Setting | Default | Effect |
|---|---|---|
detect_only_once |
F |
Report each distinct request indicator; T stops after the first finding |
max_trans_depth |
3 |
Inspect the first three HTTP transactions per connection; 0 means all |
inspect_cookies |
T |
Use session cookies for correlation |
session_cookie_names |
{"PHPSESSID"} |
Cookie names used to associate sessions |
correlation_window |
1 min |
Maximum age of pending session correlation state |
max_pending_sessions |
1000 |
Cap retained entries in each correlation table |
inspect_post_bodies |
T |
Inspect POST entity data and MIME headers |
http_body_analysis_byte_depth |
8388608 |
Conservative 8 MiB ceiling on POST-body inspection work, shared across MIME parts; 0 disables body delivery |
body_overlap_bytes |
256 |
Retained overlap for patterns crossing body chunks |
large_form_key_bytes |
65536 |
Form-name wire-byte threshold |
multipart_header_threshold |
128 |
Repeated recognized MIME headers needed for a finding |
notice_body_sample_bytes |
64 |
Maximum raw body bytes per notice sample; 0 omits samples |
detect_affected_php_versions |
T |
Enable response-header version inventory |
php_version_notice_interval |
1 hr |
Suppress version notices per server IP across ports and versions |
php_fixed_patch_versions |
8.2: 35, 8.3: 36, 8.4: 27, 8.5: 12 |
Provisional first fixed patch per branch |
For example:
redef PHP2SHELL::detect_only_once = T;
redef PHP2SHELL::notice_body_sample_bytes = 0;
redef PHP2SHELL::session_cookie_names += { "custom_session" };
POST inspection uses file-analysis chunks with bounded overlap, rather than assembling whole bodies. Form-name counting respects value separators and raw NUL termination. Only eligible POST entities receive an analyzer; body budgets and transaction limits intentionally constrain detection scope. These indicators are incomplete and can also match benign traffic.
In a Zeek cluster, workers route small session observations to proxies with
Cluster::publish_hrw(). Both stages for the same session reach the same
proxy, which matches them and raises PHP_Crash_Sequence directly. The notice
retains the follow-up's connection ID, UID and transaction depth; peer_descr
identifies the proxy. Workers do not retain full connection or HTTP records
for callbacks.
Triggers and unmatched follow-ups expire after correlation_window, with
max_pending_sessions limiting each table. Proxies retain the first unmatched
follow-up for each session key. A trigger looks up that key directly to handle
messages arriving in reverse order; later unmatched follow-ups for the same
key are coalesced until that entry matches or expires. Matching uses observation
timestamps and the client, server, server port, HTTP host and session digest.
Standalone Zeek correlates locally. Normal proxy connectivity is required.
Proxy notices use standard notice suppression keyed by UID and transaction
depth for correlation_window. This is independent of worker findings and
does not stop worker body inspection. Simultaneous matches for different
session cookies on different proxies can still produce duplicate notices
before cluster suppression propagates.
PHP version inventory
Outer Server and X-Powered-By response headers are checked, including
responses to GET. Header names are case-insensitive. The affected lower
bound is PHP 5.4.0. The following cutoffs assume the next patch after the
latest stable releases checked on 18 October 2026 will address both bugs:
| Branch | Latest stable when checked | Provisional first fixed release |
|---|---|---|
| 8.2 | 8.2.34 | 8.2.35 |
| 8.3 | 8.3.35 | 8.3.36 |
| 8.4 | 8.4.26 | 8.4.27 |
| 8.5 | 8.5.11 | 8.5.12 |
Configured fixed releases and later patches in the same branch do not alert. Branches without a configured cutoff remain candidates. Common vendor suffixes use the underlying numeric version. Banners can be absent, rewritten, or reflect a vendor build with backported fixes, so these are inventory hints.
Suricata rules
The three rules in
suricata/php2shell.rules
run independently of Zeek. Installing the Zeek package with zkg does not
load these rules into Suricata. All three rules are enabled in the file;
Zeek configuration options do not affect them.
| SID | Inspection buffer | Observation |
|---|---|---|
| 3000650 | http.request_body with method POST |
Detection marker in a request body |
| 3000651 | http.response_header |
Potentially affected PHP version advertised by Server or X-Powered-By |
| 3000652 | http.response_body |
Potentially affected PHP version in a phpinfo-shaped HTML heading |
Loading the rules
Add the rule file to the existing rule-files list in your suricata.yaml,
keeping your other rule entries. Use the absolute path to your checkout:
rule-files:
- /path/to/php2shell-detect/suricata/php2shell.rules
Validate your configuration, then reload or restart Suricata using your normal deployment procedure:
suricata -T -c /path/to/suricata.yaml
The rules were validated with Suricata 8.0.6. HTTP body inspection must be enabled; request/response body limits and stream reassembly limits determine which content reaches these rules. Encrypted HTTP requires decryption before inspection.
Interpreting alerts
Find these rules in EVE alert records using alert.signature_id, or by their
SID in your configured alert output. The POST marker can occur in legitimate
traffic and does not establish an exploit attempt. Suricata does not perform
the Zeek package's session correlation.
The version rules are software-inventory signals. Header names are matched case-insensitively, and the body rule recognizes a phpinfo-shaped HTML heading. Both use the provisional version cutoffs above; missing or rewritten banners and vendor backports limit their accuracy. TODO before release: Verify both rules' affected ranges and first fixed releases against official PHP advisories and release notes.
Each version rule limits alerts to one per server IP per hour, across clients,
ports, and advertised versions. They inspect responses, so track by_src
tracks the server. Each SID maintains its own limit; one server may therefore
produce both a header alert and a phpinfo alert in the same hour. The POST
marker rule has no hourly limit.
Regression test
Install btest and ensure zeek, zeek-cut, unzip, bash, and make
are on PATH, then run:
make check
make test
make zam
make zam repeats the test with Zeek's ZAM optimizer enabled. The single
included btest uses the compressed merged upstream Zeek HTTP corpus and
compares complete TSV HTTP and notice logs. One PHP 8.0.26 inventory notice
is expected; reporter.log must be absent. See
corpus provenance for input details and licensing.