news 2026/10/22 5:38:50

CTF实战:PHP文件包含绕过strstr过滤读取flag

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF实战:PHP文件包含绕过strstr过滤读取flag

平时刷CTF,我最喜欢先拿Web方向的题目热身。Web_php_include 这题在我记忆里地位很稳:它不算难,但在“文件包含”这个知识点上卡得刚刚好——题目把源码直接甩到你脸上,include($page) 一眼就能看到,偏偏又在参数上盖了一层过滤,逼着你去想怎么绕过 strstr 的检测。对新手来说,这一关能把伪协议、过滤绕过、Base64解码串成一条完整链路;对老手来说,它也是个不错的查漏补缺素材。这篇文章就把我实打实的尝试过程写出来,不绕弯子。

1. 题面源码一眼抓出关键代码:include与那行if判断

1.1 题目入口的静态展示

访问题目后,页面没有复杂的后台管理、没有登录框,直接就给你看了一段PHP源码。这是非常友好的“开卷考试”式出题。核心逻辑整理出来是这样:

<?php show_source(__FILE__); $page = $_GET['page']; if (strstr($page, "php://")) { echo "Hacker!!!"; } else { include($page); } ?>

也许实际版本里输出格式、分隔符略有不同,但不影响判断。第一眼扫过去,大家的目光都会落在include($page)上——这是标准的动态文件包含;而strstr($page, "php://")则是一个针对伪协议的过滤判断。

这里有个特别值得新手注意的点:题目特意调用了show_source(__FILE__),把当前文件源码直接输出。这意味着它不想让你在“猜源码”上浪费时间,考点被主动暴露出来了。很多CTF题目会藏一手,这题反而不藏,它考的就是你懂不懂include的危险性,以及能不能把被过滤的那条路径绕过去。

1.2 为什么include参数这么敏感

动态文件包含是PHP老代码里非常经典的高危写法。include会把指定的文件内容当作PHP脚本的一部分来解析:文件里是普通文本,就输出文本;文件里带了<?php ?>标签,标签内的代码就会被执行。所以一旦参数可控,攻击者至少能做两件事:

  • 读取服务器上的任意文件内容;
  • 通过包含包含恶意代码的文件,直接拿到命令执行权限。

而本题还没有完全“裸奔”,它在include前做了判断:参数里只要出现小写的php://,就直接打印Hacker!!!。这属于典型的“半拦不拦”滤波——它只堵了最显眼的路径,却没有把其他路径都封死。

1.3 第一次试探:直接传flag.php会发生什么

按照CTF选手的直觉,第一反应通常是传page=flag.php。但这道题大概率不会让你直接得手,原因可能有两个:

第一,flag文件不一定在当前位置,或者文件名不是flag.php,所以include出去只是报个Warning,页面没有任何有效输出。第二,就算你传的路径没猜错,include一个flag.php,如果文件内容是这样的:

<?php $flag = "flag{xxxx}"; ?>

include之后只是给$flag赋了个值,页面并不会输出这个变量,你照样什么都看不到。

所以直接盲猜路径和文件名,效率很低。在文件包含类的题目里,第一步该做的永远是信息收集——先把目标环境里的源码读出来,看清目录结构,再决定下一步怎么走。

2. 第一次被拦的完整心理过程:php://filter为什么会被顶回来

2.1 我最初的想法与回显

看到include($page),我的第一反应就是 PHP 伪协议三板斧:php://filter读源码、php://input写代码、data://执行命令。标准读取 payload 长这样:

index.php?page=php://filter/read=convert.base64-encode/resource=index.php

说起来很顺,但实际一提交,页面马上回了两个字:Hacker!!!。当时我就知道,这道题把php://这个字符串的精确匹配堵住了。strstr在参数里找到了小写连续的php://,于是直接执行了echo "Hacker!!!",根本没有走到include那一步。

这种“明明知道正确答案但不让你写”的感觉,在CTF里其实很常见。但也别急着觉得题目赖皮,过滤函数本身往往就是解题的突破口,你得先搞清楚它是怎么工作的。

2.2 strstr函数的小脾气:区分大小写是绕过的关键

strstr($haystack, $needle)在PHP里的行为是严格区分大小写的。也就是说,它找的是连续的小写php://这三个字符加两个斜杠。只要参数里没有这个连续小写串,strstr的返回值就是false。

那问题就变成了:能不能让include最终收到一个合法的php://伪协议,但同时字符串里又不包含小写的php://?

答案在于PHP本身的协议解析机制。PHP 的流包装器(stream wrapper)名称不区分大小写,php://、PHP://、Php://在实际解析时都会被当作同一个伪协议来处理。于是,只要把 payload 里的php://改成PHP://:

index.php?page=PHP://filter/read=convert.base64-encode/resource=index.php

此时strstr($page, "php://")找不到小写原串,返回false,于是代码正常走到include($page);而include去加载这个路径时,PHP又会把PHP://当作php://来解析,从而顺利触发伪协议。

2.3 顺手验证:strstr和stristr的本质差别

为了确认这个判断没错,我后来又试了PhP://、pHp://各种大小写混写,结果都能过。归根结底,就是一句话:strstr是区分大小写的,stristr才是忽略大小写的版本。

如果出题人在这一行写的是stristr($page, "php://"),那大小写绕过就彻底失效了,因为stristr('PHP://', 'php://')会返回真,照样被拦。所以碰到文件包含题目,第一件事永远是看它用的到底是哪种过滤函数,它区分不区分大小写,决定了你后面能不能走大小写绕过这条近路。

3. 绕过思路落地的关键认知:把php://换成PHP://之后发生了什么

3.1 第一次成功的请求长这样

我实际发出的请求是:

curl "http://目标站/index.php?page=PHP://filter/read=convert.base64-encode/resource=index.php"

页面不再返回Hacker!!!,而是输出了一段很长的Base64字符串。这段字符串开头是PD9waHA之类,懂的人一眼就能认出来,PD9waHA就是<?php的Base64编码结果。Base64能解码,说明读取成功了,只是内容被打包成了编码形式。

这里要特别提醒一点:虽然PHP://filter绕过了strstr,但真正的读取动作要交给PHP内部的流解析。如果你改完大小写之后没输出,别怀疑绕过思路,先检查是不是resource=后面的文件名写错了,或者过滤器名拼写不对。

3.2 php://filter的结构拆解

php://filter这个伪协议,本质上是一个给文件读写加过滤器的封装层。它最常见的三段式写法是:

php://filter/read=过滤器/resource=目标文件
  • read=:设置读取时经过的过滤器,比如convert.base64-encode就是在读取内容时先做一次Base64编码再返回;
  • resource=:指定实际读取的目标,可以是相对路径、绝对路径;
  • 过滤器还可以用|串起来,比如read=string.rot13|convert.base64-encode,先ROT13再Base64,属于进阶玩法。

在这个payload里,核心是read=convert.base64-encode。include到这段封装后,拿到的其实已经是被Base64编码过的字符串,里面没有任何<?php标签,所以PHP不会把它当代码执行,而是原样输出。这一步直接解决了“include文件时PHP代码会被执行、看不到源码”的难题。

3.3 解码那串Base64

拿到Base64之后,我习惯用本地命令解码:

echo 'PD9waHA...' | base64 -d

内容长的时候,推荐先存到文件里再解:

echo 'PD9waHA...' > /tmp/encoded.txt base64 -d /tmp/encoded.txt

当然用Python也方便:

import base64 print(base64.b64decode("PD9waHA...").decode())

解出来就是index.php的原始源码。看到的内容和题目贴出来的几乎一样,这就确认了一件事:伪协议读取确实打通了,接下来要做的就是在源码和目录结构里找flag的藏身之处。

4. 拿flag的完整链路复现:从读index.php到读flag

4.1 读哪些文件,按什么顺序

我拿到index.php源码后,先整体扫了一遍,确认了题目逻辑和考法,也确认了环境里应该有专门的flag文件,逻辑上没有出现在当前文件里。那么下一步就是猜flag文件的位置。CTF环境里常见的习惯是当前目录放flag.php或者flag,也可能在根目录/flag。

所以我按这个顺序试:

  • flag.php
  • flag
  • /flag

正常的做法是每个都用PHP://filter/read=convert.base64-encode/resource=套一遍,看哪个返回的Base64能解出有意义的内容。我这边是flag.php一次命中。

4.2 第二次读取与解码

请求:

index.php?page=PHP://filter/read=convert.base64-encode/resource=flag.php

返回Base64,解码后内容是典型的:

<?php $flag = "flag{模拟flag内容}"; ?>

到了这一步,题目就结束了。flag拿到了,这道题也就算通关。真正值得留意的不是“读flag.php”这最后一步,而是整个过程中对过滤规律的理解:为什么不能直接php://,为什么改成PHP://就行,为什么读取时非要用convert.base64-encode。

4.3 为什么include直接读不到明文

这里再补充一个新手容易卡住的点。如果直接include("flag.php"),而且flag.php里的代码是<?php $flag = "flag{...}"; ?>,PHP解析器会把这段代码执行掉,给$flag赋值,但页面不会输出任何东西。也就是说,你的请求大概率是成功的,但结果不可见。

用convert.base64-encode过滤器之后,文件内容在交给include之前就变成了纯Base64文本,没有PHP标签,include不会执行它,只会把编码文本输出到页面。于是源码内容就暴露了。这个思路可以迁移到任何PHP源码读取场景,是最通用的信息收集手段。

5. 复盘与扩展:data://直接执行命令,以及这类题目的通用打法

5.1 另一条路:data://伪协议直接RCE

题目只过滤了php://,那data://是不是就能用?答案是可以,前提是目标环境的allow_url_include开关处于开启状态(很多CTF靶场确实开着)。我试了这样一个payload:

index.php?page=data://text/plain,%3C%3Fphp%20system(%27ls%27)%3B%3F%3E

%3C%3Fphp是<?php的URL编码,system('ls')会在目标服务器上执行ls命令,%3B%3F%3E是;?>. 由于这个字符串里没有连续的php://,strstr检测直接放行,include就加载了一个内容为PHP代码的数据流,命令顺利执行。

这个思路比读文件更直接,等于直接从文件包含升级到了代码执行。但它的前提更苛刻:一旦allow_url_include=Off,data://和php://input这类需要开启远程包含的wrapper就会失效。相比之下,php://filter读取本地文件不需要依赖这个开关,通用性更强。所以我这题的第一解法是filter,而不是data,道理就在这。

5.2 如果过滤函数换了,需要立刻换思路

这道题的精髓在于“过滤只拦了最标准的路径”。如果把题目做点变形,过滤规则变了,招式也得跟着变。常见的变体有:

过滤方式特征可能有效的思路
strstr($page, "php://")区分大小写改为PHP://等大小写混写
stristr($page, "php://")不区分大小写大小写绕不动,考虑data://或其他wrapper
正则/php:\/\//i不区分大小写大小写绕不动,需要找其他协议或编码利用
正则 `/php://data://` 都过滤两个入口都封

每次改题,判断的重点永远是:它到底过滤了什么、用什么函数过滤、过滤发生在解码前还是解码后。只要把这几个问题问清楚,绕过路径基本就浮出水面了。

5.3 从防御者视角看:这类漏洞该怎么修

刷题不能只为了拿flag,还得想想真实开发中怎么挡住这种攻击。修复动态文件包含,最稳妥的办法是用白名单:

$allowed = ['home', 'about', 'contact']; if (!in_array($page, $allowed, true)) { exit('invalid page'); }

如果确实需要动态拼接路径,也要做路径校验:

$base = '/var/www/html/pages/'; $real = realpath($base . $page); if ($real === false || strpos($real, $base) !== 0) { exit('invalid path'); } include $real;

realpath会解析掉../和符号链接,再用strpos确认最终路径没有跳出限定目录,这比单纯替换../靠谱得多。同时,生产环境里务必确认allow_url_include=Off,这个开关能拦掉很大一类远程包含攻击;即便本地包含,也要避免直接把用户输入塞进include。这些点答题时不一定用得上,但做代码审计和写防护方案时价值很高。

5.4 以后再遇到include题目,我会按这个顺序处理

复盘了这道题之后,我把处理文件包含类题目的步骤固定成了五步,分享出来,新手可以直接照做:

  1. 打开页面先看有没有源码泄露,有源码就先读源码,找include/require的位置;
  2. 确认过滤规则:是strstr还是stristr,是正则还是替换,是否区分大小写;
  3. 先上php://filter读源码,注意先用大小写变体试探,确认伪协议是否可用;
  4. 从源码里找目录结构、文件名线索,再读取真正的flag文件;
  5. 如果目标是拿shell或验证RCE,再试data://、php://input等执行类wrapper。

这套顺序看起来简单,但能避免一个非常常见的低级错误:上来就蒙flag文件路径,蒙不到就卡住。在CTF里,信息收集永远比瞎猜值钱。文件包含题尤其如此,先读源码,路径和藏身点就都有了方向,剩下的只是一步步验证而已。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 5:37:41

Linux共享内存实战:从mmap到零拷贝,解锁高性能进程间通信

写 Linux 下多进程程序的人&#xff0c;迟早会被一个问题逼疯&#xff1a;两个进程之间&#xff0c;到底怎么交换数据才又快又稳&#xff1f;管道、消息队列、信号、本地 socket&#xff0c;这些东西文档上都写着“进程间通信”&#xff0c;可真到了要传几十上百 MB 的数据、或…

作者头像 李华
网站建设 2026/10/22 5:37:06

JWT认证实战指南:原理、签名算法与安全排坑

1. 项目概述与核心价值1.1 我为什么会专门聊JWT先说说我自己的经历。做后端开发这些年&#xff0c;几乎每个Web项目都会遇到登录鉴权的问题。早期用Session方案&#xff0c;后来维护微服务拆分的项目多了&#xff0c;Session同步就成了麻烦事。直到某次做项目重构&#xff0c;技…

作者头像 李华
网站建设 2026/10/22 5:37:05

从测试脚本到质量工程:Harness与Loop双轮驱动实践

1. 先搞清楚&#xff1a;为什么质量团队开始讨论"工程化"这件事前阵子我帮某团队做质量评审&#xff0c;负责人拍着胸脯说"我们有全套自动化测试"。结果打开仓库一看&#xff0c;几百个脚本散落在各个模块目录里&#xff0c;测试数据靠手工拷贝&#xff0c…

作者头像 李华
网站建设 2026/10/22 5:35:02

CMake+vcpkg+cline三件套:构建Windows C++高效开发流水线

在Windows上做C个人开发&#xff0c;早年间是件让人又爱又恨的事。爱的是性能和控制力&#xff0c;恨的是环境配置、依赖安装、构建脚本这三座大山。我见过太多人&#xff0c;写代码一小时&#xff0c;配环境一整天&#xff0c;最后连库都没链上&#xff0c;直接在控制台里跟LN…

作者头像 李华
网站建设 2026/10/22 5:34:56

模板代码安全审计:从硬编码密钥到自动化扫描的实践指南

接到这个任务时&#xff0c;我的第一反应是把它当作一次普通的代码审查看待&#xff0c;可真坐下来之后才发现&#xff0c;模板代码的安全审计和常规代码审计完全是两回事。所谓的模板代码安全审计&#xff0c;指的是对项目脚手架、代码生成器输出&#xff0c;以及团队内部公共…

作者头像 李华
网站建设 2026/10/22 5:34:38

LeetCode 739 每日温度:单调栈详解与机考实战

机考刷题这个系列写到第8篇&#xff0c;终于轮到一道让人“一拍大腿”的经典题了&#xff1a;LeetCode 739 每日温度。说它是经典&#xff0c;不是因为它有多难&#xff0c;而是因为它把“单调栈”这个数据结构讲得特别生动——你甚至不需要背模板&#xff0c;跟着题意走一遍&a…

作者头像 李华