尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
PHP伪协议与反序列化:CTF文件包含漏洞的审计链路
前阵子在BUUCTF上刷题ZJCTF 2019 这道 NiZhuanSiWei 把我卡了挺久。题目名字是“逆转思维”的拼音本意就是让你换一个角度去解题。表面看入口就是一个PHP页面接收 text、file、password 三个参数但真正跑通之后才发现它把PHP伪协议、正则绕过、文件包含、反序列化四件事串成了一条闭环的审计链路非常值得单独拆开讲。这道题属于典型的PHP代码审计加CTF Web综合题适合刚入门文件包含和反序列化的朋友也适合平时写PHP但没认真研究过流包装器的开发者。只要你在本地搭一个简单环境按照下面的分析走一遍对“输入点-可控点-触发点”这条审计主线会有很直观的体感。下面我按自己实战踩坑的顺序把三层解题思路完整还原出来。1. 整体思路拆解这道题到底在考什么刚打开题目页面时只有一个类似源码的PHP片段挂在面前核心逻辑大致是这样的?php $text $_GET[text]; $file $_GET[file]; $password $_GET[password]; if(isset($text) (file_get_contents($text, r) welcome to the zjctf)){ echo brget flag; if(preg_match(/flag/, $file)){ echo flag no!; exit(); } include($file); if(isset($password)){ $obj unserialize($password); echo $obj; } } ?一眼扫过去代码量不大但每个参数都不“干净”。$text会被file_get_contents读取然后和固定字符串做全等比较。$file会被正则拦截只要出现flag字样就退出否则直接include。$password更危险直接送进unserialize返回值还被echo输出。后来我又在题目配套文件里看到了一个useless.php内容是一个带__toString魔术方法的类。这个类就是最终读取flag.php的钥匙。如果按正常思路直接传fileflag.php肯定会被正则拦住。所以必须把解题节奏拆成三层关卡目标关键手段绕过点第一层让file_get_contents($text)返回指定字符串data://伪协议构造数据流无需真实文件直接提供文件内容第二层拿到useless.php源码分析可利用的类php://filter加 base64 编码输出文件名不含flag不触发正则第三层让反序列化对象触发魔术方法读取flag.php构造Flag类的序列化字符串flag关键字藏在对象属性里不在$file参数里这个思路跟平时做普通文件包含题不一样。以前我们总想着“怎么绕过正则去读目标文件”但在这道题里最巧妙的点是目标文件名flag.php的下场根本不用出现在$file参数里你只需要把flag.php写进一个对象属性然后让代码在“内部”去读它。这就是“逆转思维”的核心。2. 第一层关卡data://伪协议构造任意文件内容第一段校验条件看起来很难满足file_get_contents($text) welcome to the zjctf含义是$text必须指向一个“文件”而且这个文件的内容必须严格等于welcome to the zjctf。不能多一个空格不能少一个字母。我在刚开始做题时想过几种笨办法比如找一个现成路径、传/etc/xxx或者上传统一文件但在CTF环境里这些都是不可行的因为攻击者很难在目标机写文件。正解是 PHP 的data://流包装器。PHP 里很多文件操作函数支持“流”除了读磁盘真实文件还能读各种封装协议比如http://、php://、data://。data://做的事情很简单把 URI 里写的内容直接当成文件内容返回。也就是说你可以在 URL 里“凭空造一个文件”。基本格式是这样data://text/plain,内容所以第一层直接构造?textdata://text/plain,welcome%20to%20the%20zjctf注意中间的空格必须 URL 编码成%20。如果直接用原始空格一些 PHP 环境和中间件会对 URL 做容错处理但为了保险规范做法是编码。file_get_contents看到data://开头的参数后会把后面的内容作为数据流读取。内容只要和右边完全相同全等判断就成立页面会输出get flag进入下一层。这里还有一个细节容易被忽略file_get_contents的第二个参数r是流模式写不写都无所谓因为data://本身不是持久化流。重点是 PHP 默认允许使用这类封装协议不需要额外配置。但如果你在本地复现时发现data://不生效多半是allow_url_fopen或allow_url_include被关闭了这个我在第五部分会单独说。一个小技巧如果data://text/plain,xxx里的文本含有中文或特殊符号建议改成 base64 编码形式data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY这样不仅能避开 URL 编码问题还能减少一些字符解析方面的意外。CTF 题里两种写法都通用关键是理解它的原理data://是让你把“数据”伪装成“文件来源”而不是真的去读写某个路径。过了这一层代码会继续走到preg_match和include也就到了第二层。需要说明的是data://不仅适用于这种字符串比较场景凡是遇到“某个函数要读文件内容但你没有文件”的情况都可以第一时间想到它。3. 第二层关卡php://filter 读取源码并锁定漏洞点过了文本校验之后进入关键代码if(preg_match(/flag/, $file)){ echo flag no!; exit(); } include($file);这里preg_match(/flag/, $file)会检查$file参数里是否出现flag四个连续字符。如果出现就退出。看到这里第一反应当然是不能直接include flag.php。可是题目里真正的 flag 就在flag.php里这条路被堵死了。那就换一个思路先看还有没有其他可以利用的文件。题目既然叫 NiZhuanSiWei并且我在目录里发现了useless.php那说明这个文件很可能藏着漏洞点。问题来了直接include useless.php会发生什么在 PHP 里include一个 PHP 文件本质上会执行这个文件里的所有代码。useless.php里如果只有类定义那么执行结果是“定义一个类”但不会有任何输出。如果只靠页面回显你什么都看不到等于白包含。所以要想办法“看到源码”。PHP 的php://filter流包装器就是干这个的。它能在读取文件时对数据进行过滤处理其中最经典的就是convert.base64-encodephp://filter/readconvert.base64-encode/resourceuseless.php这个参数的意思是读取useless.php的内容在返回前先做一次 base64 编码。编码后的结果是纯文本不会再被 PHP 解析执行于是源码就通过页面输出暴露给我们了。完整请求是这样?textdata://text/plain,welcome%20to%20the%20zjctffilephp://filter/readconvert.base64-encode/resourceuseless.php页面返回的是一长串 base64 字符大致长这样PD9waHAKY2xhc3MgRmxhZ3sKICAgIHB1YmxpYyAkZmlsZTsKICAgIHB1YmxpYyBmdW5jdGlvbiBfX3Rvc3RyaW5nKCl7CiAgICAgICAgaWYoaXNzZXQoJHRoaXMtPmZpbGUpKXsKICAgICAgICAgICAgZWNobyBmaWxlX2dldF9jb250ZW50cygkdGhpcy0ZmlsZSk7CiAgICAgICAgICAgIHJldHVybiAiVVIgU08gQ0xPU0UgIS8vL0NPTUUgT04gUEzaIjsKICAgICAgICB9CiAgICB9Cn0拿到后在外面随便用一个 base64 解码工具还原就能看到useless.php的源码?php class Flag{ public $file; public function __toString(){ if(isset($this-file)){ echo file_get_contents($this-file); echo br; return U R SO CLOSE !///COME ON PLZ; } } } ?看到这里整道题的突破口就清晰了。Flag类里有一个公开属性$file还有魔术方法__toString。PHP 的__toString会在对象被当成字符串使用的时候自动触发比如echo $obj、字符串拼接、比较等。这个方法里的逻辑是如果$this-file存在就file_get_contents($this-file)并echo出来。也就是说只要我们能构造一个Flag对象并且把它的$file属性设置为flag.php然后再让这个对象被 echo__toString就会被调用flag.php的内容就直接输出到页面上了。这也是为什么第二层要用php://filter而不是直接用浏览器打开useless.php。直接打开只会得到一个空白页面因为你看到的是“执行结果”而不是“源码本身”。使用 base64 编码的目的是让源码以数据形式暴露出来绕开 PHP 解释器。这个技巧在做任意文件读取、源码泄露类题目时几乎是通用解。另外要注意preg_match只检查$file参数本身。php://filter/readconvert.base64-encode/resourceuseless.php这个串里没有flag四个字符所以轻松通过检查。这说明正则过滤是有边界感的它只盯着$file这一个变量并没有考虑文件内容里可能藏的东西。4. 第三层关卡构造反序列化对象触发魔术方法既然已经拿到了类源码接下来的目标就很明确让$password对应的反序列化操作生成一个Flag对象再想办法让它被echo。主程序里有一句$obj unserialize($password); echo $obj;这就是触发点。echo $obj会把对象当成字符串处理于是 PHP 自动调用Flag类里的__toString方法。如果我们在反序列化时把$file属性设置为flag.php方法内部就会执行echo file_get_contents(flag.php);flag 的内容就被原样输出。现在只差一件事构造一个合法的序列化字符串。在 PHP 中序列化一个对象的基本格式是O:4:Flag:1:{s:4:file;s:8:flag.php;}拆开理解O:4:Flag表示对象类名是Flag长度是 4。:1表示有 1 个属性。s:4:file表示属性名是字符串file长度是 4。s:8:flag.php表示属性值是字符串flag.php长度是 8。这个字符串其实就是serialize(new Flag())的结果只是我们把属性值手动改成了目标文件名。只要类定义存在unserialize就能把它还原成对象。所以最终请求长这样?textdata://text/plain,welcome%20to%20the%20zjctffileuseless.phppasswordO:4:Flag:1:{s:4:file;s:8:flag.php;}但这里有个容易踩的坑URL 里直接放双引号、花括号、分号非常容易被某些中间件或浏览器干扰。稳妥的写法是全部 URL 编码?textdata://text/plain,welcome%20to%20the%20zjctffileuseless.phppasswordO:4:%22Flag%22:1:%7Bs:4:%22file%22;s:8:%22flag.php%22;%7D注意为什么fileuseless.php因为反序列化要生效必须先让Flag类被加载。include useless.php会执行类定义这样unserialize才能找到这个类。如果不包含这个文件反序列化得到的只会是__PHP_Incomplete_Class魔术方法也不会触发。最终链路是text用data://满足字符串比较。fileuseless.php通过正则检查同时把Flag类定义加载进当前命名空间。password是一个序列化后的Flag对象$file flag.php。unserialize还原对象echo $obj触发__toString。__toString里执行file_get_contents(flag.php)flag 输出。到这里三层渗透链路就完全闭合了。你会发现整道题没有一个环节是特别高深的技术但它把所有点都扣得很紧少一个伪协议少一步源码分析少一次对象构造都走不通。5. 常见问题排查与实战避坑这道题我在本地环境和自己搭靶场的时候遇到过不少小问题。有些是环境原因有些是纯手滑。我把典型问题整理成一个速查表方便你直接对照排查。症状可能原因解决思路传data://后页面返回WrongURL 里的空格没编码allow_url_fopen被关闭空格改成%20或使用data://text/plain;base64,xxx形式php://filter读出来是空资源文件名大小写不对路径写错filter 关键字顺序写错先读index.php或当前文件确认 filter 可用include useless.php后页面空白这是正常的类定义不会输出任何内容改用convert.base64-encode读取源码反序列化后没有输出useless.php没有被 include类名或属性名长度写错__toString没触发确认file参数先include useless.php核对序列化格式反序列化后输出__PHP_Incomplete_Class当前环境没有加载Flag类定义让include先执行useless.php把flag.php写进$file参数直接报flag no!正则匹配到了flag不要把目标文件名放到$file参数里放进对象属性即可本地复现时data://能用但php://filter不行allow_url_include配置影响 include 类流操作检查 php.iniallow_url_includeOn部分环境还要确认allow_url_fopenOn除了表格里的问题还有一个细节值得单独拎出来说序列化字符串的长度。很多人手写 payload 时容易算错长度比如s:8:flag.php里的 8 必须和flag.php的字符数一致如果文件名换成./flag.php长度就要改成 10。长度不对unserialize会直接失败返回false。我在初学反序列化时经常栽在这个地方。建议本地先写一段 PHP 脚本直接serialize(new Flag())打印出来再改属性值避免手算出错。另一个心得是拿到源码后不要急着打先看类里有哪些魔术方法。__toString、__destruct、__wakeup、__call都是反序列化题目的高发点。尤其是__toString触发条件非常多只要对象被当作字符串使用就会调用所以它经常被设计成最终读文件的钩子。看到这类魔术方法反序列化题基本就解了一半。还有一个小坑有的环境里unserialize之前会先调用__wakeup如果题目给的类是旧版本 PHP 的题可能还会遇到__wakeup绕过也就是属性数量大于实际数量时跳过__wakeup。如果做题时发现序列化字符串被某种方式拦截或不生效可以考虑这种绕过。但这道题主流的解法用不到知道有这回事即可。6. 换个姿势不依赖 password 参数的扩展思路如果你在自己的靶场里把题目改了一下去掉了password反序列化入口那怎么打其实这道题的变体在很多平台都出现过核心思路还是“让 include 包含一段可以执行反序列化操作的代码”。常见做法是结合php://filter和php://input。利用方式是这样filephp://filter/convert.base64-decode/resourcephp://input然后 POST 一段经过 base64 编码的 PHP 代码比如PD9waHAgZWNobyAnY2FuJzsgPz4php://filter/convert.base64-decode的作用是读取php://input的内容先做一次 base64 解码再把解码结果交给include。如果解码后的结果是合法的 PHP 代码比如?php echo can; ?就会被执行。这就是一种通过 filter 把“输入数据”变成“可执行代码”的技巧。配合前面的思路POST 的内容可以改成?php unserialize(O:4:Flag:1:{s:4:file;s:8:flag.php;}); ?然后把它 base64 编码后传进去。这种扩展方案的价值在于它不依赖unserialize($password)这个入口而是直接在include这一步利用 filter 链制造代码执行点。你可能会问为什么不直接把?php echo file_get_contents(flag.php); ?base64 编码后传进去因为 POST body 里可以带任意内容正则检查不了它确实可以这样做。但题目的默认解法走的是反序列化路线更符合“代码审计”这个主题类就在那里你不去用它反而绕远路。如果让我给个偏好建议我会说优先用官方入口也就是password反序列化因为构造简单、链条清楚扩展方案作为兴趣了解即可它更偏向于那种没有现成反序列化入口的变体题。7. 从攻防视角看这道题的设计意图刷完这道题我最大的感受是它其实做了一个非常好的安全演示。三层的每一层都对应着一个真实的PHP配置或代码写法问题。file_get_contents($text)直接用用户输入作为流地址本质上是“不可信输入直接进入敏感函数”。如果开发者以为file_get_contents只能读文件就会忽略data://、php://这些流包装器带来的额外能力。修复方式是做协议白名单只允许http或本地路径屏蔽data和php协议。include($file)之前虽然有正则拦截但只拦截了flag字样没有考虑 filter 链、伪协议、大小写、编码等方式都能绕过。更好的做法是检查解析后的真实路径并且禁止 PHP 流包装器在 include 中使用。很多真实漏洞就是从这种“只挡关键字、不防协议”的过滤逻辑里出来的。unserialize($password)更不用说直接反序列化用户输入是典型的 PHP 对象注入漏洞。安全做法是禁止使用反序列化处理外部输入如果一定要用至少加一层allowed_classes白名单并且确认类里没有危险魔术方法。当然CTF 题本来就是为了展示漏洞而构造的极端场景现实中不会把所有危险点塞进同一个页面。但正是这种场景化练习能让你在写代码时下意识地去想这个参数能不能传伪协议这个反序列化入口能不能被触发这些都是实打实的防御经验。我个人练完这道题之后再遇到包含类题目基本会先走一遍固定流程找文件读取点用 filter 看源码找魔术方法构造序列化链。这套思路在不少比赛题里都能复用。NiZhuanSiWei 这道题最值钱的地方不是你拿了个 flag而是它把整个 PHP 漏洞链的闭环完整演示了一遍。
RELATED

相关推荐

可燃气体报警器电路设计:抗干扰、低漂移与硬件级可靠性实现

可燃气体报警器电路设计:抗干扰、低漂移与硬件级可靠性实现

简介:本资源是一份面向电子工程、安全监测及工业自动化领域初学者与实践者的可燃气体报警器电路设计技术文档,聚焦于解决石油、化工、采矿等高危场景中可燃气体泄漏的实时检测与联动预警问题。文档以GS-A2气敏传感器为核心,系统阐述了信号检测…

📅 2026/10/21 16:50:44
CentOS 8 内网离线安装 OpenOffice 全流程与依赖处理实战

CentOS 8 内网离线安装 OpenOffice 全流程与依赖处理实战

1. 为什么内网环境还要折腾 OpenOffice很多刚接触企业内网运维的朋友会有一个疑问:现在在线办公套件这么成熟,为什么还要在一个没有外网连接的 CentOS 8 服务器上,费劲巴拉地离线安装一个 OpenOffice?这个问题我在第一次接到类似需…

📅 2026/10/21 16:50:44
C++基础核心:内存模型、对象生命周期与智能指针实战解读

C++基础核心:内存模型、对象生命周期与智能指针实战解读

写了挺多年C,带过不少做项目的同学,也看过很多号称“基础扎实”的简历。说实话,大部分人学C不顺利,问题并不出在背不出语法,而是心里始终没有一张关于“数据和内存”的图。这张图一旦画出来,指针、引用、多…

📅 2026/10/21 16:45:40
MORE NEWS

更多资讯

📰

老MES备份操作流程:数据库、应用配置与恢复演练

接到一个工单,字面就一行:“192.168.50.2 老MES备份 操作流程”。字越少,活儿越重。干过工厂运维的都懂,MES(制造执行系统)是一天都不能停的,而标题里那个“老”字,意味着这台服务器…

📰

老MES系统备份实操指南:从数据库一致性到恢复演练

1. 老MES系统备份:为什么这活儿值得认真对待干了十来年制造企业的IT运维,我经手过不少老旧系统的维护,其中MES(制造执行系统)算是最让人又爱又恨的一类。老MES系统通常运行在虚拟化平台或老物理服务器上,承…

📰

Oracle触发器从原理到实战:类型解析、避坑指南与性能优化

Oracle里的触发器(trigger)这东西,很多人一开始把它想成“数据库里的监控摄像头”,这句话大方向没错,但真在数据库战场上摸爬滚打过几年的人,听到“触发器”三个字,往往会不自觉皱一下眉。它能做…

📰

老服务接GMSSL2.0国密库:C99代差与DLL隔离方案

简介:GMSSL2.0的Windows服务端工程,基于VS2015与VC6.0双版本构建,面向需要为Android客户端提供国密通信测试服务的开发人员。工程包含客户端与服务端完整代码,支持SM2等算法,可在Windows上编译生成服务端程序&#xff…

📰

Java生产级AI Agent:状态持久化、断点续传与意图锚定

1. 这不是又一个“Hello World”Agent:它直面生产环境的三座大山 你肯定见过太多标榜“AI Agent”的Java项目——启动一个Spring Boot,加个 RestController ,调用一次OpenAI API,返回个JSON,然后配张酷炫架构图&…

📰

Monitoring靶机实战:从SQL注入到sudo提权的完整渗透攻击链

如果你在 vulnhub 靶场列表里翻到一台叫Monitoring的机器,第一眼多半会觉得它平平无奇:开放端口不多,首页看起来还是个普通的监控面板。但我自己打下来之后最深的感受是——这类靶机考查的不是某个 0day 的利用难度,而是你能不能把…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬