Skip to content
Exploit for Improperly Controlled Sequential Memory Allocation in Php

Exploit for Improperly Controlled Sequential Memory Allocation in Php

Sploitus • October 23, 2026

Two unauthenticated RCE bug classes in the PHP core, both reachable while PHP parses

the request — before application code runs. Disclosed 2026-10-21; fixed releases

**8.2.35 / 8.3.36 / 8.4.27 / 8.5.12**. This package answers the on-shift question

three ways and ships the network coverage beside it.

# 1) all three checks with defaults (posture auto-discovers php/php8.x + package managers +

# apache/fpm config; drift hashes /tmp /var/tmp /dev/shm; log scan waits for --log)

# 2) point at the SERVED runtime (not just CLI php) + webroot

python3 -m php2shell_detector --php-bin /usr/bin/php8.4 --webroot /var/www/html

# 3) scan logs: access log + php-fpm error log (repeatable --log; .gz supported)

python3 -m php2shell_detector --log /var/log/nginx/access.log --log /var/log/php8.4-fpm.log

python3 -m php2shell_detector --webroot /var/www/html --php-bin php --json | jq .

[![Quick Start](

[![CISA Alert](

[![License](

**Developed by [IONSEC](

Every fingerprint, CVE fact, fixed version and threshold in this package comes from

the IONSEC research briefs (compiled 2026-10-23 from fetched primary sources —

php.net/GHSA advisories, ELLIO, F5 Labs, VulnCheck, Sophos, AWS MadPot, Wiz,

Corelight). **No fingerprint is invented.** Rules carry provenance in and

output: `VERBATIM` = exact documented string/shape; `DERIVED` = detection threshold

grounded in a documented observation, stated inline. Related advisory:

**CVE-2026-115560** (session upload-progress RCE on patched releases,

## What it checks — and what a finding can actually tell you

| Check | What it does | What a finding means |

| **HOST POSTURE** | PHP runtime inventory (`php -v`, and dpkg/rpm/apk/brew package queries), SAPI exposure (mod_php / php-FPM / php-cgi / FrankenPHP/LiteSpeed markers), enabling settings (`session.upload_progress.enabled`, `session.upload_progress.cleanup`, `fatal_error_backtraces` on 8.5, `memory_limit`, `post_max_size`, `upload_max_filesize`, `max_file_uploads`), webroot `.htaccess`/`.user.ini` files, edge multipart-block presence | Exposure. Not an incident. |

| **LOG SCAN** | Access logs + PHP/php-fpm error logs against the documented PHP2Shell fingerprints: scanner UAs, marker strings (JACKALPOC, P2SMARK, boundaries…), `PHP_SESSION_UPLOAD_PROGRESS` field, `php://filter` session-file reads, hardcoded session ids, OOM body-size cluster, unusable/contradicted Content-Length, worker crashes, `Allowed memory size` fatals, `Maximum execution time exceeded`, 500 bursts, rotating-session heap-grooming cadence — plus the structural multipart amplification shapes when bodies are logged | **Attempt evidence, not execution.** All brief telemetry describes attempts; no public source reported a confirmed compromise as of 2026-10-23. |

| **DRIFT CHECK** | Stream-hash every regular file in `/tmp`, `/var/tmp`, `/dev/shm` (+ `--tmpdir`) and compare against the documented binary fingerprints (Potassium Mirai-variant loader + three additional payloads ELLIO published). Local reads only. | Loader hash on disk = **suspected compromise, trigger IR**. Presence of drop *name* `nfs` without a hash match is info-only. |

- **A log hit never proves execution.** It timestamps attempts and gives

hunting context (source IPs, URIs, cadence). Correlate with the drift check,

worker crash records and response-side anomalies (Wiz's oversized/malformed

`PHPSESSID` response cookie; `PWNED::` / `JACKALPOC` in response bodies) —

the last two are not visible in access logs; WAF/CDN transaction logging

- **Posture is point-in-time** and covers the runtime you point it at. The

served runtime can differ from the CLI `php` binary (verify with phpinfo() /

the package database on the host that actually serves). Containers, panels

(cPanel/Plesk), appliances and bundled PHP each need their own inventory; this

- **Version strings lie on backported distro builds.** RHSA-2026:87080 ships a

patched build keeping the old `7.4.33` version string; RHEL9's patched php

stays `8.0.30` (RHSA-2026:87151). Vendor LTS backports for EOL branches keep

the version number, too. A version reading can false-flag AND false-clear —

- Debian/Ubuntu: `zgrep CVE-2026-109618 /usr/ /doc/php8.1-common/changelog.Debian.gz`

- RHEL/Alma/Rocky (Remi uses `php81-…` names): `rpm -q --changelog php-common | grep CVE-2026-109618`

- **EOL honesty:** PHP ≤8.1 has **no official fixed release**; advisories cover

PHP 5.4–8.5. Source backports exist for 7.4/8.0/8.1; **nothing for ≤7.3**.

Ubuntu ESM and other vendor-LTS ecosystems extend coverage with unchanged

version strings. The tool reports version verdicts but cannot know the

changelog — it says so in the finding text.

- **`fatal_error_backtraces` (PHP 8.5):** the briefs do not state its default,

so the tool does not assume one — absent directives get an "unknown, verify

with php -i" row instead of a guessed verdict.

- **After patching, restart every worker** (Apache, php-fpm, long-running PHP

processes) — the patched runtime is not loaded into running processes. The

tool cannot observe worker age; re-run posture after restarting.

- **The edge is covered by the separate rules package** — host tooling

complements, not replaces, network/WAF coverage. IONSEC's Suricata /

ModSecurity / Sigma rules live in `rules/` (see `README.md` in this

directory); external managed coverage: Cloudflare (default-on 21 Oct), AWS WAF

`PHP2Shell_BODY` (v2.3, 109619) / `PHP2Shell_BODY_Extended` (v2.4, 109618),

Azure Front Door 99006001 (off by default — enable), Talos Snort SIDs

67359/67360/67375, FortiGate 62657/62658, open-source Corelight

Zeek/Suricata: github.com/corelight/php2shell-detect.

- **Deliberately NOT checked:** `AllowEncodedSlashes` or any other httpd

directive not documented by the briefs (no speculation beyond the research).

- **php-cgi nuance:** listed "at risk" by php.net without separate published

validation; FPM's 109618 exposure is source-analysis only (failed startup

kills the child) — the tool's rating table reflects exactly this.

Nothing in this tool opens a socket. Posture shells out to **local** commands

(`php -v/-i`, `dpkg-query`, `rpm`, `apk info`, `brew list`, `apachectl -M`,

`pgrep`), reads local config files; logs are read locally; the drift check

hashes local files only. The tool has no update/IOC-feed fetching feature — IOC

| `--webroot PATH` | webroot scan for `.htaccess`/`.user.ini` (posture) |

| `--log PATH` | log file(s); repeatable; plain or `.gz` |

| `--php-bin PATH` | PHP binary to interrogate (default probes PATH for `php`/`php8.x`) |

| `--ini PATH` | extra php.ini file to parse (repeatable) |

| `--apache-conf PATH` | explicit apache config file (default: standard dirs if present) |

| `--fpm-conf PATH` | explicit php-fpm pool/master conf (default: standard paths if present) |

| `--tmpdir PATH` | extra drift tmpdir (repeatable; replaces defaults) |

| `--check NAME` | run only this check: `posture` / `logs` / `drift` (repeatable) |

| `--log-format FMT` | `auto` (default) / `combined` / `common` / `ltsv` |

| `--max-lines N` | lines scanned per file (default 200,000) |

| `--json` | JSON report (same content as text mode) |

| `--quiet` | text mode: only warn/critical finding lines |

| `0` | checks ran; no warn/critical finding |

| `1` | at least one warn/critical finding — inspect the report |

| `2` | invocation/resource problem (bad flag values, missing explicit paths) |

- Severities: `critical` (act now: mod_php exposure, unpatched web-serving

runtime, loader hash on disk), `warn` (real exposure surface or attempt

evidence), `info` (context), `ok` (verified mitigation, e.g. upload-progress

- Every finding carries `basis` (rule provenance) and, for log findings, the

file, line and a **defanged** evidence excerpt (IPs are `[.]`-mangled).

- Per rule, up to the first 8 line hits are shown with `repeat_count` showing

the real volume; the report list is capped at 60 entries severity-first.

`finding_count` per file is the untruncated total. Full detail: `--json`.

Per line auto-detect: **combined**, **common** (Apache/nginx) and **LTSV**.

LTSV keys used: `req|request`, `ua|user_agent`, `status`, `size`, `host`,

`time`, `cookie`, `body`, `content_length`, `request_length`,

Standard access logs do **not** contain request bodies; the structural rules

- LTSV with an escaped `body:` field (`\n`, `\r`, `\t`, `\x00` escapes — nginx

`$request_body`, escaped). Bodies are deep-scanned up to 2 MiB; anything

bigger is judged via the size-cluster rule instead.

- Request-length fields (`request_length`, `body_bytes_received`,

`content_length`) enable the **documented OOM body-size cluster** rule (exact

values only: 62,929 / 8,388,608 / 9,437,183 / 12,655,437 / 15,657,436 /

- `content_length: -` (logged-but-unusable) on a POST with a body → unusable

Content-Length shape. When the field is simply not logged, the tool does not

guess (no false positives from log-config differences).

PHP/php-fpm **error logs** (detected by filename or content) are scanned for

the documented server-side signatures (§10.3 of the technique brief):

`Allowed memory size … exhausted` fatals, `Maximum execution time exceeded`,

worker crash records (SIGSEGV/SIGABRT, FPM children `exited on signal`),

`PHP Startup` INI diagnostics and `Deprecated:` noise (109619 consumer

touchpoints, info), plus any documented marker string (critically, if marker

## Derived thresholds (documented shapes, conservative cut-offs)

Tune in `php2shell_detector/fingerprints.py` (`BODY_THRESHOLDS`):

| Rule | Threshold | Documented observation |

| EMPTY_FILE_PART_FLOOD | ≥100 | "hundreds of empty file fields" (ELLIO) |

| FIELD_NESTING_DEEP | depth ≥12 | "field names nested dozens of levels deep" |

| PART_HEADER_FLOOD | ≥1000/part | "X: a" × 230,000–1,872,936 (ELLIO) |

| PADDING_FLOOD | ≥30,000 B single-char run | F5 62,929×'A'; ELLIO 24 MiB×'B' |

| IDENTICAL_SMALL_PARTS | ≥64 equal-length parts ≤512 B | 149 identical 288-byte parts (ELLIO/PoC) |

| NULL_FILE_PART | all-NUL value ≤4 B | 2 literal NUL bytes file parts (F5) |

| ROTATING_SESSION_CADENCE | ≥5 distinct PHPSESSID same endpoint | F5 pattern-2 cadence |

| REPEATED_5XX | ≥5 per path | documented 500-burst side effect |

## Drift fingerprints (exactly as published in the briefs)

| Potassium Mirai-variant loader | MD5 `b61a098fc62a2c2696e196dca78da71a` · SHA1 `c472dab4ff7c16782a8037f988d4daa6d4bdb343` · SHA256 `0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486` | 179,632 B ELF x86-64 static; `/tmp/nfs`, run `./nfs php` (Sophos + ELLIO identical values) |

| DDoS booter `bot.x86_64` | SHA256 `7133612558a5d0f257ca6bdb4f765453af9465bc5298acdcf33b5761b8c59aac` | ELLIO sensor payload |

| Chinese IIS web shell `1.aspx` | SHA256 `17758cf05e3376f8448dff926564e038b763b4a46668455e2ccb5ac0839e629c` | ELLIO sensor payload |

| Go remote-access agent | SHA256 `dd98bf48899aa385035352688357742c73ddc5e47ec4ecc7d7b74c53c2e3ca3d` | ELLIO sensor payload |

Hash hits report "documented payload fingerprint … see references; no

attribution implied by this tool" — the briefs record that **no actor

attribution exists** and Potassium is a commodity botnet family; do not upgrade

that wording in your own reporting without vendor attribution.

- Synthetic benign + exploit corpora live in `php2shell_detector/corpora.py`

(exploit lines built ONLY from documented fingerprints, each with its basis;

benign traffic designed to trip nothing).

- One test back-audits the zero-invented-fingerprints property: every UA string

published in the briefs must be caught by the matcher; benign UAs (curl,

Chrome, Firefox, python-requests) must produce nothing.

- Drift machinery tests use a synthetic fingerprint (clearly labelled "not a

real-world IOC") because the real hashes cannot be tested against a

fabricated binary; the shipped constants are format-checked and tested to

- Static example corpora for manual smoke runs: `examples/logs/`

(regenerate: `python3 examples/make_examples.py`). Fixtures:

`examples/fixture_php.sh` (clearly-labelled stub, do not deploy),

`examples/fixture_webroot/`, `examples/fixture_apache.conf`,

- No Windows SAPI inventory (php-cgi.exe/bundled php.exe) — out of scope.

- Apache config scanning does not chase `Include` recursion — pass

`--apache-conf` for non-standard layouts.

- No container/volume traversal — run the tool inside if needed.

- Response-side checks (cookie canaries, response bodies) are out of scope for

a log-file tool; see the network rules package for those layers.

- The tool is attempt-oriented: it does not (and cannot) verify a runtime was

| Rule family | CVE path | Layer | Key published basis |

| Upload-progress trigger (body) | 109619 (also in 109618-labelled tooling) | WAF/IDS/body logs | PHP field name + F5 Pattern 1/2, ELLIO 743-request telemetry, Zafran lab sigs, Corelight/Talos/FortiGate coverage pattern |

| Heap-grooming cadence | 109619 | WAF/edge logs | F5 Pattern 2 (rotating 16-hex PHPSESSID + replayed trigger + x.txt) |

| OOM amplification shapes | 109618 | WAF/ModSec audit logs | GHSA-w8hw-w9fx-j79w advisory vectors (missing/untrusted Content-Length, empty file controls, deep nesting, unbounded part headers); exact ELLIO body sizes/field shapes |

| Boundary/hardcoded/canary IOCs | both | web logs/WAF/IDS/Suricata/ModSec | F5, ELLIO, VulnCheck, Sophos, Flare, KELA, Arctic Wolf published strings — copied verbatim |

| Loader-drift (file/process event) | both (delivered Potassium) | host (auditd/Sysmon-class) | ELLIO + Sophos (`/tmp/nfs`, SHA-256 0c5486f0…, `./nfs php`) |

| Disclosure canary (response) | 109618 | full-transaction logs/WAF | Wiz "unexpectedly large or malformed PHPSESSID response cookies" |

| Version inventory (response header/phpinfo) | both | Suricata response side | Corelight SIDs 3000651/52 — adapted (BSD-3) |

- **Suricata:** add `rules/suricata/ionsec-php2shell.rules` to `rule-files` in `suricata.yaml`; validate with `suricata -T`. Body-inspection depth must cover the 8 MiB+ OOM payloads. SIDs live in the documented private/local block **1000000-1999999**; our allocation **1006500-1006519** (Base 1006500-1006502 mirrors Corelight's 3000650-52 — see in-file attribution: their logic is BSD-3-Clause (c) 2026 Corelight, Inc., adapted here with the SID renumber + msg/delta documented in ).

- **ModSecurity (CRS environments):** include `rules/modsecurity/ionsec-php2shell-dropin.conf` after your CRS include. Requires `SecRequestBodyAccess On` (CRS-recommended default). Ships detection-only (`pass,log`); blocking toggle and rationale are documented in the file header. **The OWASP CRS (3.x/4.x) has no PHP2Shell rule** (verified 2026-10-23 via php2shell.eu's vendor matrix) — this drop-in fills that gap without touching CRS rule IDs.

- **Sigma:** designed for webserver access logs, WAF/proxy logs (body-aware fields documented per rule), Linux file/process events, and response-phase transaction logs. Field-name mapping per backend (e.g., nginx `$http_cookie` → `cs-cookie`, `http.request.body.bytes` → `request_body_size`) is in each rule description. Not every field exists in vanilla NCSA combined logs — the body rules require WAF/ModSec-audit/CDN-style logging.

- **No invented fingerprints.** Every fingerprint string is copied exactly from a fetched vendor publication; each rule's YAML state the source, the fetch date (2026-10-23), and the CVE mapping. Defanged strings published by vendors (`hxxp://92.118.112[.]100/nfs-setup`) are marked with their publish form and the uncloak used for matching.

- **Adapted rules cite source + license + delta** (Corelight BSD-3-Clause; Lexsah MIT; BartoszGarba Unlicense/community; Zafran lab signatures). No community rule is copied silently.

- **No attribution claims in rule text:** vendor names appear only in /references, never as claims of endorsement or contribution.

- Tuning thresholds (counts ≥100/256, nesting ≥18, PHPSESSID ≥120 chars) are explicitly labelled as **tuning parameters**, not fingerprints.

- **No invented fingerprints.** Every fingerprint string is copied exactly from a fetched vendor publication; each rule's YAML state the source, the fetch date (2026-10-23), and the CVE mapping. Defanged strings published by vendors (`hxxp://92.118.112[.]100/nfs-setup`) are marked with their publish form and the uncloak used for matching.

- **Adapted rules cite source + license + delta** (Corelight BSD-3-Clause; Lexsah MIT; BartoszGarba Unlicense; Zafran lab sigs). No community rule is copied silently.

- **No attribution claims in rule text.**

- Tuning thresholds (counts ≥100/256, nesting ≥18, PHPSESSID ≥120 chars) are labelled tuning parameters, not fingerprints.

- **Sigma:** sigma-cli / sigtool / sigmac are **not installed** in this environment (checked 2026-10-23) — no heavy tooling was installed per build policy. All 10 YAML files validate as parseable YAML (PyYAML 6.0.3; see Validation section below for the check transcript). Rules follow the Sigma 2.x schema (standard keys; custom body fields documented in-file). For strict schema/UUID4 validation, install `sigma-cli` and run `sigma check rules/sigma/*.yml`.

- **Suricata:** no Suricata binary available in this environment — syntax was written in the Suricata 8.0.6 keyword set used by the Corelight-validated rules (`http.method`, `http.request_body`, `http.response_header`, `http.response_body`, `http.cookie`, `http.user_agent`, `threshold`), each rule was parsed/checked for balanced rule delimiters, and the Corelight-adapted rules carry logic verbatim from their 8.0.6-validated file. Run `suricata -T -c your/suricata.yaml` before enforcing in production.

- **ModSecurity:** no modsec engine available here; rules use documented v2.x-compatible syntax (chain, ARGS_POST_NAMES, MULTIPART_PART_HEADERS) consistent with CRS 4.x conventions. Load-test in a preproduction vhost before enforcing.

Host tool: run `pytest -q` from this directory (see Tests in the Host tool section above).

Curated top-28 publication list (all previously fetched and verified, grouped: primary disclosure, agency advisories, cloud/WAF mitigations, sensor observations, community detection): see **[`rules/references/php2shell-sources.md`](rules/references/php2shell-sources.md)**.

Landscape snapshot (2026-10-23) for orientation — vendor SIDs live in their feeds, not in this package: ModSecurity CRS **has no PHP2Shell rule** (our documented gap); Talos Snort SIDs 67359/67360/67375 (free Community Ruleset); FortiGate IPS 62657/62658 (defs 37.317) + FortiWeb 0.00438; Corelight SIDs 3000650-52 (open source, pre-disclosure); Cloudflare 14 "Anomalous Execution" managed rules; AWS WAF `PHP2Shell_BODY` (v2.3, 109619) + `PHP2Shell_BODY_Extended` (v2.4, 109618); Azure Front Door 99006001 (disabled by default); Google Cloud Armor GLOBAL.26.015; Palo Alto Threat ID 543004; Community Sigma repos: NCSC-NL/php2shell (operational repo; no Sigma rules upstream yet), Lexsah/php2shell-detection (MIT), BartoszGarba/php2shell-sigma-rules (Unlicense).

- ModSec drop-in denies nothing by default (`pass,log`); near-zero-false-positive canary rules are the candidates for early deny mode.

- Thresholds marked as tuning parameters (empty-file ≥100, nesting ≥18, part-headers ≥256, PHPSESSID ≥120/≥20).

- Private-range registry: Suricata block 1006500–1006519 and ModSec community-convention IDs 1026xxxx sit in an internal IONSEC ledger.

- sigma-cli/sigtool and Suricata engines are not installed in the build environment; lint (`sigma check rules/sigma/*.yml`) and `suricata -T` belong in a policy container before enforcing in production.

- CVE-2026-115560 kept out of rule scope; revisit exploit shape.

- Track php-src #24488 GC regression caution on 8.4.27/8.5.12 before advising exact fixed-version pins.