最近在 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可以变成1234562024,admin可以变成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_redirects=False, timeout=10 ) 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", encoding="utf-8", errors="ignore") 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 username=admin")脚本里我判断成功的方式是用allow_redirects=False,当密码正确时,服务端返回 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 的分数,本质上却是把攻防两端的心态都模拟了一遍。做攻击手时看到的每一个脆弱点,换到防守方,都值得立刻动手去修补。