PHP的disable_functions是php.ini中一个极其重要的安全配置项。它允许管理员通过黑名单机制,直接禁止PHP脚本调用指定的内置函数。这个配置项的价值在于,即便攻击者通过文件上传漏洞成功写入了一个webshell,只要关键的高危系统函数被禁用,webshell的核心功能就会直接瘫痪,无法执行系统命令、无法反弹Shell、无法扫描内网。这就是纵深防御体系中最关键的一道防线。
很多人配置完disable_functions之后,发现网站某些功能异常,或者被安全扫描工具提示存在绕过风险,根源往往在于配置不够严谨。下面直接列出需要重点关注的高危函数类别,以及每一类函数被禁用后对攻击链的阻断作用。
命令执行类函数这类函数是攻击者拿下服务器权限的最直接途径。一旦调用成功,攻击者就能以Web服务器运行账户的身份执行任意系统命令。需要禁用的函数包括:
exec()、system()、passthru()、shell_exec()、popen()、proc_open()、pcntl_exec()。其中exec()和system()是最常见的命令执行函数,passthru()可以输出原始二进制数据,shell_exec()返回完整的命令输出字符串。popen()和proc_open()更为隐蔽,它们通过管道方式与进程交互,很多安全扫描工具容易遗漏对这两个函数的检测。pcntl_exec()是进程控制扩展中的函数,在当前进程空间内执行新程序,危害极大。
还有一个容易被忽略的函数是反引号操作符,它等同于shell_exec(),但disable_functions无法直接禁用语法层面的操作符。不过在实际webshell利用中,攻击者更倾向于使用函数调用,所以禁用上述函数已经能阻断绝大多数命令执行路径。
代码执行与文件包含类函数这类函数允许动态执行PHP代码或者包含任意文件,是webshell维持权限和绕过检测的常用手段。必须禁用的函数有:eval()、assert()、preg_replace()配合/e修饰符、create_function()、call_user_func()、call_user_func_array()、include_once()和require_once()的动态变体。
eval()和assert()是代码执行的核心。需要特别说明的是,assert()在PHP7之前的版本中支持将字符串参数作为PHP代码执行,PHP7之后这个行为发生了变化,但为了兼容性和安全,仍然建议禁用。preg_replace()在PHP5.5之前支持/e修饰符,可以执行替换结果作为PHP代码,PHP5.5起已废弃,PHP7中完全移除,但如果服务器仍运行老版本PHP,这个点必须封堵。create_function()内部实际上使用了eval(),PHP7.2起已废弃,同样建议禁用。
call_user_func()和call_user_func_array()本身是合法的回调函数调用方式,很多正常业务代码也会使用。如果业务依赖这些函数,不能直接禁用,就需要通过其他安全手段来限制回调函数的来源。但如果业务不需要动态回调,强烈建议一并禁用。
文件系统操作类函数攻击者写入webshell后,通常需要读取配置文件、修改网站源码、或者上传更多工具。文件操作函数就是他们手中的工具集。需要重点禁用的包括:file_get_contents()、file_put_contents()、fopen()、fwrite()、fread()、copy()、rename()、unlink()、mkdir()、rmdir()、file()、readfile()、parse_ini_file()、show_source()、highlight_file()。
这里有一个容易被忽视的细节:show_source()是highlight_file()的别名,两者功能完全相同,都能直接展示PHP文件的源代码,包括数据库密码等敏感信息。parse_ini_file()可以解析配置文件,攻击者常用它来读取数据库连接配置。
如果业务本身有文件上传下载需求,不能完全禁用文件读写函数,那么至少应该禁用绝对路径的读写能力,配合open_basedir限制文件操作范围。但disable_functions是全局性的,无法做细粒度控制,这种情况下就需要在代码层面做严格的路径校验。
信息探测与网络连接类函数攻击者控制一台服务器后,通常会以内网横向移动为目标。这就需要网络探测和连接能力。必须禁用的函数包括:fsockopen()、pfsockopen()、stream_socket_client()、curl_exec()、curl_multi_exec()、get_headers()、phpinfo()、ini_get()、ini_get_all()、get_loaded_extensions()、get_defined_functions()、disk_free_space()、disk_total_space()。
fsockopen()和pfsockopen()可以建立任意TCP连接,攻击者用它们来探测内网开放端口、连接数据库、或者建立反向Shell通道。stream_socket_client()功能类似,但支持更多协议。curl_exec()如果不禁用,攻击者可以通过cURL扩展发起HTTP请求,访问内网服务甚至云元数据接口。
phpinfo()输出PHP完整配置信息,包括disable_functions列表本身、扩展目录路径、服务器环境变量等。攻击者拿到phpinfo()输出后,就能精准判断哪些函数可用,从而调整攻击载荷。ini_get()和ini_get_all()可以读取任意PHP配置项的值,get_loaded_extensions()列出已加载扩展,get_defined_functions()列出所有已定义函数。这些信息探测函数组合使用,就是攻击者的侦察工具包。
进程控制与信号处理类函数这类函数允许PHP脚本与操作系统进程进行深度交互,危害性不亚于命令执行。需要禁用的有:proc_nice()、proc_terminate()、proc_close()、proc_get_status()、pcntl_fork()、pcntl_signal()、pcntl_waitpid()、pcntl_alarm()。
pcntl_fork()可以创建子进程,攻击者利用它来绕过某些基于进程名称的检测机制。pcntl_signal()配合pcntl_alarm()可以设置定时器,实现持久化运行。虽然pcntl扩展默认不安装,但很多生产环境为了性能会启用,一旦启用就必须在disable_functions中封堵相关函数。
邮件发送与日志伪造类函数mail()函数看似无害,但在某些配置下可以被利用来执行命令。当PHP的sendmail_path配置指向自定义脚本时,mail()函数的第五个参数可以传递额外参数给sendmail程序,进而实现命令注入。建议禁用mail(),或者至少确保sendmail_path配置安全。
openlog()、syslog()、closelog()这三个函数可以写入系统日志。攻击者可能利用它们来伪造日志记录,掩盖入侵痕迹,或者向日志中注入恶意内容,配合日志文件包含漏洞实现代码执行。
实际配置示例与注意事项下面给出一个生产环境中经过验证的disable_functions配置示例:
disable_functions = exec,system,passthru,shell_exec,popen,proc_open,pcntl_exec,pcntl_fork,pcntl_signal,pcntl_waitpid,pcntl_alarm,proc_nice,proc_terminate,proc_close,proc_get_status,eval,assert,create_function,preg_replace,call_user_func,call_user_func_array,file_get_contents,file_put_contents,fopen,fwrite,fread,copy,rename,unlink,mkdir,rmdir,file,readfile,parse_ini_file,show_source,highlight_file,fsockopen,pfsockopen,stream_socket_client,curl_exec,curl_multi_exec,get_headers,phpinfo,ini_get,ini_get_all,get_loaded_extensions,get_defined_functions,disk_free_space,disk_total_space,mail,openlog,syslog,closelog,apache_child_terminate,apache_setenv,putenv
这个配置禁用了约50个高危函数。但需要强调的是,这不是一个可以直接照搬的列表。每个业务系统的技术栈不同,依赖的函数也不同。比如使用WordPress的站点,如果禁用了file_get_contents(),很多插件会无法正常工作。使用Laravel框架的项目,如果禁用了proc_open(),队列工作者可能无法启动。
配置前务必在测试环境验证。可以通过以下命令快速检查当前PHP已禁用的函数列表:
php -r "print_r(explode(',', ini_get('disable_functions')));"
或者在Web环境中创建一个phpinfo()页面查看disable_functions的当前值。注意,修改php.ini后需要重启PHP-FPM或Web服务器才能生效。如果是使用PHP-FPM,执行service php-fpm reload或systemctl reload php-fpm即可。
绕过风险与加固建议disable_functions不是银弹,存在多种绕过技术。常见的绕过方式包括:利用未禁用的扩展函数,如通过ImageMagick扩展的漏洞函数执行命令;利用LD_PRELOAD环境变量劫持共享库;通过FFI扩展直接调用C函数;利用COM组件在Windows环境下执行命令;通过imap_open()函数结合恶意服务器实现命令执行。
针对这些绕过手段,除了完善disable_functions配置外,还需要配合其他安全措施:将open_basedir设置为Web目录,限制文件访问范围;关闭不必要的PHP扩展,特别是FFI、COM、imap等高风险扩展;设置严格的文件权限,Web目录下不允许出现可写的PHP文件;使用SELinux或AppArmor等强制访问控制机制;定期更新PHP版本,新版本修复了许多可能导致绕过的漏洞。
还有一个关键的加固点:putenv()函数。很多disable_functions绕过技术都依赖putenv()来设置LD_PRELOAD环境变量。如果业务不需要动态设置环境变量,务必在disable_functions中加入putenv()。apache_child_terminate()和apache_setenv()这两个Apache专用函数也应该禁用,它们分别可以终止Apache子进程和设置Apache环境变量。
监控与验证配置完成后,不能就此高枕无忧。建议定期通过安全扫描工具验证disable_functions的实际效果。可以编写一个测试脚本,逐一尝试调用被禁用的函数,确认PHP会抛出Fatal error而不是成功执行。同时监控PHP错误日志,如果发现大量函数调用被拒绝的记录,可能是正常业务受到影响,也可能是攻击者在探测。
对于使用容器化部署的环境,建议将disable_functions配置写入Dockerfile或Kubernetes ConfigMap中,确保每个容器实例都应用相同的安全策略。配置变更走CI/CD流程,避免手动修改导致配置漂移。
最后需要提醒的是,disable_functions是安全底线,但不能替代代码层面的安全防护。输入验证、输出编码、SQL注入防护、文件上传过滤这些基础安全工作同样重要。多层防御体系叠加,才能真正降低服务器被入侵的风险。
