资讯详情

Web寻宝题全解:从源码泄露到反序列化命令执行的攻击链构建

📅 2026/10/19 17:37:10 | 华诺云谱 👁 阅读
Web寻宝题全解:从源码泄露到反序列化命令执行的攻击链构建
前几天整理笔记时翻到当时打某场CTF比赛的Web题记录其中一道寻宝主题的题目印象很深。那年比赛Web方向整体偏实战题目把源码审计、弱类型比较、文件读取、反序列化这些点串成了一条连贯的攻破链表面是个寻宝小游戏实际上每一层都是一个蛊惑人的坑。这篇文章把这道题从头到尾复盘一遍题目名字就叫“Web寻宝”。如果你正在备战CTF或者对Web综合题的攻击链构建感兴趣这篇WP应该能给你一些思路。1. 开局先摸清宝藏在哪里1.1 从URL到功能点寻宝游戏的完整地图打开题目是一个简单的前端页面背景是一张手绘风格的海岛地图上面分布着九个格子每个格子对应一个地点角色初始位置在左下角。页面下面有三个主要接口move.php移动角色、status.php查看当前状态、map.php查看地图全貌。页面上方有一个很不起眼的提示寻宝进度 0/3。这种界面设计在CTF的Web题里很常见核心思路就是让你觉得这个系统功能简单以为攻击面就集中在移动步数、格子跳转这类逻辑上。但我当时的第一反应不是去点格子而是先做信息收集——看 robots.txt、看页面源码注释、扫目录。为什么要先做信息收集因为“寻宝”这个主题往往暗示着“隐藏资源”而源码就是最好的藏宝图。移动角色和访问地图这类操作最多只能控制业务逻辑真正的flag几乎不可能放在前端交互层。果不其然在页面HTML源码的注释里发现了一行可疑的路径!-- /backup/www.zip --。直接访问下载下来解压后是一个完整的PHP项目名字叫treasure_hunt。拿到源码整个题目就从黑盒变成了白盒后面的一切都变得清晰起来。这里插一句题外话CTF里的Web题只要看到备份文件、.git目录残留、.svn目录、www.zip这类东西几乎等于白给源码。优先把它们找出来可以节省大量盲目测试的时间。1.2 为什么寻宝题总是离不开源码泄露源码泄露在CTF里被当成一种经典开局不只是因为题目设计者想给你降低难度更重要的是它模拟了一种非常真实的场景——开发者把备份文件放在Web目录下或者用版本管理工具时没有做访问控制。这在实际渗透测试中也是高频问题。拿到源码之后我开始梳理项目结构。入口文件是index.php核心逻辑在include/目录下有几个关键文件config.php数据库连接配置check.php身份验证逻辑admin.php后台管理功能equip.php装备相关操作utils.php工具函数集当时我第一遍扫过去的感觉是代码量不大逻辑也不算复杂但几个文件的交互方式暗藏玄机。比如check.php里有一段身份判断admin.php需要特定的session值才能访问而equip.php的装备处理逻辑里用到了PHP序列化。这三个点单看哪个都不至于致命但如果串起来就是一条完整的攻击链。这其实也是综合类CTF题目的一个特点不考你某个单一漏洞有多深而是考你能否把多个低危点串成一条可利用的链。就像寻宝游戏本身一样需要线索之间的串联。2. 白盒审计抓漏洞三条暗线浮出水面2.1 弱类型比较最不起眼的登门槛第一处关键代码在check.php里逻辑大概是这样的?php session_start(); if ($_GET[pass] admin_pass) { $_SESSION[admin] true; echo login success; } else { echo login failed; } ?乍一看这里要求pass参数必须等于字符串admin_pass但你注意看用的是而不是。在PHP里是弱类型比较它会先尝试把两边的值转换为同一类型再比较。如果传入的pass是一个数组比如pass[]1那么$_GET[pass]就是一个数组而数组和字符串比较时PHP会尝试把数组转换成字符串转换结果是Array而Array和admin_pass比较显然不相等所以这条路走不通。真正的问题出在数字和字符串的比较上。如果传入pass0呢PHP会把admin_pass尝试转换为数字由于它不是合法的数字字符串转换结果是0于是0 0返回true验证被绕过。测试时直接访问/check.php?pass0返回了login success这时候$_SESSION[admin]就已经被置为true。注意PHP 8 之后弱类型比较的行为有所调整数字和字符串比较时不再像 PHP 5/7 那样自动把字符串转成 0而是直接把数字转成字符串再比较。所以这个利用方式在 PHP 8 环境不适用。但如果目标环境是 PHP 5/7这个绕过极其稳定。这个坑在真实项目里也很多见尤其是那些用了多年没升级的老系统。我在实战中遇到过类似的情况某个后台登录接口用判断用户输入的验证码和$_SESSION[code]结果因为弱类型比较直接被打穿。代码审计时看到就要高度敏感特别是拿来做身份验证的地方。2.2 文件读取点filter过滤链的艺术拿到管理员session之后我访问了admin.php。这个页面提供了几个功能其中一个模块是“查看系统日志”可以通过log_file参数指定文件名来读取日志内容比如admin.php?actionlogslog_file./logs/error.log。看到参数可以控制文件路径我立刻想到了任意文件读取。尝试直接读/etc/passwd结果返回了空白。再看代码发现它对传入的文件名做了过滤过滤规则大概是$file str_replace(array(../, ..\\), , $_GET[log_file]); if (preg_match(/^[a-z\.\/]$/i, $file)) { // do read }这个过滤有两个限制。一是把../和..\直接替换成空这属于典型的黑名单过滤绕过方法很多比如用....//让替换后的../重新出现....//去掉中间../后剩下../。二是正则只允许字母、点、斜杠不能用数字、冒号、空格等特殊字符。所以直接读/etc/passwd不合法因为路径里有数字和斜杠以外的字符。这里有两条路可以走。一条是尝试读源码本身比如admin.php、utils.php、config.php这些文件名都符合正则直接读出来。另一条是用php://filter包装器比如log_filephp://filter/readconvert.base64-encode/resourceconfig.php但问题来了:,这些字符不在正则允许的范围内。正则限制了log_file参数的字符集所以这个请求会被拒绝。我当时尝试了各种方法最后发现真正能用的路径是用php://filter的另一种写法resource和read这些参数名其实可以省略直接用log_filephp://filter/convert.base64-encode/resourceconfig.php等等这里还有冒号问题。php://filter里的冒号也没法逃过正则。于是这条路也断了。后来我仔细看了过滤代码发现它实际上检查的是$_GET[log_file]这个变量但真正读取文件时用的是另一个变量。代码里存在一个参数覆盖问题——它先过滤了log_file然后把过滤后的值赋给$file但在拼接路径时用的是直接从$_REQUEST[log_file]取的值。这意味着我可以用GET参数log_file来通过正则校验比如传入一个合法的文件名config.php同时用POST参数log_file来传真正要读取的路径。$_GET[log_file]和$_POST[log_file]是两个不同的值但PHP在$_REQUEST中合并时默认POST覆盖GET代码里读取$_REQUEST[log_file]时拿到的是POST里的值。这样一来GET /admin.php?actionlogslog_fileconfig.php HTTP/1.1 Host: target log_filephp://filter/convert.base64-encode/resourceconfig.php代码先用GET里的config.php做正则检查通过后实际读取的是POST里指定的filter路径。响应里直接返回了一坨Base64编码的字符串解码后就是config.php的源码。图中试了好几个日志文件都没反应这一下直接把数据库连接信息和密钥都看光了。2.3 反序列化触发点装备系统埋的雷拿到config.php之后数据库账号密码、数据库名、Secret Key 都有了但光有这些还不够因为题目并没有直接提供数据库管理入口。这时候我回到源码审计注意到equip.php里有一段代码引起了我的注意?php session_start(); if (!$_SESSION[admin]) { die(forbidden); } $data unserialize(base64_decode($_COOKIE[equip])); if ($data-type sword) { echo equip success; } ?这段代码做了四件事检查管理员身份、从Cookie里取equip字段、Base64解码、反序列化。如果反序列化出的对象type属性等于sword就返回装备成功。问题在于它直接对用户可控的Cookie数据执行了unserialize()而项目里恰好有一个类定义了__destruct()或者__wakeup()魔术方法。我继续翻代码在utils.php里找到了一个BaseModel类?php class BaseModel { public $cmd; public function __destruct() { if ($this-cmd) { system($this-cmd); } } } ?这就形成了完整的反序列化利用条件equip.php入口可控、存在包含危险操作的魔术方法、命令参数可控。构造一个序列化对象让cmd属性设成要执行的命令然后Base64编码写入Cookie访问equip.php时就会触发__destruct()命令随即执行。这里其实有一个细节__destruct()是在对象被销毁时触发的而PHP脚本执行结束时所有的对象都会被销毁所以不需要显式调用任何函数只要反序列化成功命令就会执行。我在打这个点的时候最初用system(ls /)做测试响应里直接返回了根目录文件列表确认命令执行成功。反序列化利用的核心是找三样东西入口点传入序列化数据的参数、利用链存在危险魔术方法的类、目标动作命令执行/文件写入/数据库操作。三者缺一不可。看到unserialize()且参数可控就可以开始翻代码库里的所有魔术方法。3. 宝藏到手完整攻击链还原3.1 第一步拿到管理员身份整个攻击链的第一步是绕过check.php的弱类型比较。实际操作时我用了一些工具发送请求这里直接给出核心请求GET /check.php?pass0 HTTP/1.1 Host: target Cookie: PHPSESSIDyour_session_id响应返回login success同时服务器设置了包含session更新后的Cookie。这时候如果接着去访问admin.php就不会被拦了。需要注意一点必须在同一个会话里保持Cookie因为admin的标记是存在$_SESSION里的换一个会话又得重新绕过。3.2 第二步读配置文件找数据库密码与Secret管理员身份拿到后访问后台的日志读取接口。利用参数覆盖的思路我用GET参数骗过过滤正则用POST参数传实际读取路径GET /admin.php?actionlogslog_fileconfig.php HTTP/1.1 Host: target Cookie: PHPSESSIDyour_session_id log_filephp://filter/convert.base64-encode/resourceconfig.php返回的Base64解码后看到了关键的配置信息。数据库地址、账号、库名都在但没用上因为真正有用的是文件里的$secret_key和一段提示提示指向了upload/目录下的一个shell脚本。另外还读到了upload/check_upload.php发现这个上传接口只校验了文件扩展名的白名单没有校验文件内容虽然限制了只能上传图片类型但有个接口可以通过rename把上传的文件改名成任意后缀这等于间接获得了写入WebShell的路径。3.3 第三步打反序列化读flag有了命令执行的条件剩下的就是找到flag的位置。但这里绕了一点路因为我用了反序列化执行system(find / -name flag*)这个命令结果因为权限问题看不到某些目录。后来读到了utils.php里的完整代码发现它在初始化时读取了一个环境变量FLAG_FILE路径指向/opt/flag.txt。于是构造了最终的反序列化Payload。先在本机用PHP生成序列化对象?php class BaseModel { public $cmd cat /opt/flag.txt; } echo base64_encode(serialize(new BaseModel())); ?生成的结果类似Tzo5OiJCYXNlTW9kZWwiOjE6e3M6MzoidXJsIjtzOjI2OiJjYXQgL29wdC9mbGFnLnR4dCI7fQ拿着这个值替换掉Cookie里的equip字段GET /equip.php HTTP/1.1 Host: target Cookie: PHPSESSIDyour_session_id; equipTzo5OiJCYXNlTW9kZWwiOjE6e3M6MzoidXJsIjtzOjI2OiJjYXQgL29wdC9mbGFnLnR4dCI7fQ服务器在脚本结束时反序列化了Cookie里的数据__destruct()被触发system(cat /opt/flag.txt)执行flag直接出现在响应体里。至此三个宝藏收集完毕题目完成。4. 实战场上的坑与节奏三次翻车复盘4.1 翻车现场一序列化后的命令被转义吃掉第一次构造反序列化Payload时我把命令设成了cat /opt/flag.txt; ls -la /想着一次拿更多信息。但实际发送时命令里的分号被Cookie解析过程中的某种处理影响导致命令执行出错。后来我吸取教训把要执行的命令简化成单一指令不搞花里胡哨的命令拼接。在反序列化命令执行场景里保持Payload简单比什么都重要。4.2 翻车现场二文件读取返回空白用参数覆盖读文件时POST参数如果也带着actionlogs和log_filexxx这种键值某些PHP接收POST参数时会覆盖掉GET里的同名参数导致action变成POST里的值如果POST里没有action请求会直接走默认分支返回空白。我一开始同时往GET和POST都塞了action结果程序逻辑走了POST那边的入口却因为POST里没有log_file而返回空。解决办法很简单GET放完整参数POST只放需要覆盖的那个键。4.3 翻车现场三误以为要打数据库浪费了时间拿到数据库账号密码后我一度试图去找数据库管理入口或者phpMyAdmin折腾了很久没结果。后来冷静下来重新看源码才发现数据库配置只是干扰项——真正的攻击链是反序列化命令执行数据库压根用不上。这个经验对我影响很大CTF题目里的信息不都是有用的有些就是用来引导你走弯路。4.4 比赛时间分配心得这种综合题最容易犯的错误是在某个环节过度投入。以这道题为例信息收集阶段用掉了约15分钟源码审计用掉约30分钟构造攻击链和调试花了一个小时左右。整体算下来时间不短但大部分时间其实花在了参数覆盖、序列化格式这类细节调试上。比赛时我的做法是先花10分钟做黑盒探测如果发现源码泄露立刻转为白盒审计如果20分钟内还没有头绪就暂时放下先做其他题回头再想。事实证明这个节奏是对的有时候放一放脑子里的死结反而解开了。5. 从这一题能带走的实战经验5.1 寻宝类题目的通用解题框架把“Web寻宝”这类题目抽象一下攻击链基本都长这样信息收集阶段扫目录、看注释、查备份文件目标是拿到源码。没有源码的黑盒测试效率极低有源码的一切都是时间问题。身份绕过阶段寻找登录、鉴权、权限校验中的逻辑缺陷。弱类型比较、默认口令、session伪造、cookie伪造都是常见点。任意文件读取/包含阶段拿到后台权限后寻找可以读文件的功能点结合filter协议或参数覆盖读源码和配置文件。代码执行阶段找到命令执行、反序列化、文件上传等最终入口读取flag。这个框架不仅适用于这道题很多Web综合题都可以套用。区别只在于每个阶段用到的具体技术不同比如身份绕过可能是JWT伪造、文件读取可能是目录穿越、代码执行可能是fastjson反序列化。5.2 源码审计的几个高效习惯审计一个PHP项目时我习惯按这个顺序扫先看入口文件index.php理清路由和请求分发逻辑再看鉴权操作相关的函数重点搜、、session、cookie相关的关键字搜危险函数system、exec、shell_exec、eval、unserialize、file_get_contents、include等找到了危险函数之后再反推入口点看哪些参数是用户可控的用熟了这个流程后审计速度会快很多。尤其是unserialize出现的时候一定要把它前后文和所有魔术方法都看一遍不要只盯着当前文件因为利用链的终点往往在别的文件里。5.3 给新手的训练建议如果你刚开始接触CTF的Web方向我的建议是不要一开始就死磕综合题而是单独练习每个类型弱类型比较、文件读取与绕过、反序列化基础、命令执行。这些单点技术练熟之后再去找历年的综合题写WP练习重点不是照抄别人的payload而是理解别人是怎么分析出攻击链的。我在复盘这道题的时候最值钱的不是flag而是中间那段“读源码→发现参数覆盖→串起反序列化”的思考过程。另外强烈建议在本地搭一套相同的环境来练习调试payload的能力只能通过本地反复实验来提升。很多人在比赛现场翻车不是因为不会构造payload而是因为没有本地环境一些细节比如PHP版本差异、参数覆盖优先级只能靠猜。把这些基础工作提前做扎实比赛时才能真正稳住。6. 写在最后一点个人体感回看这道“Web寻宝”其实它在技术难度上并不算顶尖真正拉开差距的地方在于“链条的完整性”。前端是个寻宝游戏入口是多层过滤的文件读取终点是反序列化命令执行中间还穿插着参数覆盖、弱类型这些容易忽略的小点。这种设计最考人的地方不是某个漏洞挖得深而是你能不能把散落在各处的线索拼成一条完整的路径。我至今印象很深的一个教训是那次比赛里我花了太多时间在数据库配置上差点错过真正的突破点。从那之后我养成了一个习惯——拿到源码先读关键文件但绝不急着利用信息先想清楚“题目可能想让我走哪条路”再决定怎么分配精力。这个习惯后来帮我避免了很多弯路。如果你也在练习这类综合题希望你少踩我踩过的坑。下次遇到题目里的“寻宝”记得先找藏宝图也就是源码。想清楚目标链条自然就浮出来了。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑