1. 漏洞原理
通常,合格的验证码机制应该遵循 “一次一验,验完即毁” 的原则。
- 漏洞逻辑(本关):用户提交表单 -> 后端校验验证码 -> 校验通过 / 失败 -> 后端没有销毁旧验证码 -> 攻击者可以一直拿着这个旧的验证码去跑字典。
- 正常逻辑 :用户提交表单 -> 后端校验验证码 -> 无论校验成功与否,立即销毁当前 Session 中的验证码 -> 生成新的验证码。
2. 测试过程
第一步:抓包分析
输入正确的验证码,随意的用户名 test 和密码 test,抓包。
第二步:验证漏洞是否存在 (关键步骤)
不要急着去爆破。先右键发送到 Repeater (重发器)。
- 保持验证码和 Cookie 不变。
- 修改密码参数,点击 Send。
- 再次修改密码参数,点击 Send。
- 观察 :两次(甚至多次)请求都返回了“username or password is not exists”(说明验证码校验通过了,只是账号密码不对),而不是提示“验证码错误”, 说明验证码可以被重复使用。

第三步:爆破 (Intruder)
- 发送到 Intruder。
- 重点 :把 username、password 设置为变量(§), 验证码参数保持抓包时的那个正确值不变。
- 载入字典,开始攻击。
- 根据响应长度找到正确密码。
3. 代码审计
if (empty($_POST['vcode'])) {$html .= "<p class='notice'>验证码不能为空哦!</p>";
} else {
// 验证验证码是否正确
if (strtolower($_POST['vcode']) != strtolower($_SESSION['vcode'])) {$html .= "<p class='notice'>验证码输入错误哦!</p>";
// 应该在验证完成后, 销毁该 $_SESSION['vcode']
// unset($_SESSION['vcode']);
}else{$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
$sql = "select * from users where username=? and password=md5(?)";
$line_pre = $link->prepare($sql);
$line_pre->bind_param('ss',$username,$password);
if($line_pre->execute()){$line_pre->store_result();
// 虽然前面做了为空判断, 但最后, 却没有验证验证码!!!
if($line_pre->num_rows()==1){$html.='<p> login success</p>';
}else{$html.= '<p> username or password is not exists~</p>';
}
}else{$html.= '<p>执行错误:'.$line_pre->errno.'错误信息:'.$line_pre->error.'</p>';
}
}
}
分析 :
代码中缺少了 unset($_SESSION[‘vcode’]); 这一行。导致只要 Session 不过期,或者不刷新页面,这个 $_SESSION[‘vcode’] 的值一直不变,前端发过来的包只要匹配这个值就能一直通过校验。
4. 修复方案 (开发视角)
在验证码校验逻辑完成后(无论成功还是失败),必须强制更新或销毁 Session 中的验证码。
if($_SESSION['vcode'] == strtolower($_POST['vcode'])) {
// 校验后立即销毁,防止复用
unset($_SESSION['vcode']);
// 或者重新生成一个新的
// $_SESSION['vcode'] = generate_new_code();
// ... 具体的登录逻辑 ...
}
正文完