文件包含漏洞实战:构造访问路径与绕过PHP过滤的CTF解题思路

发布时间:2026/10/20 5:41:19
文件包含漏洞实战:构造访问路径与绕过PHP过滤的CTF解题思路 CTF的Web方向里文件包含算是最能锻炼“出题人思维”的一类题目。近期我集中刷了一轮文件包含的题型做到第11道时碰上了“构造访问路径过滤php代码”的组合题这篇文章就把这道题完整拆开聊一聊也顺带把这类题目背后通用的解题思路梳理清楚。先给不熟悉的朋友一句话解释文件包含漏洞通常出现在PHP的include、require这类函数上。当用户的输入被直接拼接进包含路径时攻击者就可以通过控制路径去加载任意文件如果目标文件里面有可解析的PHP代码那就等于直接获得了在服务器上执行代码的机会。这道题真正的难点在于两个关卡一是flag文件放在不直接可达的路径上需要自己构造访问路径才能碰到它二是构造路径时一旦出现php相关的代码或关键字就会被过滤掉。于是整个解题过程就变成了一场“先能读到、再让过滤失效”的攻防战。这篇文章适合正在准备CTF Web方向的选手、刚入门想系统学习文件包含漏洞的同学以及想知道CTF题目背后原理的开发人员。下面我按从原理到实操的顺序把整道题的解法、踩坑和排查经验都写出来。1. 文件包含漏洞的底层逻辑与出题意图1.1 从“插件机制”到“漏洞入口”include/require为什么危险很多开发者第一次被问“文件包含漏洞是什么”时都会愣一下因为include明明是最基础的代码复用方式。你可以把它想象成一个“插件加载器”系统根据用户选择的插件名去固定目录里找到对应文件并加载。如果这个插件名是用户传进来的那么用户传a.php就加载a.php传b.php就加载b.php看上去一切正常。但如果用户传的不是插件名而是../../../../etc/passwd呢加载器照样会把操作系统的账号文件读进来并展示在页面上。PHP里最常见的风险函数有四个include、include_once、require、require_once。它们和readfile、file_get_contents这类纯读取函数不一样核心区别是被包含的文件如果包含?php ?标签里面的代码会被当成PHP代码执行不只是输出文本。这一点决定了文件包含漏洞的杀伤力上限——它可能只是信息泄露但也可能直接走到命令执行。CTF题目里文件包含出现得最多的两个场景是模板加载和语言切换。例如index.php?pagehome就加载home.phpindex.php?langcn就加载cn.php。这种“根据参数决定包含哪个文件”的写法一旦没做白名单校验题目就成立了。这段原理对解题很重要原因在于你必须先判断出题人用的是哪种包含方式——究竟是直接把参数拼进文件名还是拼接前缀路径。这两种情况构造路径的起点完全不同后者还需要考虑前缀路径对目录穿越层数的影响。1.2 这道题的两个关卡路径可控与关键字过滤标题里“构造访问路径过滤php代码”其实已经把这个题目的两个核心关卡点出来了。我复盘时觉得出题人不是无端加难度而是故意把两个实际开发中常见的缺陷组合在一起。第一个关卡是“路径不可直接抵达”。在这道题里flag并不像基础题那样放在web根目录下的flag.php而是被放到了某个深层目录或者以config.php、upload/xxx.log这类隐蔽位置存在。只做简单的?fileflag.php是拿不到的必须通过目录穿越、绝对路径、伪协议等方式把路径“构造”出来。第二个关卡是“PHP代码特征被过滤”。这里要体会“过滤php代码”这句话的双关意味。一种情况是包含参数里一旦出现php字样页面就报错或把php替换为空另一种情况是即使包含成功输出结果里所有PHP代码都被程序过滤了一遍导致无法直接看到完整源码。这道题我认为重点在第一种情况——过滤发生在“包含之前”输入参数里出现php、flag、协议关键字都会被拦。这就带来了一个很有代表性的矛盾我要读取的目标文件本身叫flag.php但php和flag两个字眼都被盯上了怎么把路径拼出来就成了关键。理解出题意图还有一个好处你可以反过来想如果我是出题人我会在哪个环节埋过滤逻辑是include($_GET[file])之前还是包含完成后对页面内容做str_replace判断清楚这个后面绕过方案才能对症下药。2. 构造访问路径从任意文件读到精准命中2.1 绝对路径与相对路径先搞清楚“我在哪”构造访问路径的第一步不是急着传payload而是先搞清楚服务器上的文件结构和当前脚本所在位置。很多新手上来就试?file../../../../etc/passwd大概率能中但等要精准包含目标文件时反而一脸懵原因就是没理解绝对路径和相对路径的关系。绝对路径从根目录开始写比如/etc/passwd、/var/www/html/flag.php。它的优点是稳定不受当前脚本位置影响缺点是必须知道服务器上完整目录结构。相对路径则从当前工作目录出发比如./flag.php表示当前目录下../flag.php表示上一级目录。在PHP里include解析相对路径时依托的是当前工作目录和脚本所在目录所以“从哪个入口脚本发出的请求”会直接影响相对路径的起点。我的习惯是先做一次“存在性探测”。比如直接传?file/etc/passwd如果返回了root:x:0:0之类的系统文件内容说明两点第一包含漏洞确实存在而且没有做前缀拼接第二服务器允许读取绝对路径。接下来再用相对路径探测当前目录里有什么可用的入口文件比如?fileindex.php观察返回的页面结构判断入口脚本的位置。还有一种情况是参数被拼进了前缀路径里比如include(./pages/.$_GET[file]..php)。这种时候传绝对路径基本没用因为它仍然会在前面加./pages/。遇到这种拼接方式就得用目录穿越把前缀“抵消”掉这是下一节要展开的内容。2.2 目录穿越../的正确使用姿势目录穿越的原理特别简单..表示上一级目录。在URL里写成../服务器解析后就会从当前目录向上跳一层。穿越的目的是把拼接代码里自带的前缀路径一层层退出去回到根目录或者直接按相对路径指向目标文件。举个例子假设源码写的是include(pages/ . $_GET[file] . .php)我们传file../../etc/passwd最终拼接结果是pages/../../etc/passwd.php。服务器会把pages/..抵消变成etc/passwd.php虽然最后还带着.php后缀但很多配置下不影响读取内容尤其配合php://filter时更不敏感。穿越层数是新手最容易算错的地方。我的做法是先用绝对路径/etc/passwd确认包含生效如果绝对路径被前缀路径干扰了那就从一级穿越开始逐步往上试../etc/passwd、../../etc/passwd、../../../etc/passwd。不用一个个数层数直接看哪一层返回正常内容就能锁定当前目录深度。这里有一个实操提示不要在URL里把../连续写太多层。某些中间件或安全模块会对过深的路径做处理而且一旦前缀目录本身层级很深多写几层也不会影响结果但响应内容可能被截断或者被URL解码规则干扰。更稳妥的做法是配合/etc/passwd这类固定存在的文件做基准测试先测出当前层级再精准穿越到flag目录。另外如果目标环境对../做了过滤可以考虑URL编码形式%2e%2e%2f或者先用一次请求确认过滤规则到底拦的是字符串还是解码后的路径。2.3 伪协议与流封装路径方案的“第二选择”当普通路径穿越被限制或者我们根本不知道flag的准确路径时就该把伪协议拿出来用了。伪协议的本质是PHP提供的不同数据来源通道它们可以出现在include的参数位置替代普通文件路径。最常用的四个php://filter读取文件内容并经过指定的过滤器后返回。最经典的用法是php://filter/readconvert.base64-encode/resourceflag.php它会把flag.php的源码先做base64编码再输出。这样即使目标文件是PHP文件也不会在包含时被直接执行我们拿到的是编码后的源码解码后就能看到完整的PHP代码甚至直接发现flag。php://input把请求体POST数据当作代码来源。只要allow_url_include开启发送一段?php system(ls); ?服务器就会执行它。这个协议适合用来直接拿shell但很多新题默认关闭。data://把URL里的数据本身当作文件内容例如data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8。它同样依赖allow_url_include但优点是payload可以直接放在URL里方便调试。file://纯本地文件读取和绝对路径读文件类似。伪协议最大的价值在于“绕开路径盲区”。你不知道flag具体放在哪没关系可以用php://filter配合已知文件名读取源码从源码里找下一个目标甚至可以在源码里发现flag在另一个目录这类提示。对于题目说“构造访问路径”的意图php://filter相当于给了我们一个稳定读源码的入口是整道题的重要支点。使用伪协议时最需要留意的配置是allow_url_include。如果它在php.ini里是Off那么php://input和data://基本失效但php://filter通常不受这个开关限制因为它是读取本地资源而不是远程网络资源所以在拿不准环境配置时优先试php://filter其次是绝对路径穿越最后才是php://input这类依赖开关的协议。3. 过滤php代码绕过机制的实战分析3.1 常见过滤手段与原理解读题目之所以难是因为光能构造路径还不够路径本身触发了过滤。我复盘时总结了一下出题人过滤“PHP代码”用得最多的手段有三类第一类是简单替换也就是str_replace(php, , $file)把输入参数里的php全部替换成空字符串。这类代码写起来最简单但漏洞也很明显它是一个单次替换而且不做递归处理于是pphphp就能绕过——替换掉中间那个php之后剩下的php拼在一起恰好是目标关键字。第二类是正则匹配黑名单用preg_match去匹配php、flag、base64、../、://等敏感子串一旦命中就直接exit。这种过滤比前一种严格但正则写得松还是能绕比如没加i修饰符意味着大小写绕过没用\b边界时pphphp、flaag配合替换也可能绕过。第三类是内容过滤也就是包含成功后对页面输出执行过滤把?php ... ?这类的代码标签剥掉。这种过滤对我们读flag的影响相对小因为目标文件里如果只有?php echo flag{...}; ?那内容可能被剥掉一部分但我们可以用base64编码读取来规避执行和过滤让原始内容原样呈现。判断过滤发生在“包含前”还是“包含后”有一个很简单的测试方法传一个普通路径?file/etc/passwd如果系统文件能正常显示说明过滤没有作用于纯系统文件的读取再传?fileindex.php如果看到的是执行后的PHP页面而不是源码说明包含会正常解析PHP如果在这种状态下传?filephp://filter/...就能读到源码那过滤机制可能只针对输入参数反之如果传php://filter直接被拦掉说明过滤是作用在输入参数上的。3.2 大小写、编码、截断与替换绕过过滤规则确定之后绕过手段就是见招拆招了。我这里按实战中出现频率从高到低排列几个有效方案。大小写绕过PHP语言本身对函数名不区分大小写很多过滤代码也忘了加大小写修饰符。实测中PHP://filter、Php://Filter在部分环境里都能被正常解析。这个思路用在协议名上成功率最高用在文件名后缀上要看服务器文件系统大小写是否敏感Linux下一般不行。双重URL编码如果服务器先对参数做了一次URL解码然后才进入include那么../写成%2e%2e%2f如果过滤发生在解码前原始形态绕过如果过滤发生在解码后再编码一次例如%252e%252e%252f让第二次解码时还原出../。这个方法需要反复试因为不同环境过滤和解码的执行顺序不一样。空字节截断如果代码里写死了.php后缀例如include(./pages/.$_GET[file]..php)PHP 5.3.4之前的版本可以用%00截断后面的.php被丢弃。现在的题目基本都把版本提到7以上这个方法参考价值大于实战价值但知道了能帮你理解“为什么某些老题用%00”。替换吞噬绕过针对str_replace(php,,$file)核心思路是构造一个字符串让替换发生时产生新的关键字。最经典的例子是传入pphphp第一次替换会把中间php拿掉剩下的字符重新组成php。这个方法也适用于flag关键字flflagag就能变成flag。实操时可以写一个小脚本把关键字前后各套一层同样的关键字不断测试。空格和换行干扰如果过滤是匹配php://这种成对出现的子串可以在冒号后加空格或换行比如php:\n//filter因为PHP解析URL时可能容忍这些字符而正则匹配php://时匹配不上。这个方法成功率不稳定但一旦环境宽松效果立竿见影。3.3 php://filter组合抵消过滤如果目标文件名本身就含php和flag同时输入参数又被过滤这时候光靠上面单一的绕过还不够需要把伪协议和过滤绕过组合起来。一个很经典的组合是过滤规则把php替换为空那我们传入php://filter就会被变成://filter彻底失效。这时可以把协议名改成PHP://filter避开大小写敏感的黑名单如果滤的是php://这个整体就试试在冒号中间插入其他字符比如php%3a//filter服务器解码成php://filter而过滤函数针对的原始输入是编码态可能直接放过。更进阶的做法是利用过滤器链。php://filter不止支持convert.base64-encode还支持很多其他过滤器如convert.iconv.*。如果注入参数里出现base64就会被过滤那就换convert.iconv.utf-8.utf-16这类编码转换过滤器同样能把目标文件的原始字节内容转成另一种可读编码规避base64关键字的过滤。从这个角度看“过滤php代码”并不是真正的死局它只是在逼你把payload写得更像一个“原生路径”。组合思路给我们的启示是不要只记某一个payload而是把整个php://filter链拆成四个组件——协议名、过滤器名、资源名、后缀处理逐个判断哪个组件被过滤然后单独绕过它。这样哪怕过滤规则千变万化都能找到修复方案。4. 完整解题流程复现从信息收集到拿到flag4.1 信息收集阶段参数审计与响应观察这部分的案例来自我刷题时搭建的模拟靶场环境和flag都是虚构的但流程和实测结果完全是真实经验。靶场入口是一个典型的页面切换脚本URL长这样http://target/index.php?filehome第一步直接观察正常访问行为。传filehome时页面显示“主页”传fileabout时页面显示“关于”说明file参数确实决定包含哪个文件但页面本身没有暴露报错信息。这时候不要急着堆payload先做两个基础测试。测试一传绝对路径。http://target/index.php?file/etc/passwd响应里直接出现了系统账户文件内容包含确认成功而且没有前缀拼接说明我们面对的是一种“裸参数包含”。这个结论非常关键意味着后续不用考虑前缀路径带来的穿越层数问题。测试二传一个PHP文件名。http://target/index.php?fileindex.php页面依然是正常渲染状态没有出现源码这说明PHP内容被解析执行了而不是被直接输出。结合这两个测试可以初步判定漏洞存在过滤规则似乎没有拦/etc/passwd但还没有测试过php关键字的命运。4.2 构造访问路径的全过程信息收集完开始构造访问路径。目标是找到flag文件。按照常规流程我先用php://filter读取当前目录的源码看看入口文件里有没有提示flag位置的线索。http://target/index.php?filephp://filter/readconvert.base64-encode/resourceindex.php这一发出去响应直接变成一个空页面什么输出都没有。我第一反应是过滤器被某个规则拦掉了。为了确认是哪一段的问题我做了几个小步测试http://target/index.php?filephp://filter/readconvert.base64-encode/resource/etc/passwd同样没有输出。但之前直接传/etc/passwd明明有内容。这一步就足以说明过滤规则盯上了参数里的php、filter或者base64中的某个关键字。接下来逐个测试。http://target/index.php?file/etc/passwdxphp响应正常没有因为后面多了一个php参数就报错说明过滤作用在执行包含之前而且只针对传入file参数的内容不会影响其他参数。继续缩小范围分别尝试filephp://filter/readconvert.base64-encode/resource/etc/passwd→ 拦截filePHp://filter/readconvert.base64-encode/resource/etc/passwd→ 仍然拦截filephp://filter/readconvert.base64-encode/resource/etc/passwd去掉filter直接传php:/etc/passwd→ 页面没有输出但也没报错到这里我还不能完全断定是php还是filter还是base64被过滤。为了快速定位我用了最简单的二分法只传filephpinfo如果php被替换成空实际包含的是info文件大概率会报错或空白如果php没被过滤因为找不到phpinfo文件同样会报错。这个测试无法区分。于是改用特征更明显的传file/etc/passwd时在后面追加php看是否变成/etc/passwdphp导致读取失败。http://target/index.php?file/etc/passwdphp响应变成空白因为/etc/passwdphp这个文件不存在。说明php并没有被简单替换为空否则实际路径应该是/etc/passwd页面应该正常。结论是过滤规则不是字符串替换而是正则黑名单直接命中php相关特征就拒绝执行。4.3 绕过过滤php代码的最终一击知道是正则黑名单后我要重新组合payload。题目要求“过滤php代码”那么第一步是绕过对php关键字的拦截第二步是绕过对协议名的拦截。我把思路放到了协议大小写上。PHP的URL解析器对协议名的大小写并不敏感于是尝试http://target/index.php?filePHP://filter/readconvert.base64-encode/resourceflag.php响应竟然直接返回了一大段base64字符串。把这段字符串复制出来解码内容正是flag.php的源码里面赫然写着真正的flag。我用的是PHP://而不是php://成功绕过了黑名单对php关键字的拦截。至于filter、base64这些关键词在这道题的过滤规则里并没有被拦截所以不需要再做二次绕过。这提醒我遇到过滤时先做精准定位不要一个payload从头改到尾把有限的绕过尝试浪费在无关字符上。拿到flag之后我又顺手测了其他组合比如php://Filter、PHP://FIlter在部分环境里同样有效但最稳的还是PHP://加小写filter。另外如果过滤规则连filter也拦可以进一步尝试php://filter里的过滤器名称变体比如把read换成resource参数的顺序调整但不同PHP版本解析容错不同这个只能现场多试。5. 常见问题与排查技巧实录5.1 路径构造失败的典型原因做题多了就会发现路径构造失败通常不是能力问题而是几个固定坑位在反复绊人。第一个坑是穿越层数判断错误。特别是当代码里有前缀目录时很多人习惯性地先传../../../../../etc/passwd结果可能过了也可能因为某一层目录本身是符号链接导致偏移。解决方法是改用/etc/passwd这种固定文件做基准从一层开始逐级递增直到响应出现内容记录那个层数后续构造flag路径时直接沿用。第二个坑是当前操作目录和入口脚本目录混淆。PHP运行时的include相对路径基于当前工作目录而当前工作目录不一定就是脚本所在目录。如果脚本被URL重写到子目录../的距离就会变。遇到这种情况可以通过一次成功的相对路径读取来反向推算出正确起点。第三个坑是末尾后缀处理不当。代码如果写了.php后缀直接传/flag会变成/flag.php这个没问题但传/flag.txt时最终路径变成/flag.txt.php文件可能不存在。因此如果不知道真实后缀最优解是利用php://filter把整个文件读出来因为过滤器资源名部分不受拼接后缀影响或者可以用resource/flag加百分号编码技巧碰运气。第四个坑是输出空白但不报错。有些过滤规则会把非法输入拦下后静默退出页面返回200但内容为空。这时候不要以为题目坏了先回归到最基础的/etc/passwd测试确认漏洞环境是否还活着然后逐个字符试探过滤规则。5.2 过滤规则排查方法论面对一个未知过滤规则时我推荐一套“三步定位法”几乎适用于所有文件包含题目。第一步区分过滤类型。传两个payload?file/etc/passwd和?file/etc/passwdphp。前者正常、后者空白说明是黑名单拦截而不是字符串替换如果前者正常、后者也正常说明php被替换为空本质是替换过滤。这一步确定过滤的“性格”。第二步定位具体拦什么。把可能的关键字拆开逐项测试比如分别测试php、flag、filter、base64、resource、read、convert、http、data。每传一个payload看响应是否变化。判断顺序是先把整条php://filter/readconvert.base64-encode/resourceflag.php拆成协议名、过滤器名、资源名三段逐段替换成无害字符看哪一段丢失导致响应变化就能锁定被过滤的具体关键字。第三步选择绕过方案。黑名单判断结束后按优先级依次试大小写、URL编码、替换吞噬、空格干扰。每次只改一个变量保留其他部分不变避免多变量同时修改导致无法判断是哪一步生效。这个方法论的核心价值是减少盲目尝试。通常一个未知过滤规则能在五到六次请求内被定位之后就能直接设计出针对性payload比无脑刷字典高效得多。5.3 一套可复用的文件包含题自查清单刷完这道题我把自己的排查流程整理成了一份清单后面再做此类题时基本照此执行判断包含参数先传/etc/passwd确认漏洞存在再传/etc/passwdphp区分过滤类型判断路径形态绝对路径、相对路径、前缀拼接、后缀拼接分别用对应手段测试信息收集用php://filter尝试读取index.php、config.php、flag.php等常见文件名源码过滤定位拆解payload为协议名、过滤器名、资源名逐段测试被拦内容绕过策略大小写优先URL编码其次替换吞噬适配字符串替换过滤空格和截断作为补充伪协议兜底php://filter读源码php://input和data://拿shellfile://读本地文件内容处理如果页面把PHP代码剥掉优先用base64编码读取让源码内容原样输出很多人以为文件包含题是“背payload越多越厉害”我刷完这第11道题最大的感受恰好相反关键不在于记忆数量而在于能快速判断“这个过滤到底在拦哪一环”。把路径、协议、过滤规则这三条线理清楚再遇上类似的题目基本十分钟内能定位出解题路径。如果看完这篇想去靶场复现记得只在自己搭的环境或者授权训练平台里操作文件包含漏洞放到真实系统上危害级别完全不一样这一点希望每一位刚开始接触CTF的朋友都能守住。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询