⚠️免责声明:本文仅用于授权范围内的安全测试、CTF 靶场练习与防御建设。未经授权对他人系统进行测试属于违法行为。文中所有技术细节的目的,都是帮助开发者理解攻击原理、进而写出更安全的代码。
一句话概括:业务逻辑漏洞不是"代码写错了",而是"流程设计漏了"——代码本身能正常跑,只是没考虑到有人会不按设计好的流程走。
一、先搞清楚:业务逻辑漏洞到底"漏"在哪
1.1 与 SQL 注入 / XSS 的本质区别
| 维度 | 代码层漏洞(SQL 注入 / XSS) | 业务逻辑漏洞 |
|---|---|---|
| 成因 | 代码语法 / 函数使用错误 | 业务流程设计缺陷 |
| 表现 | 有明确的危险函数、明确 payload | 一步步走正常流程,只是"跳了一步" |
| 检测工具 | 扫描器能发现(sqlmap、Xray) | 扫描器基本发现不了,必须人工理解业务 |
| 修复 | 改一个函数、加一层过滤 | 重新设计流程,可能要改产品逻辑 |
| 典型例子 | ' or 1=1#打进查询 | 收自己的验证码,去改别人的密码 |
关键认知:业务逻辑漏洞的发现方式不是"扫描",而是"把正常流程拆开,问自己:如果这一步我不做 / 多做 / 乱做,系统会拦我吗?"
1.2 通用测试思路
正常流程:A → B → C → D → 完成 攻击思路:逐个环节下手 ① 跳过:不做 B,直接请求 C (例如不输验证码直接提交) ② 篡改:把 B 请求里的参数改成别人的(越权) ③ 重放:把 B 请求原样再发一次(重复使用) ④ 枚举:把 B 请求里的值一个个试(爆破) ⑤ 逆序:先做 D 再做 B (顺序依赖缺陷) ⑥ 并发:同时发两次 B (竞态,薅羊毛的经典手法)📌记住六个词:跳过、篡改、重放、枚举、逆序、并发。绝大多数业务逻辑漏洞都落在这六类里。
1.3 它在 OWASP Top 10 里算哪一类?
很多文章会说"业务逻辑漏洞属于失效的访问控制",这个说法不准确。查过 OWASP Top 10:2021 官方文档后,准确归类如下:
| 具体表现 | OWASP Top 10:2021 分类 |
|---|---|
| 验证码 / 流程被绕过、并发薅羊毛、频率不设限 | A04 不安全设计(Insecure Design) |
| 改 Cookie / 改 userid 切换用户 | A01 失效的访问控制 |
| 弱口令、爆破、用户名枚举、明文存密码 | A07 身份识别和认证失败 |
A04 页面明确映射了下面这几个 CWE,建议记住编号:
CWE-840Business Logic Errors(业务逻辑错误)
CWE-841Improper Enforcement of Behavioral Workflow(行为流程执行不当)
CWE-799Improper Control of Interaction Frequency(交互频率控制不当)← 短信轰炸就是这条
CWE-602Client-Side Enforcement of Server-Side Security(用前端实现服务端安全)← "前端做了校验"就是这条
💡顺带一提:OWASP Top 10 对业务逻辑漏洞的覆盖本来就有限(它面向"通用风险类别")。系统讲逻辑漏洞的是 OWASP 的WSTG(Web Security Testing Guide)里的 Business Logic Testing 章节,以及OWASP ASVS。别指望靠背 Top 10 找逻辑漏洞。
二、短信验证码漏洞
2.1 正常业务全链路
用户输入手机号 → 前端校验格式 + 启动 60s 倒计时 → 调用后端发送接口 → 后端生成验证码(4 / 6 位) → 绑定手机号存入 Redis / MySQL(设置 TTL) → 调用第三方短信服务商 → 下发到用户手机 → 用户输入验证码,提交校验接口 → 后端三重校验(验证码匹配 + 未过期 + 未使用) → 标记已使用 → 执行业务(登录 / 改密 / 绑卡)🎯核心原则:验证码的生成、存储、校验、限流,全部必须在后端完成。前端做的一切(格式校验、倒计时、隐藏输入框)都只是体验优化,不能作为安全防线。
2.2 技术栈与存储设计
| 层级 | 常见技术 | 说明 |
|---|---|---|
| 后端 | Java / Python / PHP / Go / Node.js | 实现验证码业务逻辑 |
| 存储 | Redis(首选)/ MySQL | Redis 支持 TTL 自动过期、读写快,适合临时数据 |
| 短信通道 | 主流云厂商短信服务 / 专业短信服务商 | 第三方 API 对接运营商 |
| 前端 | HTML + JS | 手机号格式校验、60s 倒计时(仅 UI,可绕过) |
| 测试工具 | Burp Suite(Repeater / Intruder) | 抓包、重放、爆破 |
| 协议 | HTTP / HTTPS(JSON) | 前后端接口通信 |
Redis 存储的合理设计(示意):
key: sms:code:138xxxx1234 ← 手机号是 key 的一部分(这就是"绑定") value: 8f4a1c ← 不要存明文,存哈希更稳妥 ttl: 300s(5 分钟)⚠️ 注意
key里带手机号,意味着"这个验证码属于这个手机号"——这正是不绑定漏洞的防线所在。如果 key 写成sms:code:<随机id>,而校验时只比对 value,就会出现"A 的码能验 B 的手机"。
2.3 六大常见漏洞类型速览
| 漏洞 | 成因 | 危害 |
|---|---|---|
| 验证码回显 | 响应包明文返回验证码,调试代码遗留 | 抓包直接读取,无需收短信 |
| 短信轰炸 | 发送接口无后端限流,只靠前端倒计时 | 可无限调用,骚扰任意手机号 |
| 验证码与手机号未绑定 | 只存验证码,未关联手机号 / 会话 | A 手机验证码可验证 B 手机,接管他人账号 |
| 后端不校验验证码 | 校验逻辑写在前端,后端省略校验 | 任意输入均可通过 |
| 验证码暴力破解 | 无失败次数限制,纯数字可枚举 | 批量爆破,登录任意账号 |
| 验证码可重复使用 | 校验通过后未标记作废 | 同一验证码反复使用 |
2.4 逐条拆解:怎么测、怎么防
(1)验证码回显
现象:发送验证码的响应包里直接带着验证码。
// ❌ 危险的响应 {"code": 0, "msg": "发送成功", "data": {"verifyCode": "886655"}}为什么会出现:开发阶段为了方便调试,把验证码打进日志和响应,上线时忘删;或者短信通道没配好时用来"兜底"。
怎么测:发送验证码时抓包,看响应体、响应头、以及Set-Cookie里有没有那串数字。
怎么防:响应包只返回{"code": 0, "msg": "发送成功"},验证码只进服务端存储,不进任何返回给客户端的数据。
(2)短信轰炸
现象:前端有 60 秒倒计时,但后端完全没限流。
测试要点:
把发送请求用 Repeater 连续重放 10 次 → 若手机收到 10 条 → 确认无后端限流。
后端可能只按手机号限流或只按 IP 限流,这就是绕过点:
只按手机号限流 → 用手机号的等价写法(
+86前缀、空格分隔等)试探后端是否做了规范化;只按 IP 限流 → 观察是否需要
X-Forwarded-For,以及后端是否信任它。
再注意多接口:注册、登录、找回密码可能是三个不同的发送接口,限流往往只做了其中一个。
危害:短信是要花钱的(可能被刷爆短信费),同时会对任意手机号造成骚扰。
怎么防:手机号 + IP + 设备三重限流(如 60s / 1 次、单日上限);图形验证码 / 滑块前置;对短信服务商侧也做余额告警。
(3)验证码与手机号未绑定 —— 危害最大的一条
攻击模型(这是最有价值的理解):
攻击者用自己的手机号 138xxxx0001 请求验证码 → 收到真实验证码 886655 → 提交校验时,把请求里的 phone 参数改成受害者的 139xxxx0002 → 若后端只校验"886655 对不对",不校验"这个码是不是 139xxxx0002 的" → 攻击者通过校验,登录 / 重置了受害者的账号根本原因:校验接口信任了请求里的手机号,而不是使用服务端会话中记录的手机号。
怎么测:抓取校验请求,改phone/mobile/username参数为目标手机号,看是否通过。
怎么防:校验时必须用服务端记录的手机号(与当前会话绑定),请求里的手机号只作展示用途;验证码 key 必须包含手机号。
(4)后端不校验验证码
现象:校验逻辑写在前端 JS 里,或者后端接口干脆不比对。
测试要点:
随便填个 6 位数(如
000000)提交;或者直接把验证码参数删掉,看接口是否仍然返回成功;
或者把
code参数改成null/ 空字符串。
怎么防:校验逻辑只写在后端;参数缺失时按"校验失败"处理,不能因为参数没传就跳过校验——这是最典型的"参数污染"式疏漏。
(5)验证码暴力破解
先算一笔账:
| 位数 | 组合数 | 无限制时爆破耗时(按 10 次/秒) |
|---|---|---|
| 4 位纯数字 | 10,000 | ≈ 17 分钟 |
| 6 位纯数字 | 1,000,000 | ≈ 28 小时 |
看起来 6 位"很安全",但 Burp Intruder 多线程跑起来、或用脚本直接压接口,几小时就能覆盖。所以位数只是拖延时间,真正的防线是"失败次数限制 + 短有效期"。
怎么测:连续提交错误验证码 20 次,观察是否出现锁定、延迟、验证码失效或需要重新获取。
怎么防:同一手机号 / 会话对同一验证码最多错 5 次即作废并强制重新获取;有效期压到 5 分钟以内。
(6)验证码可重复使用
现象:验证码校验通过后没有被删除,同一个码可以反复提交成功。
怎么测:校验成功后,把同一个请求原样重放一次(Burp Repeater 直接 Send),看是否仍然返回成功。
怎么防:校验成功的那一刻立即删除该验证码(先在 Redis 里DEL,再执行业务),保证一次性消费。
2.5 渗透测试 Checklist
获取验证码阶段:
- 抓包看响应包(含响应头、Cookie),是否有验证码回显
- 重复发送请求,测试是否有后端限流(短信轰炸)
- 前端倒计时能否绕过(抓包直接重放)
- 注册 / 登录 / 找回密码三个接口是否都做了限流
提交验证码阶段:
- 随便输入数字(如
000000),测试后端是否真的校验 - 删除验证码参数,测试是否会"跳过校验"
- 多次提交错误验证码,测试是否有失败次数限制
- 用 A 手机号的验证码去验证 B 手机号,测试绑定是否生效
- 校验成功后重放同一请求,测试是否一次性作废
- 修改请求中的手机号参数,测试越权
2.6 防御方案汇总
防回显:响应包严禁返回验证码明文(含响应头、日志)
服务端校验:校验逻辑必须全部在后端,且参数缺失按失败处理
强绑定:验证码与手机号、会话三者绑定,校验用服务端的手机号
一次性:校验通过立即删除,保证只用一次
防爆破:失败 5 次锁定 + 有效期 ≤ 5 分钟 + 尽量用 6 位
防轰炸:手机号 + IP 双重限流(60s / 1 次、单日上限)+ 图形验证码 / 滑块
三、登录认证漏洞
3.1 正常登录流程
用户输入账号密码 → 前端做非空 / 格式校验 → 提交表单 → 后端拼接 SQL 查询数据库(❌ 危险)/ 参数化查询(✅ 正确) → 匹配成功 → 创建 Session / Cookie → 进入系统 → 匹配失败 → 返回错误提示身份认证的核心同样是:后端校验,不能只依赖前端。
3.2 通关思路一:暴力破解(弱口令爆破)
原理:系统没有登录失败次数限制,用字典批量遍历"账号 + 密码"组合。
工具:Burp Suite → Proxy 抓包 → 右键
Send to Intruder→ 加载字典(Sniper / Cluster bomb 模式)。判断成功的三个维度(不要只看页面文字):
响应长度——失败提示固定,成功必然长度不同;
HTTP 状态码——302 跳转通常意味着登录成功;
响应内容——出现用户名、"欢迎回来"等关键词。
前提:无失败次数限制且存在弱口令(这两条缺一不可)。
💡实战经验:先跑一遍
admin/admin、admin/123456、admin/admin888这类高频组合,往往不用上字典就进去了。同时留意账号大小写、前后空格,以及 JSON 与 form 两种提交格式是否被同一套逻辑处理。
3.3 通关思路二:SQL 注入万能密码
原理:后端直接拼接 SQL、没有预编译,攻击者闭合单引号后篡改查询逻辑。
Payload 示例:
admin' or '1'='1 ' or 1=1 limit 1#拼接后的效果(以SELECT * FROM users WHERE username='$u' AND password='$p' LIMIT 1为例):
输入 u = ' or 1=1 limit 1# 拼接结果: SELECT * FROM users WHERE username='' or 1=1 limit 1#' AND password='任意值' LIMIT 1 ↑ 注释符,后面全被丢掉1=1恒为真 →or让整个条件永真 → 返回第一条用户数据 → 校验通过。
容易忽略的细节一:payload 里的limit 1是干什么的?
因为or 1=1会让查询返回全部用户行。而后端代码通常只fetch一行、或对结果集取[0],一旦返回多行就可能抛异常或走偏逻辑,反而"打不进去"。加上limit 1就能稳定只取第一行,兼容性最好。
⚠️ 注意区分两个
LIMIT 1:模板里自带的那个是开发者写的(被 payload 的#注释掉了),payload 里补的这个是攻击者加的。所以万能密码的"标准姿势"是' or 1=1 limit 1#,而不是裸的' or 1=1。
容易忽略的细节二:admin' or '1'='1为什么能过?
这个 payload没有注释符,它的原理完全不同——靠的是or的短路求值:
输入 u = admin' or '1'='1 拼接结果: SELECT * FROM users WHERE username='admin' or '1'='1' AND password='任意值' LIMIT 1因为AND优先级高于OR,实际等价于:
WHERE (username='admin') OR (('1'='1') AND (password='任意值'))只要username='admin'成立,左边为真,整个 WHERE 就为真,密码压根不参与判断。
🔍推论:这个 payload依赖
admin账号真实存在——如果用户名不存在,左边为假,就退化成'1'='1' AND password='任意值',照样需要正确密码。所以它的实际效果是"登录成 admin",而不是"随便登成一个用户"。这也解释了为什么它通常写作admin' or '1'='1、而不是裸的' or '1'='1。
关于注释符:MySQL 里#和--(必须跟一个空格)都能注释,裸写--会语法报错。所以等价写法是' or 1=1 limit 1#或' or 1=1 limit 1--+(URL 里+会被解码成空格)。
本质:这是 SQL 注入漏洞。之所以放在"逻辑漏洞"里讲,是因为它攻击的目标是"认证流程"——不是猜出密码,而是让流程直接放行。
3.4 两种思路对比
| 对比 | 暴力破解 | 万能密码 |
|---|---|---|
| 原理 | 猜真实账号密码 | 利用 SQL 注入篡改查询逻辑 |
| 前提 | 无失败限制 + 弱口令 | 存在 SQL 注入漏洞(未预编译) |
| 速度 | 慢,依赖字典质量 | 快,一步到位 |
| 修复方向 | 加失败次数限制 + 强密码策略 | 改参数化查询(预编译) |
3.5 登录模块常见漏洞汇总
暴力破解 / 弱口令:无失败次数限制,账号使用简单密码
用户名枚举:报错信息差异化("用户名不存在" vs "密码错误"),可先遍历出合法用户名
SQL 注入(万能密码):直接拼接 SQL,未参数化
前端校验绕过:校验逻辑写在前端 JS,抓包修改即可绕过
Cookie / Session 越权:修改 Cookie 中的
userid直接切换用户记住我漏洞:Cookie 加密缺陷,可伪造身份
💡关于用户名枚举:不要只盯着登录接口的报错文案,还要看HTTP 状态码、响应长度、响应时间(用户存在时要查库 + 验哈希,会稍慢),以及注册接口("该用户名已被占用"是最直接的枚举入口)和找回密码接口。后两者往往比登录接口更容易枚举。
3.6 补充:密码找回流程(逻辑漏洞的重灾区)
原稿没有这一节,但 OWASP Top 10 的 A04 页面第一个攻击场景就是它,值得单独列出:
密保问题:"你母亲的姓氏是?"这类问题被NIST SP 800-63b和 OWASP ASVS 明令禁止——答案往往能从社交媒体查到,而且不止一个人知道。现代系统应当直接删除这个设计。
改密接口不校验旧密码:已登录状态下调用改密接口,不要求提供原密码 → 结合 XSS 或 CSRF 可直接改密。
重置 token 可预测 / 可枚举:token 是自增 id、时间戳、简单位置编码等。
验证码复用:重置密码的短信验证码没有一次性作废,可反复使用。
步骤跳跃:验证身份 → 设置新密码 是两步流程,若直接请求第二步接口就能改密。
测试思路:把"找回密码"多步流程的每一步都单独抓出来,尝试直接请求最后一步。
3.7 测试标准步骤
① 测试报错信息 → 判断是否可枚举用户名 ② 尝试万能密码 → 测试 SQL 注入 ③ 抓包重放 → 测试是否可暴力破解 ④ 检查 Cookie/Session → 测试越权登录 ⑤ 走一遍找回密码 → 测试流程能否跳跃、token 能否枚举3.8 防御方案
防爆破:失败次数限制(5 次锁定)、人机验证、IP 限流
防注入:强制参数化查询(预编译),禁止直接拼接 SQL
统一报错:返回统一的"用户名或密码错误",防止用户名枚举
密码安全:加盐哈希存储(
bcrypt/argon2),禁止明文,禁止弱口令不信任前端:所有身份校验在后端执行
会话安全:服务端存储会话状态,客户端只持有不可预测的随机 token;Cookie 设置
HttpOnly+Secure+SameSite找回密码:敏感操作要求二次验证(旧密码或已验证的会话),token 使用密码学随机数且一次性有效
四、核心原则速记
业务逻辑漏洞通用测试思路:把正常流程的每一步拆开,逐个尝试"跳过、篡改、重放、枚举、逆序、并发"。
| ❌ 错误做法 | ✅ 正确做法 |
|---|---|
| 前端校验,后端不校验 | 所有安全判断必须在服务端 |
| 参数缺失就跳过校验 | 参数缺失按"校验失败"处理 |
| 前端倒计时,后端不限流 | 后端做手机号 + IP 双重限流 |
| 验证码不绑定手机号 | 验证码与手机号、会话强绑定 |
| 返回包明文返回验证码 | 响应包绝不泄露敏感信息 |
| 不限制尝试次数 | 限制失败次数,超限锁定 |
| 验证码永久有效 | 短有效期 + 一次性作废 |
| 报错信息差异化 | 统一报错,防止枚举 |
| 密码明文存储 | 加盐哈希存储(bcrypt / argon2) |
| 用自增 id / 时间戳当重置令牌 | 用密码学随机数当一次性令牌 |
五、总结
5.1 一句话理解逻辑漏洞
代码层漏洞是"墙有洞",业务逻辑漏洞是"门没锁"——墙和门看起来都很完整,只是没人想到有人会从没锁的门走进来。
5.2 记住三件事
扫描器发现不了逻辑漏洞,靠的是人工理解业务 + 拆解流程。
"前端做了"等于"没做"(CWE-602)。一切安全判断都要在后端重做一遍。
验证码的四个必须:必须服务端生成、必须绑定手机号与会话、必须短时有效、必须一次性作废。