PHP文件包含漏洞与php://filter协议利用深度解析 1. 从一个真实的渗透测试场景说起那天下午我正在对一个内部系统进行授权安全测试。目标是一个典型的文件上传功能界面看起来平平无奇一个选择文件的按钮一个上传按钮。我随手传了个图片成功了。接着我尝试上传一个包含一句话木马的PHP文件结果页面弹出了一个醒目的红色提示“文件类型不允许”。这在意料之中开发者通常会通过检查文件扩展名如.php或MIME类型来防御。于是我祭出了常规的绕过手段将文件改名为shell.php.jpg并修改了HTTP请求中的Content-Type为image/jpeg。再次上传系统竟然提示“上传成功”。我心里一喜以为找到了突破口立刻去访问上传后的文件路径。然而浏览器返回的却是乱码——服务器并没有把这个文件当作PHP脚本来解析它只是被当作一个普通的、内容为PHP代码的图片文件存储了起来。这意味着虽然绕过了前端的检查但文件并没有获得执行权限。这种“上传成功但无法执行”的困境在渗透测试和代码审计中非常常见。就在我思考下一步是去挖掘解析漏洞还是目录穿越时突然想起了php://filter这个奇特的协议。它不像http://或file://那样指向一个网络或本地资源而是PHP内置的一个“处理器”或“过滤器”专门用于在数据流上执行各种转换操作比如编码、解码、压缩、字符串处理等。更重要的是在某些特定配置下结合文件包含漏洞php://filter能够实现一种被称为“死亡绕过”的技巧——即在不直接写入可执行文件的情况下通过编码、转换等方式将恶意代码“注入”到目标文件中最终达成命令执行的目的。这听起来有点绕但却是理解现代Web应用安全中一些深层漏洞的关键。今天我们就来彻底拆解php://filter的机制并一步步还原如何利用它进行“死亡绕过”。2. 深入理解php://filter协议栈在PHP中php://是一个用于访问各种输入/输出流I/O streams的封装协议。而php://filter是其中功能最强大、也最复杂的一个子集。你可以把它想象成一个流水线上的加工站。原始数据比如一个文件的内容是原材料它流经这个“过滤器”流水线经过一系列预定义的“加工”如编码、替换字符最终输出处理后的成品。2.1 核心语法与过滤器类型php://filter的基本语法结构是php://filter/过滤器链/resource目标资源。其中“过滤器链”是核心它决定了数据要经过哪些处理。过滤器之间用竖线|连接表示依次执行类似于Linux管道。PHP内置了多种过滤器主要分为几大类字符串过滤器string.rot13 执行ROT13编码这是一种简单的字母替换密码。string.toupper/string.tolower 将字符串转换为全大写或全小写。string.strip_tags 尝试去除PHP和HTML标签。注意这个过滤器的行为在复杂情况下可能不可靠不应作为安全过滤的唯一手段。转换过滤器主要用于处理编码。convert.base64-encode/convert.base64-decode 进行Base64编码和解码。convert.quoted-printable-encode/convert.quoted-printable-decode 进行Quoted-Printable编码和解码常用于电子邮件。convert.iconv.* 进行字符集转换例如convert.iconv.UTF-8.UTF-16BE。压缩过滤器如zlib.deflate和zlib.inflate用于压缩和解压数据。加密过滤器如mcrypt.*和mdecrypt.*已废弃。一个简单的例子是读取一个文件并对其进行Base64编码$content file_get_contents(php://filter/convert.base64-encode/resource/etc/passwd); echo $content; // 输出 /etc/passwd 文件的Base64编码内容在这个例子中数据流从/etc/passwd文件流出经过convert.base64-encode过滤器加工然后被file_get_contents读取。2.2 过滤器链的“栈式”执行顺序这是理解高级利用的关键。过滤器链的执行顺序是“从左到右”吗并不完全是。更准确地说它类似于一个栈Stack的操作遵循“后进先出”的原则但体现在读写操作上。对于读操作如file_get_contents,include过滤器链的执行顺序是从左到右。数据从resource流出先经过最左边的过滤器处理再传递给下一个最后到达你的PHP代码。示例readconvert.base64-encode|string.rot13/resourcetest.txt过程test.txt内容 →convert.base64-encode→string.rot13→ 输出给PHP。对于写操作如file_put_contents过滤器链的执行顺序是从右到左。数据从你的PHP代码流出先经过最右边的过滤器处理再传递给前一个最后写入resource。示例writestring.rot13|convert.base64-decode/resourcetest.txt过程PHP输出数据 →convert.base64-decode→string.rot13→ 写入test.txt。这个反向特性在构造攻击链时至关重要。例如如果你想通过写操作最终让文件里包含一段Base64编码后的内容你需要将convert.base64-encode放在写链的最右侧即最后执行。2.3 与文件包含漏洞的天然结合php://filter最常见的危险暴露点是与本地文件包含LFI漏洞的结合。假设有一段问题代码// file: include.php $page $_GET[file]; include($page . .php);攻击者可以传入?filephp://filter/convert.base64-encode/resourceindex代码最终会执行include(‘php://filter/convert.base64-encode/resourceindex.php’)这时include函数会尝试读取index.php文件但数据流先经过了Base64编码。由于Base64编码后的内容不是有效的PHP代码include会失败但编码后的文件内容会被直接打印到页面上。这就实现了对服务器上任意PHP文件源码的读取只要有读取权限即使这些.php文件因为配置原因无法直接通过Web访问。注意include、require等函数在包含php://filter流时如果流的内容不是有效的PHP代码会引发一个警告但编码后的内容通常会作为警告信息的一部分或直接输出到页面从而被攻击者捕获。这正是利用的起点。3. “死亡绕过”的核心利用过滤器链制造PHP代码“死亡绕过”这个听起来很骇人的词实际上描述的是一种利用php://filter过滤器链的编码转换特性将非PHP内容如纯文本、图片转换为有效PHP代码的技术。它的核心应用场景通常围绕文件包含和文件写入两个点。3.1 场景一从日志文件、Session文件到Webshell这是最经典的“死亡绕过”利用链。假设我们有一个文件包含漏洞include($_GET[‘file’]);并且我们知道服务器上一个文件的可写路径和大致内容例如/var/log/apache2/access.log我们能否通过包含这个日志文件来执行代码直接包含日志文件由于里面是HTTP访问记录的纯文本不是PHP代码所以不会执行。但是如果我们能向这个日志文件写入一个包含PHP标签的HTTP请求然后再去包含它不就行了吗问题在于很多Web服务器或应用框架会对写入日志的内容进行转义或过滤比如将转义为?php导致其失效。这时php://filter的转换过滤器就派上用场了。我们可以利用convert.iconv过滤器进行字符集转换。字符集转换本质上是一种字节到字节的映射如果我们在一个字符集如UTF-8中构造一个特殊字节序列当它被用另一种字符集如US-ASCII, UTF-16BE解码或编码时可能会意外地产生这样的字节序列。一个简化示例攻击者向服务器发送一个精心构造的HTTP请求User-Agent设置为?php system(‘id’); ?这个请求被记录到Apache的access.log中内容可能是纯文本。攻击者利用LFI漏洞尝试包含日志文件?file/var/log/apache2/access.log失败因为日志内容是文本。攻击者现在使用过滤器链进行包含?filephp://filter/convert.iconv.UTF-8.UTF-16BE/resource/var/log/apache2/access.logPHP在读取日志文件时会尝试将文件内容从UTF-8编码转换为UTF-16BE编码。在这个转换过程中日志文件中原本代表?php system(‘id’); ?的文本字节可能因为编码规则的差异被错误地解释或重组使得在输出的数据流中偶然形成了一个有效的标签及其后的代码。如果这个转换后的流被include函数处理PHP引擎可能会识别并执行这段“意外生成”的代码。这个过程高度依赖于具体的字符集、原始字节和PHP版本需要精心构造和测试。它利用了编码转换过程中的“歧义”来“制造”出恶意代码。3.2 场景二配合文件写入与二次包含另一个更直接的场景涉及文件写入。假设有一个功能允许你将php://filter链的结果写入一个文件例如通过file_put_contents或者有一个文件上传点允许你控制文件的部分内容。攻击思路目标在一个我们已知路径的文件比如一个图片文件avatar.jpg或者一个临时文件中注入PHP代码。限制直接写入会被过滤或转义。绕过利用php://filter的写操作链。我们构造一个过滤器链使得我们输入的“无害”文本在经过链式解码/转换后写入目标文件时恰好变成有效的PHP代码。触发再通过另一个文件包含漏洞LFI去包含这个已经被“污染”的文件。构造示例假设我们可以控制file_put_contents的第一个参数路径。我们想向/tmp/test.txt写入一句话木马。 我们不能直接写因为?可能被过滤。我们可以这样构造// 攻击者控制的输入 $malicious_payload PD9waHAgZXZhbCgkX1BPU1RbJ2MnXSk7Pz4; // 这是 的Base64编码 $path php://filter/convert.base64-decode/resource/tmp/test.txt; file_put_contents($path, $malicious_payload);执行后$malicious_payload字符串在写入/tmp/test.txt之前会先经过convert.base64-decode过滤器解码。于是解码后的原始字节——也就是——就被写入了/tmp/test.txt文件。这样我们就在目标服务器上创建了一个纯文本文件但其内容是一个完整的Webshell。接下来如果存在LFI漏洞攻击者包含/tmp/test.txt这个文件就会被当作PHP代码执行。这里的关键在于写入操作和包含操作是分离的。写入时我们利用了过滤器的解码功能“绕过”了内容检查包含时该文件因为扩展名是.txt可能不会被Web服务器直接解析但PHP的include函数不在乎扩展名只关心文件内容是否包含有效的PHP标签。4. 实战演练构造一个完整的攻击链让我们通过一个模拟的、简化的CTF场景将上面的知识串联起来。假设我们有一个Web应用漏洞点A文件写入/上传有一个“设置个性签名”的功能签名内容会保存到服务器的一个固定文件/tmp/signature_[userid].txt中。服务器对输入进行了严格的过滤移除了所有、、?、php等字符。漏洞点B文件包含在个人主页有一个LFI漏洞?page../../tmp/signature_123会尝试包含对应用户的签名文件自动补全.php扩展名但我们的文件是.txt不过include依然会尝试读取它。目标在服务器上执行命令ls /。攻击步骤第一步利用过滤器链写入Webshell我们无法直接写入。但我们可以写入它的Base64编码形式。 我们知道Base64解码器会忽略不在其字符集A-Za-z0-9/内的字符。因此我们可以构造一个payload使其Base64解码后恰好是我们想要的代码。?php system(‘ls /’); ?的Base64编码是PD9waHAgc3lzdGVtKCdscyAvJyk7ID8但是直接写入这个字符串解码后就能得到原语句吗不一定。因为Base64解码以4个字符为一组如果字符串长度不是4的倍数需要用填充。我们的字符串是PD9waHAgc3lzdGVtKCdscyAvJyk7ID8长度是36是4的倍数所以可以正确解码。那么我们如何利用“个性签名”功能写入呢我们需要让服务器执行类似这样的代码file_put_contents($user_signature_path, $_POST[‘signature’]);我们无法直接控制$user_signature_path但假设服务器愚蠢地允许签名内容包含换行并且路径是固定的。我们更可能的情况是我们需要找到一个能控制完整文件路径的写入点。为了演示我们假设存在一个更脆弱的端点允许我们指定写入路径和内容但内容会经过过滤。构造payload我们无法写入?但我们可以写入php://filter/convert.base64-decode/resource/tmp/shell.php作为文件名如果服务器允许然后将Base64编码后的代码作为文件内容。但很多情况下路径和内容是分开的。实际上更常见的利用是“死亡绕过”的变种通过控制过滤器链本身来写入文件。如果应用有这样的代码$data $_GET[‘data’]; $file $_GET[‘file’]; file_put_contents($file, $data);那么攻击者可以filephp://filter/convert.base64-decode/resource/tmp/shell.phpdataPD9waHAgc3lzdGVtKCdscyAvJyk7ID8这样data参数中的Base64字符串在写入/tmp/shell.php前被解码于是/tmp/shell.php文件中就包含了原始的PHP代码。第二步触发文件包含执行Webshell拿到LFI漏洞点?page../../tmp/shell服务器代码执行include(‘../../tmp/shell.php’);成功加载并执行我们写入的Webshell执行ls /命令输出根目录列表。整个攻击链的关键在于找到了一个“写入点”并且这个写入点允许我们使用php://filter协议来指定目标文件从而在写入过程中对内容进行解码或转换绕过了直接的内容安全检查。然后再利用另一个“包含点”来执行写入的恶意文件。5. 防御之道如何避免成为“死亡绕过”的受害者理解了攻击原理防御思路就清晰了。核心原则是最小化攻击面对用户输入进行严格、多层、上下文相关的过滤。5.1 严格限制文件包含与文件操作的参数白名单机制对于include、require、file_get_contents、file_put_contents等函数的路径参数尽可能使用白名单。如果必须动态包含只允许包含预定义的、安全的文件列表。路径固定避免用户输入直接、未经处理地拼接到文件路径中。如果需要基于用户输入应使用映射关系如ID到文件名而不是直接使用输入作为路径的一部分。禁用危险协议在PHP配置文件php.ini中使用allow_url_fopen Off和allow_url_include Off来禁用php://、data://、http://等URL封装协议。这是最有效的一劳永逸的方法之一。在生产环境中除非有绝对必要否则应保持关闭。allow_url_fopen Off allow_url_include Off5.2 对用户输入进行深度过滤与验证上下文相关过滤过滤不是简单地删除、。对于文件路径要检查是否包含目录遍历序列../、空字节%00在PHP老版本中可用于截断以及php://、data://等协议字符串。规范化与绝对路径使用realpath()函数获取规范化的绝对路径并检查该路径是否在允许的目录范围内例如Web根目录之外。$user_file $_GET[‘file’]; $base_dir ‘/var/www/html/uploads/’; $real_path realpath($base_dir . $user_file); if ($real_path false || strpos($real_path, $base_dir) ! 0) { // 路径非法拒绝访问 die(‘Access denied.’); }内容安全检查对于文件上传除了检查扩展名和MIME类型还应该对文件内容进行安全检查如病毒扫描、图片重渲染。对于用户输入将要写入文件的情况要根据文件的预期用途进行严格的过滤或转义例如如果写入的是HTML就用htmlspecialchars如果写入的是配置文件就严格限制字符集。5.3 安全的服务器与PHP配置将用户上传的文件存储在Web根目录之外这是黄金法则。这样即使攻击者上传了恶意文件也无法通过直接的HTTP请求访问到它必须结合服务器本身的其他漏洞如文件包含、解析漏洞才能触发大大增加了攻击难度。为上传文件分配随机、不可预测的文件名避免使用用户提供的原始文件名。限制PHP执行权限通过Web服务器配置如Apache的.htaccess或Nginx的location规则禁止在特定目录如上传目录中执行PHP脚本。Apache示例(在上传目录的.htaccess中)FilesMatch “\.(php|php5|phtml|phps)$” Order Allow,Deny Deny from all /FilesMatchNginx示例(在server配置中)location ^~ /uploads/ { location ~ \.php$ { deny all; } }保持PHP版本更新新版本通常会修复旧版本中协议处理器或过滤器的潜在安全问题。5.4 开发层面的安全意识避免动态包含从根本上审视业务逻辑是否真的需要动态包含文件很多时候可以用路由、模板引擎等更安全的方式替代。代码审计定期对代码进行安全审计特别关注所有包含文件操作、文件读写操作的函数调用点检查其参数是否用户可控。使用安全函数例如用basename()去除路径中的目录部分但注意它可能无法处理所有情况仍需结合白名单。“死亡绕过”听起来神秘但其本质是攻击者对PHP流过滤器特性与应用程序逻辑缺陷的深度结合利用。防御的关键不在于封堵某一个特定的payload而在于建立纵深防御体系从协议开关、路径控制、内容过滤到服务器配置层层设防让攻击者即便掌握了一个漏洞点也难以串联成有效的攻击链。作为开发者理解这些攻击手法不是为了实施攻击而是为了在编写每一行代码时都能清晰地意识到数据流动的边界与风险从而构建出更坚固的应用。