资讯详情

CTF新生赛Web方向复盘:PHP弱类型、SQL注入与反序列化实战

📅 2026/10/20 5:09:45 | 华诺云谱 👁 阅读
CTF新生赛Web方向复盘:PHP弱类型、SQL注入与反序列化实战
写这篇复盘之前我刚从新生赛的讨论区里刷到一整排“卡死在反序列化”的哀嚎。Web方向每年都是参赛人数最多的赛道但新生赛的Web题往往不会出得特别偏而是把PHP、SQL注入、反序列化这三块基本功揉进几道题里考的是你底子扎不扎实。这篇文章就把我从一次新生赛里拆出来的考点脉络、每类题目的真实解题过程以及中途踩进去又爬出来的坑完整梳理一遍。无论是准备下次比赛还是想在Web安全上补基础这三千多字的复盘应该能帮你少走不少弯路。先说结论新生赛Web题并不追求刁钻但非常讲究“你能不能在一个真实题目里认出考点”。同一个知识点脱离刷题列表放到环境里很多人就认不出来了。这篇文章的核心就是带你把“考点识别”和“实操绕过”两件事打通。1. 新生赛Web方向的考点全览为什么是PHP、SQL、反序列化1.1 选题思路三类题对应三种基本功CTF新生赛的Web题通常不会碰复杂中间件漏洞也不会考太新的框架0day而是选那些“学过就会、没学就不会”的基础点。PHP弱类型与代码审计、SQL注入、反序列化漏洞正好覆盖了Web安全里最常见的三类问题代码逻辑层漏洞、数据访问层漏洞、对象持久化层漏洞。PHP弱类型题考的是语言特性特别是与的区别、松散比较下的类型转换、MD5绕过这类经典套路。SQL注入考的是你对数据库语法和Web输入点拼接逻辑的理解从联合注入到报错注入再到盲注难度可以直线拉升。反序列化则考的是对象属性控制与魔术方法利用链构造理解成本比前两者高也是很多新手最头疼的一类。新生赛把这三个点串起来其实是在暗示一条学习路径先懂代码再懂数据最后懂对象。三者难度递进但底层都是同一个思维——攻击者能不能控制程序中的某个关键变量并让它流向危险函数。1.2 从签到到进阶难度梯度设计今年新生赛Web方向的题目分布大概分三档。签到题是一道PHP弱类型比较只需要构造两个MD5值相等等价条件几分钟就能过。第二档是一道SQL注入过滤了一些关键字但没过滤注释符和联合查询属于“稍微动手就能注”的程度。第三档是反序列化给了目标类的源码要求构造POP链执行命令。这种梯度设计值得点赞。它不会让你第一题就劝退但也绝不会让你只靠扫描器自动过。每道题都逼着你手动分析尤其是反序列化那道自动化工具基本帮不上忙。从学习角度看这种“不依赖工具依赖脑子”的题目才是最有价值的。1.3 环境与工具准备参赛前建议把环境调顺手。我个人习惯准备这样一套组合一个自带Python环境的虚拟机写脚本用Burp Suite抓包改包本地PHP环境用来复现和调试反序列化载荷再加上一个常用的SQL注入自动化工具备用但重点还是手工测试。调试反序列化时本地跑一遍序列化与反序列化比直接对着远程疯狂盲试快得多。还有一点容易被忽略比赛平台的题目容器有时会有限时和环境差异比如PHP版本不同导致__wakeup绕过失效这一点在反序列化题里尤其致命。所以看到题目先别急着打先判断目标环境的PHP版本区间再决定用哪种绕过手段。2. PHP考点精讲弱类型比较与文件包含2.1 MD5弱类型比较一张非常经典的“签到牌”比赛里这道PHP题大概长这样if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b])) { include flag.php; }考的是MD5值以0e开头时的松散比较特性。PHP里如果一个字符串形如0e123在与整数比较时会被当作科学计数法即0 * 10^123 0。于是只要两个MD5值都是0e开头两者相等就成立。实际做题时可以直接用两个经典值a240610708 bQNKCDZO这两串的MD5分别是0e462097431906509019562988736854和0e830400451993494058024219903391都是0e开头的“魔法串”用比较结果就是相等。在比赛环境里我直接放包就出了flag。这个点本身不难但它背后牵扯的PHP松散比较机制值得彻底搞懂。很多人只知道背魔法串一旦题目改成md5($a) md5($b)且参数只能传数组就会因为“数组与字符串比较永远为false”而卡住。实际上PHP 5.x和7.x对数组与字符串比较的行为并不一致7.x里数组和字符串用比较时直接返回false所以数组绕过只适用于某些特殊场景不能无脑套。2.2 伪协议读文件filter处理器与data流的实战另一道PHP题给的是文件包含点$file $_GET[file]; include($file);目标是读取flag.php的内容。新手最容易踩的坑是直接爆破路径或尝试目录穿越但真正通用解法是PHP伪协议。读取源码最稳的方式是?filephp://filter/convert.base64-encode/resourceflag.php这条payload把目标文件内容经过base64编码后输出避免flag.php里的PHP代码被执行导致看不到源码。拿到base64字符串再解码就能直接看到完整的PHP代码和隐藏的flag。如果不只满足于读文件还想直接执行命令可以用data://协议。一个常见的payload?filedata://text/plain,?php system(whoami);?注意PHP 5.2之后data://默认不可用需要allow_url_include开启。新生赛为了放水一般会开。这个考点的重要性在于文件包含点加上可控输入几乎等于直接给攻击者开了一扇门。做题时养成先试伪协议、再试路径穿越的习惯能省下大量时间。2.3 踩坑记录版本差异、强比较与数组绕过PHP这一块真正的坑不在题目本身而在环境差异。我这次比赛就遇到一个情况本机PHP 7.4构造的payload放到线上变成了PHP 5.6表现截然不同。第一个差异是弱类型比较的精度与模式。PHP 8.0彻底改变了字符串与数字比较的逻辑很多0e魔法串直接失效。如果比赛环境是PHP 8这类题目就不会出现或者需要换成MD5强比较的解法。第二个差异是强比较下的绕过只能靠数组或MD5碰撞。数组绕过在PHP 7.x里依然有效因为两个数组用比较时如果数组元素不相同可以直接返回false不对需要仔细看md5($_GET[a]) md5($_GET[b])在传数组时因为md5()函数会报错并返回nullnull null为true所以这也是一个绕过方式。前提是PHP不会把Warning直接输出打断流程。我在本地调试时发现有些环境把错误显示开成display_errorsOn虽然不会中断但页面输出一堆Warning容易干扰做题判断。第三个坑是PHP伪协议中的编码问题。php://filter读文件时如果目标文件名含特殊字符或路径带中文建议先URL编码再提交。还有一次我试php://filter/resourceflag.php没加convert.base64-encode结果因为flag.php里有PHP标签执行后什么都没输出血泪教训。3. SQL考点精讲联合注入到报错盲注3.1 联合查询注入的标准姿势SQL注入题通常是一个登录查询或者文章详情页的参数拼接$id $_GET[id]; $sql SELECT * FROM articles WHERE id $id;判断注入点的第一步永远是加单引号看报错或者传一个1 and 11与1 and 12对比页面差异。但新生赛题目里如果开了MySQL报错回显可以直接用联合查询快速出表。标准联合注入流程-1 UNION SELECT 1,2,3--这里重点在-1目的是让前面的查询结果为空从而让Union查询的数据显示在页面里。字段数量靠ORDER BY或者逐步增加NULL来探测。比如-1 ORDER BY 3--如果报错就降低数字如果不报错就说明当前查询至少有3列。字段数确定后把要查的内容替换到对应显示位。拿表名和字段名-1 UNION SELECT 1,database(),3-- -1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()--这套打法很机械但非常可靠。关键是理解每一步在干什么database()拿库名information_schema是MySQL自带的元数据库所有表名、字段名都存在里面。3.2 updatexml报错注入的几个坑这次的新生赛SQL题过滤了information_schema直接堵死联合注入里最常用的爆表路径。但MySQL还有一个通用报错注入手法利用updatexml函数在XML解析出错时把报错信息回显到页面。核心payload-1 AND updatexml(1,concat(0x7e,database(),0x7e),1)--0x7e是波浪号~放在中间主要是为了让报错信息足够“显眼”。如果直接查database()而不加符号某些情况下报错内容会被截断或看不清楚。updatexml最多显示32个字符所以字段值长了就要用substr分多次取。我第一次打这道题时犯了低级错误把0x7e写成了0x7d大括号回显就没有预期的分隔符半天找不到flag在哪。后来规规矩矩用~包裹一眼就看清了报错内容。报错注入虽然高效但我建议在比赛中不要过度依赖因为很多题目开始专门针对extractvalue和updatexml做过滤。替代方案是floor(rand(0)*2)报错或者join爆错不过相对冷门。3.3 手写盲注脚本与自动化工具翻车的场景如果页面既不回显查询结果也不回显报错信息只能靠页面是否正常显示来判断条件真假那就得上盲注。这次比赛有一道SQL题就是这种场景过滤了and、or、空格但没过滤substr和ascii。我手写的布尔盲注脚本核心逻辑如下import requests url http://target/query.php characters abcdefghijklmnopqrstuvwxyz0123456789{}_- def inject(payload): r requests.get(url, params{id: payload}) return exists in r.text flag for i in range(1, 50): for c in characters: payload f-1 OR substring(database(),{i},1){c}-- if inject(payload): flag c print(flag) break空格被过滤时可以用/**/代替空格或者用%0a换行绕过。注释符--在Burp里要记得把原文提交在Python的requests库里直接把留在payload里即可但如果你用URL编码提交可能被解码成空格反而导致注释失效。我还犯过一个经典错误盲注脚本没加延迟结果并发请求太快把比赛靶机打崩了页面直接500我一度以为是自己注错了。后来在脚本里加上time.sleep(0.05)才恢复正常。这也是一个实战经验盲注也要注意请求频率别把自己打成DDoS攻击者。自动化工具方面sqlmap在这种题目里确实能一把梭但我建议至少前两道SQL题手工打一遍。考试能够过关不等于理解原理。工具只会告诉你“存在注入”不会告诉你为什么-1 UNION SELECT 1,2,3能出数据。面试时问到SQL注入如果只能说“用sqlmap一把梭”基本就凉了。4. 反序列化考点精讲魔术方法与POP链4.1 序列化格式里的三个细节反序列化题拿到源码后第一件事是把序列化字符串的格式彻底看明白。给大家一个最小示例class A { public $cmd; function __destruct() { system($this-cmd); } }对new A()执行serialize()得到O:1:A:1:{s:3:cmd;s:6:whoami;}这里面有三个细节决定成败。第一O:1:A里对象类名长度必须准确多了少了都会报错。第二属性用s:3:cmd表示字符串长度是3不是属性名本身的字符数再加引号而是“cmd”三个字符。第三可见性不同时序列化格式完全不同。public属性直接写属性名protected属性会变成\0*\0属性名也就是%00*%00属性名private属性会变成\0类名\0属性名。这里的\0是ASCII码0不是反斜杠和0两个字符。手动构造payload时最容易出错的就是这里因为字符串长度计算要包含\0。比如一个有private $cmd的类属性名在序列化里是\0A\0cmd一共6个字符注意是\0一个字节 A一个字节 \0一个字节 cmd三个字节。很多人在这里少算一个字节导致整个序列化作废。4.2 __wakeup绕过与属性计数伪装如果目标类里有__wakeup()方法它会在反序列化时自动执行。很多题目在__wakeup里写了过滤逻辑比如清空$cmd或者禁止命令执行。这时候就需要绕过。经典的__wakeup绕过是CVE-2016-7124当序列化字符串中表示对象属性个数的值大于真实属性个数时PHP在5.6.25以下、7.0.10以下版本会跳过__wakeup()。例如O:1:A:2:{s:3:cmd;s:6:whoami;}本来A类只有1个属性但序列化字符串里写成了2个旧版本PHP反序列化时就会跳过__wakeup直接进入__destruct。这个绕过有两个坑。第一版本限制非常严格。现在比赛环境PHP 7.4居多这个绕过直接失效。替代思路是属性个数正常但想办法让__wakeup里的逻辑不生效比如利用引用让变量被覆盖——这个技巧新手理解起来比较绕。第二如果环境是PHP 8.0以上序列化格式本身也有变化字符串长度计算更严格构造时需要反复本地测试。4.3 从开箱构造到POP链延伸这次新生赛反序列化题没让直接构造单个对象而是给了多个类属性之间存在调用关系需要构造一条调用链也就是POP链。比如有这样一个场景class A { public $b; function __destruct() { $this-b-run(); } } class B { public $cmd; function run() { system($this-cmd); } }一个简单的POP链就是让A对象的b属性指向B对象且B对象的cmd属性为要执行的命令。序列化时先构造B再构造A并把B赋给A的b属性最后嵌套序列化。手动串payload时我的习惯是先在本地环境用PHP代码生成序列化字符串而不是手写。因为嵌套结构容易出错本地生成再粘贴到请求里省去大量调试时间。如果题目不允许本地复现比如远端环境PHP版本过高可以先把目标类结构抄到本地保持类名和属性完全一致用serialize()生成后再手动修属性个数做__wakeup绕过。POP链的核心不是背特定题目而是建立“属性可控、函数可达”的思维。从__destruct、__toString、__call等魔术方法出发看看它们的调用链里有没有sink点如system、eval、file_put_contents然后反向推导需要控制哪些属性。这个思维通了后面遇到真实框架的反序列化漏洞也能快速定位利用链。5. 竞赛复盘与自学避坑总结5.1 从题目看漏洞原理的映射打完比赛回头复盘我发现每道题表面上是“一个考点”但背后都对应着一整条Web安全链路。PHP弱类型题对应的是输入校验不严导致的逻辑绕过。线上真实系统里这类问题很多比如校验用户角色时用in_array但没开严格模式攻击者传一个字符串PHP会自动转成数字就可能绕过权限判断。SQL注入题对应的是拼接SQL语句对应实际开发中“字符串拼接查询”的坏习惯。参数化查询能解决99%的问题但很多老项目、导出报表模块、搜索功能里依然能看到拼接痕迹。反序列化题对应的是不可信数据被反序列化。真实系统里unserialize($_COOKIE[user])这种代码虽然看着智障但确实存在。许多框架的缓存、session、消息队列都会处理序列化数据一旦输入可控就可能变成RCE。用“考点”去对照“真实漏洞类型”学习才不白费。比赛不是目的比赛题目是理解漏洞本质的标本。5.2 搭建本地练习环境的建议如果你是第一次接触CTF建议先搭一套本地复现环境。PHP题用phpStudy或者Docker装一个php:7.4-apache容器SQL题用MySQL 5.7或者MariaDB反序列化题可以在命令行下用php -a交互式调试。我常用的本地调试流程是把题目源码下到本地用php -S 127.0.0.1:8080起一个简易服务。用Burp Suite把线上的请求原样发到本地对比差异。反序列化用时直接在本地用var_dump(serialize($obj))生成payload。每一步都打日志观察__wakeup和__destruct的执行顺序。一个可以大幅提效的小技巧在本地源码里临时加error_reporting(E_ALL); ini_set(display_errors, 1);把PHP错误全部显示出来。很多反序列化失败原因在远程页面不会显示但本地一眼就能看到“Class not found”或者“Cannot access private property”之类的报错。5.3 后续提升方向如果新生赛这三类题都能独立做出来下一步可以尝试三个方面。第一反序列化拓宽到真实框架。比如PHP的Laravel、ThinkPHP反序列化链Java的Fastjson、Shiro反序列化这些利用链比CTF题复杂得多但原理完全一致只是类更多、调用链更长。第二SQL注入从MySQL扩展到其他数据库。PostgreSQL、SQL Server、Oracle的注入语法和注释符都不同报错函数也不一样。能灵活跨库才算真正掌握了注入的本质。第三把Web安全的基础再打深一层。PHP弱类型之后可以学代码审计SQL注入之后可以学数据库安全配置反序列化之后可以学内存马和Webshell检测。这些方向都能从“CTF选手”过渡到“安全工程师”。最后分享一个我自己的习惯每次比赛结束不管成绩如何都会把做过的三道题写成复盘笔记每题一句话总结考点、两行payload、三个坑。不写长篇大论只记关键。半年下来这个笔记就成了我复习的目录比任何视频课都实用。比赛会结束但思考和记录不会。希望你也能从一场新生赛里挖到比flag更值钱的东西。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑