CTF实战入门:从Web4题目解析PHP弱类型与反序列化漏洞

📅 2026/6/29 23:27:15
CTF实战入门:从Web4题目解析PHP弱类型与反序列化漏洞
1. 项目概述从一道CTF题看Web安全实战入门最近有不少朋友私信问我想入门Web安全但面对一堆专业术语和工具感觉无从下手。我的建议一向是别急着啃大部头理论书先动手玩起来。CTFCapture The Flag竞赛里的Web题目就是绝佳的实战沙盒。它们把真实世界中的安全漏洞浓缩在一个可控的环境里让你能安全地“搞破坏”从而理解攻击者是怎么想的防御者又该如何应对。今天我们就以CTF.show平台上一个非常经典的入门级题目“Web4”为例手把手带你走一遍完整的漏洞发现、分析和利用流程。这道题之所以经典是因为它巧妙地融合了信息泄露、逻辑漏洞和权限绕过这几个Web安全中最常见、也最容易被忽视的“组合拳”。很多新手一上来就想着用自动化工具扫漏洞往往会在这种需要动脑分析的题目面前卡壳。通过复现这道题你不仅能学会几个具体的技巧更重要的是能建立起一种“黑客思维”——如何像解谜一样从有限的线索中拼凑出完整的攻击路径。无论你是计算机专业的学生还是对网络安全感兴趣的开发者甚至是运维人员想了解自己的应用可能面临哪些风险跟着这篇教程走一遍你都能获得实实在在的收获。我们不会涉及任何高深的0day漏洞所有用到的工具和方法都是开源且易于上手的。我们的目标很明确看懂题目找到线索利用漏洞拿到最终的“Flag”旗帜。准备好了吗让我们开始这次Web安全实战之旅。2. 题目环境准备与初步侦察2.1 题目环境搭建与访问CTF.show的Web4题目通常提供了一个在线的靶场环境。为了方便大家理解和复现我强烈建议你在本地搭建一个模拟环境。这不仅能让你反复练习还能深入查看后端代码理解漏洞产生的根源。一种简单的方法是使用Docker。假设题目是一个PHP应用我们可以创建一个简单的目录结构web4-challenge/ ├── index.php ├── login.php └── flag.php其中index.php是入口login.php是登录逻辑而flag.php则是存放最终Flag的关键文件通常设置了访问限制。你可以使用PHP内置的Web服务器快速启动cd web4-challenge php -S 127.0.0.1:8080现在在浏览器中访问http://127.0.0.1:8080你就看到了题目的初始界面。在真实的CTF比赛中你会直接获得一个类似http://challenge-ip:port/的链接。注意在本地复现时请务必确保你的实验环境与外部网络隔离避免因配置不当导致安全风险。永远不要在公网服务器上随意部署存在漏洞的代码。2.2 信息收集一切攻击的起点面对一个陌生的Web应用第一步绝不是盲目的扫描而是细致的观察和信息收集。这就像侦探勘查现场任何细微的线索都可能成为突破口。1. 页面源代码审计F12大法这是最基本也最有效的一步。右键点击页面选择“查看页面源代码”。你需要关注HTML注释开发者有时会留下调试信息或未删除的注释里面可能包含路径、参数甚至密码提示。JavaScript代码前端逻辑可能隐藏着API接口、加密方式或逻辑缺陷。特别是那些处理表单提交、参数校验的JS函数。隐藏的表单字段hidden input例如input typehidden nameis_admin value0这暗示后端可能会检查这个值来判断用户权限。引用的外部资源JS文件、CSS文件的路径可能透露目录结构比如/admin/static/js/xxx.js暗示存在/admin目录。2. 网络请求分析打开浏览器的开发者工具F12切换到“网络”Network选项卡然后刷新页面或进行一些操作如点击登录。观察请求与响应头关注Cookie、Set-Cookie、Location重定向、Server服务器类型如Apache/2.4.41等信息。一道经典的CTF技巧就是修改Cookie中的某个字段如admin0改为admin1来尝试权限提升。请求参数GET请求的参数在URL中POST请求的参数在请求体里。注意参数的名字和格式。3. 目录与文件扫描虽然手动猜测很重要但使用工具可以提高效率。这里推荐dirsearch或gobuster。以dirsearch为例python3 dirsearch.py -u http://127.0.0.1:8080 -e php,html,js,txt,bak,swp,zip这个命令会尝试枚举网站可能存在但未链接出来的目录和文件。你需要特别关注备份文件如index.php.bak、login.php.swpvim交换文件这些文件可能直接暴露源代码。配置文件如config.php、.env、robots.txt。测试或管理页面如/test/、/admin/、/phpinfo.php。4. 技术栈指纹识别了解对方用什么技术写的就能推测它可能有什么样的漏洞。通过观察Cookie格式PHPSESSID提示是PHP。HTTP响应头X-Powered-By: PHP/7.4.3。页面特征特定的错误页面、URL路由模式如.asp、.jsp。使用whatweb或Wappalyzer浏览器插件可以自动化完成这部分工作。在Web4这道题中我们的侦察阶段可能会发现一些不寻常的线索。例如在页面源代码的注释里发现一段看似乱码的字符串或者一个指向/secret_path_12345/的隐藏链接。记住在CTF中任何“不自然”的东西都值得深究。3. 核心漏洞原理深度解析通过初步侦察我们假设在Web4题目中发现了一个登录页面 (login.php)并且通过目录扫描找到了一个疑似备份文件的login.php.bak。下载这个备份文件我们得以一窥后端逻辑。这是CTF中常见的“源代码泄露”漏洞场景。让我们来深入分析这段代码可能存在的安全问题。3.1 漏洞一弱类型比较与哈希绕过我们来看一段模拟的、在CTF中非常常见的PHP登录验证代码// login.php 部分代码 $username $_POST[username]; $password $_POST[password]; $stored_user admin; $stored_hash 0e123456789012345678901234567890; // 例如 md5(240610708) 的结果 if ($username $stored_user) { if (md5($password) $stored_hash) { $_SESSION[is_admin] true; header(Location: /flag.php); exit; } else { $error 密码错误; } } else { $error 用户名不存在; }漏洞原理 关键在于第6行的松散比较。在PHP中在进行比较前会尝试进行类型转换。$stored_hash的值是‘0e123456...’这是一个以0e开头的字符串。在PHP的科学记数法中0e123456被视为0 * 10^123456其结果就是整数0。MD5哈希函数有可能产生这种以‘0e’开头的哈希值。例如md5(‘240610708’)的结果就是‘0e462097431906509019562988736854’。如果用户输入的密码经过MD5后也得到一个以‘0e’开头的哈希值比如md5(‘QNKCDZO’)的结果是‘0e830400451993494058024219903391’那么在进行比较时‘0e462097431906509019562988736854’ ‘0e830400451993494058024219903391’PHP会将两者都转换为科学记数法都等于0于是0 0成立验证通过为什么是漏洞攻击者无需知道真正的密码‘240610708’只需要找到任意一个MD5哈希以‘0e’开头的字符串如‘QNKCDZO’、‘s878926199a’作为密码输入即可通过验证。这就是“哈希碰撞”在弱类型比较下的实际利用。实操心得遇到PHP的登录验证第一反应就是检查比较符是还是。严格比较会同时检查值和类型可以防御此类攻击。在代码审计时对所有用户输入参与的比较操作保持警惕。3.2 漏洞二不安全的反序列化假设我们在flag.php中看到了这样的代码// flag.php 部分代码 session_start(); if (!isset($_SESSION[is_admin]) || $_SESSION[is_admin] ! true) { die(Access Denied!); } // 从Cookie中获取用户数据 if (isset($_COOKIE[user_data])) { $user_data unserialize(base64_decode($_COOKIE[user_data])); // 可能有一些基于 $user_data 的逻辑... } echo “恭喜你Flag是” . file_get_contents(‘/flag.txt’);漏洞原理unserialize()函数是PHP中一个极度危险的功能它可以将序列化的字符串还原为PHP的值或对象。如果攻击者能够控制传入unserialize()的字符串他就可以构造特殊的“序列化载荷”POP Chain在反序列化过程中触发对象中的__wakeup()、__destruct()或__toString()等魔术方法从而执行任意代码、读取任意文件甚至获取系统权限。在上面的代码中user_data从客户端的Cookie中获取并经过base64_decode后直接传给unserialize()。这意味着攻击者可以完全控制输入。例如他可以构造一个序列化字符串指向一个包含恶意代码的类对象。一个简单的例子 假设网站定义了一个FileHandler类class FileHandler { public $filename; public function __destruct() { echo file_get_contents($this-filename); } }攻击者可以序列化一个该类的对象并将其filename属性设置为/etc/passwd或/flag.txt$obj new FileHandler(); $obj-filename ‘/flag.txt’; $payload base64_encode(serialize($obj)); // 得到payload然后将这个payload作为user_dataCookie的值发送。当flag.php反序列化它时会创建FileHandler对象页面执行完毕后该对象销毁触发__destruct()方法从而读取并输出/flag.txt的内容。注意事项反序列化漏洞的利用通常需要知道网站代码中定义了哪些类即“攻击面”。这可以通过前面的源代码泄露如.bak文件获得或者通过报错信息、自动加载机制等推测。在实际CTF中题目往往会提供部分源代码。3.3 漏洞三路径遍历与文件包含这是另一个常见漏洞。假设flag.php中有一段这样的代码$page $_GET[‘page’] ?? ‘home.php’; include(‘./templates/’ . $page);漏洞原理include、require等函数用于包含并运行指定文件。如果用户输入$_GET[‘page’]未经任何过滤就直接拼接进文件路径攻击者就可以使用目录遍历符号../跳出限制目录读取系统上的任意文件。例如读取系统密码文件?page../../../../etc/passwd读取网站源码?page./index.php(利用PHP的include会执行PHP代码但如果配合php://filter伪协议可以读取源码)更危险的是如果攻击者能上传一个文件如图片并在其中嵌入PHP代码他就可以通过文件包含来包含自己上传的文件从而执行任意代码即“文件包含文件上传”组合拳。利用php://filter读取源码 有时直接包含.php文件其中的PHP代码会被执行我们看不到源代码。这时可以利用php://filter伪协议进行编码转换将源码以文本形式输出。?pagephp://filter/convert.base64-encode/resourceflag.php这会将flag.php的源码进行base64编码后包含进来我们在页面上看到的就是一串base64字符串解码后即可获得源代码。在Web4题目中这些漏洞可能不是孤立存在的。往往是信息泄露拿到源码让你发现了反序列化点而反序列化漏洞的利用链POP Chain中可能又需要触发文件包含来读取最终的Flag。我们需要像拼图一样把这些点串联起来。4. 手把手漏洞利用实战流程假设通过前面的侦察和代码分析我们确定了Web4题目的攻击路径首先通过弱类型哈希碰撞绕过登录然后利用Cookie中的反序列化点构造POP链最终实现任意文件读取拿到Flag。下面我们一步步来操作。4.1 第一步绕过登录验证访问登录页面打开题目链接通常是一个login.php页面。分析请求尝试用任意用户名密码如 admin/admin登录用Burp Suite或浏览器开发者工具截获这个POST请求。观察请求体通常是usernameadminpasswordadmin。实施哈希绕过我们已知或从泄露的源码中推测后端使用md5($password) $stored_hash进行验证且$stored_hash是一个0e开头的字符串。我们需要找到一个字符串其MD5值也是0e开头。已知的魔法字符串包括240610708、QNKCDZO、s878926199a、s155964671a。我们将密码参数改为其中一个例如usernameadminpasswordQNKCDZO。发送请求修改请求并发送。如果绕过成功服务器通常会设置一个登录成功的会话Cookie如PHPSESSID并跳转到下一个页面如flag.php。此时浏览器会携带这个新的Cookie。实操心得如果不知道具体的魔法字符串可以写一个简单的Python脚本进行爆破。原理是生成大量字符串计算其MD5检查是否以‘0e’开头且后续字符全是数字。这在CTF中也是常见考点。4.2 第二步分析会话与寻找反序列化点登录成功后的观察登录后你被重定向到flag.php但页面显示“Access Denied”或“您不是管理员”。这说明通过了第一道登录验证但还有第二道权限检查。检查Cookie查看浏览器Cookie或Burp Suite的响应除了PHPSESSID很可能还有一个额外的Cookie比如user_data。它的值看起来像是一长串base64编码的字符串通常包含结尾。解码分析将这个user_data的值进行base64解码。你可以使用浏览器的开发者工具控制台atob(‘你的cookie值’)。解码后你可能会看到类似O:8:”UserInfo”:2:{s:4:”user”;s:5:”admin”;s:4:”role”;s:5:”guest”;}的字符串。这是一个PHP序列化对象的表示。O:8:”UserInfo”表示这是一个8个字符类名UserInfo的对象。2表示有2个属性。{s:4:”user”;s:5:”admin”;s:4:”role”;s:5:”guest”;}描述了属性名和值。推断后端逻辑这个序列化数据被传回服务器服务器会调用unserialize()来还原对象。权限判断很可能基于这个对象中的某个属性如role。当前role是guest所以被拒绝。我们的目标就是修改这个序列化数据将role改为admin。4.3 第三步构造反序列化Payload修改序列化字符串将解码后的字符串中的s:5:”guest”修改为s:5:”admin”。注意前面的s:5表示字符串长度为5“admin”正好是5个字符所以长度不用改。如果要改成“superadmin”11个字符则需要同步修改为s:11:”superadmin”。重新编码将修改后的序列化字符串进行base64编码。在控制台使用btoa(‘修改后的字符串’)。替换Cookie并重放请求在Burp Suite的Repeater模块中将请求中的user_dataCookie值替换为你新生成的base64字符串。发送请求。结果预期如果后端只是简单比较role属性那么修改后应该就能通过验证看到Flag。更复杂的情况——POP链利用 如果题目没那么简单直接修改属性无效说明后端有更复杂的逻辑。这就需要我们利用反序列化触发魔术方法。这时我们必须有类的定义来自源代码泄露。假设我们从login.php.bak中找到了如下类定义class SecretFlagReader { private $filename ‘/dummy.txt’; public function __toString() { return file_get_contents($this-filename); } } class User { public $info; public function __destruct() { echo $this-info; // 这里会触发 __toString() } }攻击链POP Chain是unserialize() - 创建User对象 - 脚本结束/对象销毁 - 触发User::__destruct() - echo $this-info - 如果info是SecretFlagReader对象触发SecretFlagReader::__toString() - 读取$this-filename。我们的构造步骤实例化SecretFlagReader对象并将其filename属性设置为/flag.txt。实例化User对象将其info属性设置为上面这个SecretFlagReader对象。序列化User对象。将序列化字符串进行base64编码作为Cookie发送。PHP构造Payload的脚本示例class SecretFlagReader { private $filename ‘/flag.txt’; } class User { public $info; } $obj new User(); $obj-info new SecretFlagReader(); $payload serialize($obj); echo base64_encode($payload);运行这个脚本得到最终的Payload。4.4 第四步获取最终Flag将构造好的Payload替换到请求的Cookie中发送给flag.php。如果一切顺利页面的返回内容将不再是“Access Denied”而是执行了我们构造的POP链输出SecretFlagReader::__toString()的返回值即/flag.txt文件的内容也就是我们梦寐以求的Flag。通常Flag的格式是ctfshow{一串由字母数字和符号组成的字符串}。恭喜你成功通关5. 实战中常见问题与排查技巧即使跟着教程走在实际操作中你也可能会遇到各种问题。下面我整理了一些常见坑点和排查思路这都是我踩过坑后总结的经验。5.1 登录绕过失败问题使用了QNKCDZO等魔法密码但登录依然返回“密码错误”。排查确认比较方式回顾泄露的源码确认后端使用的是而不是。如果是此方法无效。确认哈希算法源码中用的是md5吗会不会是sha1或其他哈希函数你需要寻找对应哈希函数的“0e”碰撞字符串。例如sha1(‘10932435112’)的结果是0e07766915004133176347055865026375。确认密码处理密码是否在哈希前经过了拼接或编码例如md5($salt . $password)。你需要找到$salt的值可能也在源码中然后计算md5(salt ‘QNKCDZO’)看是否还是0e开头。使用工具爆破写一个Python脚本自动生成并测试。这是最可靠的方法。5.2 反序列化Payload不生效问题精心构造的Payload发送后服务器返回空白、500错误或没有任何变化。排查字符长度这是新手最常犯的错误在PHP序列化字符串中s:5:”admin”里的5必须和后面字符串“admin”的实际字节长度严格一致。如果你把“guest”改成“admin”长度没变是对的。但如果把“user”改成“admin”就必须把s:4改成s:5。一个字符的差错都会导致unserialize()失败。类名和属性可见性如果Payload涉及类必须确保服务器上已经定义了这些类且类名、命名空间完全一致。此外注意属性的可见性public、private、protected。private属性在序列化字符串中格式为%00类名%00属性名protected为%00*%00属性名。如果你在本地构造的Payload是public而服务器上是private就会出错。最好直接从服务器泄露的源码中复制类定义来构造。魔术方法唤醒问题__wakeup()方法会在unserialize()后立即调用。如果目标类中存在__wakeup()方法并且其中有重置属性或退出逻辑可能会中断我们的攻击链。有时可以通过修改序列化字符串中对象属性的数量来绕过__wakeup()CVE-2016-7124。例如序列化字符串中表示属性数量的数字大于实际数量可能使__wakeup()被跳过。编码问题确保你的Payload在传输过程中没有被URL编码或二次编码。在Burp Suite中直接替换原始Cookie值即可。5.3 文件包含读不到目标文件问题使用../或伪协议包含文件时返回警告或找不到文件。排查路径深度../的数量可能不够。你需要猜测Web根目录与目标文件如/etc/passwd之间的层级关系。常见的Linux Web根目录是/var/www/html那么到/etc/passwd需要../../../../etc/passwd。可以尝试不断增加../的数量。目录穿越过滤程序可能过滤了../。尝试双写绕过....//或使用URL编码..%2f或绝对路径/etc/passwd如果PHP配置open_basedir没限制。文件后缀拼接代码可能是include($page . ‘.php’)这会给你的输入自动加上.php后缀。这时你可以尝试使用%00空字节截断在PHP版本 5.3.4 且 magic_quotes_gpcoff 时有效或利用?号../../../../etc/passwd?这样拼接后变成../../../../etc/passwd?.php?之后的部分在文件系统中会被视为查询参数而忽略。伪协议使用php://filter伪协议非常强大。如果包含时报错检查协议格式是否正确。php://filter/readconvert.base64-encode/resourcefilename是标准写法。5.4 工具使用与思维误区误区一过度依赖自动化扫描器像AWVS、Nessus这样的重型扫描器在CTF中往往效果不佳甚至可能因为请求过多被屏蔽。CTF考察的是手动的、精细化的分析能力。信息收集、代码审计、逻辑推理才是关键。误区二忽视前端代码JavaScript文件、HTML注释、隐藏字段里常常藏着关键的提示或漏洞。一定要养成仔细查看前端源码的习惯。误区三不关注HTTP交互细节重定向302状态码、Cookie的设置与修改、响应头里的提示信息如X-Debug-Token都可能是解题线索。始终开着开发者工具的“网络”面板。技巧利用Burp Suite的Comparer和DecoderComparer可以对比两次请求或响应的差异对于分析代码修改后的变化非常有用。Decoder模块可以方便地进行base64、URL、十六进制等各种编码解码是处理Payload的利器。最后保持耐心和好奇心。Web安全就像解谜每一步的发现都可能引导你走向最终的Flag。这道Web4题目融合了多个知识点成功复现它你的Web安全实战能力就已经迈出了坚实的第一步。真正的安全之路还很长但每一个这样的小胜利都会积累成你的经验和直觉。