最近在 CTFShow 上刷到一道印象挺深的 Web 题题名叫“密码逻辑脆弱”。单看名字就能猜到这题考的不是脑洞而是密码校验逻辑本身存在的缺陷。整个 Writeup 的核心就三件事信息收集、目录扫描、字典攻击。这三步看着基础但组合起来却能从头到尾还原出一个登录型漏洞的完整利用链对新手了解 Web 题怎么做非常有用。这道题不需要具备什么高深的内核知识会看页面源码、会跑目录扫描工具、会写一点简单的爆破脚本就足够攻进去了。我更想说的是这类题在真实渗透测试里也频繁出现因为很多系统后台的登录逻辑本质上就是这段靶场代码的简化版。下面我按实际做题的步骤一步步拆开讲。1. 破题前的基础侦查与信息收集1.1 目标速览一个看似普通的登录框打开靶场页面第一眼只有一个用户名输入框、一个密码输入框和一个登录按钮除此之外没有任何注册入口、验证码、找回密码功能。整个界面干净得有点反常。按照我做 CTF Web 的习惯看到登录框脑子里就要过几条主线弱口令、SQL 注入、前端校验绕过、密码重置逻辑漏洞、备份文件泄露。这道题标题已经把方向指得很明确了“密码逻辑脆弱”大概率是在比较逻辑上做文章。所以我也不急着提交admin/admin去试而是先把页面源码完整看了一遍。CTF 题页面越简单后端往往越有故事而且很多线索就藏在你想不到的地方。1.2 HTML 注释与 robots.txt 里的两条线索第一个突破口出现在页面源码最底部有一行注释!-- 早期版本代码没有删干净备份文件别乱看手动狗头 --这种注释在真实站点里其实也不少见开发人员清理代码时漏掉一段结果把隐藏路径或者依赖信息全暴露了。结合题目描述里的信息收集要求我基本确定下一步要去找备份文件或者隐藏目录。紧接着我访问了/robots.txt。这是 Web 题信息收集阶段必查的路径很多时候管理员的疏漏就藏在里面。返回结果里有两条 Disallow 记录分别是/admin/和/backup/。单独看都是普通路径但配合刚才源码注释里提到的“备份文件”这两条路径的价值一下就上来了。我的思路也清晰了先看/admin/页面的访问权限再查/backup/目录下到底放了什么。2. 目录扫描把隐藏资源一条条挖出来2.1 扫描选型背后的理由robots.txt 已经明确给了路径提示理论上我可以直接访问/backup/看看。但为了摸清整个站点的目录结构我还是决定跑一遍目录扫描这种方式能发现更多意外之物。目录扫描工具我平时常用三个dirsearch、gobuster、ffuf。这道题我选的是 dirsearch原因很简单中小型靶场里它出结果稳定字典覆盖够全输出格式也直观。实际命令大概这样python3 dirsearch.py -u http://目标地址/ -e php,txt,bak,git,zip,html -t 20 --random-agent为什么扩展名要特别加bak和git因为在 CTF Web 题里备份文件和版本控制泄露是两大信息富矿。index.php.bak、login.php~、/.git/HEAD这些路径一旦存在基本等于把源码直接递到你手上。加zip是因为有些站点会把整站打包放在根目录扫一下不亏。线程数我控制在 20。这里提个醒扫描线程不是越大越好开太高容易触发靶场防护或者被服务端限流导致一堆误报。宁可多等半分钟也别让结果乱七八糟。2.2 扫描结果的第一轮判断扫描大约跑了六十秒有价值的输出有这么几条200 /backups/ 200 /.git/HEAD 200 /admin/ 200 /login.php 200 /index.php 403 /flag.php/flag.php返回 403 很正常说明有权限校验不是直接能访问的。/login.php是登录页面这没什么意外。真正值得关注的是两个目录和.git泄露。我把/backups/直接在浏览器打开发现开启了目录索引里面静静躺着一个index.php.bak文件。果断下载到本地用编辑器打开旧版登录逻辑就在里面。与此同时我验证了/.git/HEAD确实可读说明整个项目的版本控制目录被原封不动地部署到了线上。这种信息泄露在真实环境里也算高危攻击者不仅能读源码还能翻 git 历史对比出哪些漏洞是修复过的哪些还遗留在当前版本。到了这一步我已经拿到了两个关键入口备份代码文件和潜在的用户字典文件。接下来就是通过读代码来还原登录逻辑。3. 读备份源码看穿“弱密码”到底弱在哪3.1 从 index.php.bak 还原登录校验流程把index.php.bak的内容复制到本地格式化后核心代码并不复杂大致结构是这样的?php session_start(); if (isset($_POST[username]) isset($_POST[password])) { $user $_POST[username]; $pass $_POST[password]; $db new mysqli(localhost, root, , ctf); $row $db-query(SELECT * FROM users WHERE username {$user}); if ($row $row-num_rows 0) { $data $row-fetch_assoc(); if (md5($pass) $data[password]) { $_SESSION[admin] $user; header(Location: admin.php); } else { echo 密码错误请重试; } } } ?这段代码里有两个非常典型的漏洞特征。第一个是用户名直接拼接进 SQL 查询存在明显的注入点。第二个也是本次题目名称点出来的关键是md5($pass) $data[password]这一行比较逻辑。在 PHP 中是松散比较它会把两边的值先做类型转换再判断是否相等。如果两边都能被解释成科学计数法数字并且都以0e开头那么它们会被转成数值 0比较自然就通过了。也就是说如果数据库里存的密码哈希是一个0e开头的字符串攻击者只需要提交一个 MD5 值同样以0e开头的明文密码就能绕过校验。这就是所谓“魔法哈希”的经典场景。3.2 为什么叫“密码逻辑脆弱”而不是“密码强度弱”很多新手看到“密码逻辑脆弱”第一反应是密码太简单比如123456。但这道题重点不在密码本身够不够复杂而在校验逻辑存在缺陷。这种缺陷通常有三大类前端 JS 直接做了验证后端没有严格校验绕过方式是直接调用后端接口。后端比较方式不安全比如用而不是甚至拿密码和用户名做直接比对。密码哈希算法太弱比如 MD5、SHA1不带盐导致字典击中率极高。这道题属于第二种但同时把第三种也包进来了。因为代码里用md5($pass)去比对数据库里的哈希所以爆破的时候我不需要提交哈希值只需要提交一个个明文密码服务端会自动计算 MD5。真正的难点就落在了“这个明文密码到底是什么”上。为了确定管理员密码我回头翻了备份目录和.git历史发现/backups/里除了index.php.bak还有一个password.txt文件。打开一看里面是几十行常见弱口令格式很像一份精简的字典文件。到了这一步出题人的设计思路已经很清楚了先通过信息收集找到注释和 robots 线索再通过目录扫描拿到备份文件源码里还原出弱比较逻辑最后用字典文件对管理员账户做爆破测试。3.3 我踩过一次的坑拿到源码不等于会读源码这里插一句我个人做题时的教训。很早之前我做过一道类似的题源码里也写着md5($pass) $row[password]我当时一眼看到 SQL 注入就兴奋得不行直接去测注入点结果被引到别的岔路上浪费了很久。后来才反应过来题目标题既然强调“密码逻辑脆弱”核心考点就是理解密码比较的缺陷而不是把所有漏洞点都试一遍。所以读源码的时候要顺着出题人的思路走。看到马上想魔法哈希看到md5()马上想字典攻击的可行性看到文件名和注释的暗示马上想备份文件和字典的位置。把这三条线串起来解题路径就特别清晰了。4. 字典攻击的完整实操4.1 字典构造与请求构造密码逻辑已经明确剩下的就是组织字典。我先核对了一遍password.txt里的内容基本都是常见弱口令比如123456、admin、password、admin888、123456789、woaini、5201314。这些口令在真实环境里命中率确实很高因为在很多系统后台管理员的密码就是这种简单好记的组合。我不打算一次把整个字典全打出去而是按经验排序先跑最常见的 20 条不中再加变体规则。常见变体包括基础密码后面加年份、首字母大写、键盘路径、常见特殊字符后缀。比如123456可以变成1234562024admin可以变成Admin123或admin!#。这套规则在 CTF 靶场上效果不错在真实的授权测试里也经常命中。同时我还确定了登录请求的完整形态POST 到/login.php参数名是username和password没有验证码、没有 CSRF token也没有明显的失败次数限制。这一点非常关键因为没有防护暴力枚举才真正可行。4.2 从单点尝试到自动化爆破我先手工提交了一组admin/123456服务端返回“密码错误”。随后打开浏览器抓包确认了提交地址和参数格式无误。接着我写了一个简单的 Python 爆破脚本逻辑是循环字典、提交请求、根据响应判断结果import requests base_url http://目标地址/login.php def try_login(user, pwd): resp requests.post( base_url, data{username: user, password: pwd}, allow_redirectsFalse, timeout10 ) if resp.status_code 302 and admin.php in resp.headers.get(Location, ): return True, resp if flag in resp.text or 欢迎 in resp.text: return True, resp return False, resp with open(password.txt, r, encodingutf-8, errorsignore) as f: passwords [line.strip() for line in f if line.strip()] for pwd in passwords: ok, resp try_login(admin, pwd) print([*] trying:, pwd) if ok: print([] success:, pwd) print(resp.headers) break else: print([-] not found with usernameadmin)脚本里我判断成功的方式是用allow_redirectsFalse当密码正确时服务端返回 302 并跳转到admin.php。这个响应头差异比匹配页面文本更可靠因为失败页面也可能包含“flag”这类字样容易误判。第一轮用用户名admin跑完全部字典没中。我随即扩大用户名池用root、test、user、admin888这些常见组合做了交叉尝试。跑了几十条后脚本在某个密码处停下了服务端返回了 302 跳转。命中成功。这里有一个细节想强调爆破目标不只有admin。很多新手把用户名定死在admin上一个劲跑结果真正的弱口令账号是test或者root白白浪费大量时间。做交叉测试通常能更快锁定目标。4.3 登录成功后的 flag 提取登录成功后访问/admin.php进入管理后台页面。页面上方是一段欢迎语从 HTML 源码里搜索flag关键字很快在页面底部的注释和隐藏内容中找到了最终 flag。整道题的链路到这里就闭环了信息收集找到注释和 robots 提示目录扫描定位到备份文件源码还原出弱比较逻辑字典攻击确定了弱口令最后拿到 flag。回头看真正啃时间的不是爆破本身而是前面信息收集和读代码的耐心。如果一开始就无脑打登录接口恐怕很难找到正确的密码。5. 常见问题、避坑技巧与实战思考5.1 新手最容易翻车的三个地方登录型 Web 题最常见的失败方式就是对着登录框一顿猛提交完全忽略信息收集。结果备份文件没发现密码逻辑也不清楚最后只能靠猜。另一个翻车点是字典选择一上来就挂几个 G 的大字典不仅跑得慢还容易被服务端断开反而错过最基础的弱口令。第三个翻车点是提交参数没抓准有人折腾半天发现死活爆破不了最后一看字段名写错了密码字段不是password而是passwd。我的建议是做题节奏可以先慢后快。信息收集和读代码各占三成时间爆破只占四成。前面的基础工作做得越扎实后面真正打点的时候就越顺。5.2 排查清单爆破失败时按顺序检查最后分享一份我自用的排查清单按顺序核对基本能解决九成问题是否确认了请求方法、参数名、请求头的 Content-Type是否带了必要的 Cookie、Referer、Token是否用响应内容差异化判断结果而不是只看延迟或状态码是否遗漏了常见用户名比如 root、admin、test、user字典里是否包含了上下文线索比如题目名、目录名、备份文件内容是否被服务端限流导致后续请求全部超时或返回异常这份清单不只在 CTF 里有效。我在做真实授权测试时也会按这个思路排查登录接口的弱口令问题区别只在于真实系统的防护更强需要加上规避策略和请求频率控制。5.3 从这道题延伸开来的真实防御建议写完这篇记录我又回头看了那段备份代码。说句实在话平时大家喜欢嘲笑靶场里的弱智逻辑但真实环境里相似的代码并不少。早期很多系统后台用 MD5 存储密码不限制错误次数没有验证码和锁定机制用的还是比较。一旦管理员的用户名泄露攻击者拿着现成的弱口令字典很快就能拿到后台权限。我在一次授权测试中遇到过类似情况某内部系统管理员密码是姓名拼音加生日完全落在常规字典命中范围内。那次测试之后对方立刻加上了验证码、失败锁定和强密码策略。成本不高但挡住了一大批自动化攻击。所以我建议每个写过后端登录功能的朋友都把自己写过的代码再翻出来对照一下密码哈希是不是用的现代算法比较是不是用的安全函数接口有没有限制失败次数有没有验证码这几件事不难做但能大幅提高系统的防守底线。这也是我乐于刷这类题的原因。它表面上是 CTF 的分数本质上却是把攻防两端的心态都模拟了一遍。做攻击手时看到的每一个脆弱点换到防守方都值得立刻动手去修补。