// Description: This query attempts to identify post-exploitation command execution within the context of a PHP web server or worker process.
// Notes: This hunting query is not specific to CVE-2026-109618 and CVE-2026-109619. It is capable of identifying exploitation of various web application vulnerabilities. This query may also produce false positive findings depending on the specific web applications running. We recommend validating if any identified activity is expected for the web applications running on systems.
config case_sensitive = false
| dataset = xdr_data
| filter event_type = ENUM.PROCESS and event_sub_type = ENUM.PROCESS_START and action_process_image_name != null
| fields _time, agent_hostname, agent_id, event_id, actor_effective_username, actor_process_image_name, actor_process_command_line, action_process_image_name, action_process_image_path, action_process_image_command_line, causality_actor_process_image_name, causality_actor_process_command_line
// Lowercase fields once
| alter
actor_effective_username = lowercase(actor_effective_username),
actor_process_image_name = lowercase(actor_process_image_name),
actor_process_command_line = lowercase(actor_process_command_line),
action_process_image_name = lowercase(action_process_image_name),
action_process_image_path = lowercase(action_process_image_path),
action_process_image_command_line = lowercase(action_process_image_command_line),
causality_actor_process_image_name = lowercase(causality_actor_process_image_name)
// Anchor: PHP web worker (or the launcher that started it) as causality, PHP worker as direct parent,
// a shell directly under an IIS worker, or the web service account outside an interactive login session
| filter causality_actor_process_image_name ~= “^(?:apache2|httpd|apachectl|php-fpm[0-9.]*|php-cgi[0-9.]*(?:\.cagefs)?|php-fcgi|sw-engine(?:-fpm)?|frankenphp|lsphp[0-9]*|lshttpd|litespeed|xampp-control|wampmanager|laragon|de\.appsolute\.mampprohelper)(?:\.exe)?$”
or actor_process_image_name ~= “^(?:apache2|httpd|php-fpm[0-9.]*|php-cgi[0-9.]*(?:\.cagefs)?|php-fcgi|sw-engine(?:-fpm)?|frankenphp|lsphp[0-9]*|lshttpd|litespeed)(?:\.exe)?$”
or (causality_actor_process_image_name = “w3wp.exe” and actor_process_image_name in (“cmd.exe”, “powershell.exe”, “pwsh.exe”))
or (actor_effective_username ~= “(?:^|[^a-z0-9_-])(?:www-data|apache|nginx|wwwrun|iusr)$”
and coalesce(causality_actor_process_image_name, “”) !~= “^(?:sshd(?:-session)?|screen|tmux|login|mosh-server|dropbear)$”)
or actor_effective_username contains “iis apppool”
// Known-benign workloads (monitoring plugins, hosting control panels) and commands launched directly by CDK Drive DMS scripts
| filter action_process_image_command_line !~= “(?:\/usr\/lib(?:64)?\/nagios\/plugins\/|\/usr\/local\/cpanel\/|\/usr\/local\/psa\/|\/opt\/psa\/|\/usr\/local\/directadmin\/|\/usr\/local\/ispconfig\/|\/usr\/local\/virtualizor\/|richdocumentscode|phpsysinfo|librenms|zabbix)”
and coalesce(actor_process_command_line, “”) !~= “^(?:\S+\s+)?(?:\/adp\/(?:bin|3party|etc)\/|\/local\/bin\/uminfo)”
// Phusion Passenger app spawns run id/uname/env from their spawn helpers
| filter coalesce(action_process_image_command_line, “”) !~= “^(?:\/usr\/bin\/)?(?:id(?:\s+-[a-z]+)?|uname(?:\s+-[a-z]+)?|hostname|env)$|passenger-config\s+system-metrics”
or lowercase(coalesce(actor_process_command_line, “”)) !~= “passengeragent\s+spawn-|spawn(?:preparer|envsetupper)shell|\/helper-scripts\/rack-preloader\.rb|passenger_ruby_wrapper|phusion_passenger”
// RHEL-family /etc/profile runs these in every bash login shell
| filter coalesce(action_process_image_command_line, “”) !~= “^\/usr\/bin\/(?:id\s+-[ug]n|hostname)$”
or lowercase(coalesce(actor_process_command_line, “”)) !~= “^(?:\S*\/)?bash\s+-[a-z]*l|^-bash$”
// parsed_lookup: the tool is the first command of its parent “sh -c” and its output is piped into a text filter (scripted lookup)
| alter tool = replace(action_process_image_name, “.exe”, “”),
shell_first_cmd = arrayindex(regextract(coalesce(actor_process_command_line, “”), “^-?(?:\S*\/)?(?:ba|da|k)?sh\s+-c\s+(\S+)”), 0)
| alter parsed_lookup = if(arrayindex(regextract(coalesce(shell_first_cmd, “”), “([^\/]+)$”), 0) = tool
and actor_process_command_line ~= “^-?(?:\S*\/)?(?:ba|da|k)?sh\s+-c\s+[^;&|]+\|\s*(?:e?grep|awk|cut|sed|wc|head|tail|sort|tr)\b”, 1, 0)
// Signals – one event can carry several
| alter signals = concat(
// SCORED – host/user/network/container recon, excluding scripted lookups (distinct tools are counted later)
if(parsed_lookup = 0 and (tool in (“whoami”, “hostname”, “hostnamectl”, “ifconfig”, “netstat”, “ss”, “arp”, “route”, “w”, “who”, “last”, “lastlog”, “getent”, “lsb_release”, “lscpu”, “printenv”, “env”, “getcap”, “capsh”, “systeminfo”, “ipconfig”, “nltest”, “tasklist”, “quser”, “qwinsta”)
or (tool = “uname” and action_process_image_command_line ~= “\buname\s+(?:-[a-z]*[arv]|–all|–kernel-)”)
or (tool = “id” and action_process_image_command_line !~= “\bid\s+-[ugnrz]+\b”)
or (tool = “ip” and action_process_image_command_line ~= “\bip\s+(?:-[a-z0-9]+\s+)*(?:a|addr|address|r|route|n|neigh|link)\b”)
or (tool = “ps” and action_process_image_command_line ~= “\bps\s+(?:-?aux|-ef|-e\b|axjf|-auxf)”)
or (tool in (“net”, “net1”) and action_process_image_command_line ~= “\bnet1?(?:\.exe)?\s+(?:user|group|localgroup|accounts|view|share)\b”)
or (tool = “cat” and action_process_image_command_line ~= “\/etc\/(?:passwd|shadow|group|issue|os-release|hosts|hostname|resolv\.conf|crontab|sudoers)|\/proc\/(?:version|net\/|(?:self|[0-9]+)\/(?:cgroup|mountinfo))|\/\.dockerenv”)
or (tool = “find” and action_process_image_command_line ~= “-perm\s+(?:-4000|-u=s|\/4000|-2000)”)
or (tool = “sudo” and action_process_image_command_line ~= “\bsudo\s+-l\b”)), “recon,”, “”),
// SCORED – payload transfer to a file (curl with an output file, wget not to stdout), excluding /dev/null and localhost
if(((tool = “curl” and action_process_image_command_line ~= “\s(?:-[a-z]*o\b|–output\b|–remote-name)|>\s*\S”)
or (tool = “wget” and action_process_image_command_line !~= “\s-[a-z]*o\s*-(?:\s|$)|–output-document=-|–spider”)
or (tool in (“tftp”, “fetch”) and action_process_image_command_line ~= “(?:https?|ftp|tftp):\/\/|\s-[a-z]*o(?:\s|-|$)”)
or (tool = “certutil” and action_process_image_command_line ~= “urlcache|verifyctl”)
or (tool = “bitsadmin” and action_process_image_command_line contains “/transfer”)
or (tool in (“powershell”, “pwsh”) and action_process_image_command_line ~= “downloadstring|downloadfile|invoke-webrequest|\biwr\b|net\.webclient|start-bitstransfer”))
and action_process_image_command_line !~= “-o\s*\/dev\/null|–output-document=\/dev\/null|:\/\/(?:localhost|127\.0\.0\.1|\[::1\])?[:\/]”, “download,”, “”),
// SCORED – chmod making a file in a tmp-class directory executable
if(tool = “chmod” and action_process_image_command_line ~= “\bchmod\s+(?:-[a-z]+\s+)*(?:[0-7]?(?:[1357][0-7]{2}|[0-7][1357][0-7]|[0-7]{2}[1357])|[ugoa,+=rwst-]*[+=][rwst]*x[ugoa,+=rwxst-]*)\s” and action_process_image_command_line ~= “\s\/(?:tmp|var\/tmp|dev\/shm|run\/shm)\/”, “staging,”, “”),
// STANDALONE – binary executed from a writable/memory-backed dir, or from a deleted/memfd image
if(action_process_image_path ~= “^\/(?:tmp|var\/tmp|dev\/shm|run\/shm|run\/user)\/|^\/proc\/(?:self|[0-9]+)\/fd\/|^\/memfd:|\(deleted\)\s*$”, “writable_exec,”, “”),
// STANDALONE – reverse/bind shell constructs
if(action_process_image_command_line ~= “\/dev\/(?:tcp|udp)\/|(?:^|[\s;&|(\/])(?:nc|ncat|netcat)(?:\.[a-z]+)?\s+(?:\S+\s+){0,6}?(?:-[a-z]{0,3}[ec]|–exec|–sh-exec)\s+\S*(?:sh|bash|cmd|python[0-9.]*|perl)\b|\bsocat\b[^;&|]{0,80}exec:|\bmkfifo\b|\b(?:ba|da|z|a)?sh\s+-i\b|socket\.socket\(|fsockopen\(|\bpty\.spawn\(“, “revshell,“, ““),
// STANDALONE – remote content piped straight into an interpreter
if(action_process_image_command_line ~= “(?:curl|wget|fetch)\b[^|]*\|\s*(?:\/\S*\/)?(?:(?:ba|da|z|a)?sh|python[23]?|perl|php)\b“, “pipe_to_shell,“, ““),
// SCORED – encoded/obfuscated execution
if(action_process_image_command_line ~= “base64\s+(?:–d|—decode)|frombase64string|–e(?:nc|ncodedcommand)?\s+[a–z0–9+\/]{40,}|echo\s+[a–z0–9+\/]{40,}|\bopenssl\s+(?:enc\b[^|;]*\s–d\b|base64\s+-d\b)|\bxxd\s+(?:–[a–z]+\s+)*-[a–z]*r“, “encoded,“, ““),
// SCORED – persistence / backgrounding (crontab edits, authorized_keys writes, new users, nohup/setsid of tmp-class binaries or shells,
// scheduled tasks, Run keys, systemd units, chattr +i)
if((tool = “crontab” and action_process_image_command_line !~= “\bcrontab\s+-l\s*$“)
or (action_process_image_command_line ~= “(?:>>?\s*|\btee\s+(?:–a\s+)?|\b(?:cp|mv|install|ln)\s+(?:–\S+\s+)*\S+\s+)\S*authorized_keys“
and action_process_image_command_line !~= “\bcp\s+\S*\/\.ssh\/id_[a–z0–9]+\.pub\s+\S*\/\.ssh\/authorized_keys“)
or action_process_image_command_line ~= “\b(?:useradd|adduser)\b|\b(?:nohup|setsid)\s+(?:–\S+\s+)*(?:\.\/|\/(?:tmp|var\/tmp|dev\/shm|run\/shm)\/|(?:ba|da|z)?sh\s+-[ci]\b)|schtasks\s+\/create|currentversion.run\b|\bsystemctl\s+(?:—user\s+)?enable\b|\.config\/systemd\/user\/|\/etc\/systemd\/system\/|\bchattr\s+\+[a–z]*i“, “persistence,“, ““),
// SCORED – inline interpreter one-liners, excluding PHP version/feature probes and Ruby gem-path probes
if(action_process_image_command_line ~= “(?:^|[^a–z0–9._–])(?:python[23]?(?:\.[0–9]+)?\s+-c|perl\s+-e|ruby\s+-e|node(?:js)?\s+-e|lua\s+-e|php[0–9.]*\s+-r)\b“
and action_process_image_command_line !~= “phpversion\(|php_version|date\(‘t’\)|function_exists\(|extension_loaded\(|gem\.path“, “interpreter,“, ““),
// SCORED – cloud/container credential and secret access (credential files, env-var names, instance metadata service, .env reads)
if(action_process_image_command_line ~= “\.aws\/(?:credentials|config)|aws_secret_access_key|aws_session_token|\/\.azure\/|azure_client_secret|identity_endpoint|\/\.config\/gcloud\/|application_default_credentials\.json|\/secrets\/kubernetes\.io\/serviceaccount|\/run\/secrets\/|\/\.docker\/config\.json|169\.254\.169\.254|metadata\.google\.internal|\/proc\/(?:self|[0-9]+)\/environ|\b(?:cat|head|tail|less|more|strings|grep)\b[^|;]*\.env\b”, “cred_access,”, “”)
)
| filter signals != “”
| alter recon_tool = if(signals contains “recon”, tool, null)
// Group per host, PHP worker and account in 1-hour buckets
| bin _time span = 1h
| comp count() as events,
count_distinct(recon_tool) as recon_tool_count,
values(recon_tool) as recon_tools,
values(signals) as signals,
values(action_process_image_command_line) as action_process_image_command_line,
values(actor_process_command_line) as actor_process_command_line,
values(causality_actor_process_command_line) as causality_actor_process_command_line,
first(event_id) as event_id
by _time, agent_id, agent_hostname, causality_actor_process_image_name, actor_effective_username
| alter all_signals = arraystring(signals, “,”)
// Threshold: any standalone signal, OR 3+ distinct recon tools, OR recon plus a follow-on scored signal
| filter all_signals ~= “revshell|pipe_to_shell|writable_exec”
or recon_tool_count >= 3
or (recon_tool_count >= 1 and all_signals ~= “download|staging|encoded|persistence|interpreter|cred_access”)
