资讯动态

BuyFlag实战解析:业务逻辑漏洞与PHP弱类型绕过

发布时间:2026/9/10 6:10:08 来源:尧图企业网站定制
这个标题我印象太深了。2019年极客大挑战的Web方向我在BuyFlag这道题上卡的时间比同场好几道题加起来都长。页面看起来简单得不像话只有一个Buy按钮、一个价格、一行“余额不足”可恰恰是这种表面干净的题目把Web后端最容易被人忽略的一类漏洞——业务逻辑漏洞考得明明白白。后来我复盘时才真正意识到这道题的低分和它的教学价值完全不成正比。这篇文章不贴flag也不做那种“照着敲就能出答案”的复现式WP。我会从信息收集开始把源码审计、漏洞定位、利用链构造的完整判断过程摊开来讲然后把这类题背后通用的Web逻辑漏洞排查方法一并整理出来。无论你是刚接触CTF的web新手还是想系统补一下业务逻辑漏洞思维的选手这篇应该都能给你一些可落地的思路。1. 初见BuyFlag页面场景与信息收集1.1 页面形态和交互逻辑打开题目主页面是一个很常见的导航站风格菜单里能明显看到一个“Buy Flag”入口。点进去页面提示大致是“你需要花很多很多钱来购买flag但你的账户余额不足”。页面没有多余的信息没有注册口也没有登录框只有一个醒目的购买按钮。这种“简约到极致”的页面其实是一种信号。出题人把所有漏洞都藏在了后端逻辑里而前端几乎不给你任何可以操作的空间。此时最忌讳的事情就是对着购买按钮反复乱点或者凭感觉去爆破请求参数。正确做法是先冷静下来把页面上所有能被你看到、读到的信息全部收集完再考虑下一步。我习惯打开浏览器的开发者工具从Sources面板把页面引用的JS、CSS、图片全部过一遍然后在Network面板里把页面加载过程中的所有请求都扫一眼。别小看这一步很多题目会在HTML注释、隐藏的meta标签、被注释掉的JS代码里留下关键线索甚至直接把源码文件的路径写在里面。1.2 源码级线索注释、隐藏字段与敏感文件打开BuyFlag页面的源代码除了常规的HTML结构外我在注释里看到了一些疑似提示性的信息大意是“你需要以管理员身份购买”或者“只有特定用户被允许购买”。这种注释看着不起眼但往往直接决定了你后面的破解方向——它暗示这道题有一个“身份”的概念而当前你可能是以一个默认的访客或普通用户身份在访问。除此之外我习惯顺手探测几个常见路径比如robots.txt、index.php.bak、buy.php.bak、www.zip、.git目录等。CTF题目中很多源码泄露不是出题人故意要让你轻松而是为了模拟真实世界中开发者把备份文件遗留在服务器上的场景。尤其对于2019年的老题环境里的源码泄露概率很高值得逐一尝试。我第一次探测时并没有直接命中后来换了Burp Suite抓包看响应才在一处不影响页面展示的接口返回中发现了异常。这不是什么玄学就是提示你信息收集阶段浏览器里看到的东西永远只是冰山一角真正有价值的线索往往藏在响应头、接口返回、注释和备份文件中。1.3 顺手完成的HTTP基础探测在拿到更多线索之前我还做了一组基础探测确认了题目环境只支持HTTP检查了同源目录是否能列出文件用不同的请求方法GET、POST、OPTIONS访问了BuyFlag页面观察响应差异同时留意Cookie中的字段因为不少题目会把用户身份放在Cookie里比如userguest或者roleuser之类的键值对。这一套动作下来我对题目的整体结构已经有一个粗略判断这是一个典型的“商城购买flag”场景后端大概率只根据某些可被客户端控制的参数来决定“你是否能买、买不买得起”而不会在服务端做严格的状态管理。这正是业务逻辑漏洞最好的温床。2. 源码审计破局这题的漏洞核心藏在哪里2.1 把功能点翻译成后端逻辑分支拿到足够多的页面信息后我开始把“购买flag”这个功能翻译成后端可能存在的代码逻辑。任意一个购买功能核心无非几步判断当前用户是谁身份认证判断用户是否有购买权限授权校验判断用户余额是否足够金额校验执行扣款并返回flag翻译成伪代码大致是if (当前用户是admin) { if (提交的金额满足某个条件) { echo $flag; } else { echo 余额不足; } } else { echo 你不是管理员不能购买; }这个翻译过程很重要。它不要求你看到真实源码就能立刻定位漏洞但你得先在心里形成一个“出题人会在哪里设卡”的预判。比如身份判断这里就可能有弱比较、参数覆盖、Cookie伪造的考点金额判断这里就可能有is_numeric绕过、intval截断、负数金额、科学计数法这类考点。顺着这个思路我重新检查了之前找到的源码/备份文件把里面涉及条件判断的部分单独拎出来看果然发现了核心问题后端在做判定时使用了宽松的变量比较并且把多个本应由服务端控制的属性全部交给了请求参数决定。2.2 核心漏洞点参数与判断条件的错位这道题里最典型的漏洞是后端在处理购买请求时把“身份标识”和“金额”这类关键数据直接取自客户端提交的内容并且没有做严格校验。这里的“错位”体现在两个层面。第一层身份信息应该是会话级别的数据要么存在服务端Session里要么至少经过签名防止篡改但题目直接信任了客户端传上来的字段。我用Burp Suite拦截购买请求后能看到请求中带着一个形如userguest的参数把它改成useradmin后页面提示从“不是管理员”变成了“余额不足”。这个变化很关键。它证明我的判断方向正确也意味着距离flag只剩最后一道校验了。第二层金额校验存在典型的PHP弱类型比较问题。很多用PHP写的后端逻辑会这样判断if ($_POST[money] 999999) { // 允许购买 }猛一看没什么问题但如果money这个参数可以被客户端控制而服务端又没有使用严格类型比较问题就大了。PHP的比较运算符在遇到数字字符串时会把字符串转换成数字可如果传入的字符串含有非数字字符转换规则就会变得非常灵活。更常见的玩法是直接传一个数组或者传科学计数法形式的字符串让后面的is_numeric、intval、等函数出现预期之外的行为。2.3 弱类型比较PHP的宽松判断是怎么被绕过的既然聊到这里我索性把PHP里几种常见的弱类型绕过方式展开说清楚因为这道题考的本质上就是这套东西。先看最基础的和的区别。是比较值如果两边类型不同PHP会尝试把其中一个转换成另一个的类型再比较则要求类型和值都完全一致才返回真。一个经典场景$password md5($input); if ($password 0e123456) { // 通过校验 }如果$input是某个字符串其MD5值恰好以0e开头后面全是数字而代码里又碰巧拿它和一个以0e开头的字符串做比较PHP就会把两边都当作科学计数法数字结果是0 * 10^某次方仍然等于0判断直接通过。这就是著名的MD5魔幻哈希绕过。再看strcmp的数组绕过。如果后端用了类似strcmp($flag_password, $_POST[password])这种判断当$_POST[password]是数组时某些PHP版本下strcmp会返回NULL或抛出异常而在老版本中NULL 0为真校验同样能被绕过。再看is_numeric。这个函数会把1e6、1.5、0x1A这类字符串都当作数字所以如果后端先is_numeric校验、再和某个数值比较大小我们完全可以传一个科学计数法字符串来绕过。而intval这类函数在转换时遇到非数字字符会直接截断也存在边界问题。这道题里实际用到的正是其中一两种方式的组合。所以千万别觉得弱类型绕过只是CTF里的花活在真实业务代码里只要用了PHP、只要参数来自请求、只要校验时用了宽松比较同样的洞就可能被真实攻击者利用。3. 一步步利用漏洞从“余额不足”到拿到flag3.1 第一步把身份从访客改成管理员确定了漏洞点之后利用过程就变得清晰了。我打开Burp Suite的Repeater把拦截到的购买请求完整地看了一遍。请求大概是这样的注意不同环境参数名可能有差异思路一致POST /buy.php HTTP/1.1 Host: target Cookie: userguest Content-Type: application/x-www-form-urlencoded money0我把Cookie中的userguest改成useradmin点击发送。响应从“您不是管理员”变成了“您的余额不足”。这说明身份校验已经通过后端把user字段的值直接当作了身份凭证没有任何服务端状态校验。这个步骤看似简单但它揭示了第一个核心教训永远不要相信客户端传来的身份标识。在真实系统中身份必须由服务端会话管理或者至少要经过签名、加密防止篡改。如果你在公司代码里看到哪条逻辑直接从请求中读取user、role之类的字段那基本可以判定这个系统存在越权风险。3.2 第二步绕过金额校验身份校验通过后剩下的问题就是怎么让后端认为我的余额足够。此时我再回头观察后端逻辑发现金额并不是一个简单的“账户余额”概念而是直接从我提交的参数中读取。于是我开始试不同的金额参数值第一种尝试是直接把money改成一个大于阈值的整数比如money1000000。理论上这应该能通过但响应仍然提示余额不足。这说明后端在比较前对金额做了额外的处理或者我的参数名不对。第二种尝试是看后端到底怎么处理金额。我耐心试了几个常见参数名比如price、amount、num同时也试着从源码泄露的文件里找了一下字段名线索最终确认了正确的参数名。当我能控制正确的金额参数后问题就变成了“如何让金额校验通过”。这里就是弱类型绕过的主场。我把money的值从普通数字改成数组形式money[]1后端如果用了is_numeric($money)这样的校验数组会在校验时直接返回非预期结果甚至让程序跳过校验。如果判断逻辑是if (is_numeric($money) $money 999999)那么数组传入时is_numeric会得到false整条判断短路不会继续执行金额比较除非逻辑本身写得有问题否则这种方式不一定通。所以大多数情况下我会优先试这类组合money1e6 money1000000abc money0x1e61e6在PHP里会被当作数字1000000能直接通过is_numeric和大小比较1000000abc这种字符串在某些比较场景下会被转换成1000000也能碰巧通过。具体哪个有效取决于后端用的函数是is_numeric、intval还是单纯比较。我在实测时是用money1e6直接通过了校验。响应回来了flag也出来了。3.3 第三步拿到flag后的验证与收尾拿到flag之后我没有立刻收工。因为这类比赛题目有个特点同一套环境里可能有多个相似的题目或者同一题在不同的访问路径下会出现不同的判断分支。我习惯性地把流程又走了一遍重置浏览器会话重新抓包依次修改身份和金额观察每一步的响应变化确认整个利用链是稳定可复现的。这种复现习惯在CTF里非常有用。很多选手报出flag后用同样的payload再打一次却不成功往往是因为环境开了随机token或者Session状态已经发生了变化。确认利用链稳定可复现才算真正“做完了”一道题而不是“碰巧出了一次flag”。4. 实战中的弯路与细节这类题真正卡人的地方4.1 为什么直接改价格没生效我一开始直接改money参数为1000000响应仍然是余额不足这个问题卡了我一段时间。后来排查发现原因是后端在判断时并不直接比较我提交的金额而是要先把金额和一个“商品单价”做乘法然后判断总价是否达到某个阈值。换句话说我以为自己在控制总价实际上我能控制的只是一个“购买数量”或者“单价”的输入后端还会做一层运算。这种情况在真实业务里太常见了。下单接口提交的price、count后端往往会用另一套逻辑重新计算总价而不是信任客户端传上来的金额。所以盲改价格往往是无效的正确思路是找到后端计算链条中那个“真正参与判断”的变量或者反向利用计算逻辑比如把数量改成负数总价变负反而让余额增加。遇到这类题一定要多看几次响应变化多试几个参数别在同一个参数上死磕。4.2 数值型参数的前后端双重校验另一种常见的坑是前端页面用JS做了校验导致你在浏览器里根本提交不了非法的数值。比如页面上写死了输入框只能填数字或者用parseInt把输入转成了数字再提交。这种情况下单纯改前端参数是没用的因为后端可能也有校验。绕过方式很简单直接绕过前端用Burp Suite或curl手工构造请求把最终发给服务器的数据改掉。你要记住前端的任何校验都只是用户体验层面的不是安全机制。服务器只认它收到的HTTP请求里的数据而HTTP请求是可以完全由客户端控制的。我在实测这道题时也遇到了前端校验把money1e6直接拦掉的情况。解决方式就是不经过页面直接在Burp里构造POST请求发送。遇到这类问题请一定要有一个执念前端管不到BurpBurp发出的请求才是后端真正看到的。4.3 请求重放与工具使用细节用Burp Suite做这类利用时有几个细节我后来养成了习惯第一Repeater里发送请求之前确认Cookie与目标环境一致。很多题目环境会为每个访客生成独立的会话ID如果你用错了会话前面的身份篡改可能就不生效。第二关注响应长度和状态码。页面提示“余额不足”和“成功”的响应长度差异往往很大用Burp的Compare功能快速对比比自己一个个肉眼看要高效得多。第三如果请求中带有token或者时间戳注意每次发送前更新。有些环境会校验token不更新的话请求会直接失败但这不是你的利用链有问题而是环境反重放机制导致的。curl -X POST http://target/buy.php \ -b useradmin \ -d money1e6用命令行工具做验证的最大好处是干净、可复现。一旦确认命令行能出flag就可以把整个利用过程写成脚本或记录方便复盘。4.4 关于PHP版本差异的提醒最后这点容易被忽略同一个绕过手法在不同PHP版本下的表现可能完全不同。比如前面提到的strcmp传入数组在PHP 5.x和部分PHP 7.x版本中会返回NULL导致NULL 0成立但在PHP 8.x中strcmp遇到数组会直接抛出TypeError整个请求会变成500。所以当你做题时发现某个payload理论上有理有据但就是不通先别急着怀疑思路去确认一下环境版本。很多老题的官方环境在2025年这个时间点早就升级过系统镜像了当年网上那批答案可能已经失效。我在实际测试这道题时就曾经在strcmp数组绕过上吃过亏后来检查环境才发现PHP版本和当年网上的WP环境不一样。换成其他绕过方式后问题才迎刃而解。这也提示我们看题解时要有批判性任何payload都要结合当前环境重新验证。5. 从BuyFlag往外走Web逻辑漏洞的通用排查法5.1 支付与交易类CTF题的常见套路BuyFlag本质上是一道交易支付类CTF题。从它出发能延伸出一整类题目常见考点包括金额篡改直接改价格、改单价、改数量目标是让后端算出来的总价变小甚至为负。负数数量把数量改成负数配合乘法运算导致总价为负最终反给账户加钱。精度边界利用浮点数精度问题让0.10.2这类计算出现误差绕过总额校验。逻辑覆盖同时提交多个隐藏优惠字段或者通过参数污染让某个字段被后提交的值覆盖。越权操作用A用户的身份调用B用户的订单接口或者篡改订单所属用户ID。条件竞争在下单和扣款之间通过并发请求制造时间差实现“用一份钱买多份东西”。BuyFlag这题还算收敛只在身份和金额两个点上做了文章。更进阶的题目会把上面这些点混在一起要求你完整构造一条攻击链。所以我在刷题的时候会把每道交易类题分别归到这些套路里总结出自己的一套检查顺序。5.2 一套可复用的Web逻辑漏洞排查清单这组清单是我后来做众测和代码审计时也一直在用的分享出来供你参考先把系统里所有可交互的接口列全重点标出涉及钱、权限、状态的接口。对每个接口确认哪些参数是服务端可信数据哪些是客户端可控数据。检查服务端是否对可控参数做了类型校验尤其注意PHP的宽松比较和弱类型函数。沿着请求参数从进入到退出的完整链路看看中间有没有经过数值计算、数据库查询、权限判断。重点关注“先校验后处理”还是“先处理再校验”以及校验和处理之间是否有时间窗口。对任何价格、数量、用户ID、订单号这类字段尝试改成边界值、负值、极大值、字符串、数组、科学计数法观察响应差异。最后别忘了看接口有没有做幂等控制。很多逻辑漏洞之所以能反复利用就是因为后端没有做防重放。这套清单不仅对CTF有用拿到真实的Web应用上去过一遍往往也能发现一些开发人员注意不到的隐患。反正我后来再看代码的时候会下意识检查有没有哪个字段是直接从$_POST、$_GET或$_COOKIE里读取然后直接参与权限或金额判断的。一旦看到这种代码我就会警觉起来。5.3 建议的练习思路如果你是想通过刷题巩固这套知识我的建议是不要只盯着BuyFlag这一道题把它和同系列的其他题目一起练。比如2019极客大挑战里那些被反复讨论的SQL注入、文件上传、包含类题目虽然方向不同但信息收集的思路、抓包分析的流程、源码审计的习惯是完全一致的。你今天在BuyFlag上积累的“先理清逻辑再动手”的习惯到了下一道题里面可能就是你比别人快一步的关键。另外补一句刷这类题时建议自己动手把Burp的Repeater、Intruder基本操作练熟同时会写最简单的Python脚本来构造请求。很多逻辑漏洞的利用不是一次请求就能完成的而是多个请求的组合。这时候手工点来点去效率太低脚本化的能力就变得很重要。我再分享一个小经验每次做完一道题我会把这道题涉及到的“关键判断代码”和“最终payload”对应着记录下来。这样过段时间再回头看每一道题都像一个“漏洞模式样本”。比如BuyFlag这道题记录的样本就是“身份字段由客户端控制 金额校验存在弱类型绕过”。下次在别的题目里看到类似的字段组合我就能快速意识到这里可能有同样的坑。这个习惯陪我从CTF一直走到实际项目效果一直很好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价