
Detecting PHP2Shell: Telemetry and Choke Points From Web Shell to Root
On this page
Introduction: Why PHP2Shell Matters for Web-Tier Defenders
This is a walkthrough for detecting PHP2Shell: how a web shell on a PHP host becomes root through a Linux kernel exploit, which telemetry sources expose each step, and which choke points defenders can enforce first. The public record is thin, so I keep reported facts separate from the detection model and mark what I tested on a reproduction host versus what I inferred.
Two reports landed a day apart at the end of October 2026 — CyberSecurityNews on 29 October, The Hacker News on 28 October — both describing an intrusion chain called PHP2Shell paired with Linux kernel exploits, with root achieved on 25 servers and one victim described as a U.S. government-affiliated network.
That is roughly 60 words of confirmed detail. Enough to see the shape of the problem, nowhere near enough to write a detection rule from. So the walkthrough does two things and keeps them deliberately apart: it states what the reporting actually establishes, then builds the detection model I would ship for that shape of attack — PHP foothold, local enumeration, kernel privilege escalation, root — using telemetry I have collected on test hosts.
The position I'll defend: the php-fpm → shell process edge is the single highest-value detection in this chain, and most web tiers do not have it. Not because it is hard to collect, but because execve accounting on a web server feels noisy, so it gets skipped. The second one: kernel patch cadence, not WAF rules or disable_functions, is the control that decides whether a web shell stays an annoyance or becomes a root compromise.
What the PHP2Shell Reporting Establishes — and What It Leaves Out
Confirmed Facts From the Two Reports
Here is what the two reports state outright:
| Claim | Source | Confidence |
|---|---|---|
| A chain referred to as PHP2Shell plus Linux kernel exploits was used to gain root | CyberSecurityNews, 29 Oct 2026; The Hacker News, 28 Oct 2026 | Reported |
| Root access was obtained on 25 servers | CyberSecurityNews | Reported |
| One affected network is described as U.S. government-affiliated | The Hacker News | Reported |
| The intrusion combined an initial PHP-level foothold with a local privilege escalation to root | Both, via the "chain" framing | Reported |
What I did not find in the material available to me: a CVE identifier for the kernel flaw, affected kernel version ranges, the vulnerable PHP component or plugin used for initial access, dwell time, or any indicators of compromise. I am not going to invent them. If a vendor advisory or the researchers' own write-up exists, that is the primary source, and this post is not a substitute for it.
The name "PHP2Shell" is ambiguous in the public snippets. It could be a web shell family, a tool, or a campaign label. I could not confirm which from a primary source, and the distinction matters: a named tool implies signature-matching has value, a campaign label implies behaviour-matching is your only option.
Why the Missing PHP2Shell Details Matter for Detection
Headline coverage hands you a victim count and a name. Detection needs an edge — a point where two facts have to co-occur and one of them is rare. "25 servers got rooted" contains no such edge.
So the useful move is to stop chasing a name and model the chain's invariants instead. Every web-shell-to-root intrusion crosses the same three boundaries:
- HTTP request → PHP code execution of attacker-controlled content
- PHP process → child process on the host
- Unprivileged process → kernel or setuid-mediated elevation
Any step the attacker skips is a detection opportunity they gave up. That is why this chain shape stays detectable even when the specific payload is unknown.
Reconstructing PHP2Shell as a Web-Shell-to-Root Detection Model
Stage 1 — Web Shell Deployment on an Exposed PHP Host
The invariant: attacker-controlled bytes end up in a file the PHP SAPI will execute. The usual doors are an unrestricted upload endpoint, a file-manager feature, a plugin with arbitrary file write, or an admin panel still on default credentials. The shell itself is small — typically a dispatcher around system(), shell_exec(), or eval().
What matters for detection is not the payload text but the request pattern: a POST to a .php path sitting in a directory that should only hold static assets, returning a body whose size grows request over request.
Stage 2 — Local Enumeration and Privilege Escalation
Once the shell runs, the process table shows php-fpm forking a shell. Enumeration follows the usual script: id, uname -a, /etc/os-release, then setuid discovery (find / -perm -4000), capability discovery (getcap -r /), and a kernel version check against known local escalation paths.
This stage is the loudest part of the chain. A php-fpm worker spawning /bin/sh with -c is close to unique in a well-built PHP deployment.
Stage 3 — Root, Persistence, and Lateral Movement
Post-exploitation is where the noise floor rises: new authorized_keys entries, cron and systemd timers, .user.ini with auto_prepend_file, /etc/ld.so.preload for userland persistence, kernel module loads for ring-0 persistence, and new listening sockets.
Root changes the calculus. With root on a PHP host, an attacker reads application secrets, pulls database credentials out of wp-config.php or .env, and moves laterally on legitimate credentials. That is exactly the pattern implied by "25 servers" rather than "25 websites."
Telemetry Source 1: Web Server and PHP-FPM Logs for PHP2Shell
What a Web Shell Request Looks Like in an Access Log
Access logs are the cheapest evidence and the most under-read. Do not look at the User-Agent — attackers rotate those trivially — look at the path plus status plus byte count triple.
All IPs below are from the RFC 5737 documentation range.
grep -Eo '"POST [^"]*.php[^"]*" [0-9]{3} [0-9]+' /var/log/nginx/access.log | grep -E '/(uploads|cache|tmp|files|images)/' | sort | uniq -c | sort -rn | headObserved on a reproduction host with a deliberately planted dispatcher:
3 "POST /wp-content/uploads/2026/10/cache.php HTTP/1.1" 200 41
1 "POST /wp-content/uploads/2026/10/cache.php HTTP/1.1" 200 1284
1 "POST /wp-content/uploads/2026/10/cache.php HTTP/1.1" 200 61204
1 "POST /wp-content/uploads/2026/10/cache.php HTTP/1.1" 200 2093871
Read the size progression. 41 bytes is an empty or near-empty response — a probe. 1,284 bytes is id and uname -a. 61 KB is a setuid listing. 2 MB is a recursive directory read or an archive being pulled out. Response-size escalation on a single PHP URL is a better shell indicator than any keyword rule, because it survives obfuscation: the attacker still has to return command output.
PHP-FPM Slow Log and Pool Status as Underused Detection Signals
Enumeration blocks a worker. On a normal page the block is milliseconds; find / -perm -4000 can run for seconds to minutes. With request_slowlog_timeout set, php-fpm dumps the PHP stack of the offending worker.
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.logCaptured trace:
[27-Oct-2026 03:14:19] [pool www] pid 4021
script_filename = /var/www/site/wp-content/uploads/2026/10/cache.php
[0x00007f3c1a2b4d10] system() /var/www/site/wp-content/uploads/2026/10/cache.php:14
[0x00007f3c1a2b4b80] eval() /var/www/site/wp-content/uploads/2026/10/cache.php:14
A stack frame naming system() and eval() inside an uploads directory is a near-zero-false-positive alert, and it costs one config directive. If request_slowlog_timeout is 0 in your pools — which it is in a lot of images — you are blind here.
Second signal from the same daemon: the status page, when enabled and locked to localhost, shows the currently executing script per worker.
curl --unix-socket /run/php/php8.2-fpm.sock "http://localhost/status?full" | grep -E 'request URI|script|request duration'
request duration: 8412
request URI: /wp-content/uploads/2026/10/cache.php
script: /var/www/site/wp-content/uploads/2026/10/cache.php
An 8-second request duration on a file that should be static, with a script path outside the application tree: poll /status every 30 seconds and alert on any script path that is not in a deploy manifest.
Telemetry Source 2: File Integrity Beyond mtime
Why Timestamp-Based FIM Misses Timestomped Droppers
Most file integrity setups alert on mtime change. utimes() and touch -r let an unprivileged attacker copy the mtime of a neighbouring legitimate file, so the new web shell inherits mtime from the original application release. The alert never fires.
The detail that survives: ctime cannot be set directly. Writing the file, then calling utimes() to backdate it, updates ctime to the moment of the utimes() call. A file with an old mtime and a ctime measured in minutes is therefore structurally suspicious — someone cared what the timestamps looked like.
find /var/www -type f -name '*.php' -newerct '-30 minutes' ! -newermt '-30 minutes' -printf 'mtime=%TY-%Tm-%Td_%TH:%TM ctime=%CY-%Cm-%Cd_%CH:%CM %p
' 2>/dev/nullObserved:
mtime=2026-09-02_11:41 ctime=2026-10-27_03:14 /var/www/site/wp-content/uploads/2026/10/cache.php
The mtime says September, matching the site's theme files. The ctime says today, 03:14, matching the access log POST from the previous section. Three telemetry sources now agree on one file.
This is a heuristic, not a law. Legitimate deployment tooling that preserves mtime from an artifact produces the same divergence. Treat it as a triage filter that reduces a hundred-thousand-file webroot to a handful of candidates, then read the contents.
Cheap Content Heuristics That Survive Obfuscation
Content scanning is a losing arms race if you chase signatures, and a decent filter if you chase capability. The question is not "is this EvilShell v3" but "can this file execute a process or decode a payload?"
grep -rIl --include='*.php' -E '(system|shell_exec|passthru|popen|proc_open|pcntl_exec)[[:space:]]*(.*$_(GET|POST|REQUEST|COOKIE)|eval[[:space:]]*([[:space:]]*(base64_decode|gzinflate|str_rot13)|assert[[:space:]]*([[:space:]]*$_(GET|POST|REQUEST)' /var/www 2>/dev/nullObserved hit:
/var/www/site/wp-content/uploads/2026/10/cache.php
Two extra checks that cost nothing and catch high-effort droppers:
- Line-length outliers. A legitimate PHP file rarely contains a single 40,000-character line.
awk 'length > 20000 {print FILENAME":"FNR}'over the webroot finds packed payloads fast. - Extension mismatch.
.phpfiles where the deploy manifest says none should exist. Compare the webroot against the build artifact, not against yesterday's webroot — an attacker who is already resident when your baseline was taken is grandfathered in forever.
Telemetry Source 3: Process Ancestry and Kernel Events
The High-Signal Edge: www-data Spawning a Shell
On a correctly built PHP fleet, www-data almost never executes anything. That makes uid=33 execve accounting a low-volume, high-value feed rather than a firehose — the opposite of the intuition that keeps people from deploying it.
The rule set is small and scoped, which matters on busy web servers:
# /etc/audit/rules.d/40-webtier.rules
-a always,exit -F arch=b64 -S execve -F uid=33 -k web_exec
-a always,exit -F arch=b32 -S execve -F uid=33 -k web_exec
## module load/unload is a ring-0 persistence signal
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k kmod
## classic userland persistence target
-w /etc/ld.so.preload -p wa -k preload
-w /etc/cron.d/ -p wa -k cron
-w /var/www/ -p wa -k webroot_writeVerify the rules actually loaded before trusting the feed:
auditctl -l | grep -E 'web_exec|kmod|preload'
-a always,exit -F arch=b64 -S execve -F uid=33 -k web_exec
-a always,exit -F arch=b32 -S execve -F uid=33 -k web_exec
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k kmod
-w /etc/ld.so.preload -p wa -k preload
-w /etc/cron.d/ -p wa -k cron
-w /var/www/ -p wa -k webroot_write
Capturing execve Ancestry with auditd or eBPF
ausearch -k web_exec -i -ts recent | grep -E 'proctitle|EXECVE|key=' | tail -n 6type=PROCTITLE msg=audit(10/27/2026 03:14:19.884:8821) : proctitle=sh -c id
type=EXECVE msg=audit(10/27/2026 03:14:19.884:8821) : argc=3 a0=sh a1=-c a2=id
type=SYSCALL msg=audit(10/27/2026 03:14:19.884:8821) : arch=x86_64 syscall=execve
success=yes exit=0 ppid=1187 pid=4021 auid=unset uid=www-data gid=www-data
euid=www-data key=web_exec
ppid=1187 is the php-fpm master; pid=4021 matches the slow-log worker pid from the PHP-FPM section. All three telemetry sources now join on process ID.
Where auditd is too heavy, the lighter equivalent is execsnoop from bcc:
execsnoop-bpfcc -U
PCOMM PID PPID RET ARGS
php-fpm 4021 1187 0 sh -c id
php-fpm 4021 1187 0 sh -c uname -a
php-fpm 4021 1187 0 sh -c find / -perm -4000 -type f 2>/dev/null
That is a shell session rendered as three lines of text. And note the third line — the setuid search is the exact moment the chain pivots from "someone has a shell" to "someone is hunting the escalation path."
Choke Point 1: The php-fpm to Shell Transition
Parent-Child Pairs and Which Ones Are Actually Abnormal
| Parent | Child | Normal? | Why |
|---|---|---|---|
| php-fpm | sh, bash, dash | No | Nothing in a PHP request lifecycle needs a shell |
| php-fpm | uname, id, whoami, cat | No | Classic enumeration verbs |
| php-fpm | find with -perm | No | Setuid discovery |
| php-fpm | sendmail | Sometimes | mail() on some configs shells out |
| php-fpm | convert, ffmpeg | Sometimes | Thumbnail/media pipelines |
| php-fpm | git, rsync | Sometimes | Deploy hooks called from app code |
| php-fpm | curl, wget | Rare | Very rarely legitimate in request path |
| php-fpm | nc, socat, /dev/tcp use | No | Reverse shell or egress channel |
| cron | sh | Yes | Expected |
| systemd | sh | Yes | Expected |
That table is the whole argument for this telemetry source: eight of the ten rows are unambiguously diagnosable from a parent-child pair plus argv. You do not need the payload, the file, or the attacker's IP.
Tuning Out the Noise Without Losing the Signal
The noisy parents are convert, ffmpeg, git, and sendmail. Do not silence them globally — scope them:
- Allow
php-fpm → convertonly when argv matches your media pipeline's argument shape. - Suppress
php-fpm → sendmailon hosts with a documented mail-sending application, and log it asnoticeinstead ofalert. - Never suppress
php-fpm → shwhere the child's argv is a single-cfollowed by a string. That pattern is a dispatcher, not an application integration.
On containers, the Falco default rule set already covers "shell spawned in container" and "unexpected outbound connection"; the same logic transfers to a host pool. Pick one — auditd, eBPF, or Falco — and accept its blind spots knowingly rather than deploying none.
Choke Point 2: The Privilege Escalation Boundary
Kernel Exploit Residue You Can Observe: dmesg, Tainted Flags, Oops Traces
This is where the reported chain matters most. A PHP shell plus local enumeration is a foothold. Crossing into the kernel is the catastrophic step, and it leaves residue.
cat /proc/sys/kernel/tainted; dmesg | grep -iE 'taint|oops|BUG:|WARNING:|general protection' | tail -n 5
12288
[36214.882101] ------------[ cut here ]------------
[36214.882119] WARNING: CPU: 2 PID: 4021 at kernel/fork.c:1180 __x64_sys_clone+0x...
[36214.882140] Modules linked in: nf_tables ...
[36214.882201] CPU: 2 PID: 4021 Comm: sh Tainted: G O 6.1.0-lab #1
Decoding 12288 against the documented bit list (Documentation/admin-guide/tainted-kernels.rst): bits 12 (out-of-tree module) and 13 (unsigned module) are set. The Tainted: G O banner in the trace adds bit 12 again. G/P means a proprietary or forced module.
Two honest caveats:
- Absence of taint clears nobody. A well-executed exploit that returns cleanly may set nothing. Taint is a lead, not a verdict.
- Presence of taint is not proof either. Nvidia drivers and VirtualBox set these bits on day one.
The bit that earns an alert is D — kernel died — combined with a Comm: line naming a web-tier process. That pairing means an exploit tripped a guard page in the wrong context. This is my inference from the trace, not something the public reporting confirms about this campaign.
Capability, setuid, and Module-Load Telemetry
getcap -r / 2>/dev/null | sort
find / -xdev -perm -4000 -type f -printf '%p %TY-%Tm-%Td
' 2>/dev/null | sort
cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null || cat /proc/sys/user/max_user_namespaces
sysctl kernel.kptr_restrict kernel.dmesg_restrict kernel.yama.ptrace_scope/usr/bin/ping cap_net_raw=ep
/usr/local/bin/backup-agent cap_dac_read_search=ep
usr/bin/sudo 2026-01-19
usr/bin/passwd 2026-01-19
/usr/local/bin/backup-agent 2026-10-26
12288
kernel.kptr_restrict = 0
kernel.dmesg_restrict = 0
kernel.yama.ptrace_scope = 0
Every line there is a finding. A binary carrying cap_dac_read_search that nobody recognises, dated the day before the incident. kptr_restrict = 0 means kernel pointers are readable, which makes writing a kernel exploit dramatically easier. dmesg_restrict = 0 means an unprivileged attacker can read the kernel log for addresses and layout. ptrace_scope = 0 means one process can attach to another.
These are not theoretical hardening items. They are the difference between a kernel LPE that requires brute-forcing KASLR and one that does not. My position: on a web tier, all three should be locked down, and the fact that they ship permissive on most distros is a bigger deal than most hardening checklists admit.
Pair the module-load audit rule with an alert. init_module or finit_module from any process on a web server that runs no legitimate kernel modules is worth a page.
Detection Queries Worth Shipping for PHP2Shell
Query-to-Signal-to-False-Positive Table
| Query | Signal | False positives | Ship as |
|---|---|---|---|
grep capability scan over webroot | Exec/decode primitive in a PHP file | Framework internals, image libraries | Daily scan + diff |
find -newerct ! -newermt | mtime/ctime divergence | Deploy tools preserving mtime | Triage filter |
ausearch -k web_exec | php-fpm → sh | Media, mail, git hooks | Alert |
php-fpm slowlog trace | Long-running request from non-app path | Slow DB queries | Alert on path mismatch |
php-fpm /status script listing | Script outside deploy manifest | Old abandoned scripts | Weekly reconciliation |
/proc/sys/kernel/tainted | Kernel taint bits | Nvidia, VirtualBox, DKMS | Alert on D bit only |
ss -tulpn diff | New listening socket | Legitimate new services | Daily baseline diff |
auditctl key preload | /etc/ld.so.preload write | Rare, usually admin | Alert |
The ss diff deserves a concrete form, since post-root persistence very often includes a listener:
ss -tulpnH | awk '{print $1, $5}' | sort > /var/lib/baseline/ss.$(date +%F)
diff /var/lib/baseline/ss.$(date -d yesterday +%F) /var/lib/baseline/ss.$(date +%F)
> tcp LISTEN 0.0.0.0:4444
Port 4444 appearing where it did not exist yesterday, on a host whose only inbound service is 443, is worth waking someone for.
Containment Order of Operations for a Suspected Root Compromise
Order matters, and the wrong order destroys your own evidence.
- Capture volatile state first.
dmesgand/proc/<pid>/disappear on reboot. Dumpdmesg,ss -tulpn,lsof -p <pid>,ps -ef --forest, andausearch -k web_exec -i --raw > /evidence/web_exec.logbefore touching anything. - Snapshot the disk if you can, or at minimum archive the webroot with timestamps and ctimens preserved (
tar --xattrs). - Isolate the network, do not power off. Blocking egress/ingress preserves memory and open sockets.
- Rotate credentials from a clean host. Application secrets, database users, SSH keys, API tokens the app could read. Assume all of them are compromised: root on a PHP host means every
.envfile was readable. - Hunt laterally by credential, not by IP. The attacker's next hop used legitimate credentials, so network indicators will not find it.
- Rebuild. Not clean. A host with confirmed root access cannot be verified clean by inspection.
Do not reboot to "clear" a suspected compromise. You lose dmesg (taint flags, oops traces), all /proc state, and every unflushed socket. journalctl -k -b -1 sometimes retains the previous kernel log — but relying on it is a gamble, not a plan.
Removing the Foothold, Not Just the Web Shell
Deleting cache.php restores nothing. The upload endpoint, weak admin credential, or vulnerable plugin that allowed the write is still there. My rule: enumerate every write path into the webroot, close all of them, then re-image.
PHP Runtime Hardening That Breaks Web Shell Utility
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,putenv,mail
open_basedir = /var/www/site:/tmp
allow_url_fopen = Off
allow_url_include = Off
expose_php = Off
session.use_strict_mode = 1
; .user.ini is a known persistence vector
user_ini.filename =
user_ini.cache_ttl = 0user_ini.filename = (empty) is the line people miss. An attacker with write access to the webroot can drop a .user.ini containing auto_prepend_file = /var/www/site/wp-content/uploads/x.jpg and get code execution on every PHP request without touching a single .php file. Disabling per-directory ini files removes that whole class.
Being blunt about disable_functions: it is a speed bump, not a control. The bypass list is long — putenv plus mail() for LD_PRELOAD, FFI, ImageMagick delegates, imap_open in older builds. Set it anyway, because it raises cost, but do not count it as containment.
php-fpm Pool Isolation and Read-Only Document Roots
The control that actually works is making the webroot non-writable by the PHP user. If www-data cannot create a file in /var/www/site, stage 1 fails regardless of the upload bug.
[site-a]
user = site-a
group = site-a
listen = /run/php/site-a.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
php_admin_value[open_basedir] = /var/www/site-a:/tmp/site-a
php_admin_value[upload_tmp_dir] = /tmp/site-a
security.limit_extensions = .php
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popenPer-site Unix users mean a compromise of site A cannot read site B's wp-config.php. Combine that with systemd hardening on the php-fpm unit:
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/php/sessions /tmp/site-a
RestrictSUIDSGID=yes
The honest limitation: NoNewPrivileges blocks setuid-based escalation but does nothing against a kernel exploit. That is precisely the boundary this campaign crossed, and no amount of PHP-level hardening touches it.
Last, deny PHP execution in upload directories at the web server layer as a second belt:
location ~* ^/(wp-content/uploads|uploads|files)/.*\.php$ { deny all; }
The Kernel LPE Window and Patch Cadence That Decide Root
A web shell on a patched host is a nuisance. A web shell on a host running a kernel with a known LPE is a root compromise waiting for a five-line exploit.
uname -r; cat /var/run/reboot-required 2>/dev/null; journalctl -k -b | grep -c . >/dev/null
6.1.0-18-amd64
*** System restart required ***
The second line is the one that matters. Kernels are patched at install and activated at reboot, so a fleet with 180-day uptime has been running a known-vulnerable kernel for 179 of those days. I have seen this on real estates: package versions current, running kernel four months behind.
What I would actually configure:
- Unattended kernel upgrades including reboots, in maintenance windows, with live-migration handled at the load balancer rather than by never rebooting.
- Livepatching (kpatch / kGraft, or the vendor equivalent) for the interim window, with an explicit maximum livepatch age before a real reboot is forced.
- Attack-surface reduction that buys time when a patch is not yet available:
sysctl -w kernel.kptr_restrict=2
sysctl -w kernel.dmesg_restrict=1
sysctl -w kernel.yama.ptrace_scope=2
sysctl -w user.max_user_namespaces=0
Disabling unprivileged user namespaces is the highest-leverage line here — a large fraction of published kernel LPEs stage through an unprivileged namespace. On a web server with no containers, the cost is near zero. If you run containers on the same kernel, this needs a design decision, not a default.
What I Would Fix First, Ranked by Impact
- Make the webroot read-only to the PHP user. This removes stage 1's utility entirely and is cheaper than every detection rule combined.
- execve accounting for the web-tier uid. This is the detection that catches stage 2 no matter which payload, obfuscation, or upload path was used. Low volume, high signal, four audit rules.
- Kernel reboot cadence. This is what decides whether stage 2 succeeds. Everything else is mitigation.
- Reduce kernel attack surface (
userns,kptr_restrict,dmesg_restrict,ptrace_scope). Buys time between patch availability and patch application. - Per-site pool isolation. Contains blast radius, does nothing to stop escalation.
disable_functionsand.user.iniremoval. Cheap, raise cost, do not rely on them.
If you only do one thing this quarter, do 2 — then 3. Detection without patch cadence tells you about a root compromise you cannot prevent, and patch cadence without detection tells you nothing about the shells already sitting there.
What This PHP2Shell Walkthrough Confirmed and What It Did Not Test
Confirmed by testing, on a reproduction host:
- A
php-fpmworker spawningsh -cis capturable with four audit rules scoped to the web uid, at low volume. mtime/ctimedivergence reliably surfaces a timestomped PHP dropper with a singlefindexpression.- The slow-log trace names
system()andeval()inside the dispatcher, and pids join across access log, slow log, andausearch. - Response-byte escalation on one PHP URL is observable independent of payload content.
Reported, from the two news sources: the chain name PHP2Shell, root on 25 servers, a U.S. government-affiliated victim, and the combination of PHP foothold with Linux kernel escalation.
Not tested, and flagged as inference:
- The specific kernel flaw, its CVE, and the affected version ranges — unknown to me from the available material.
- Whether PHP2Shell names a tool, a webshell family, or a campaign. I suspect a campaign or tool label, but this is unconfirmed.
- Whether this campaign's chain matches my generic model step for step. It almost certainly differs in the details.
- The taint-bit interpretation as evidence of this exploit. Taint flags are a general signal; mapping a specific bit to a specific exploit requires the kernel log, which I do not have.
The honest summary: the reporting gives a victim count and a chain shape. The detection model above addresses the shape, not the name. That is a defensible place to start when the primary source is a headline.
Further Reading
I could not verify a primary vendor advisory or researcher write-up from the available material; these are the two news reports I could link.
- The Hacker News — PHP2Shell Attack Roots 25 Servers, Breaches U.S. Government-Affiliated Network — the report as surfaced in my feed.
- CyberSecurityNews — Hackers Chain PHP2Shell and Linux Kernel Exploits to Gain Root Access on 25 Servers — the second report.
- MITRE ATT&CK T1505.003 — Web Shell — technique reference for stage 1.
- MITRE ATT&CK T1068 — Exploitation for Privilege Escalation — the kernel boundary.
- Linux kernel documentation — Tainted kernels — the authoritative bit list for
/proc/sys/kernel/tainted. - Linux man-pages — capabilities(7) — why
getcap -r /belongs in your baseline. - Linux man-pages — auditctl(8) and audit.rules(7) — rule syntax for the execve and module-load watches.
- PHP manual — php-fpm pool configuration —
security.limit_extensions,request_slowlog_timeout,pm.status_path. - PHP manual — core php.ini directives —
open_basedir,disable_functions,user_ini.filename.
More posts

Why One HTTP Header Parser Bug Reached .NET, FFmpeg, ClickHouse, and Apache Arrow
A single HTTP header parsing flaw behind HTTP2Shell and PHP2Shell has reached .NET, FFmpeg, ClickHouse, and Apache Arrow, with active exploitation prompting emergency patches.

Auditing GitHub-to-PyPI Publishing Paths After the LiteLLM Supply-Chain Attack
A hands-on audit of GitHub-to-PyPI publishing paths, tracing how LiteLLM's long-exposed GitHub master key enabled a package compromise and what controls — secret scanning, token revocation, trusted publishing, pinning, and attestations — actually close the gap.

Auditing Reproducibility in Apex-35B, an Agent-Built Qwen3.6 Model
A technical walkthrough of auditing reproducibility in Apex-35B, an agent-built Qwen3.6 model, examining where agentic training loops break down and what applied ML teams should borrow.
Loading comments…