This detection identifies adversaries executing dynamic code via obfuscated PHP webshells that construct function names at runtime to evade static analysis and signature-based defenses. Proactive hunting for this behavior in Azure Sentinel is critical because such techniques are frequently used by threat actors to establish persistent command-and-control channels or execute arbitrary payloads within compromised web applications without triggering traditional alerts.
rule WEBSHELL_PHP_Dynamic
{
meta:
description = "PHP webshell using function name from variable, e.g. $a='ev'.'al'; $a($code)"
license = "Detection Rule License 1.1 https://github.com/Neo23x0/signature-base/blob/master/LICENSE"
author = "Arnim Rupp (https://github.com/ruppde)"
reference = "Internal Research"
date = "2021/01/13"
modified = "2024-12-09"
score = 60
hash = "65dca1e652d09514e9c9b2e0004629d03ab3c3ef"
hash = "b8ab38dc75cec26ce3d3a91cb2951d7cdd004838"
hash = "c4765e81550b476976604d01c20e3dbd415366df"
hash = "2e11ba2d06ebe0aa818e38e24a8a83eebbaae8877c10b704af01bf2977701e73"
id = "58ad94bc-93c8-509c-9d3a-c9a26538d60c"
strings:
$pd_fp1 = "whoops_add_stack_frame" wide ascii
$pd_fp2 = "new $ec($code, $mode, $options, $userinfo);" wide ascii
$pd_fp3 = "($i)] = 600;" ascii
//strings from private rule capa_php_old_safe
$php_short = "<?" wide ascii
// prevent xml and asp from hitting with the short tag
$no_xml1 = "<?xml version" nocase wide ascii
$no_xml2 = "<?xml-stylesheet" nocase wide ascii
$no_asp1 = "<%@LANGUAGE" nocase wide ascii
$no_asp2 = /<script language="(vb|jscript|c#)/ nocase wide ascii
$no_pdf = "<?xpacket"
// of course the new tags should also match
// already matched by "<?"
$php_new1 = /<\?=[^?]/ wide ascii
$php_new2 = "<?php" nocase wide ascii
$php_new3 = "<script language=\"php" nocase wide ascii
//strings from private rule capa_php_dynamic
// php variable regex from https://www.php.net/manual/en/language.variables.basics.php
$dynamic1 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(\$/ wide ascii
$dynamic2 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\("/ wide ascii
$dynamic3 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\('/ wide ascii
$dynamic4 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(str/ wide ascii
$dynamic5 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(\)/ wide ascii
$dynamic6 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(@/ wide ascii
$dynamic7 = /\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(base64_decode/ wide ascii
// ${'_'.$_}["_"](${'_'.$_}["__"]
$dynamic8 = /\$\{[^}]{1,20}}(\[[^\]]{1,20}\])?\(\$\{/ wide ascii
$fp1 = { 3C 3F 70 68 70 0A 0A 24 61 28 24 62 20 3D 20 33 2C 20 24 63 29 3B } /* <?php\x0a\x0a$a($b = 3, $c); */
$fp2 = { 3C 3F 70 68 70 0A 0A 24 61 28 24 62 20 3D 20 33 2C 20 2E 2E 2E 20 24 63 29 3B } /* <?php\x0a\x0a$a($b = 3, ... $c); */
$fp3 = { 3C 3F 70 68 70 0A 0A 24 61 20 3D 20 6E 65 77 20 73 74 61 74 69 63 3A 3A 24 62 28 29 3B} /* <?php\x0a\x0a$a = new static::$b(); */
$fp4 = { 3C 3F 70 68 70 0A 0A 24 61 20 3D 20 6E 65 77 20 73 65 6C 66 3A 3A 24 62 28 29 3B } /* <?php\x0a\x0a$a = new self::$b(); */
$fp5 = { 3C 3F 70 68 70 0A 0A 24 61 20 3D 20 5C 22 7B 24 76 61 72 43 61 6C 6C 61 62 6C 65 28 29 7D 5C 22 3B } /* <?php\x0a\x0a$a = \"{$varCallable()}\"; */
$fp6 = "// TODO error about missing expression" /* <?php\x0a// TODO error about missing expression\x0a$a($b = 3, $c,); */
$fp7 = "// This is an invalid location for an attribute, "
$fp8 = "/* Auto-generated from php/php-langspec tests */"
$fp_dynamic1 = /"\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\[\]'"]{0,20}\s{0,20}\(\$/ wide ascii // e.g. echo "$callback($text)";
condition:
filesize > 20 and filesize < 200 and (
(
(
$php_short in (0..100) or
$php_short in (filesize-1000..filesize)
)
and not any of ( $no_* )
)
or any of ( $php_new* )
)
and (
any of ( $dynamic* )
)
and not any of ( $pd_fp* )
and not 1 of ($fp*)
}
This YARA rule can be deployed in the following contexts:
This rule contains 35 string patterns in its detection logic.
Here are 5 specific false positive scenarios for the “PHP webshell using function name from variable” detection rule, along with recommended filters or exclusions:
Scenario: Legitimate CMS Plugin Execution
*/wp-content/plugins/*, */modules/*) and restrict the rule to only trigger if the executing process is not the standard web server user (e.g., www-data or apache). If the execution originates from a trusted plugin directory, suppress the alert.Scenario: Scheduled Maintenance Scripts via Cron
daily_maintenance.php calling $func = 'clean_logs'; $func();)..php, the parent directory matches known maintenance paths (e.g., /opt/scripts/maintenance), and the process command includes specific flags like --cron-job.Scenario: CI/CD Pipeline Deployment Artifacts