These rules are experimental, not necessarily novel (there are other “php spawning shell” rules out there) and come with no guarantees!
Sigma rule
title: PHP Worker Spawned Shell or Recon Utility (Possible PHP2Shell RCE)
id: 8f3b1c2a-6d4e-4a9b-bc77-2e5c9a1f0b42
status: experimental
description: |
Detects a PHP worker (php-fpm, php-cgi, LiteSpeed lsphp, FrankenPHP, or Apache with mod_php)
spawning a shell, interpreter, download/network tool or recon utility, or running a
reverse-shell or temp-dir dropper one-liner. Written for CVE-2026-109618 / CVE-2026-109619
("php2shell"), where suspicious PHP child processes are the expected exploitation signal.
Reported scope is PHP 5 onward with no known configuration prerequisite, so parent
matching covers legacy (php5-*) packaging. Root cause is unconfirmed by PHP; the rule is
deliberately behavioural. Also fires on PHP web shells and other PHP RCE.
references:
- https://www.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities
- https://watchtowr.com/intelligence/php-cve-2026-109618-faq/
- https://github.com/Neo23x0/process-watcher/blob/main/emerging-threats/php-rce-october-26/README.md
- https://github.com/Neo23x0/process-watcher/blob/main/emerging-threats/php-rce-october-26/guide.md
- https://www.php.net/archive/2026.php#2026-10-20-1
- https://jackalsyn.com/posts/php-up-2026.html
author: threatbear
date: 2026-10-21
modified: 2026-10-21
tags:
- attack.initial-access
- attack.t1190
- attack.execution
- attack.t1059.004
- attack.persistence
- attack.t1505.003
- cve.2026-109618
- cve.2026-109619
- detection.emerging-threats
logsource:
category: process_creation
product: linux
detection:
# Versioned binaries (php-fpm8.3, lsphp82) and vendor prefixes
# (/opt/remi/php74/..., /opt/cpanel/ea-php81/..., /opt/plesk/php/8.1/...) need 'contains'
parent_php:
ParentImage|contains:
- '/php-fpm'
- '/php-cgi'
- '/php5-fpm' # PHP 5 era packaging (Debian 8 / Ubuntu 14.04): EOL, so unpatched
- '/php5-cgi'
- '/lsphp'
- '/frankenphp'
# mod_php runs PHP inside the Apache worker
parent_modphp:
ParentImage|endswith:
- '/apache2'
- '/httpd'
# Telemetry usually reports the resolved binary (sh -> dash, nc -> nc.openbsd)
child_tools:
Image|endswith:
- '/sh'
- '/dash'
- '/bash'
- '/zsh'
- '/ash'
- '/ksh'
- '/busybox'
- '/curl'
- '/wget'
- '/nc'
- '/nc.openbsd'
- '/nc.traditional'
- '/ncat'
- '/netcat'
- '/socat'
- '/socat1' # Rocky/RHEL 9
- '/telnet'
- '/inetutils-telnet' # Debian 13
- '/openssl'
- '/whoami'
- '/id'
- '/uname'
- '/hostname'
- '/ifconfig'
- '/base64'
- '/mkfifo'
# Catches versioned interpreters (python3.11, perl5.36)
child_interpreters:
Image|contains:
- '/python'
- '/perl'
- '/ruby'
# PHP CLI below a web worker: common for php -r reverse shells and stagers.
# Regex keeps php-fpm/php-cgi themselves out of the match.
child_php_cli:
Image|re: '/php[0-9.]*$'
# High-confidence patterns: never suppressed by the allowlist below.
# '!notwrtbl!' is the marker from the PHP FastCGI temp-dir dropper Huntress observed.
cmd_revshell_or_dropper:
CommandLine|contains:
- '/dev/tcp/'
- '/dev/udp/'
- 'fsockopen'
- 'mkfifo'
- 'bash -i'
- 'sh -i'
- 'nc -e'
- 'ncat -e'
- 'TCP-LISTEN' # socat bind shell
- 'EXEC:' # socat exec to shell
- 'pty.spawn' # python TTY upgrade
- 'socket.socket' # python one-liner shells
- '!notwrtbl!'
# PHP exec()/system()/shell_exec()/popen()/mail() run "sh -c -- <command>" (the "--"
# is optional for older builds). Allowlist only when the command STARTS with a known
# tool, so "curl ...; sh #ffmpeg" can't ride along.
filter_known_good:
CommandLine|re: '^(/usr)?(/bin/)?(sh|dash|bash) -c (-- )?(/usr/(s)?bin/|/usr/local/bin/)?(sendmail|convert|magick|ffmpeg|ffprobe|pdftotext)( |$)'
# ...and never when the command chains or substitutes further commands
shell_chaining:
CommandLine|contains:
- ';'
- '|'
- '&&'
- '`'
- '$('
condition: 1 of parent_* and (cmd_revshell_or_dropper or (1 of child_* and not (filter_known_good and not shell_chaining)))
falsepositives:
- Applications that shell out to tools beyond the allowlist (git deploy hooks, backup plugins, control panels) - add anchored entries to filter_known_good
- Non-PHP CGI scripts under Apache when mod_cgi is enabled
- Apps that exec the PHP CLI (artisan, wp-cli, composer from admin panels) - allowlist per host
level: high
Elastic EQL - experimental
Tested in a lab with a custom php program that spawned a shell.
process where host.os.type == "linux" and event.type == "start" and
event.action in ("exec", "exec_event", "start", "executed", "process_started") and
/* PHP worker parents: versioned / vendor-prefixed paths, PHP 5 packaging, LiteSpeed, FrankenPHP, mod_php */
process.parent.executable like~ (
"*/php-fpm*", "*/php-cgi*", "*/php5-fpm*", "*/php5-cgi*", "*/lsphp*", "*/frankenphp*",
"*/apache2", "*/httpd"
) and
(
/* High confidence: reverse/bind shells and the Huntress temp-dir dropper marker. Never allowlisted. */
process.command_line like~ (
"*/dev/tcp/*", "*/dev/udp/*", "*fsockopen*", "*mkfifo*", "*bash -i*", "*sh -i*",
"*nc -e*", "*ncat -e*", "*TCP-LISTEN*", "*EXEC:*", "*pty.spawn*", "*socket.socket*",
"*!notwrtbl!*"
) or
(
/* Shells, network/download tools, recon utilities, interpreters, PHP CLI */
(
process.name in (
"sh", "dash", "bash", "zsh", "ash", "ksh", "busybox",
"curl", "wget", "nc", "nc.openbsd", "nc.traditional", "ncat", "netcat",
"socat", "socat1", "telnet", "inetutils-telnet", "openssl",
"whoami", "id", "uname", "hostname", "ifconfig", "base64", "mkfifo"
) or
process.name like~ ("python*", "perl*", "ruby*") or
process.name regex~ "php[0-9.]*"
) and
/* Allowlist: PHP's "sh -c -- <tool>" for mail()/ImageMagick/ffmpeg/pdftotext,
only when the command STARTS with the tool and does not chain further commands */
not (
process.command_line regex~ "(/usr)?(/bin/)?(sh|dash|bash) -c (-- )?(/usr/s?bin/|/usr/local/bin/)?(sendmail|convert|magick|ffmpeg|ffprobe|pdftotext)( .*)?" and
not process.command_line like~ ("*;*", "*|*", "*&&*", "*`*", "*$(*")
)
)
)
Hopefully this will help some defenders :)