ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

支付系统安全攻防:8类业务逻辑漏洞实战拆解与修复

支付系统安全攻防:8类业务逻辑漏洞实战拆解与修复 最近团队做了一轮支付系统安全专项评估把线上事故、外部攻击样本、历史渗透测试报告全部翻了一遍最后整理下来真正造成资金损失或业务事故的大多不是加密算法被破解也不是服务器被打穿而是那些“看起来再正常不过”的业务逻辑出了缝隙。尤其是支付场景钱和利益都在那里攻击者盯得比产品经理还紧。这篇文章就把我在支付场景攻防实战中反复遇到的8类逻辑漏洞逐个拆开讲清楚从攻击思路到防御修复从工具使用到排查实录全部基于实际项目经验。过一遍相当于给支付系统做一次“漏洞体检”干这行的兄弟也能拿这份清单当对照表看看自家的信任边界有没有漏风。1. 支付逻辑漏洞为什么“看着很正常”却总是出事1.1 漏洞的本质攻击者在和业务假设较劲普通Web漏洞讲究的是利用某个技术缺陷比如SQL注入、文件上传、反序列化。逻辑漏洞则完全不一样攻击者不需要什么高级工具也不需要多深的底层知识只需要看懂业务流程然后想方设法破坏业务里的“默认信任”。支付系统里充满这类默认信任。比如“用户提交的金额就是他想支付的金额”“回调消息来自支付平台就一定安全”“订单号比较长就没人能猜到”“退款接口只有管理员才知道”。这些假设在开发时看起来合理但在攻击者眼里每条假设都是一个可以盘的点位。我把这类问题的本质总结成一句话逻辑漏洞 业务流程中未经验证的信任假设 攻击者可控的输入。只要某个关键数据从客户端传上来而服务端没有重新确认或者某个操作没有做权限和幂等校验那这个地方基本就是一个漏洞候选点。这也是为什么支付逻辑漏洞特别适合用“攻防实战”的视角去梳理——你只有站在攻击者的位置顺着流程走一遍才能看清哪些环节在裸奔。1.2 一条支付链路上的信任边界要理解支付场景的漏洞分布得先看一条完整支付链路上有哪些环节。以最常见的“用户下单→跳转支付→异步回调→业务发货”为例大致是这样的用户在前端选择商品点击下单前端携带商品ID、数量、价格等信息请求后端创建订单后端生成订单并返回支付参数前端拉起微信/支付宝等支付工具完成付款支付平台异步通知商户后端“支付成功”后端校验通知更新订单状态发放商品或确认服务在这条链路上第2步客户端提交价格参数就是一个经典信任假设第5步回调通知是又一个经典信任假设第6步订单状态更新可能引入重复和越权问题。后端的每个外部输入点本质上都是一道“信任边界”。我把这条链路上的常见攻击方式整理成一个映射关系链路环节表面信任假设实际攻击手法下单请求客户端提交的价格/数量就是真实意愿篡改金额、数量、商品编号唤起支付支付参数由后端生成就安全重放旧参数、篡改签名支付回调收到成功通知即为真实支付伪造通知、重复通知、金额不匹配订单操作登录用户只能操作自己的订单水平越权、遍历订单号退款流程退款金额不可超过原支付金额退款篡改、并发超退发货/服务订单中心状态正确流转直接改状态、跳过支付环节理解了这张表再去看具体的8类漏洞思路会清晰很多。2. 核心思路先画信任链再逐环节找可操纵点2.1 我习惯用的四步分析法接手任何一个支付系统安全评估我不会上来就急着用Burp抓包改数据而是先花时间把业务彻底理清楚。具体方法可以归纳成四步第一步梳理业务状态机。支付订单有哪些状态比如待支付、已支付、已退款、已关闭状态之间允许怎么流转谁有权限触发状态变化。状态机一旦模糊就容易出现第三步和第四步的各种怪问题。第二步标记所有资金动作。下单、支付、退款、提现、优惠券抵扣这些都是直接和钱打交道的操作。资金动作最怕的不是并发高而是没有约束条件。第三步审查每个关键参数的数据来源客户端传入、后端生成、第三方回调还是数据库查询。凡是客户端传上来的资金相关字段一律默认不可信。第四步验证每一个信任假设。设计测试用例去尝试打破它改金额、改签名、改状态、并发重放、越权访问把假设一条条打脸试试。这套流程我用了很多年几乎每次都能在“业务设计得不错”的系统里翻出点东西来。2.2 把8类漏洞放进同一张链路地图这8类漏洞其实不是孤立的它们分别落在支付链路的各个节点上。我在给团队分享时经常画一张“漏洞地图”用文字描述大概是这样的下单环节对应的主要是金额篡改和参数校验缺失唤起支付环节对应的是签名验证绕过和重放攻击回调处理环节对应的是回调伪造和竞态条件订单/退款管理对应的是越权操作和退款逻辑缺陷整个流程最怕的是状态机绕过这样分类有个好处排查问题时能按图索骥。比如用户反馈“没付款就收到了商品”第一反应就是查状态机绕过和回调伪造而不是去翻日志大海捞针。3. 8类典型漏洞逐个拆解与攻防实操3.1 金额与商品参数篡改改一个字段就能免费购物这类漏洞可以说是支付场景的“入门款”也是开发最容易踩的坑。常见到几乎每个做过支付系统的人都见过下单请求里的price、totalAmount、quantity、discount字段竟然直接从客户端接收后端拿到后直接生成订单不查数据库商品价格不复算总价。攻击者的操作方式比想象中还简单。用Burp拦截下单请求把原来100元的商品价格改成0.01元或者把ID改成另一个低价商品或者把数量改成负数让总价变成负值甚至有人把优惠券字段改成任意金额。如果后端没有做二次校验订单就会按篡改后的金额走完整个支付流程。我碰过一个真实案例某个电商活动页面支持优惠券抵扣前端把抵扣金额直接放进创建订单的请求里后端也没有核对优惠券是否真实存在、是否属于当前用户。攻击者直接把抵扣金额改成等于订单总额最后0元支付拿到商品。这不是多高深的手法就是单纯地利用后端“图省事”。修复这类问题没有技巧只有一条硬规则所有资金相关参数必须由服务端根据业务规则重新计算。商品价格必须从商品表读取数量必须校验为正整数且不能超过库存折扣必须基于用户实际的优惠券记录总价必须在后端用BigDecimal做精确计算禁止使用Double或Float。前端传上来的金额、折扣、币种字段只允许作为展示参考绝对不能作为订单的最终依据。3.2 签名验证绕过签名不是拼对了字符串就安全支付接口的签名机制本意是防止请求被篡改。微信支付、支付宝都要求开发者在请求参数中附加签名对方用约定好的密钥和算法验签。但实际开发里签名验证这关频频出问题而且出问题的原因五花八门。最常见的一类问题是“只验签名不验逻辑”。比如后端验签通过后就直接信任整个请求数据但没有意识到签名只保证数据“在传输过程中没被改”并不保证数据本身是合理合法的。攻击者没法改签名但他可以拿一个真实支付成功的旧请求重放或者利用自己的合法签名请求尝试越权操作。更典型的是签名覆盖类缺陷。有些接口允许携带额外参数而开发者验签时只取固定字段拼接字符串攻击者在URL里加一个同名参数如果后端使用的是PHP语言且没有过滤同名参数就可能出现“签名验的是A值业务用的是B值”的情况。这类问题在参数解析环节非常多见属于典型的“签名存在但没有真正保护字段”。微信支付用户经常遇到的“提示用户态签名signature错误”很多人以为是签名算法问题排查半天发现是参数值大小写、空值剔除规则、字段排序、编码格式不一致导致的。这块我后面在常见问题速查里专门展开这里先记住一条原则签名拼接规则必须完全按照官方文档来并优先使用官方SDK完成签名和验签自己手工拼字符串等于给自己埋雷。3.3 回调通知的信任危机伪造一个“支付成功”有多容易异步回调是支付场景里最能体现攻防对抗的环节。用户付款后支付平台会往商户填写的notify_url发送一个异步通知告诉商户“这笔订单支付成功了”。服务端收到通知后一般会做验签、校验订单金额等操作。问题恰恰出在“一般会做”这三个字上。很多早期系统或者快速上线的业务回调处理写得极其草率只看通知里success字段是不是true或者验签了但没核对订单金额或者根本没有验签直接信任调用方。攻击者发现这类漏洞后会怎么玩不需要真的付钱直接在Burp里向你的回调接口发一个POST请求body里带着“trade_statusSUCCESS”和任意orderNo如果你的服务端不验证订单金额是否与支付平台记录一致就可能直接触发发货流程。更进阶一点的攻击者会先正常支付一笔小额订单截获回调请求然后修改其中的订单号和金额参数看服务端会不会把“小额支付”和“大额订单”对上。我之前参与修复的一个系统就是这种问题。回调函数里验签逻辑写了一半AppSecret配置错误导致验签一直失败开发为了赶上线直接把验签结果注掉线上运转了半年没有被攻击纯属运气好。修复方案是补全验签同时增加一条二次确认规则收到回调后后端主动调用支付平台的订单查询接口核对订单号、金额、支付状态三者一致再更新本地订单。宁可多一次网络请求也不要在资金安全上省时间。3.4 越权操作登录用户凭什么动别人的订单越权漏洞在支付场景里往往不像金额篡改显得那么“值钱”但危害一点都不小。水平越权的典型场景是用户的订单详情页URL里带着orderId10001攻击者把参数改成10002发现竟然能查看别人的订单信息甚至能对别人的订单执行退款操作。垂直越权的场景就更直接了。某个退款接口只有管理后台用普通用户不知道怎么调用攻击者抓包发现接口地址后用普通用户身份直接POST请求如果后端只校验登录态而没有校验角色权限这个人就能替任意订单发起退款。这类问题的根源在于开发阶段没有统一的数据权限控制。最常见的错误是在Controller层只做了“是否登录”的校验校验完直接把订单ID丢给Service层Service层也没有把“订单归属用户”和“当前登录用户”做比对。修复方案其实不复杂核心就三步第一所有涉及订单、支付记录、退款单的查询和操作接口必须携带当前用户上下文第二数据访问层统一校验资源归属比如查询订单时强制带上WHERE user_id 当前用户ID而不是仅仅接收一个orderId第三管理类接口统一走后台权限体系接口路径和功能权限绑定防止用户直接拼URL调用。3.5 重放攻击同一个请求反复生效重放攻击在支付场景里属于“低调但高收益”。攻击者截获一个合法的支付请求或者回调请求然后原封不动地再次发送。如果后端没有幂等控制就可能出现重复扣款、重复退款、重复发放虚拟商品等一系列问题。举一个实际案例某个知识付费系统用户购买课程后微信支付平台会反复发送支付成功的异步通知。正常流程下通知会发送多次WS的服务器端如果处理逻辑没有做到幂等每次收到通知都执行“将订单置为已支付”“给用户开通课程权限”“给推广员加佣金”那同一笔订单可能被重复处理十几次。用户买一次课推广员拿十几次佣金。另一个经典场景是对支付下单接口做重放。用户点击“立即支付”按钮前端调后端下单接口由于网络抖动或者用户连点同一个prepay_id被多次使用。如果后端不校验预支付单状态每调一次就创建一个新的支付订单用户钱包余额账户就可能出现多笔相同金额的扣款记录。解决重放攻击的思路核心是幂等。下单接口用订单号做业务幂等键重复创建直接返回已存在订单回调处理用支付平台返回的交易流水号做唯一索引重复入库直接报错或忽略退款接口用退款单号做幂等同一退款单不能发起第二笔退款请求。幂等这件事看起来不起眼真出事的时候全是资金事故。3.6 竞态条件并发一高逻辑漏洞自动放大并发场景下的逻辑漏洞属于“系统自己给自己挖坑”的典型代表。攻击者不需要改参数只需要利用正常的业务功能在极短时间窗口内发送大量并发请求就能让系统状态错乱。最常见的竞态漏洞是库存扣减。用户看到一个限时抢购商品攻击者开脚本同时发送100个下单请求后端如果使用“先读库存判断大于0再扣减”这种经典不安全写法100个请求可能都读到库存是1然后全部通过校验最终超卖99件。别以为只有秒杀系统才有这问题任何涉及数量扣减的业务都会遇到。回调处理同样是竞态重灾区。支付平台的回调通知并不是只会发一次在极端情况下几条通知可能同时到达后端如果用的是多线程处理消息队列消费者两个线程同时读到订单状态是“待支付”同时执行“改为已支付”和“发放商品”那就等于发了两次货。我之前排查过一个订单重复发货事故最后定位就是回调处理没有加锁。修复竞态条件有几个层次的手段。数据库层面用行锁或者乐观锁更新时加上WHERE stock 0或WHERE order_status 待支付这种条件判断更新影响行数为0则拒绝代码层面用分布式锁确保同一笔订单的回调处理串行执行更底层一点为关键业务表加上唯一约束从数据库层面彻底杜绝重复数据写入。多管齐下才能经得起高并发冲击。3.7 退款与逆向流程漏洞退款金额比支付金额还高正向支付流程大家盯得紧逆向的退款流程反而常常被忽略。我在测试中见过几种典型的退款漏洞每个都能直接造成资金损失。一种是退款金额可篡改。退款接口从客户端接收退款金额后端没有校验退款金额不能超过原始支付金额。攻击者先是小额支付一笔订单然后申请退款时把金额改成大额如果后端不做上限校验平台就得按这个金额打钱。另一种是退款次数不受限制。逻辑上一笔订单退款后就应该终止后续退款流程但有些系统只判断订单总额和已退总额是否相等攻击者可以创建多笔退款单并发提交利用时间差让系统来不及判断“已退总额是否超过订单总额”多次退款叠加后超过原支付金额。还有一类逆向流程漏洞和苹果IAP相关。iOS端的虚拟商品支付走Apple In-App Purchase用户通过App Store购买后可以直接从“报告问题”里申请退款退款由苹果审核。如果App服务端给用户发放虚拟币或者解锁功能后没有监听退款通知、没有重新校验用户购买凭证用户退完款照样能继续使用虚拟商品等于白嫖。谷歌支付也有类似的服务端校验问题测试中常见的错误是只调客户端SDK确认支付成功没有在服务端验证消费凭证。修复退款漏洞的要点我用下面几条概括退款单必须关联原始支付流水金额不能超过可退余额退款操作必须以退款单号幂等重复提交只处理一次退款单生命周期状态机严格管理已退款不允许再次退款虚拟商品发放依赖服务端校验购买凭证客户端支付结果只能上传到服务端后二次确认定期跑对账任务把支付平台账单和本地订单、退款单逐笔核对这些点看着基础每一行背后都是从实际资金损失事故里总结出来的。3.8 支付状态机绕过订单状态只能由后端驱动最后一类漏洞说起来有点“小儿科”但实际发生率非常高特别是在前后端分离、接口设计混乱的系统中。攻击者不走支付流程直接通过各种手段让订单状态跳到“已支付”或“已完成”。最粗糙的攻击方式是前端篡改。有些页面在支付完成后用JavaScript直接跳转到“支付成功”页面后端没有校验这个页面是从哪儿跳来的攻击者拿到订单号后直接拼接URL打开成功页。如果后端认为“访问成功页订单已支付”那整个流程就被绕过了。另一个常见场景是直接调用状态修改接口。有些系统为了方便管理提供了“订单状态修改”接口原本只给内部运维用后端只校验登录态没校验角色。攻击者发现这个接口后把自己的待支付订单直接改成已支付静等发货。在我的评估经验里这类漏洞暴露的是整个系统对订单状态机的约束力不足。防御思路没有捷径只有一条订单状态的流转必须由服务端事件驱动用户支付完成、回调通知确认、主动查询支付平台返回成功这三种情况下才可以由后端逻辑修改订单状态。任何用户可直接触达的接口尤其是订单状态类接口一律不允许修改资金相关状态。4. 实战工具与多端支付场景避坑记录4.1 从Burp靶场到真实业务搭建可复现的测试环境想练手或者做评估工具链其实不需要多高级Burp Suite加一个测试环境就够了。很多安全新手问怎么学业务逻辑漏洞我通常建议先在Burp靶场里练基础抓包改包再拿自己项目里的支付宝沙箱和微信支付测试号做真实演练。Burp的使用核心就三板斧Proxy拦截改包、Repeater重放数据、Intruder做并发遍历。在支付场景里我经常用Proxy把下单请求拦下来改金额改数量用Repeater把回调请求复制一份再发一次测幂等用Intruder对订单号做遍历测越权。这三个功能覆盖了前面说的绝大多数漏洞测试场景。支付宝沙箱环境是个好东西。在支付宝开放平台申请沙箱应用后会拿到独立的AppID和沙箱版支付宝客户端沙箱网关和正式环境完全隔离不用担心把测试数据打到生产环境。配置沙箱时要注意沙箱的支付宝网关地址和正式环境不一样很多新手配完SDK后请求一直失败检查一下网关地址基本就能定位。微信支付也有类似的测试方式商户平台可以配置测试授权目录、测试AppID和测试小程序配合sandbox API完成下单和回调验证。在沙箱或测试环境里把漏洞全测一遍再上生产就是从业者该有的习惯。4.2 多端支付接入实操网站、App、小程序到底差在哪搜索里高频出现的一个问题是“做网站如何支付”很多从零接支付的同学经常在这块绕弯。做一个网站需要收益核心就是对接微信支付或支付宝。基本流程是注册商户号签约对应的产品然后后端根据支付平台文档创建预支付订单拿到支付链接跳转支付最后处理异步回调。整个过程最繁琐的不是代码而是各种配置和签名问题。另一个高频问题是“uniapp打包App支付和微信小程序支付时支付流程和参数是否相同”。这个问题的答案很明确完全不同。虽然统一下单的后端接口在创建订单层面类似但拉起支付层的参数差异很大。微信小程序支付后端要先调用微信商户平台的统一下单接口获取prepay_id然后返回一组timeStamp、nonceStr、package、signType、paySign参数前端用wx.requestPayment拉起支付。而App支付需要接入微信开放平台的SDK后端返回的是partnerid、prepayid、timestamp、nonceStr、package、sign等参数安卓端通过微信SDK唤醒微信iOS端也需要配置对应的URL Scheme和SDK回调。这里有个经验之谈不管你用uniapp还是原生开发后端一定要区分支付渠道按渠道返回不同的支付参数结构。一个接口返回统一JSON前端再根据当前端类型取不同字段是最容易踩坑的设计。我更推荐后端直接暴露两个不同的接口比如/pay/wx/miniapp和/pay/wx/app各自返回对应前端的参数。数据结构清晰排查问题也方便。关于“微信小程序可以加入支付宝支付渠道吗如何设计”这个问题也需要说清楚。微信小程序作为一个封闭宿主环境内置的支付能力只有微信支付直接在小程序内调用支付宝是不行的。如果业务上必须支持支付宝常见的做法是把支付动作引导到H5页面在H5里集成支付宝的网页支付SDK或者引导用户到App端使用支付宝App支付。做这类设计时要遵守微信小程序平台对虚拟支付和跳转的相关规则页面跳转、唤起支付的方式都要按平台约束来实现别为了功能强行绕过平台限制合规风险比支付风险更致命。说一下gin-vue-admin接入支付宝这类的后端场景。这套技术栈里接支付宝本质还是那几步先申请支付宝应用的AppID配置RSA2密钥对用openssl命令生成应用私钥和应用公钥把公钥填到开放平台再把支付宝公钥下载到后端。后端用alipay-go-sdk之类的库初始化一个客户端调用TradePagePay或WapPay接口生成支付表单设置好NotifyURL指向自己的回调方法。最关键的一步是回调验签签名验证代码建议直接用SDK提供的方法不要自己拼字符串。4.3 支付网关设计的一点建议聊完接入细节还是想给正在做支付系统的同学一个整体建议不管业务多复杂前端有多少个端后端最好都收敛到一个“支付网关”。这个网关统一负责三件事第一签名与验签标准化所有外部支付平台的请求和响应都在网关层完成加解密第二业务字段标准化不管是微信、支付宝还是苹果IAP进到网关后都换算成内部统一的订单模型第三幂等与对账公共逻辑网关提供统一的幂等校验和定时对账任务。按这套思路设计支付模块后续新增支付渠道会很省事。你只需要在网关层新加一个渠道适配器把外部平台的回调翻译成内部统一的“支付成功事件”根本不用动上层业务代码。这个思路和很多人关心的“支付网关设计文档PRD”是吻合的平时做技术方案评审我建议把支付网关作为独立模块去设计。5. 常见问题速查与修复优先级建议5.1 支付系统常见问题速查表把测试过程中遇到的高频问题整理成一张速查表方便开发和安全同学排查。这里挑几个最常见的现象可能原因排查建议微信支付提示“签名错误”参数顺序不一致、空值未剔除、编码不一致、密钥配置错误用官方SDK验签比对拼接串和官方文档重点检查大小写和空值支付回调验签失败支付宝公钥配错、使用了应用公钥而不是支付宝公钥到开放平台重新下载支付宝公钥核对配置的是“公钥”还是“应用公钥”支付成功但订单未更新回调处理异常、验签失败被拦截、回调日志丢失查看网关日志确认回调是否到达用支付平台订单查询接口主动核对用户重复点击导致多笔订单下单接口缺少幂等用业务订单号或用户ID加商品ID做幂等重复请求直接返回已创建订单用户未付款但订单显示已支付状态机被绕过、回调伪造成功检查所有可修改订单状态的接口确认只有后端事件能驱动状态流转退款金额超过原支付金额退款金额未校验上限、退款流程不幂等退款单关联原始支付流水加可退余额校验和唯一索引苹果IAP用户退款后仍能用虚拟商品未处理退款通知、服务端未重新校验收据接入App Store服务端通知定时重新校验receipt凭证谷歌支付报错OR-PFGVEM包名、SHA-1指纹不一致或服务器校验失败核对Google Play配置的包名和签名证书指纹服务端用Google Play Developer API二次确认5.2 漏洞修复优先级与验收清单修复逻辑漏洞不能东修一块西补一块要有优先级。我的习惯是先按资金风险和攻击难度排序第一梯队直接造成资金损失的比如金额篡改、回调伪造、退款超扣、并发重复扣款这类必须立即修复第二梯队造成数据泄露或越权操作的比如订单越权查看、订单信息遍历这类影响面大但不如资金损失紧急可以排在一周内第三梯队影响体验和合规的比如签名错误、支付成功状态不同步这类按正常迭代节奏处理就行。修复后的验收我建议至少回归这几个用例用Burp拦截下单请求修改价格参数确认最终订单金额不变伪造支付回调请求确认服务端拒绝并记录日志并发发送10个回调请求确认订单只被处理一次使用用户A的Token操作用户B的订单确认被拒绝对已退款订单再次发起退款确认无法成功。这五项测试全部通过这轮攻防修复才算真正落地。6. 最后一点个人体会做了一阵子支付安全之后我最大的体会是逻辑漏洞没法靠一次渗透测试解决它更像是一个需要在每次业务迭代时反复检查的工程习惯。支付系统不是上线后就一劳永逸的只要业务方加了新玩法、商品多了新类型、渠道接了新平台就都会引入新的信任假设也就可能带来新的漏洞。我个人的习惯是每次有支付相关需求评审都要拉上开发和安全一起过一遍“信任边界清单”这个需求里哪些数据来自客户端哪些状态可以被用户触发哪些接口需要幂等哪些回调需要二次确认真实性。把这些问题变成评审会议的固定议程比事后补漏洞要省力得多也比出一次资金事故再复盘要体面得多。
返回列表