Detecting PHP2Shell: Telemetry and Choke Points From Web Shell to Root

Detecting PHP2Shell: Telemetry and Choke Points From Web Shell to Root

pr0h0••21 min read•hacking
AI Usage (79%)
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:

ClaimSourceConfidence
A chain referred to as PHP2Shell plus Linux kernel exploits was used to gain rootCyberSecurityNews, 29 Oct 2026; The Hacker News, 28 Oct 2026Reported
Root access was obtained on 25 serversCyberSecurityNewsReported
One affected network is described as U.S. government-affiliatedThe Hacker NewsReported
The intrusion combined an initial PHP-level foothold with a local privilege escalation to rootBoth, via the "chain" framingReported

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:

  1. HTTP request → PHP code execution of attacker-controlled content
  2. PHP process → child process on the host
  3. 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.

http-shell-hunt.sh
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 | head

Observed 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.

php-fpm pool: slow log settings
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log

Captured 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.

mtime-ctime-divergence.sh
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/null

Observed:

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?"

php-capability-scan.sh
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/null

Observed 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. .php files 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:

audit.rules: web-tier execve accounting
# /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_write

Verify 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

query
ausearch -k web_exec -i -ts recent | grep -E 'proctitle|EXECVE|key=' | tail -n 6
type=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

ParentChildNormal?Why
php-fpmsh, bash, dashNoNothing in a PHP request lifecycle needs a shell
php-fpmuname, id, whoami, catNoClassic enumeration verbs
php-fpmfind with -permNoSetuid discovery
php-fpmsendmailSometimesmail() on some configs shells out
php-fpmconvert, ffmpegSometimesThumbnail/media pipelines
php-fpmgit, rsyncSometimesDeploy hooks called from app code
php-fpmcurl, wgetRareVery rarely legitimate in request path
php-fpmnc, socat, /dev/tcp useNoReverse shell or egress channel
cronshYesExpected
systemdshYesExpected

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 → convert only when argv matches your media pipeline's argument shape.
  • Suppress php-fpm → sendmail on hosts with a documented mail-sending application, and log it as notice instead of alert.
  • Never suppress php-fpm → sh where the child's argv is a single -c followed 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

escalation-surface-snapshot.sh
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

QuerySignalFalse positivesShip as
grep capability scan over webrootExec/decode primitive in a PHP fileFramework internals, image librariesDaily scan + diff
find -newerct ! -newermtmtime/ctime divergenceDeploy tools preserving mtimeTriage filter
ausearch -k web_execphp-fpm → shMedia, mail, git hooksAlert
php-fpm slowlog traceLong-running request from non-app pathSlow DB queriesAlert on path mismatch
php-fpm /status script listingScript outside deploy manifestOld abandoned scriptsWeekly reconciliation
/proc/sys/kernel/taintedKernel taint bitsNvidia, VirtualBox, DKMSAlert on D bit only
ss -tulpn diffNew listening socketLegitimate new servicesDaily baseline diff
auditctl key preload/etc/ld.so.preload writeRare, usually adminAlert

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.

  1. Capture volatile state first. dmesg and /proc/<pid>/ disappear on reboot. Dump dmesg, ss -tulpn, lsof -p <pid>, ps -ef --forest, and ausearch -k web_exec -i --raw > /evidence/web_exec.log before touching anything.
  2. Snapshot the disk if you can, or at minimum archive the webroot with timestamps and ctimens preserved (tar --xattrs).
  3. Isolate the network, do not power off. Blocking egress/ingress preserves memory and open sockets.
  4. 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 .env file was readable.
  5. Hunt laterally by credential, not by IP. The attacker's next hop used legitimate credentials, so network indicators will not find it.
  6. 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

php.ini: web-tier hardening
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 = 0

user_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.

php-fpm pool: per-site isolation
[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,popen

Per-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

  1. Make the webroot read-only to the PHP user. This removes stage 1's utility entirely and is cheaper than every detection rule combined.
  2. 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.
  3. Kernel reboot cadence. This is what decides whether stage 2 succeeds. Everything else is mitigation.
  4. Reduce kernel attack surface (userns, kptr_restrict, dmesg_restrict, ptrace_scope). Buys time between patch availability and patch application.
  5. Per-site pool isolation. Contains blast radius, does nothing to stop escalation.
  6. disable_functions and .user.ini removal. 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-fpm worker spawning sh -c is capturable with four audit rules scoped to the web uid, at low volume.
  • mtime/ctime divergence reliably surfaces a timestomped PHP dropper with a single find expression.
  • The slow-log trace names system() and eval() inside the dispatcher, and pids join across access log, slow log, and ausearch.
  • 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.

Share this post

More posts

Loading comments…