从“低价签约”到“金额溢出”:盘点那些年我在SRC遇到的奇葩支付逻辑Bug
从“低价签约”到“金额溢出”:盘点那些年我在SRC遇到的奇葩支付逻辑Bug
在数字化支付日益普及的今天,支付系统的安全性直接关系到企业和用户的切身利益。作为一名长期活跃在SRC(安全应急响应中心)的白帽子,我有幸见证了各种令人啼笑皆非的支付逻辑漏洞。这些漏洞往往不是传统意义上的技术缺陷,而是开发者在业务逻辑设计上的思维盲区。本文将分享几个真实案例,剖析这些漏洞背后的逻辑缺陷,以及如何从防御角度避免类似问题。
1. 低价签约漏洞:当并发遇上解约
案例背景:某知名会员服务系统推出新用户专享活动——首月会员仅需10元(原价100元)。这本是一个常见的营销手段,却因为支付逻辑的漏洞变成了"薅羊毛"的重灾区。
漏洞重现:
- 新用户同时通过支付宝和微信打开签约页面
- 先完成微信端的支付并立即解约
- 再完成支付宝端的支付
- 系统错误地认为这是两次"首月优惠",导致用户以20元获得两个月服务(价值200元)
问题根源:
- 并发控制缺失:系统未对同时发起的多个支付请求做互斥锁处理
- 状态校验不足:解约后未及时更新用户优惠资格状态
- 业务隔离不彻底:不同支付渠道间的数据同步存在延迟
防御建议:
# 伪代码示例:安全的签约逻辑实现 def sign_contract(user, payment_channel): with transaction.atomic(): # 数据库事务 if not can_use_discount(user): # 检查优惠资格 raise Exception("优惠资格已使用") mark_discount_used(user) # 立即标记优惠已使用 process_payment(user, payment_channel) # 处理支付 # 支付成功后创建会员 create_membership(user)2. 优惠券循环利用:时间差攻击
典型案例:某电商平台优惠券使用漏洞,用户可"无限循环"使用同一张优惠券。
攻击步骤:
- 使用优惠券创建订单并进入支付页面
- 返回APP取消订单,系统自动返还优惠券
- 完成之前的支付操作
- 结果:既完成了优惠购买,又保留了优惠券
关键缺陷分析:
| 问题环节 | 错误实现 | 正确做法 |
|---|---|---|
| 优惠券锁定时机 | 支付完成后才扣除 | 创建订单时立即锁定 |
| 订单取消逻辑 | 无条件返还优惠券 | 检查支付状态后再决定 |
| 支付超时处理 | 无超时回滚机制 | 设置合理的支付超时窗口 |
防御方案:
重要提示:优惠券状态管理应遵循"早锁定、晚释放"原则,在订单创建时即锁定优惠券,仅在支付失败且订单超时后才释放。
3. 金额溢出:当数字超出认知范围
震惊案例:某平台充值系统因整数溢出漏洞,允许用户支付2元获得2亿余额。
技术细节:
- 系统使用32位有符号整数存储金额(最大值2,147,483,647)
- 当充值金额超过此值时发生溢出
- 后端验证逻辑缺失,仅依赖前端校验
漏洞复现过程:
- 用户输入充值金额:2,147,483,650
- 系统实际存储值:-2,147,483,646(32位溢出)
- 支付网关只处理了最后两位"50"对应的2元
- 余额计算错误地将溢出值当作正数处理
防护措施清单:
- 使用Decimal或BigInteger类型存储金额
- 前后端实施双重金额校验
- 设置合理的充值上下限
- 关键操作添加审计日志
4. 并发提现:多线程下的资金魔术
某金融App漏洞:用户通过并发请求,可用0.1元本金成功提现0.12元。
攻击技术剖析:
- 准备阶段:抓取正常提现请求包
- 攻击实施:
- 使用Burp Suite的Intruder模块
- 设置18个并发线程
- 每个请求提现0.01元
- 结果:由于竞态条件,系统处理了12次请求
并发问题防御矩阵:
| 防御层级 | 具体措施 | 实现示例 |
|---|---|---|
| 应用层 | 乐观锁控制 | UPDATE account SET balance=balance-? WHERE user_id=? AND balance>=? |
| 中间件层 | 分布式锁 | RedisSETNX命令 |
| 架构层 | 限流措施 | 令牌桶算法实现API限流 |
| 数据层 | 事务隔离 | 设置合适的数据库隔离级别 |
// 线程安全的提现逻辑示例 public synchronized boolean withdraw(long userId, BigDecimal amount) { Account account = accountDao.getForUpdate(userId); // 加锁查询 if (account.getBalance().compareTo(amount) >= 0) { account.setBalance(account.getBalance().subtract(amount)); accountDao.update(account); return true; } return false; }5. 异常金额处理:边界情况的灾难
经典漏洞集锦:
- 负数提现:修改amount为-1成功增加余额
- 小数截断:支付0.019元被系统记为0.02元
- 超大金额:提交超过数据库字段长度的金额导致异常
防御编码规范:
- 所有金额字段必须使用定点数类型
- 关键业务操作实施参数白名单校验
- 负数、零值等边界情况必须显式处理
- 前后端金额单位保持一致(建议统一使用分单位传输)
常见错误模式检测表:
| 错误类型 | 检测方法 | 修复建议 |
|---|---|---|
| 负数金额 | if(amount < 0) | 拒绝并记录异常行为 |
| 超大数值 | 比较数据库字段长度 | 设置合理的业务限制 |
| 小数位数 | 检查小数点后位数 | 统一处理为整数分单位 |
| 格式异常 | 正则表达式校验 | 严格限制输入格式 |
在多年的SRC众测经历中,我发现支付逻辑漏洞往往源于开发者对业务场景考虑不周。比如最近遇到的一个案例:某平台允许用户使用积分+现金组合支付,但由于积分和现金的校验逻辑分离,导致可以通过修改请求参数实现"全积分支付"。这种漏洞看似简单,却可能造成重大损失。
