ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP文件包含漏洞LFI深度解析与全链路防御

PHP文件包含漏洞LFI深度解析与全链路防御 1. 为什么“include”不是语法糖而是PHP里最危险的双刃剑你写过include config.php;也见过require_once vendor/autoload.php;——在绝大多数PHP项目里这两行代码像呼吸一样自然。但就在你敲下回车、页面顺利加载的瞬间可能已经把服务器的文件系统大门悄悄推开了一条缝。这不是危言耸听文件包含漏洞File Inclusion Vulnerability的本质不是程序员写错了某行代码而是PHP语言设计中“动态路径解析”机制与Web应用输入校验缺失之间的一次致命耦合。它不依赖复杂的加密算法或零日漏洞只需要一个可控的参数、一次未过滤的字符串拼接就能让攻击者读取/etc/passwd、执行任意PHP代码甚至完全接管服务器。我第一次真正意识到这个问题的严重性是在给一家本地教育机构做安全审计时。他们用的是自研的CMS其中有一个“模板切换”功能URL形如/admin/theme.php?themeblue。开发同学的逻辑很朴素include templates/ . $_GET[theme] . .php;。他觉得“theme参数只允许传blue、green、dark三种值”所以加了个白名单校验。但问题出在——他忘了PHP的include会自动尝试补全.php后缀而攻击者传入theme../../../../etc/passwd%00%00截断了后面自动加的.php最终实际包含的路径变成了templates/../../../../etc/passwd。那天下午我坐在客户办公室看着终端里一行行Linux用户信息滚动出来心里只有一个念头这个漏洞不是“能不能被利用”而是“谁先发现它”。LFILocal File Inclusion和RFIRemote File Inclusion这两个术语常被初学者当作并列概念来记。但它们的底层原理和防御逻辑完全不同。LFI是利用include对本地文件路径的解析能力绕过目录限制读取敏感文件RFI则依赖于PHP配置中allow_url_includeOn这一危险开关让include能直接加载远程HTTP/FTP资源。现实中95%以上的生产环境都已关闭RFI因为风险过于直观但LFI依然泛滥——因为它藏在业务逻辑深处伪装成“正常功能”。比如DVWA里的文件包含实验表面看是“查看日志”背后却是include $_GET[page];这样赤裸裸的危险操作。而像#include qvtkopenglwidget.h这种C头文件包含或者VS Code里#include报红波浪线完全是编译器/IDE层面的静态检查与PHP运行时的动态包含毫无关系——混淆这两者是很多初学者踩坑的第一步。提示include和require在错误处理上不同前者警告后继续执行后者致命错误终止但在漏洞利用层面没有本质区别。真正决定风险等级的是路径是否可控、是否可被截断、是否可绕过白名单。别被语法细节带偏重点。2. LFI的三重渗透路径从读文件到拿Shell的完整链路很多人以为LFI就是读个/etc/passwd顶多算个信息泄露。但实际攻防中LFI是一整套渗透链条的起点其威力取决于你能否打通“读文件→执行代码→持久化控制”这三道关卡。下面我用真实渗透案例拆解这三步每一步都对应着不同的技术要点和绕过技巧。2.1 第一重突破路径限制读取任意文件核心矛盾在于include默认只认相对路径和绝对路径但Web应用常通过参数拼接路径如include pages/ . $file . .html;。攻击者要做的就是让$file变成../../../etc/passwd。这里的关键是路径遍历Path Traversal但现实比教科书复杂得多编码绕过原始参数file../../etc/passwd可能被WAF拦截但file..%2f..%2f..%2fetc%2fpasswdURL编码、file..%c0%af..%c0%af..%c0%afetc%c0%afpasswdUTF-8双字节编码常能绕过简单正则。空字节截断Null Byte Injection这是PHP 5.3.4之前最经典的技巧。当include $_GET[file] . .php;遇到file../../etc/passwd%00时%00会截断后面的.php实际包含/etc/passwd。虽然现代PHP已禁用但大量老旧系统仍在运行。伪协议注入当allow_url_fopenOn时filephp://filter/readconvert.base64-encode/resource/etc/passwd能将文件内容Base64编码后输出规避ASCII控制字符导致的页面乱码。这是目前最通用的读取方式。我实测过某电商后台的LFI点它对../做了简单替换替换成空但没处理....//四个点加两个斜杠。传入file....//....//....//etc/passwdPHP解析时会把....//当成../处理成功绕过。这种“看似修复实则留后门”的情况在代码审计中极其常见。2.2 第二重从文件读取到代码执行单纯读文件价值有限真正的高危在于执行任意PHP代码。这需要两个前提一是找到可写入的文件如日志、缓存、上传目录二是让include加载该文件。典型场景有Apache日志包含攻击者构造恶意请求GET ?php system($_GET[cmd]); ? HTTP/1.1请求头被记录到/var/log/apache2/access.log。再通过LFI包含该日志文件即可执行任意命令。关键点在于日志路径必须可预测且include能访问到。Session文件包含PHP Session默认存于/var/lib/php/sessions/文件名形如sess_abc123。若应用将用户可控数据如$_GET[user]写入Session攻击者可注入PHP代码再通过LFI包含对应session文件。PHPINFO页面利用某些调试页面会输出phpinfo()其中包含_REQUEST、_SERVER等超全局变量的完整内容。若存在LFI可包含该页面再结合php://input伪协议POST PHP代码实现RCE。去年审计一个社区论坛时我发现它的“用户头像上传”功能会把原始文件名含扩展名直接存入数据库。攻击者上传shell.php.jpg文件被保存为uploads/shell.php.jpg。由于LFI点支持php://filter我构造filephp://filter/readconvert.base64-encode/resourceuploads/shell.php.jpg解码后看到PHP代码再用fileuploads/shell.php.jpg直接包含执行——整个过程没用到任何0day全是标准PHP特性组合。2.3 第三重绕过WAF与加固措施的实战技巧现代WAF如云WAF、ModSecurity会对../、php://等关键词做规则拦截。但绕过方法始终存在关键在于理解WAF的匹配逻辑绕过手法原理实测有效率注意事项双写编码..%2f..%2fetc%2fpasswd→..%252f..%252fetc%252fpasswd二次URL编码高需确认WAF是否解码多次大小写混淆Php://filter/...、pHp://filter/...中大部分WAF规则区分大小写内联注释php://filter/readconvert.base64-encode/**/resource/etc/passwd低仅对部分老版本WAF有效协议变体data://text/plain,?php phpinfo(); ?Data URI极高需allow_url_includeOn但比RFI更隐蔽注意data://协议是当前最稳定的RCE方式之一。它不依赖外部服务器直接将PHP代码作为URI内容传递。例如filedata://text/plain,?php system(id); ?只要allow_url_include开启就能执行。但很多开发者误以为“关闭RFI就安全了”却忽略了data://同样属于远程包含范畴。3. 代码审计中的LFI识别模式从函数调用到上下文语义发现LFI不能靠运气而要建立一套系统化的审计路径。我总结了三个关键层次函数层识别→参数流追踪→业务逻辑验证。下面以真实代码片段为例演示如何像扫描仪一样逐层穿透。3.1 函数层锁定所有危险函数调用PHP中能触发文件包含的函数不止include和require还有include_once、require_once、file_get_contents配合eval、highlight_file等。审计第一步就是用正则或IDE搜索这些函数// 高危模式直接拼接用户输入 include $_GET[page] . .php; require $config_path . /db.php; // 中危模式经过简单处理但未彻底净化 $page str_replace(../, , $_GET[page]); include pages/ . $page . .html; // 低危但需警惕使用白名单但逻辑有缺陷 $allowed [home, about, contact]; if (in_array($_GET[page], $allowed)) { include pages/ . $_GET[page] . .php; }特别注意include的别名用法。有些框架会封装load_template()、render_view()等方法内部仍调用include。这时必须跟进函数定义不能只看表层调用。3.2 参数流追踪绘制完整的数据流向图找到危险函数后要逆向追踪参数来源。以include $file;为例需回答$file从哪里来$_GET、$_POST、$_COOKIE、$_SERVER中间经过哪些处理trim()、basename()、str_replace()、正则匹配是否有类型转换如(int)$_GET[id]会强制转为整数但$file (string)$_GET[file]反而可能引入类型混淆。我曾在一个支付回调接口中发现$callback $_GET[callback]; if (strpos($callback, ..) ! false) { die(Invalid callback); } include callbacks/ . $callback . .php;表面看有检测但strpos只检查..而....//能绕过。更致命的是$callback未做basename()处理传入/etc/passwd会直接包含绝对路径。审计时永远假设“所有输入都是恶意的”然后验证每一层过滤是否真正堵死了所有路径。3.3 业务逻辑验证结合场景判断真实风险即使代码存在include $_GET[x]也不代表一定可利用。必须结合部署环境判断PHP版本低于5.3.4的系统才需考虑空字节截断配置项allow_url_fopen、allow_url_include是否开启php -i | grep allow_url文件权限Web用户能否读取目标文件如/etc/shadow通常不可读路径结构include的当前工作目录是什么getcwd()可确认。举个反例某CMS的index.php中有include lang/ . $lang . .php;$lang来自$_SESSION[lang]。虽然$_SESSION看似可控但实际$lang在登录时被严格限定为en、zh、ja且存储前经过preg_match(/^[a-z]{2}$/, $lang)校验。这种情况下即使代码写法不完美风险也极低——安全不是看代码有多“丑”而是看攻击面是否真正开放。4. 从开发到运维的全链路防御方案不止是加个basename()很多团队把LFI防御简化为“加个basename()”或“加个白名单”这就像给炸弹装个塑料盖子。真正的防御必须覆盖开发、测试、部署、监控全生命周期。下面是我实践多年总结的四层防线每一层都解决特定维度的问题。4.1 开发层用设计模式杜绝动态路径拼接根本解决方案是避免在include中使用用户输入。推荐两种架构级改进模板引擎隔离用Twig、Blade等模板引擎替代原生include。它们的{% include header.html %}是编译时解析路径硬编码无法被运行时参数影响。路由映射表将动态参数映射为预定义文件名。例如$route_map [ product pages/product.php, category pages/category.php, search pages/search.php ]; $page $_GET[page] ?? home; if (isset($route_map[$page])) { include $route_map[$page]; } else { include pages/404.php; }这种方式彻底消除路径拼接且易于维护。经验我在重构一个老项目时把所有include $_GET[xxx]替换为路由映射表代码量减少30%审计报告中的高危漏洞直接归零。最好的防御是让漏洞根本没有存在的土壤。4.2 测试层自动化检测与模糊测试人工审计效率低必须引入工具静态分析PHPStan、Psalm可配置自定义规则检测include参数是否来自超全局变量动态扫描Burp Suite的Intruder模块对page参数批量发送../../../../etc/passwd、php://filter/...等payload观察响应差异模糊测试用FFUF或Gau生成大量路径变异..%2f,..%c0%af,....//等自动探测可遍历深度。特别提醒不要只扫404响应。LFI成功时可能返回200但内容为空文件不存在或返回500PHP解析错误。要关注响应体长度、关键词如root:x:0:0:、HTTP头变化如Content-Type: text/plain。4.3 运维层PHP配置与文件系统加固开发再严谨配置错误也会功亏一篑关闭危险配置在php.ini中设置allow_url_fopenOff、allow_url_includeOff这是RFI和data://利用的前提限制包含路径用open_basedir指定include只能访问的目录如open_basedir /var/www/html:/tmp文件权限最小化Web用户对/etc/、/root/等目录无读取权限对日志目录无写入权限防止日志包含。我管理的服务器上open_basedir是必配项。某次上线新版本开发同学忘了配置扫描器立刻告警。我们立即回滚并在CI/CD流程中加入php -i | grep open_basedir检查确保配置生效。4.4 监控层实时检测与响应最后防线是行为监控日志审计在Web服务器日志中提取所有包含..、php://、data://的请求用ELK或Splunk告警进程监控用auditd监控/var/log/、/tmp/等敏感目录的写入事件内存取证当怀疑被植入WebShell时用gcore抓取PHP-FPM进程内存搜索eval(、system(等函数调用痕迹。去年处理一次入侵事件正是通过Nginx日志发现大量filephp://filter/...请求结合ps aux | grep php发现异常进程最终定位到被篡改的wp-config.php文件。防御不是一劳永逸而是持续对抗的过程。5. 真实攻防复盘从DVWA到极客大挑战的漏洞利用细节理论终需落地。下面以两个经典靶场为例还原从发现到利用的完整过程所有步骤均经我亲手验证参数和响应均来自真实环境。5.1 DVWA Low级别最基础的LFI利用DVWA的“File Inclusion”模块Low级别代码如下$file $_GET[page]; include($file);利用步骤访问http://dvwa/vulnerabilities/fi/?pagefi.php确认功能正常尝试http://dvwa/vulnerabilities/fi/?page../../../../etc/passwd返回Warning: include(): Failed opening ...说明路径遍历被阻断因PHP默认工作目录为web根目录..超出范围改用php://filterhttp://dvwa/vulnerabilities/fi/?pagephp://filter/readconvert.base64-encode/resource/etc/passwd页面返回Base64编码的/etc/passwd内容解码后得到root:x:0:0:root:/root:/bin/bash证明LFI存在进一步尝试/etc/apache2/sites-enabled/000-default.conf获取网站配置确认DocumentRoot路径。关键洞察DVWA Low级别之所以“低”是因为它没做任何过滤但受限于PHP的open_basedir默认开启无法直接读取/etc/passwd。必须用php://filter绕过。这说明即使漏洞存在利用方式也高度依赖环境配置。5.2 极客大挑战2019PHP序列化与LFI的组合利用题目[极客大挑战 2019]php的源码关键片段class Handle { private $file; public function __construct($file) { $this-file $file; } public function __wakeup() { if (preg_match(/flag|php/i, $this-file)) { echo No; exit(); } include $this-file; } } unserialize($_GET[data]);利用链分析存在反序列化入口unserialize($_GET[data])Handle类的__wakeup()方法会include $this-file且有过滤/flag|php/i过滤可被绕过fla%67URL编码、f\l\a\g转义、FLA*通配符更巧妙的是include支持phar://协议。构造phar://压缩包将恶意代码写入stub存根当include phar://xxx.phar时stub会被执行。实操步骤创建恶意PHP文件shell.php?php system($_GET[cmd]); ?用phar扩展打包$phar new Phar(exploit.phar); $phar-startBuffering(); $phar-addFromString(test.txt, test); $phar-setStub(?php __HALT_COMPILER(); ?); $phar-setMetadata([file shell.php]); $phar-stopBuffering();生成序列化payloadO:6:Handle:1:{s:11:Handlefile;s:20:phar://exploit.phar;}URL编码后访问?dataO%3A6%3A%22Handle%22%3A1%3A%7Bs%3A11%3A%22Handle%24file%22%3Bs%3A20%3A%22phar%3A%2F%2Fexploit.phar%22%3B%7D成功执行?cmdid返回uid33(www-data) gid33(www-data)。这个案例揭示了LFI的进阶玩法它不再孤立存在而是作为反序列化、SSRF等漏洞的“执行载荷投递通道”。防御时必须考虑漏洞组合的可能性。6. 被低估的LFI衍生风险日志伪造、SSRF联动与供应链污染LFI的危害远不止于服务器沦陷。在现代云原生架构中它常与其他漏洞形成“组合拳”产生更隐蔽、更持久的影响。6.1 日志伪造从LFI到隐蔽后门当LFI点无法直接执行代码时攻击者会转向日志文件。但传统日志包含需满足“日志路径可预测可写入”而现代应用常将日志写入/var/log/nginx/access.log或/var/log/apache2/access.log。攻击者可构造特殊User-AgentGET / HTTP/1.1 User-Agent: ?php system($_GET[cmd]); ?Nginx会将此UA记录到access.log。再通过LFI包含该日志即可执行。但问题在于access.log通常权限为640Web用户www-data无读取权。此时攻击者会转向错误日志error.log因为PHP错误日志常由Web用户写入且路径固定如/var/log/php_errors.log。我曾在某金融API网关中发现其错误日志配置为log_errorsOn、error_log/var/log/php_errors.log且该文件权限为664。攻击者发送?file?php system(id);?触发PHP解析错误错误信息Parse error: syntax error...被写入error.log。再用LFI包含该日志即可执行任意命令。这种利用方式无需上传文件完全依赖服务端自身日志机制极难被WAF识别。6.2 SSRF联动LFI作为内网探测跳板当LFI点位于内网服务如管理后台时可结合SSRF扩大攻击面利用php://filter读取/proc/self/environ获取环境变量发现内网Redis地址用gopher://协议需allow_url_fopenOn向Redis写入WebShell或通过LFI包含/etc/hosts修改DNS解析将internal-api.local指向攻击者控制的服务器。某政务系统审计中其内网管理后台存在LFI且allow_url_fopenOn。我构造filegopher://127.0.0.1:6379/_SET%20shell%20%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22成功向本地Redis写入PHP代码再通过filefile:///var/www/html/shell.php包含执行。LFI在此成为SSRF的“协议转换器”将网络请求转化为文件操作。6.3 供应链污染开源组件中的LFI隐患很多LFI漏洞藏在第三方库中。例如旧版PHPExcelClasses/PHPExcel/Reader/Excel2007.php中include $pFilename未校验WordPress插件某SEO插件的admin/import.php中include $_POST[file]Composer包monolog/monolog早期版本中StreamHandler的$filename参数可被注入。2023年爆发的laravel-debugbarLFI漏洞CVE-2023-30547就是典型供应链风险。攻击者只需访问/debugbar/open?file../../../.env即可读取数据库密码。这类漏洞的特点是开发者完全不知情因为问题在依赖包中修复需等待上游发布补丁存在时间差。因此必须将composer audit、npm audit纳入CI/CD流程实时扫描已知漏洞。最后分享一个血泪教训三年前我负责的一个SaaS平台因phpmailer旧版本LFI被利用攻击者读取了.env文件获取了AWS密钥。虽然后续重置了密钥但客户数据已被导出。自此我坚持所有PHP项目必须启用composer.lock文件校验并在部署时运行snyk test。安全不是某个环节的事而是贯穿代码从编写到运行的每一秒。
返回列表