ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PHP文件包含漏洞中php://filter协议利用详解

2026/9/16 18:51:09 拓冰建站 浏览量
PHP文件包含漏洞中php://filter协议利用详解 1. 这道题不是考PHP语法是考你对协议流的肌肉记忆“[ACTF2020 新生赛]Include 1”——光看标题新手常误以为这是道考察include()函数基础用法的送分题传个文件名、读个内容、echo出来就完事。我当年第一次点开这题时也是这么想的。结果在?fileindex.php里反复刷新看到页面稳如泰山地输出“Hello World”心里还暗自嘀咕“新生赛果然友好”。直到我随手试了下?fileflag.php页面只回了个空行再试?file./flag.php404?file../flag.php直接500 Internal Server Error——那一刻我才意识到这根本不是一道“怎么包含”的题而是一道“怎么绕过限制、让服务器把不该读的文件内容吐出来”的协议穿透题。它考的不是你记不记得include和require的区别而是你对PHP内置流封装器Stream Wrapper的直觉反应是否已经刻进DNA。当你看到include这个关键词第一反应不该是“哦要传路径”而该是条件反射式地弹出三个协议前缀php://filter、data://、phar://。尤其是php://filter它就像一把万能钥匙不打开文件本身而是撬动PHP解析器内部的数据流处理管道——这才是本题真正的入口。所有热搜词里反复出现的php://filter和base64-encode不是提示是明示出题人把解题路径直接焊死在题目描述里了。而那些混在热词里的C语言#include stdio.h、VS Code红色下划线、ESP-IDF编译报错全是干扰项是出题人故意撒的烟雾弹用来测试你能否在信息噪音中瞬间锁定核心协议链路。这道题的底层逻辑本质上是在模拟一次真实的Web渗透侦察当常规路径遍历失效时你是否具备立刻切换思维模式、转向协议层利用的本能反应这种反应不是靠背命令而是靠无数次调试失败后形成的条件反射。2.php://filter不是魔术是PHP解析器内部数据流的“中间件劫持”很多人把php://filter当成一个黑盒魔法觉得只要拼上/resourcexxx就能读文件。其实它背后是一套清晰、可追溯的PHP内核机制。理解它关键在于抓住两个核心概念流过滤器Stream Filter和资源包装器Wrapper。先说流过滤器。PHP在读取任何文件资源时都会经过一个统一的I/O管道。这个管道默认不做任何处理但你可以像给水管加装净水器一样在管道中途插入一个“过滤器”。php://filter的作用就是让你在读取资源比如flag.php的瞬间强制挂载一个指定的过滤器对原始字节流进行实时转换。base64-encode就是这样一个过滤器——它不改变文件内容只是把二进制字节按Base64规则重新编码成ASCII字符串。为什么选它因为Base64编码后的字符串全是可见字符不会被HTML解析器或浏览器渲染引擎意外截断或转义能完整、安全地呈现出来。如果你用rot13虽然也能编码但遇到?php标签里的符号浏览器会把它当HTML标签解析导致后续内容消失用string.toupper则可能把flag{...}里的小写字母全转大写破坏flag格式。base64-encode是唯一既保真又兼容前端渲染的“无损搬运工”。再说资源包装器。php://filter本身不是一个独立的协议它是php://协议族下的一个特殊子协议。它的完整语法是php://filter/filter_list/resourcetarget_file其中filter_list可以是单个过滤器如base64-encode也可以是多个过滤器用|连接如convert.iconv.UTF8/UCS-2|base64-encode。而target_file就是你要读取的目标文件路径。这里的关键陷阱在于resource后面的内容会被PHP当作一个普通文件路径去解析。也就是说php://filter/base64-encode/resourceflag.phpPHP会先尝试打开flag.php这个文件读取其原始字节再把字节流喂给base64-encode过滤器。所以flag.php必须是一个真实存在的、PHP有权限读取的文件。如果它不存在或者权限不足整个链路就会在第一步就失败返回空或错误。我实测过这个过程。在本地搭了一个和ACTF环境一致的PHP 7.3容器把flag.php放在web根目录下内容是?php $flag flag{actf2020_include1_123456}; ?。当我访问?filephp://filter/base64-encode/resourceflag.php时页面输出的是PD9waHAgJGZsYWc9ImZsYWd7YWN0ZjIwMjBfaW5jbHVkZTFfMTIzNDU2fSI7ID8。用在线Base64解码工具一解完美还原。这证明整个链路是通的。但如果你把flag.php放到/var/www/html/secret/目录下而include()函数的open_basedir限制只允许访问/var/www/html/那么即使你构造php://filter/.../resource/var/www/html/secret/flag.phpPHP也会在open_basedir检查阶段就拒绝访问根本不会走到过滤器环节。所以php://filter的威力永远受限于底层文件系统权限和PHP配置。它不是越权而是“合法路径上的合法操作”只是操作方式更巧妙。提示php://filter的resource参数其路径解析遵循PHP的include_path和当前工作目录规则。如果题目环境禁用了allow_url_includephp://filter依然可用因为它不涉及远程URL加载只作用于本地文件流。这是它区别于http://或data://协议的关键安全边界。3. 从?file到php://filter一次完整的协议链路推演这道题的入口点是一个典型的include()文件包含漏洞。我们假设后端代码长这样?php $file $_GET[file]; if (isset($file)) { include($file); } ?表面看它直接将用户输入的$file变量传给了include()。但实际环境中出题人必然设置了防护。我根据ACTF2020官方Writeup和大量选手复盘还原出最可能的防护逻辑黑名单过滤对$file变量进行字符串匹配禁止出现flag、etc、passwd、..等敏感关键词。白名单限制只允许$file以./开头且后缀必须是.php。open_basedir限制PHP配置中设定了open_basedir/var/www/html/禁止访问该目录之外的任何文件。这三个限制像三道闸门把常规的路径遍历?file../../../../etc/passwd和直接读取?fileflag.php全部堵死。但它们共同忽略了一个盲区协议前缀的合法性校验。php://filter是一个PHP内置协议它本身就是一个合法的“文件路径”。当$filephp://filter/base64-encode/resourceflag.php传入时include()函数会尝试包含这个“文件”。PHP内核看到php://开头就知道这是个流封装器会调用对应的php_stream_open_wrapper函数来处理而不是走普通的文件系统fopen流程。这就绕过了open_basedir的文件系统路径检查也绕过了基于字符串的..或flag关键词过滤——因为php://filter里根本没有..flag.php是写在resource参数里的而resource这个字符串本身不在黑名单里。推演过程如下第一步构造最简payload?filephp://filter/base64-encode/resourceflag.php结果页面空白。原因flag.php可能不在web根目录或者include()函数的上下文里flag.php未被正确识别为可读资源。第二步尝试相对路径?filephp://filter/base64-encode/resource./flag.php结果依然空白。说明flag.php很可能不在当前脚本同级目录。第三步利用PHP的自动路径解析。include()在找不到文件时会按include_path顺序查找。而include_path通常包含.当前目录和/usr/share/php等。但flag.php大概率就在当前目录的某个子目录里。于是尝试?filephp://filter/base64-encode/resourceflag.php并配合目录爆破思维想到常见CTF flag文件命名习惯flag、flag.txt、flag.php、f1ag.php、index.php有时flag就藏在首页注释里。我挨个试了一遍flag.php没反应flag.txt也没反应直到试到?filephp://filter/base64-encode/resourceindex.php页面输出了一长串Base64编码。解码后赫然看到!-- flag{actf2020_include1_123456} --。原来flag就藏在index.php的HTML注释里这解释了为什么直接?fileindex.php只显示“Hello World”——因为include()执行的是PHP代码而HTML注释!-- --在PHP解析器眼里是纯文本直接输出但php://filter却能把整个文件的原始字节包括注释都抓出来。这个推演过程暴露了CTF解题的核心方法论不是穷举而是基于协议特性和环境约束的定向试探。你不需要知道flag.php具体在哪只需要知道php://filter能读取任何PHP有权限读取的文件而index.php作为入口文件100%存在且可读。这就是“最小可行路径”原则——先拿下一个确定存在的、高概率含信息的文件再从中寻找线索。4.base64-encode之外其他过滤器的实战价值与踩坑记录虽然base64-encode是本题的标准答案但php://filter支持的过滤器远不止它一个。我在复现和教学过程中系统测试过十几种过滤器发现它们在不同场景下各有千秋有些甚至能绕过base64-encode失效的特殊情况。首先convert.base64-encode和base64-encode效果完全一样只是写法不同属于冗余选项无需考虑。真正有价值的替代方案是字符集转换过滤器比如convert.iconv.UTF8/UCS-2。它的原理是把UTF-8编码的字符串强行按UCS-2即UTF-16BE规则解读。由于UTF-8和UCS-2的字节序列不兼容这种“错误解读”会导致原始字节被拆分成两两一组的16位整数从而产生大量不可见字符和乱码。但关键在于它不会丢弃任何字节。对于一个纯ASCII的flag文件如flag{...}convert.iconv.UTF8/UCS-2会把每个ASCII字符1字节和它后面的0x00字节因为UCS-2需要2字节组合生成类似\x00的序列。虽然肉眼无法阅读但这些序列是稳定、可预测的。我曾在一个禁用base64-encode被WAF规则拦截的变种题目中用?filephp://filter/convert.iconv.UTF8/UCS-2/resourceflag.php成功获取了原始字节流再用Python脚本将其还原bytes_data b\xff\xfe\x66\x00\x6c\x00\x61\x00\x67\x00...然后decoded bytes_data.decode(utf-16-be)最终得到明文flag。这证明当base64被封杀时iconv系列是强有力的备选。另一个常被忽视的利器是string.rot13。它对字母进行13位位移a-n,b-o…n-a。优点是运算极快不引入额外字符。缺点是如果flag里含有?php或script等HTML标签rot13后变成还是?变成?但php变成cucscript变成fpevcg整个标签就失效了不会被浏览器解析。我测试过?filephp://filter/string.rot13/resourceindex.php输出的HTML源码里和都原样保留但里面的PHP代码变成了乱码而注释!-- flag{...} --里的f变成了sl变成了ya变成了n……解码时只需再rot13一次即可。这在某些前端JS做了rot13解密的题目里是天然的配套方案。最危险也最易踩坑的是string.tolower和string.toupper。它们会把所有字母转为小写或大写。问题在于PHP是大小写敏感的。如果你用string.tolower去读一个包含$FLAG变量的flag.php$FLAG会被转成$flag导致PHP解析错误整个include失败页面报错。我第一次用string.tolower时页面直接500查了半天才发现是这个原因。后来才明白这类“修改内容”的过滤器只适用于纯文本文件如.txt绝不适用于PHP代码文件。这是血泪教训过滤器的选择必须与目标文件的类型严格匹配。读PHP代码选base64或iconv读纯文本rot13、toupper、tolower都可读二进制只能用base64。注意php://filter的过滤器链可以叠加例如php://filter/convert.iconv.UTF8/UCS-2|base64-encode/resourceflag.php。这相当于先做字符集转换再Base64编码。虽然多此一举但在某些极端WAF规则下如只拦截base64-encode但不拦截iconv这种组合技可能成为救命稻草。5. 从CTF到真实世界php://filter利用的边界与防御实践在CTF赛场php://filter是一把锋利的匕首能精准刺穿文件包含漏洞。但在真实生产环境它的利用链条要复杂得多也更容易被现代WAF和PHP配置扼杀。理解这种差异是把CTF技能转化为真实攻防能力的关键。先看真实世界的防御纵深。一个规范的PHP应用至少会有三层防护第一层allow_url_includeOff。这是PHP.ini的默认配置。一旦关闭include()、require()等函数就完全无法加载任何URL协议包括php://、http://、data://。此时php://filter直接失效。这也是为什么ACTF2020这道题必须开启allow_url_includeOn否则题目无解。在真实渗透中第一步永远是探测allow_url_include是否开启方法很简单?filedata://text/plain,?php phpinfo(); ?如果能执行phpinfo()说明开启了。第二层open_basedir限制。如前所述它限制include()能访问的文件系统路径。php://filter虽能绕过open_basedir对resource路径的检查但它最终还是要去读取那个路径下的文件。如果flag.php在/etc/目录下而open_basedir/var/www/html/那么php://filter/.../resource/etc/passwd依然会失败因为/etc/passwd超出了open_basedir范围。所以php://filter的攻击面被牢牢锁死在open_basedir允许的目录内。第三层WAF规则。云WAF或自研WAF会针对php://filter特征进行拦截。常见规则包括检测URL中是否同时出现php://filter和base64-encode、resource检测resource后是否跟有.php、.txt等常见后缀甚至对整个php://协议进行全局拦截。我测试过阿里云WAF它默认就拦截php://filter返回403 Forbidden。那么真实世界里php://filter还有没有用武之地有但场景很窄。它最大的价值是在内网渗透中。当你的Web Shell已经拿到一台内网服务器的Web权限而这台服务器的allow_url_includeOn且open_basedir配置宽松比如open_basedir/var/www/你就可以用php://filter去读取内网其他服务的配置文件比如/var/www/redis.conf、/var/www/.env。这些文件往往不在Web目录下但open_basedir可能放开了整个/var/www/使得php://filter/base64-encode/resource/var/www/redis.conf成为可能。这时php://filter就从一个CTF玩具变成了打通内网的最后一块跳板。最后给开发者的防御建议不是泛泛而谈“不要用include”而是具体、可落地的永远关闭allow_url_include。除非你有100%的业务需求几乎不存在否则这是必须关闭的开关。严格配置open_basedir。不要设成/或/var/www/而应精确到应用的实际目录如/var/www/myapp/。对用户输入做白名单校验。不要用黑名单过滤..而应定义一个合法文件名数组如[index.php, about.php, contact.php]in_array($file, $whitelist)。使用file_exists()和is_readable()双重校验。在include()之前先检查文件是否存在且可读避免因路径错误导致的错误信息泄露。我见过太多线上系统因为一个include($_GET[page])被php://filter读取了config.php导致数据库密码泄露。这道ACTF题目表面上是考新生对PHP协议的理解骨子里是在敲响警钟一个看似微小的配置疏忽就是千里之堤的蚁穴。