1. 环境配置与报错场景还原
1.1 核心需求解析:为什么要在练习平台上研究文件包含
说个真实的场景。前不久带一个新同事熟悉安全测试流程,给他在本地部署了一套开源的漏洞演练环境,就是用来练习Web漏洞的那个经典平台。结果他跑了半天跑回来告诉我:"环境装好了,页面能打开,但是File Inclusion那个模块一点进去就报错,根本练不了。"
我过去一看,报错信息大致是:
Warning: include(): http:// wrapper is disabled in the server configuration by allow_url_include=0其实这个报错不复杂,就是一个PHP配置项的问题。allow_url_include默认是关闭的,导致远程文件包含的功能没法用。但是这个问题让很多刚开始接触文件包含漏洞的人卡在第一步,连练习都做不下去,更别提理解漏洞原理了。
先说清楚为什么要研究这个东西。文件包含漏洞(File Inclusion),简单说就是程序在通过include、require这类函数引入文件时,没有对传入的文件路径做严格过滤,攻击者可以通过控制参数去引入任意文件。按照能引用的资源范围,又分成本地文件包含(LFI)和远程文件包含(RFI)两种。因为PHP把include函数设计得比较"宽泛",既能接受本地路径,也能通过URL封装协议读取远程资源,所以一旦参数可控、过滤不严,攻击面就很大。
在漏洞演练平台上研究这个漏洞,不是为了搞破坏,而是为了在合法授权的测试环境里理解它的成因、利用条件和防护方法。你需要先把环境跑起来,把报错解决掉,才能一步步去分析代码逻辑、参数传递和过滤规则。所以这篇文章就把话摆在这:我们聊的是配置修复、原理分析和防护方案,练习对象是本地实验环境。
1.2 报错现象与根因梳理
回到刚才那个报错。allow_url_include=0 这个配置项出现的目的很简单:禁止PHP的include、require这类函数通过URL协议去加载远程文件。
PHP在读取文件时有多种"流"(stream)封装协议,比如file://、http://、https://、php://等等。allow_url_fopen控制的是file_get_contents()、fopen()这类函数能否访问远程URL;而allow_url_include单独控制include/require是否允许从URL加载文件。为什么把它单独拎出来控制?因为在include的场景下,远程文件里的PHP代码会被直接执行,危害级别比单纯读取文件高太多。默认关闭,是PHP官方早就做好的安全取舍。
然而问题在于,漏洞演练平台需要在实验里演示RFI(远程文件包含)的完整过程,所以默认配置反而是"不够用"的。你如果再叠加一个新手环境,比如Windows上用phpStudy、Mac上用Homebrew装的PHP、Linux上用apt装的PHP-FPM,配置文件的位置和服务重启方式都不一样,报错自然会来。问题的根子在于:PHP的配置文件没有把allow_url_include打开,甚至可能你的环境根本没有对应的配置文件入口。
这里还要注意,很多人以为改一下php.ini就行,实际改完却发现没生效。为什么?因为你运行PHP的方式不同,加载的配置文件路径就不一样。命令行跑的PHP和Apache模块跑的PHP可能各自读不同的ini文件,改了一个另一个没改,自然不生效。
1.3 配置文件的三条路径:php.ini、PHP-FPM、.htaccess
配置文件路径这个坑,真的值得单独挑出来讲。我用过太多环境,每个环境的路子都不一样,总结下来就三种情况。
第一种,常规php.ini文件。不管你是Windows还是Linux,只要你用的是经典的LNMP或者LAMP环境,总有一个php.ini在某个位置。Windows下一般是PHP安装目录下的php.ini;Linux下要看你是用apt装的还是编译安装的,apt装的话一般在/etc/php/版本号/下面,比如/etc/php/8.1/apache2/php.ini和/etc/php/8.1/cli/php.ini就是两个不同的文件。命令行和Web走的是不同入口,这一点非常容易搞错。
第二种,PHP-FPM的配置文件。现在大部分生产环境和练习环境都用PHP-FPM,它的配置可能是pool级别的,比如/etc/php/8.1/fpm/pool.d/www.conf,里面可以单独覆盖php.ini的设置。如果你改了php.ini发现没生效,多半就是这个www.conf里面有php_admin_value之类的指令把设置覆盖了。
第三种,.htaccess文件。Apache环境下,你可以在站点根目录放一个.htaccess,里面写php_value allow_url_include On。这个方法在Apache开启AllowOverride的前提下是有效的,也是最省事的——不用动全局配置,改完立刻生效。但是Nginx环境不支持.htaccess,这条就走不通。
我的建议是:先用phpinfo()查一下当前加载的配置文件路径,再决定改哪个文件。别一上来就全局搜索php.ini,拿过来就改,回头发现根本不是你服务在用的那份。
2. allow_url_include参数深度理解
2.1 参数含义与默认值
allow_url_include这个参数的意思,官方文档给的定义是:是否允许include/require函数通过URL方式获取文件。默认值是Off,从PHP 5.2开始默认关闭,这是从安全角度做的一个收紧。
它和allow_url_fopen是两码事,千万别混。allow_url_fopen控制的是文件读取类函数能不能访问远程资源,默认是On。因为file_get_contents读远程接口、fopen开远程资源这些功能在日常开发中太常见了,你要是默认关闭,很多抓取外部数据的程序直接就废了。但是include就不一样了,include进来的内容会被PHP解释器当作PHP代码解析执行。允许include远程文件,等于给了攻击者一个"远程执行代码"的入口,权衡之下PHP官方选择默认关掉。
我遇到不少人有疑问:"我把allow_url_fopen关掉不也一样吗?"不一样。allow_url_fopen和allow_url_include是独立控制的,你可能设置了allow_url_fopen=Off,但allow_url_include如果是On,include远程URL依然是允许的。反过来也一样,allow_url_fopen=On不代表include就能读远程文件。所以排查问题的时候,两个参数分开看,这是第一原则。
2.2 四种方式修改配置,总有一种适合你
根据我实操的经验,修改这个参数通常有四条路,看你自己的环境选一条就行。
方式一:直接改php.ini全局配置。找到php.ini里的allow_url_include这一行,默认是:
allow_url_include = Off改成:
allow_url_include = On改完保存,重启Web服务或PHP-FPM。这种方式覆盖面最广,但是要注意,如果你的php.ini里找不到这一行,说明这个参数没被显式配置,你可以手动加一行。加在哪?找到文件里动态扩展相关配置之后的位置,写在[PHP]这个section下面就行。
方式二:通过PHP-FPM pool配置覆盖。有些环境不让你动全局配置,或者全局配置被其他项目共享,你改了影响面太大。这时候可以在PHP-FPM的pool配置文件里加:
php_admin_value[allow_url_include] = On注意,php_admin_value和php_value有区别。php_admin_value设置的项,在PHP运行期间无法被ini_set()修改,优先级更高;php_value则有可能被代码覆盖。如果你想要一个"铁了心不让你在代码里关掉"的设置,用php_admin_value。改完需要重启PHP-FPM服务。
方式三:.htaccess文件内设置。Apache环境下,在站点根目录的.htaccess里加:
php_value allow_url_include On这个方式的好处是不用重启服务,改完立刻生效。坏处是如果你所在环境的Apache禁用了AllowOverride All,这个文件里的php_value指令会被直接无视。
方式四:运行时用ini_set()动态设置。在练习环境里,如果你只是想验证一下Remote File Inclusion的逻辑,不想到处改配置文件,可以在入口文件开头加一行:
<?php ini_set('allow_url_include', '1');但是这里有个大前提:allow_url_include这类PHP_INI_SYSTEM级别的指令,是否能被ini_set()覆盖,取决于配置里的设置方式。如果之前用的是php_admin_value设置的,运行时改不了;如果只是php.ini默认值,那ini_set()可以生效。这个方法我一般只建议在练习代码里临时用,生产环境千万别碰。
2.3 修改后如何确认生效
配置改了,怎么确认真的生效?三个办法。
第一个办法,也是最直观的,直接看phpinfo()页面。在浏览器打开phpinfo,搜索allow_url_include,会看到两个值:Local Value和Master Value。Master Value是系统配置文件里的值,Local Value是当前目录/当前虚拟主机里有没有被覆盖后的值。如果你两个都看到On,那就说明生效了。
第二个办法,用命令行快速检测:
php -i | grep allow_url_include如果你运行的是命令行PHP,这条命令会直接给出当前CLI环境的配置值。
第三个办法,从一个动作看效果。创建一个包括远程URL的PHP脚本,在练习环境里指向你本机起的测试文件,比如:
<?php include('http://127.0.0.1/test.txt'); ?>如果不再报上面那个warning,说明配置生效了。这里要强调一下,只在自己本机实验环境用这种方式验证,不要跑到远程站点上乱试。
3. 报错处理的完整实操过程
3.1 先定位当前PHP加载的配置文件
真正动手改之前,先搞清楚PHP加载的是哪个配置文件。这一步没做对,后面所有修改都白费。
我常用的方法:在Web根目录放一个测试文件,内容就一行:
<?php phpinfo(); ?>在浏览器打开,搜索'Loaded Configuration File'这一项。这一项会告诉你当前PHP进程加载了哪个php.ini。同时还要看'Scan this dir for additional .ini files',如果有值,说明PHP启动时还会额外加载这个目录下的所有.ini配置文件。
举个例子,我在一台Linux服务器上看到的是:
Loaded Configuration File => /usr/local/lib/php.ini Scan this dir for additional .ini files => /usr/local/php/conf.d那么我修改的目标就是/usr/local/lib/php.ini这个文件。如果你用的是phpStudy,通常路径是类似于安装目录下的php/php版本/php.ini。如果是Homebrew的PHP,一般在/opt/homebrew/etc/php/版本号/php.ini。
这里有个小坑:phpinfo()页面显示的Loaded Configuration File有时候是"none"。这个情况多见于PHP-FPM环境,说明没有显式加载php.ini,或者加载的是编译时默认路径。遇到这种别慌,命令行再执行一下php --ini看看CLI的配置,如果CLI能输出配置文件路径,就顺着那个路径去看。
3.2 编辑配置文件的具体操作
拿到路径之后就可以改了。我先给出一个通用的处理思路,你用任意编辑器打开对应文件都行。
第一步,搜索关键词allow_url_include。在php.ini里搜一下,大部分情况下你会看到一行:
allow_url_include = Off有些版本可能直接没有这一行,那就手动添加。更稳妥的操作是先搜一下allow_url_fopen,通常就在它附近,能看到类似:
; Whether to allow the treatment of URLs (like http:// or ftp://) as files. ; http://php.net/allow-url-fopen allow_url_fopen = On ; Whether to allow include/require to open URLs (like http:// or ftp://) as files. ; http://php.net/allow-url-include allow_url_include = Off把allow_url_include的值从Off改成On。注意分号是注释符号,别把分号一并去掉,否则那一行会变成纯注释,不算数。
第二步,保存退出。然后根据你的服务类型重启服务。如果是Apache+mod_php,就重启Apache;如果是Nginx+PHP-FPM,就重启php-fpm,同时重新加载Nginx的fastcgi配置也不需要,直接:
systemctl restart php8.1-fpm systemctl restart nginxWindows上的phpStudy用户,直接重启Apache或者Nginx服务就行,或者用面板的一键重启。
第三步,重新访问刚才报错的页面。之前那个Warning应该消失,页面能正常加载出练习内容。
3.3 重启服务与验证的那点事
重启服务这一步,细说起来还真有几个翻车现场。
有一种情况,你改了php.ini之后不重启服务,然后打开页面,发现报错还在。原因很简单:PHP-FPM或者Apache模块在启动时才读取php.ini,你不重启,它就一直用旧内存里的配置。你可以这么理解,php.ini是菜谱,服务是正在炒菜的师傅,你改了菜谱但不告诉师傅,师傅还是按老做法出菜。
还有一种情况,你重启了,发现还是不行。那就得回到phpinfo()页面,注意你访问的域名对应的虚拟主机配置可能覆盖了php.ini。搜索allow_url_include的时候,看Local Value是不是Off,Master Value是On。如果Local Value是Off,说明有更高优先级的配置覆盖了。这时候去查虚拟主机配置、.htaccess、php-fpm的pool配置里有没有设置allow_url_include=Off。
验证的时候也别只盯着报错消失这一个信号。最好做一个更直接的验证:把刚才那个include远程URL的脚本跑一下,确认能拿到远程文件内容。因为在有些情况下,allow_url_include虽然开了,但allow_url_fopen是Off,远程URL照样读不了,只是报错信息不一样。
3.4 三种常见配置报错的现场还原
我在实际环境里见过不少配置报错的变种,把最常见的三种列在这里,对照查一下就知道怎么办了。
报错一:Warning: include(): http:// wrapper is disabled in the server configuration by allow_url_include=0这个就是最经典的,allow_url_include还没打开。看到这个报错,直接去改配置即可,不用考虑其他原因。改完记得重启。
报错二:Warning: include(): Unable to find the wrapper "http" - did you forget to enable it when you configured PHP?这个报错跟allow_url_include反而不是一回事,它说明PHP编译时直接没启用HTTP封装协议,连URL都不认识。这个情况多见于编译安装PHP又没带openssl扩展的极简环境。解决办法是重新编译PHP,加上openssl和相关的扩展;如果是apt装的,检查一下php版本对应的扩展有没有装全。
报错三:Fatal error: Maximum execution time of 30 seconds exceeded这个报错出现在include一个不存在的远程URL时,因为连接超时了。它不是配置问题,而是你远程地址本身不对,或者网络不通。练习环境里最常见的坑就是写了错误的URL格式,比如忘记写协议头,或者用了外网地址但防火墙挡着连不上。
4. 从原理出发理解File Inclusion的深层逻辑
4.1 LFI与RFI的差异和练习目的
配置问题解决之后,建议别急着去"打"命令,先花几分钟把原理盘一盘。
本地文件包含(LFI),英文叫Local File Inclusion,指的是程序在include文件时,参数传入的是服务器本地路径。比如一个页面通过GET参数指定要加载的模板文件:
<?php include($_GET['page'] . '.php'); ?>如果传入page=../../etc/passwd,路径拼接之后成了../../etc/passwd.php,虽然多了一个后缀,但配合一些特殊手法或者直接在路径末尾加截断符,仍然可能读到不该读的文件。
远程文件包含(RFI),英文是Remote File Inclusion,指的是include函数可以加载远程URL上的代码并执行。攻击者可以在自己的服务器上放一个恶意PHP文件,然后让目标站的include函数把这个文件加载进去执行。RFI的威力比LFI更大,因为它直接通向代码执行。而allow_url_include正是RFI利用的关键开关。
在练习平台上把这两个场景都跑一遍,你才能真正体会到为什么使用者输入一个路径就能决定include读哪个文件,这种"用户的输入直接影响文件系统操作"的模式有多危险。
4.2 输入点、路径拼接与绕过思路的剖析
理解文件包含漏洞,要盯住两个东西:输入点和拼接规则。
输入点指的是用户能控制的参数位置。最常见的是URL参数,比如index.php?page=xxx,也可能是POST参数、Cookie、Referer字段等。判断一个输入点是否存在漏洞,要看这个值是不是没有经过严格过滤就直接进了文件操作相关的函数。
拼接规则是另外一个大坑。很多代码为了省事,会在include前加一些前缀或后缀,比如:
include('pages/' . $_GET['page'] . '.php');这样用户传入../../../../etc/passwd,最终拼接就是pages/../../../../etc/passwd.php。你可能会觉得后缀.php挡住了很多操作,但别忘了还有几个经典绕过手法:一是路径截断,利用超长路径让系统自动截断后面的字符,老版本PHP有这个毛病;二是用%00空字节截断,在特定编码场景下会截掉后面的字符串;三是配合服务器的URL解析特性,把后面的内容当作请求参数绕过。现代PHP版本对这些老手法基本都有防护,但拼接逻辑写得不好照样能被搞出问题。
在练习平台里,每个级别的代码都代表一种典型的过滤策略。从完全不过滤,到加前缀后缀,再到用正则白名单过滤,逐级递增。你在做实验时真正要关注的是:过滤策略放在哪个环节、用户输入会经过哪些转换、拼接规则有没有留下缝隙。这才是练习File Inclusion的核心价值。
4.3 为什么默认关闭更安全
allow_url_include默认关掉,不是PHP官方闲着没事折腾人,是有现实教训的。
一个PHP程序如果允许include远程URL,攻击者只需要找到任意一个可控参数,然后在自己的服务器上准备一段PHP代码,就能让目标服务器执行。这种情况的破坏力几乎等同于直接拿下服务器权限。所以在一个没有特殊需求的普通业务系统里,allow_url_include=Off是一条非常合理的安全基线。
明白了这一点,你对"如何正确启用allow_url_include"这个问题的理解就不一样了:这不是一个"要利用漏洞所以就打开"的开关,而是一个"因为有明确测试需求且环境隔离,所以临时打开"的操作。在练习环境里打开它来理解漏洞原理,没问题;在生产环境想打开,你得先回答一个问题:为什么你的业务需要让include函数去加载远程文件?绝大多数业务答案都是"不需要"。
5. 常见问题与排查技巧实录
5.1 问题速查表:照着查就行
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| include远程URL报allow_url_include=0警告 | allow_url_include配置未开启 | 修改php.ini或php-fpm配置,重启服务 |
| 改了php.ini之后不生效 | 服务没重启,或加载的不是这个php.ini | 用phpinfo()确认实际配置路径,重启服务 |
| phpinfo()里Local Value是Off,Master Value是On | 虚拟主机/php-fpm pool/.htaccess覆盖了配置 | 检查覆盖源,修改对应层级的配置 |
| 报错Unable to find the wrapper | PHP编译时缺少HTTP封装扩展 | 重新编译PHP或安装对应扩展 |
| 报错Maximum execution time exceeded | 远程URL不可达,include连接超时 | 检查URL地址、网络连通性、防火墙规则 |
| 实验页面还是加载失败 | 路径拼接规则导致找不到目标文件 | 分析代码里的前缀后缀规则,调整传入参数 |
| 命令行php -i看到On但页面仍报错 | Web服务用的是另一套PHP配置 | 区分CLI配置和Web配置,分别修改 |
5.2 日志与调试命令的实战技巧
改配置的时候别盲目试,多用日志和命令判断方向。
查看PHP-FPM日志。默认地址通常在/var/log/php-fpm.log或者/var/log/php8.1-fpm.log。一旦配置文件语法有问题,服务重启的时候会在日志里直接报一行Parse error之类的信息,一眼就能定位。我遇到过有人改了php.ini后重启失败,页面直接502,最后发现是php.ini里某个扩展路径写错了。日志里那种错,比你去瞎猜快多了。
查看Web服务器错误日志。Apache在/var/log/apache2/error.log,Nginx在/var/log/nginx/error.log。PHP的Warning和Fatal error默认都会写到这里。如果你不想每次都去翻日志,还可以在实验脚本里临时开启错误显示:
<?php error_reporting(E_ALL); ini_set('display_errors', '1'); ?>注意这是临时调试用的,别留着不删。
命令行PHP与Web环境联动验证。一个很好用的技巧是在命令行里先跑一遍PHP脚本,看它报什么错:
php -r "include('http://127.0.0.1/test.txt');"命令行如果没问题,Web环境如果还有问题,就明确是Web服务的配置差异,可以针对性去查Web这边加载的php.ini和扩展。
5.3 独家实操心得与避坑笔记
这篇文章写到最后,我把自己这几年在配置文件包含实验环境时踩过的坑汇总一下,都是花时间换来的教训。
第一,Docker环境别乱改容器里的php.ini。现在很多人用Docker跑漏洞演练平台,容器里的php.ini一重启就恢复原样。正确做法是用挂载的方式把配置文件映射到容器外,或者干脆用环境变量、自定义Dockerfile去覆盖。你要是直接在容器里改了文件没提交,容器一删全白干。
第二,生产环境永远不要开allow_url_include。这话我说多少遍都不嫌多。就算你只是临时想验证一个功能,也得确认它不会影响线上业务。更合理的做法是在独立的测试环境里跑,并且限定来源IP,用完立刻关掉。
第三,练习结束后把修改还原。每次在练习平台做完实验,我都会顺手把allow_url_include改回Off,再把php.ini恢复原样。这样做的好处是:下次再开练习环境,你会重新走一遍排查流程,对配置项的把握会更深。而且如果你用的是多人共用的服务器,不还原真的会坑到下一个用这台机器的人。
第四,理解配置优先级比记住配置路径更重要。我见过最迷惑的案例是,一个同学在php.ini里设好allow_url_include=On,但是会在站点根目录放一个.htaccess写php_value allow_url_include Off,于是怎么试都不生效。配置优先级从高到低大致是PHP-FPM pool的php_admin_value高于Apache的.htaccess,再高于其他地方的php_value,最后才是php.ini默认值。你只要掌握了这个优先级,大部分配置不生效的问题都能迎刃而解。
我个人在实际操作中的体会是,解决allow_url_include报错这件事情本身并不难,难的是养成先查原因、再动手改配置的习惯。很多人看到报错就搜答案,照着网上的命令敲一遍,敲完还是不行,再搜一遍,来回折腾浪费大量时间。如果你先花五分钟看看phpinfo()里的Loaded Configuration File,再用php -i确认一遍命令行的配置,大概率能少走很多弯路。
最后再补一个小技巧:如果你只是想单次验证RFI的效果,不想改动系统级配置,那就别去碰php.ini了。在练习代码的入口处临时用ini_set('allow_url_include', '1'),做完实验直接删掉这一行,干净利落,不留尾巴。当然,前提是这个环境本身就是给你做实验用的,隔离风险可控。
文件包含漏洞的练习,关键不只是把配置调通、把报错消掉,更要在跑通之后静下心来想想:为什么一个参数能把include函数引到任意文件上去?为什么PHP要把allow_url_include单独拆出来管?理解了这些问题,你再去看各种漏洞报告和防御方案,整个思路都会清晰一大截。