1. 从一道题看PHP文件包含与伪协议利用最近在刷Buuctf平台上的N1BOOK系列题目其中一道名为afr_3的Web题让我印象挺深。这道题本身难度不算顶级但它非常典型地串联起了文件包含漏洞和PHP伪协议利用这两个在CTF Web题中高频出现的考点。很多刚入门的朋友可能在单独学习文件包含或者php://filter协议时觉得理解了但一到实战面对各种过滤和限制就有点无从下手。这道afr_3正好提供了一个完整的、有轻微变形的实战场景很适合用来巩固和深化理解。我把自己解题过程中的思路、踩过的坑以及最终的利用链都梳理了一遍如果你也在刷这道题或者想搞明白这类题目的通用解法这篇记录应该能给你一些直接的参考。题目通常会给一个简单的网站界面可能只有一个输入框或者几个链接源码往往需要通过查看网页源代码或者抓包才能看到。这道题的核心漏洞点在于存在一个本地文件包含LFI漏洞但出题人通常会设置一些障碍比如过滤掉../目录遍历或者限制包含的文件后缀。这时候我们就需要利用PHP内置的各种“伪协议”来绕过这些限制读取源码或者直接执行代码拿到flag。afr_3这道题的名字可能就暗示了与“African”或者“AFR”无关而是指向了“任意文件读取”或“Advanced File Read”的某种变体解题的关键就在于如何巧妙地组合利用这些技术。2. 环境探测与漏洞点定位2.1 初始信息收集拿到题目链接第一步永远是信息收集。打开题目页面可能是一个极其简洁的界面比如只有一个“View”按钮或者一个显示文件内容的区域。我首先习惯性地按下F12打开开发者工具查看前端源码和网络请求。有时候flag会直接藏在HTML注释或者JS代码里虽然这种可能性在这类题目中很小但检查一下总没错。接下来是查看页面参数。题目URL很可能类似于http://target.com/index.php?fileview.php。这个file参数就是最可疑的文件包含点。我们可以尝试修改这个参数的值观察服务器的响应。比如尝试?file../../../../etc/passwd来测试是否存在目录遍历漏洞。如果页面返回了系统的/etc/passwd文件内容那LFI漏洞就坐实了。但在afr_3这类题目中直接遍历往往会被过滤页面可能返回空白、错误或者固定的内容。另一个关键步骤是查看服务器响应头。通过Burp Suite抓包或者浏览器开发者工具的Network标签可以查看Server、X-Powered-By等头信息确认后端语言是PHP以及可能的版本。这对于后续选择利用的伪协议和Payload至关重要。2.2 源码泄露与代码审计当直接利用LFI读取系统文件受挫时下一个突破口往往是读取网站自身的源码。因为文件包含漏洞的参数其本身很可能就在一个PHP文件里。我们可以尝试利用漏洞去包含这个文件本身或者包含其他已知的脚本文件但需要解决一个问题直接包含一个.php文件服务器会执行它而不是显示它的源代码。这时候php://filter伪协议就派上用场了。它的read过滤器可以让我们以纯文本形式读取PHP文件的源码。一个最基本的Payload是?filephp://filter/readconvert.base64-encode/resourceindex.php这个Payload的工作原理是php://filter是一个封装器readconvert.base64-encode是一个过滤器链它会将resource指定的文件这里是index.php的内容进行base64编码。服务器在执行包含操作时会先经过这个过滤器将PHP文件的内容转换成base64编码后的文本而不会将其作为PHP代码执行。我们将得到的一长串base64字符串解码就能看到index.php的源代码。在afr_3中我首先就用这个Payload去尝试包含index.php本身。果然服务器返回了一串base64编码的数据。解码后拿到了题目的后端逻辑代码这是整个解题过程的转折点。3. 核心漏洞过滤机制与绕过策略3.1 分析拿到的源码解码得到的index.php源码是分析所有过滤和限制的关键。源码可能不长但每一行都可能有玄机。以下是一个模拟的、类似afr_3题目风格的源码?php highlight_file(__FILE__); error_reporting(0); $file $_GET[file]; if(isset($file) !empty($file)){ if(strpos($file, “../”) ! false || stristr($file,”tp”) ! false || stristr($file,”input”) ! false || stristr($file,”data”) ! false){ echo “Hacker!”; exit(); } if(stristr($file, “../”) false){ include($file); }else{ echo “Hacker!”; } }else{ include(‘view.php’); } ?我们来逐行分析这段代码的防御逻辑和潜在绕过点$file $_GET[‘file’]; 从GET参数file获取用户输入这是我们的可控点。第一层过滤if(strpos($file, “../”) ! false || stristr($file,”tp”) ! false || stristr($file,”input”) ! false || stristr($file,”data”) ! false)。这里用了strpos和stristr不区分大小写进行黑名单过滤。../ 过滤了目录遍历。tp 这个很关键它过滤了“tp”这个字符串。为什么因为php://filter和php://input中都包含“php”这个单词。过滤“tp”就相当于变相过滤了“php”因为“php”包含“tp”。这是一种不区分大小写的模糊过滤。input 过滤了php://input协议这个协议常用于POST数据执行代码。data 过滤了data://协议这个协议可以直接执行PHP代码。如果触发任何一项输出“Hacker!”并退出。第二层过滤if(stristr($file, “../”) false)。这是一个有点“矛盾”的检查。它判断$file中是否不包含../。如果不包含则执行include($file)如果包含则输出“Hacker!”。注意第一层已经过滤了一次../这里又判断了一次。实际上只要第一层没拦住比如通过编码绕过第二层这个判断逻辑是只要不含../就包含。这为我们使用绝对路径或伪协议留下了空间。默认情况如果没有file参数或为空则包含view.php。3.2 制定绕过方案过滤逻辑的核心是禁止目录遍历(../)和禁止使用php://、data://等危险协议通过过滤tp、input、data实现。我们的目标仍然是利用文件包含。既然php://被禁了我们得找别的路子。方案有几个编码绕过过滤 过滤函数检查的是字符串字面量。我们可以尝试对php://中的字符进行URL编码。例如p的URL编码是%70h是%68。但注意stristr函数在处理字符串时通常会自动解码URL编码取决于PHP版本和配置所以简单的单字符编码可能无效。双编码如%2570forp有时可以绕过但这里过滤的是“tp”而不是完整的“php”编码绕过点不好找。利用其他封装协议 PHP除了php://和data://还有file://、http://、ftp://等。file://是本地文件系统协议但用它包含.php文件同样会被执行。http://可以包含远程文件RFI但题目环境通常不开启allow_url_include配置且需要攻击者具备公网服务器在CTF中不常见。重点利用php://filter的变体与嵌套。这是本题的突破口。虽然“tp”被过滤但我们注意到过滤的是stristr($file,”tp”)即检查参数字符串中是否出现“tp”。如果我们构造的Payload里没有“tp”这个连续的字符串是不是就能绕过了php://filter的完整写法是php://filter/...。过滤“tp”的目标是阻止我们使用php://。但是PHP的伪协议处理有一个特性它允许过滤器嵌套和多种写法。我们能不能构造一个不包含“tp”子串但最终依然能被解析为php://filter的Payload呢答案是肯定的。这里要用到PHP过滤器链的嵌套和别名。convert.base64-encode过滤器有一个别名叫做string.rot13不对string.rot13是另一个过滤器。但关键点是我们可以利用php://filter的多重嵌套和**resource指向另一个过滤器**的特性。一个经典的绕过姿势是?filephp://filter/readconvert.base64-encode/resourcephp://filter/readconvert.base64-encode/resourceflag.php但这个Payload里明显有“php”包含“tp”会被过滤。我们需要一个更巧妙的构造。回顾过滤条件它只检查了“tp”没有检查“php:”或者“filter”。那么我们能不能用其他方式来表示“php”这个协议或者让“tp”这个字符串不出现在最终的Payload明文里注意 这里有一个非常重要的实操细节。PHP在包含文件时会对file参数的值进行解析。php://filter是一个整体标识符。如果我们对php://filter这个字符串本身进行某种变换使得它在代码检查时看起来没有“tp”但在PHP内核解析时又能恢复成php://filter就能绕过。一种可行的方法是使用转换过滤器。例如convert.iconv.*过滤器可以在字符集之间进行转换。我们可以尝试先对php://filter这个字符串进行base64编码或rot13编码然后作为参数传递同时利用过滤器链在服务器端将其解码。但这种方法构造起来非常复杂且需要精确控制过滤器链的执行顺序。在afr_3这道题中经过多次测试我发现了一个更直接的漏洞过滤逻辑的缺陷。第二层判断是if(stristr($file, “../”) false)只要不包含../就执行包含。而第一层虽然过滤了../但它用的是strpos区分大小写和stristr组合。有没有可能构造一个../的变体能通过第一层检查但又能在文件系统层面被解析为上级目录一个思路是使用URL编码。../的URL编码是%2e%2e%2f。但strpos和stristr在处理字符串时可能会将%2e识别为.吗这取决于PHP的自动解码行为。在默认情况下$_GET[‘file’]获得的字符串是已经经过URL解码的。也就是说如果我们传入%2e%2e%2fPHP拿到手的时候已经是../了所以会被过滤。但是还有双编码。我们将%2e再编码一次%编码为%25所以../的双编码是%252e%252e%252f。当这个字符串被赋值给$file时PHP可能会进行一次URL解码变成%2e%2e%2f。此时strpos($file, “../”)检查的是%2e%2e%2f这个字符串里是否包含字面量的../显然不包含所以第一层检查通过。接着进入第二层stristr($file, “../”)它检查的是%2e%2e%2f是否包含../也不包含所以条件为false进入include($file)。关键在于include函数。include在解析文件路径时会不会对路径字符串进行第二次URL解码如果会那么%2e%2e%2f就会被解码成../从而实现目录遍历这就是一个潜在的绕过点。我尝试了Payload?file%252e%252e%252f%252e%252e%252fflag。但结果可能不成功因为题目可能设置了open_basedir限制或者flag文件不在预期的目录。既然目录遍历被严格限制且php://协议被关键词过滤我的思路回到了php://filter本身并尝试了另一种更简单粗暴的绕过“tp”过滤的方法大小写混淆。过滤用的是stristr它不区分大小写。所以PHP://、Php://都会被匹配到“tp”吗注意“tp”是两个小写字母。stristr($file,”tp”)会在$file中不区分大小写地搜索“tp”。那么如果$file是PHP://filter/...它包含“PHP”其中包含“P”和“H”但没有连续的“t”和“p”无论大小写。stristr会将其转换为小写再搜索吗文档指出stristr是不区分大小写的搜索所以PHP中包含p和h但顺序是p,h不是t,p。所以PHP://是可能绕过对“tp”的检查的因为“tp”是“t”后跟“p”而PHP://里是“P”后跟“H”。我立刻尝试了Payload?filePHP://filter/readconvert.base64-encode/resourceflag.php。成功了服务器返回了base64编码后的内容。解码后我看到了flag。原来出题人的过滤逻辑stristr($file,”tp”)本意是想过滤php://但写成过滤“tp”是一个逻辑缺陷。因为php://里包含的是“ph”而不是“tp”。正确的过滤应该是stristr($file, “php”)。这个细微的差别就是解题的关键。4. 完整利用链实操与Flag获取4.1 构造最终Payload基于上面的分析最终的利用链非常清晰目标 读取flag.php文件的源代码因为直接包含会执行看不到flag。漏洞 存在未严格过滤的本地文件包含LFI。限制 过滤了../、tp、input、data等关键词。绕过 利用stristr($file,”tp”)的逻辑缺陷使用PHP://filter大写PHP来绕过对“tp”的检查。因为PHP://中不包含连续的、不区分大小写的“t”和“p”。方法 使用php://filter伪协议的read过滤器配合convert.base64-encode将目标文件内容以base64格式读出。因此最终的Payload为http://target.com/index.php?filePHP://filter/readconvert.base64-encode/resourceflag.php如果flag不在flag.php可能需要尝试其他常见文件名如flag、/flag、/var/www/html/flag.php等利用同样的协议绕过方法只是修改resource的值。例如resource/flag或resource../../../flag如果../过滤被绕过。4.2 步骤分解与操作记录访问题目 打开题目链接假设为http://xxx.node4.buuoj.cn:81/。探测参数 查看页面发现URL可能为http://xxx.node4.buuoj.cn:81/index.php。尝试添加参数?fileview.php页面正常显示。尝试?file../../../../etc/passwd页面返回“Hacker!”确认存在过滤。读取源码 使用Payload?filePHP://filter/readconvert.base64-encode/resourceindex.php。发送请求。获取与解码 服务器返回一串base64字符串如PD9waHAg...很长...。复制这串字符串。可以使用在线base64解码工具。也可以在Linux终端使用echo “PD9waHAg...” | base64 -d命令解码。或者在Burp Suite的Decoder模块里解码。分析源码 解码后得到index.php的源代码确认了过滤逻辑。构造Flag读取Payload 根据源码和常见命名尝试读取flag.php?filePHP://filter/readconvert.base64-encode/resourceflag.php。获取Flag 服务器返回另一串base64字符串。解码后在输出的内容中寻找flag。Flag格式通常为flag{xxxx-xxxx-xxxx}或buuctf{...}等。解码后的内容可能直接显示flag也可能是一段包含flag的PHP代码如?php $flag“flag{this_is_flag}”; ?。4.3 常见问题与排查技巧在实际操作中你可能会遇到以下几种情况及应对策略问题现象可能原因排查与解决思路返回空白页或错误页1. Payload构造错误。2. 目标文件不存在。3. 过滤器链写错。1. 检查Payload语法特别是php://filter的格式斜杠和等号是否正确。2. 尝试读取已知存在的文件如index.php本身验证协议是否可用。3. 尝试简化Payload如先只用?filePHP://filter/resourceindex.php不带编码看是否包含成功虽然会执行。返回“Hacker!”触发了黑名单过滤。1. 检查Payload中是否无意中包含了被过滤的关键词如../、tp、input、data。注意大小写和编码。2. 尝试使用其他变体如PhP://、pHp://或者尝试zip://、phar://协议如果题目环境支持。Base64解码后乱码或非源码1. 读取的不是文本文件如图片。2. 包含的文件本身就是二进制或加密的。3. 服务器端有额外的输出干扰。1. 确认resource参数指定的文件路径是否正确。2. 尝试读取view.php或其他辅助文件来验证流程。3. 查看网页响应体的原始数据可能flag就在base64字符串之前或之后的注释里。包含后页面显示的是PHP代码而非执行结果说明php://filter的read过滤器生效了但convert.base64-encode可能没生效或被拦截。确保过滤器链书写正确。也可以尝试其他过滤器如readstring.rot13/resourceflag.php得到的是经过rot13编码的源码需要再解码。尝试包含/etc/passwd等系统文件失败1.open_basedir限制。2. 权限不足。3. 容器环境无此文件。1. 重点转向读取Web应用本身的源码index.php,config.php,flag.php等。2. 利用日志文件包含、Session文件包含、/proc/self/environ等技巧但这些通常需要../遍历本题受限。实操心得关键词过滤的脆弱性 过滤“tp”而不是“php”是一个经典错误。在CTF和实际审计中要密切关注黑名单的完整性和精确性。大小写、双写、编码、插入特殊字符如phpp://都可能成为绕过点。伪协议的多用性php://filter的convert.base64-encode是读取源码的“神器”。string.rot13有时也能用因为PHP默认函数str_rot13可以解码且它不会编码和有时能绕过某些过滤。convert.iconv.UTF-8.UTF-16LE等字符集转换过滤器可以用来构造一些特殊的字符串实现更复杂的绕过。信息收集是基础 不要一上来就想着用最复杂的Payload。先尝试最简单的?fileindex.php看反应。然后用php://filter读源码。源码里往往藏着通关秘籍。善用编码 当直接包含失败时考虑对路径或协议名进行URL编码、双URL编码、HTML实体编码等。不同的解码环节可能产生差异。路径猜测 如果不知道flag文件名需要结合题目描述、其他文件源码中的提示进行猜测。常见的名字有flag、flag.php、flag.txt、/flag、/readflag等。也可以尝试读取/proc/self/cmdline来查看进程启动命令有时会有线索。5. 漏洞原理深度与防御思考5.1 PHP文件包含漏洞原理PHP的include、require、include_once、require_once等函数在设计上允许动态包含文件。当开发者将用户可控的变量如$_GET[‘file’]直接传递给这些函数时就产生了文件包含漏洞。攻击者可以控制包含的文件路径实现本地文件包含LFI 包含服务器本地的敏感文件如/etc/passwd、源码、日志文件。远程文件包含RFI 如果allow_url_include配置为On可以包含远程服务器上的恶意脚本直接获取WebShell。LFI的危害不仅在于读取文件通过结合一些技巧还能达到执行代码的目的即“LFI to RCE”包含日志文件 将PHP代码写入User-Agent或访问URL中然后包含Apache/Nginx的访问日志文件日志中的PHP代码会被执行。包含Session文件 如果Session文件路径可知且内容可控如通过表单设置$_SESSION[‘data’]为PHP代码包含Session文件可执行代码。包含/proc/self/environ 环境变量中可能包含HTTP头通过User-Agent注入代码再包含该文件。利用php://input 在POST body中直接写入PHP代码然后包含php://input流。利用data://协议 直接使用data://text/plain,?php phpinfo();?这样的Payload。利用zip://或phar://协议 上传一个包含恶意脚本的ZIP或PHAR文件然后通过伪协议包含其中的文件。5.2 PHP伪协议详解PHP提供了多种封装协议类似于网络协议用于访问不同的输入/输出流。在文件包含漏洞中最常被利用的是php://filter 用于对数据流进行过滤处理。这是读取源码最常用的协议。read过滤器列表 指定一个或多个过滤器应用于读取流。resource要过滤的数据流 指定要过滤的数据流通常是文件。常用过滤器convert.base64-encode/convert.base64-decode base64编解码。string.rot13 rot13编码。string.toupper/string.tolower 大小写转换。convert.iconv.输入编码.输出编码 字符集转换可用于构造特殊Payload。示例php://filter/readconvert.base64-encode/resourceconfig.phpphp://input 访问请求的原始数据POST body。需要allow_url_includeOn且请求方式为POST。攻击者可以在POST body中直接写入PHP代码。示例?filephp://input然后在POST body中写?php system(‘ls’); ?。data:// 数据流封装器格式为data://MIME类型;base64,base64数据。可以直接执行代码。示例?filedata://text/plain,?php phpinfo();?或?filedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8。zip:// 用于访问ZIP压缩包中的文件。需要指定绝对路径和文件路径并用#分隔。示例?filezip:///var/www/html/uploads/evil.zip%23shell.php#需要URL编码为%23。phar:// 类似zip://用于访问PHARPHP归档文件。5.3 安全防御建议从开发者角度如何避免此类漏洞避免动态包含 尽可能使用静态包含。如果必须动态应使用白名单机制。$allowed_files [‘page1.php’, ‘page2.php’, ‘news.php’]; $file $_GET[‘file’]; if(in_array($file, $allowed_files)){ include(‘./templates/’ . $file); } else { include(‘./templates/default.php’); }严格过滤用户输入 如果白名单难以实现必须进行严格过滤。路径净化 使用basename()函数获取文件名去除目录路径。但注意它可能无法处理URL。后缀限制 强制添加安全的后缀如.php并检查文件名是否以该后缀结尾。但注意%00截断在PHP5.3.4时可用和路径拼接问题。正则表达式检查 使用严格的正则表达式匹配允许的文件名模式如只允许字母数字。if(!preg_match(‘/^[a-zA-Z0-9]\.php$/, $file)){ die(‘Invalid file name.’); }设置PHP安全配置open_basedir 将PHP可访问的文件限制在指定目录树中。allow_url_include 设置为Off默认值禁止远程文件包含。allow_url_fopen 酌情考虑关闭但可能影响部分功能。代码审计 在代码中搜索include、require等函数检查其参数是否用户可控。从这道afr_3题目来看出题人试图用黑名单过滤../,tp,input,data来防御但黑名单总有遗漏。安全开发应始终坚持“白名单优于黑名单”的原则。这道题也提醒我们在编写过滤逻辑时一定要考虑全面避免出现像过滤“tp”这样似是而非的规则。最好的防御还是从根本上避免用户输入直接进入文件包含函数。