PHP安全:php://filter协议在CTF与渗透测试中的死亡绕过技巧

📅 2026/8/3 12:44:15
PHP安全:php://filter协议在CTF与渗透测试中的死亡绕过技巧
1. 项目概述从一道“送分题”说起最近在和一些刚入行安全研究的朋友交流时发现一个挺有意思的现象很多人在做CTF题目或者代码审计时一看到php://filter这个协议就觉得是“送分题”直接套用网上流传的php://filter/convert.base64-encode/resourceindex.php这类Payload去读文件。但一旦题目稍微加了点“料”比如结合了include、file_get_contents这些函数再设置一些过滤规则很多人就懵了不知道该怎么绕过去更别提理解其背后“死亡绕过”的精妙逻辑了。php://filter这个PHP内置的元封装器远不止是一个简单的文件读取工具。它在渗透测试、代码审计和CTF比赛中扮演着“瑞士军刀”般的角色既能用于信息收集读取源码也能用于构造特殊的攻击链比如配合包含函数执行代码。而所谓的“死亡绕过”并不是指这个技术本身致命而是形容它能够绕过一些看似严密的防御逻辑让过滤规则“死亡”或失效。理解它不仅是掌握一个技巧更是理解PHP流包装器、编码转换和代码执行上下文等深层原理的钥匙。无论你是Web安全新手还是想深化PHP代码审计能力的老手这篇文章都将带你从“会用”到“懂原理”再到“能创造”。2. 核心原理深度拆解php://filter 是如何工作的在开始各种“骚操作”之前我们必须先扎稳马步搞清楚php://filter到底是个什么东西。它不是魔法其行为完全遵循PHP官方文档的定义和底层实现逻辑。2.1 PHP流包装器一切的基础PHP有一套强大的“流”Streams抽象层它用统一的接口处理文件、网络数据、压缩数据等。php://、file://、http://这些都被称为“流包装器”Stream Wrapper。php://是一个用于访问PHP输入输出流的包装器而php://filter则是其中的一个“过滤器链”包装器。它的核心功能是在数据流到达目的地之前对其应用一个或多个过滤器进行处理。你可以把它想象成一个流水线原始数据从一端进入经过一系列“过滤器机器”如编码转换、字符串处理的加工再从另一端输出加工后的数据。2.2 filter 资源格式与过滤器链php://filter的标准格式是php://filter/过滤器1|过滤器2|过滤器3/resource要过滤的数据流这里有几个关键点过滤器Filter这是加工数据的单元。PHP内置了许多过滤器如convert.base64-encodeBase64编码、string.rot13ROT13加密、string.toupper转大写等。管道符|用于连接多个过滤器形成“过滤器链”。数据会依次经过这些过滤器。顺序至关重要不同的顺序可能导致完全不同的结果。resource这是数据流的来源。它可以是一个本地文件路径如resource./index.php。另一个php://流如resourcephp://input用于读取POST原始数据。标准输入php://stdin。当php://filter与文件读取函数如file_get_contents()、fread()结合时它的工作流程是先读取resource指定的文件内容然后将内容作为数据流依次通过过滤器链进行处理最后将处理结果返回给函数。2.3 为何能“死亡绕过”—— 理解执行上下文php://filter的威力倍增出现在它与文件包含函数如include、require结合时。这是“死亡绕过”的核心场景。这里存在一个根本性的认知差异include一个文件和file_get_contents读取一个文件PHP引擎的处理方式完全不同。file_get_contents()仅仅获取文件的内容字符串不关心内容是什么。include()/require()将目标文件的内容作为PHP代码来解析和执行。当include遇到php://filter时发生了以下事情PHP引擎尝试“打开”include参数指定的“文件”即php://filter/...这个字符串。识别出这是php://filter流包装器于是启动过滤器链从resource读取数据并应用过滤器。关键一步过滤器处理后的结果数据一个字符串会被include函数当作一个临时生成的PHP文件内容送入Zend引擎进行解析。这就产生了一个神奇的效果我们通过过滤器动态地“创造”了一段PHP代码然后让include去执行它。即使服务器上并不存在一个包含该代码的物理文件。“死亡绕过”的“死亡”体现在哪里很多防御代码会检查用户传入的文件路径是否以.php结尾是否在白名单内或者是否包含../等路径穿越字符。但是php://filter根本不是一个真正的文件路径它是一个协议流。检查文件后缀的逻辑在php://filter面前完全失效因为它不指向任何具有后缀的物理文件。这就是一种典型的“协议绕过”或“流包装器绕过”。3. 核心攻击手法与实操解析理解了原理我们来看具体怎么用。我将攻击手法分为两大类信息收集和代码执行。3.1 信息收集读取源代码这是最基础也是最常用的功能。当网站对用户输入的文件名参数处理不当允许我们控制文件读取路径时就可以利用php://filter来读取服务器上的PHP源码而不是执行它。经典Payloadphp://filter/convert.base64-encode/resourceindex.php为什么是Base64编码因为file_get_contents()会原样返回文件内容。如果直接读取.php文件返回的是原始的PHP代码包含?php ... ?标签和HTML。当这个内容被输出到网页时浏览器不会显示PHP代码因为服务器已经解析执行过了。但如果我们通过过滤器先进行Base64编码PHP引擎读取到的就是一串Base64字符串。这串字符串被echo或print输出到页面时浏览器会直接显示这串编码。我们再将其解码就能得到原始的、未执行的PHP源代码。实操步骤与注意点找到参数寻找像?fileabout.php、?pagenews这样的参数。构造Payload将参数值替换为php://filter/convert.base64-encode/resource目标文件路径。路径可以是相对路径./config.php、绝对路径/var/www/html/admin.php或利用目录穿越../../../etc/passwd但需注意resource部分对路径的处理。发送与解码发送请求在响应中获取一大串Base64编码。使用Burp Suite的Decoder模块、CyberChef或在线工具进行Base64解码即可得到源码。注意resource参数后的等号有时需要根据上下文进行URL编码%3d特别是当整个Payload作为URL参数传递时避免被错误解析。另外如果网站对输入进行了addslashes()或类似的转义单引号、双引号可能会被转义此时可以考虑不使用引号的写法如resourceindex.php或者使用编码绕过。3.2 代码执行配合文件包含这是“死亡绕过”的精华所在。场景是存在一个本地文件包含LFI漏洞但我们可以包含的文件受到限制比如只能包含.log、.txt等非PHP后缀文件或者文件内容我们无法直接控制。核心思路利用php://filter的过滤器将一段我们想要的PHP代码“写入”到一个数据流中然后让include去包含这个流从而执行我们的代码。3.2.1 基础Payload构造假设存在漏洞代码include($_GET[page] . .php);我们想让其执行?php phpinfo();?。我们不能直接包含一个写有此代码的物理文件。但我们可以这样做pagephp://filter/convert.base64-decode/resourcedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8让我们拆解这个“套娃”Payloadresource后面指向的是data://协议流其内容是一段Base64编码后的字符串PD9waHAgcGhwaW5mbygpOz8即?php phpinfo();?的Base64形式。convert.base64-decode过滤器会对resource读取到的Base64字符串进行解码。解码后的结果是什么正是原始的?php phpinfo();?字符串。这个字符串被传递给include函数include将其作为PHP代码执行于是phpinfo()页面就出来了。3.2.2 进阶利用多重过滤器与编码绕过现实中的防御不会这么简单。常见的过滤包括过滤关键字如php、die、system等被str_replace()或preg_replace()删除。检查协议禁止php://、data://等危险协议。强制后缀像前面例子强制添加.php后缀。死亡绕过技巧一嵌套过滤器处理过滤字符假设过滤了php这个字符串。我们的Payload?php phpinfo();?会被变成? info();?无法执行。 我们可以利用多个过滤器进行“编码-解码”的变换。例如php://filter/string.rot13|convert.base64-decode|convert.base64-encode|string.rot13/resourcedata://text/plain,XXXX这里的XXXX需要精心构造。思路是先想好最终要执行的代码如?php phpinfo();?然后反向推导。最终代码经过string.rot13过滤器会变成乱码。为了抵消这个影响我们需要在原始输入时先进行一次str_rot13。但中间可能还有Base64解码/编码。这需要根据过滤器链的顺序进行逆运算。 这个过程通常需要编写简单的PHP脚本进行辅助计算手动构造出合适的XXXX内容。这体现了对过滤器链顺序的深刻理解。死亡绕过技巧二利用强制后缀对于include($_GET[page] . .php);我们的Payload末尾会被添上.php。如果直接使用php://filter/...最终会变成php://filter/....php这是一个无效的流路径。 一个经典的绕过方法是利用php://filter自身对resource的处理特性。我们可以这样构造pagephp://filter/convert.base64-decode/resourcedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8/../解释Payload的resource部分指向一个data://流其Base64内容解码后为?php phpinfo();?。末尾的/../是关键。当PHP处理include的参数时会先进行路径规范化。php://filter/.../../在规范化后/../会试图回退到上一级目录。由于php://是协议流没有实际的目录结构这个/../在规范化过程中可能会被部分处理但最终形成的路径可能仍然是一个有效的php://filter流具体行为可能因PHP版本略有差异。更重要的是后面拼接的.php形成/../.php在路径规范化后可能被解释为一个位于“上一级目录”的.php文件但resource参数已经指定了数据源来自data://协议因此.php后缀实际上被“忽略”了其作为文件后缀的意义而变成了路径的一部分被过滤器链处理。在某些环境下这种方法能成功绕过后缀拼接。3.2.3 死亡绕过之王的技巧convert.iconv 字符集转换这是更高级、也更强大的技巧利用了convert.iconv.*过滤器。这个过滤器可以进行字符集转换如convert.iconv.UTF-8.UTF-16BE。 它的强大之处在于某些字符集转换过程会引入或删除特定的字节。攻击者可以精心构造原始文本经过特定的字符集转换后恰好生成?php ... ?这样的PHP标签和有效代码。例如从UTF-8转换到UTF-16BE或UTF-16LE会在字符串前添加BOM字节顺序标记\xfe\xff或\xff\xfe并且每个字符会用两个字节表示。如果我们构造一个以特定字符开头的字符串经过转换后这两个字节可能正好对应?即\x3c\x3f的字节表示。通过组合多个convert.iconv过滤器可以像玩魔方一样将任意输入“转换”成我们想要的PHP代码。这种手法极其灵活可以绕过很多基于字符串匹配的过滤如黑名单关键字过滤因为攻击载荷在过滤检查阶段和最终执行阶段呈现的是完全不同的字节序列。研究和构造这类Payload需要深厚的字符编码知识和对PHP过滤器行为的精确把握。4. 实战场景与防御绕过案例实录光说不练假把式我们结合几个简化但真实的场景看看如何综合运用上述技巧。4.1 场景一基础文件包含后缀拼接漏洞代码$page $_GET[page]; include($page . .html);目标执行任意PHP代码。分析限制了后缀为.html直接传PHP文件无效。php://filter的resource可以指向data://流来承载代码。Payload尝试?pagephp://filter/convert.base64-decode/resourcedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8结果预测服务器会尝试包含php://filter/....html。如果路径规范化后.html被当作resource参数的一部分即resourcedata://...;base64,PD9waHA....html这通常会导致data://协议解析失败因为Base64字符串里混入了.html这个非法字符。进阶Payload?pagephp://filter/convert.base64-decode|convert.base64-encode/resourcedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8这里增加了一个convert.base64-encode。为什么有时为了确保数据流格式正确或者绕过某些检查需要进行一次“无效”的编码再解码。但在此场景下核心问题.html后缀破坏resource可能依然存在。更可靠的绕过如果绝对路径已知可以尝试包含/proc/self/environ或日志文件等并利用php://filter向其中注入代码。或者寻找不需要后缀的包含点。4.2 场景二关键字过滤文件包含漏洞代码$file str_replace(array(php, flag), , $_GET[file]); include($file);目标包含并执行/tmp/flag.php文件中的代码。分析直接传入/tmp/flag.php会变成/tmp/.php无效。我们需要让resource指向/tmp/flag.php但在过滤器链中消除flag字样。死亡绕过Payload构思我们可以利用php://filter读取/tmp/flag.php但读取的内容中如果包含flag字符串会被替换成空。如果文件内容里有$flag变量就会被破坏。 思路转换我们不直接传递文件名而是通过过滤器让包含的内容本身不出现flag这个词。但这很难因为文件内容不可控。 另一个思路双写绕过对于简单的str_replace有时有效但这里替换为空双写也会被删光。 真正的“死亡绕过”可能依赖于更复杂的场景例如利用convert.iconv将/tmp/flag.php这个路径名本身进行转换使得在检查阶段flag这个词不以原始形式存在但在include内部解析流时又能正确找到该文件。这通常非常困难因为include的参数在替换过滤后已经丢失了关键信息。更实际的解法这种过滤通常结合了文件包含和读取可能题目本意是让你用php://filter读取/tmp/flag.php的源码Base64编码后flag这个词在编码里不存在了然后解码获取flag而不是执行它。所以Payload可能是?filephp://filter/convert.base64-encode/resource/tmp/flag.php。经过替换后变成php://filter/convert.base64-encode/resource/tmp/.php但/tmp/.php文件不存在失败。 这说明不是所有过滤都能用php://filter绕过需要具体分析过滤发生的阶段和位置。4.3 场景三利用临时文件与竞争条件这是一个高阶场景。假设我们可以上传一个文件但服务器会检查文件头如?php并删除。我们可以上传一个内容为php://filter流路径的文件。上传一个.txt文件内容为php://filter/convert.base64-decode/resourcedata://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4解码后是?php system($_GET[cmd]);?。服务器可能会将这个文件保存在临时目录并赋予一个随机名如/tmp/upload_abc123.txt。如果存在LFI漏洞我们可以尝试包含这个临时文件include($_GET[f]);传入f/tmp/upload_abc123.txt。include会打开这个文本文件读取其内容php://filter/...然后会尝试将这个字符串作为文件路径去包含由于该字符串是一个有效的php://filter流PHP引擎会启动过滤器链最终解码并执行我们隐藏在Base64中的PHP代码。这种手法成功的关键在于文件内容被include第二次解释。它绕过了对上传文件内容的直接检查因为检查时它只是一串无害的字符串却在包含阶段触发了代码执行。这需要精确的时机把握竞争条件因为临时文件可能很快被删除。5. 防御策略与安全开发建议了解了攻击手法作为开发者如何防御避免动态包含用户输入这是根本。如果必须动态包含使用固定的白名单映射例如$allowedPages [home home.php, about about.php];然后include($allowedPages[$userInput]);。严格限制允许的协议使用php.ini中的allow_url_include和allow_url_fopen指令。在生产环境中务必将其设置为Off。这能直接禁用php://、data://、http://等远程和伪协议从根本上杜绝此类攻击。对输入进行严格校验和过滤如果无法使用白名单应对用户输入进行强校验。不仅检查后缀更要检查协议头。可以使用parse_url()函数解析输入检查scheme协议是否为允许的如空scheme或file并禁止php、data、http等危险协议。同时将输入限制在特定目录内使用basename()或限制起始路径。使用安全的文件操作函数对于只需要读取文件内容非执行的场景使用file_get_contents()并确保路径安全比使用include更安全。如果需要配置考虑使用json、ini等非PHP格式的配置文件并用专门的解析函数读取。代码审计与安全意识在代码审计中将include、require、file_get_contents、fopen等函数作为重点审计对象追踪其参数是否用户可控。开发人员应了解php://filter等伪协议的潜在风险。6. 常见问题与排查技巧实录在实际利用和调试php://filterPayload时你肯定会遇到各种问题。这里记录一些常见坑点和解决思路。Q1: 我构造的Payload返回了空白页或错误怎么办A1: 按以下步骤排查开启错误显示在测试环境切勿在生产环境的PHP脚本开头添加ini_set(display_errors, 1); error_reporting(E_ALL);。这能直接告诉你语法错误或包含失败的原因。检查协议是否被禁用使用phpinfo()页面查看allow_url_include和allow_url_fopen的值。如果是Off那么php://filter和data://用于包含时可能失效但用于file_get_contents可能仍可行取决于后者。逐层测试过滤器链从最简单的Payload开始。先试试php://filter/resourceindex.php能否读取到文件内容可能是乱码。再加上convert.base64-encode看能否得到Base64输出。逐步添加复杂过滤器定位是哪个环节出错。URL编码问题确保、|、/等特殊字符在作为URL参数时被正确编码。可以在Burp Suite的Repeater模块中手动修改。查看服务器原始响应使用Burp Suite或浏览器开发者工具的Network标签查看原始响应体而不是渲染后的页面。Base64编码可能被隐藏在HTML注释或某些标签属性中。Q2: 包含php://filter时遇到了“No such file or directory”错误A2: 这个错误信息具有误导性。它通常不是因为resource指定的文件不存在而是因为PHP在初始化php://filter流时遇到了问题。可能的原因过滤器名称拼写错误如convert.base64encode少了中间的横线-。过滤器链格式错误管道符|使用错误或过滤器顺序导致数据流无法处理。resource部分格式错误resource后面跟的路径或协议不正确。确保data://协议的格式正确如data://text/plain;base64,xxxx。Q3: 如何利用php://filter进行目录遍历A3:php://filter本身不直接用于目录遍历但resource参数可以包含相对或绝对路径。例如php://filter/resource../../../etc/passwd。但要注意include函数在包含非PHP文件如/etc/passwd时如果allow_url_include为On会将其内容作为PHP代码执行这通常会导致语法错误。而file_get_contents()则会正常读取其文本内容。因此用于读取系统文件时通常结合file_get_contents()或highlight_file()等函数。Q4:convert.iconv过滤器具体怎么用有例子吗A4: 这是一个高级话题。基本格式是convert.iconv.输入编码.输出编码。例如convert.iconv.UTF-8.UTF-16LE会将UTF-8字符串转换为UTF-16 Little Endian格式。 一个经典的例子是构造一个Payload使得经过iconv转换后产生?php标签 假设我们想得到?php其ASCII十六进制是3c 3f 70 68 70。 我们可以尝试从其他编码转换而来。例如从UCS-4到UTF-8的转换过程可能产生特定字节。实际操作中攻击者会编写脚本暴力尝试或基于已知的转换表进行构造。例如已知Payloadphp://filter/convert.iconv.UTF-8.UTF-7|convert.iconv.UTF-8.UTF-16|convert.iconv.UTF-8.UTF-32/resourcedata://text/plain,XXXX通过精心计算XXXX可以使最终输出为恶意代码。这需要大量的试验和对字符编码的深刻理解通常在CTF比赛中出现。Q5: 在CTF中遇到php://filter的题目一般有哪些套路A5:简单读取直接Base64编码读取源码获取flag或下一步提示。配合包含执行LFI漏洞用php://filter和data://或php://input构造代码执行。编码绕过过滤了php、data等关键字需要用convert.iconv、string.rot13、zlib压缩等过滤器进行编码转换来绕过。过滤器链构造题目给出一串过滤器让你逆推出输入或者给输入和输出让你构造过滤器链。考察对每个过滤器作用的理解和顺序逻辑。结合其他漏洞如文件上传上传内容为Payload的文件、Session文件包含通过php://filter向Session中写入代码、日志注入将Payload写入访问日志再包含日志文件等。掌握php://filter的死亡绕过本质上是在学习PHP语言特性与安全边界之间的博弈。它要求我们不仅记住Payload更要理解数据流、编码、协议和函数行为之间的相互作用。每一次成功的绕过都是对底层原理的一次深刻验证。