ARTICLE DETAIL

资讯详情

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

支付逻辑漏洞排查指南:8类常见漏洞与修复方案

支付逻辑漏洞排查指南:8类常见漏洞与修复方案 做了这么多年支付风控和渗透测试我最深的体会是真正让企业一夜之间损失惨重的往往不是SQL注入、不是RCE而是那些看起来人畜无害打起来刀刀见血的支付逻辑漏洞。它不依赖你用了什么框架、什么中间件只依赖你写业务代码时那一两个“想当然”的判断。从微信支付回调没验签到优惠券可以重复领取再到订单号换个数字就能查别人订单每一条都是真实发生过的线上事故。这篇文章就以支付场景为靶场拆解8类最常见的逻辑漏洞每类都会给攻击思路、复现方法、修复方案也会聊一聊大家踩坑最多的第三方支付集成问题比如微信支付signature错误、uniapp打包App和微信小程序支付参数不一致、支付宝沙箱测试、苹果IAP退款等。适合正在做支付模块的开发、搞安全测试的同学也适合想系统排查一遍自己业务里有没有同类问题的架构师。1. 支付逻辑漏洞的认知框架与分类1.1 为什么支付场景是逻辑漏洞的重灾区支付业务天然就是逻辑漏洞的沃土因为它不是单一功能而是由商品、订单、用户、金额、优惠券、支付渠道、异步回调、退款等多个子系统拼起来的。每个子系统之间通过接口传递参数、通过状态字段流转一旦某个环节的设计者默认“客户端传上来的值都是合法的”或者默认“这一步只有我们自己系统会调用”漏洞就出现了。传统漏洞比如SQL注入、XSS本质是代码层面的输入输出处理不当扫描器可以靠特征识别。但逻辑漏洞没有通用特征它是对业务规则本身的违背。比如后端没校验金额就入库这在扫描器看来就是一个普通的整数参数响应还是200只有人肉去改一下1分钱才能发现问题。这也是为什么很多企业安全扫描报告一片绿却被羊毛党薅到倒闭。支付逻辑漏洞的攻防本质是攻击者与业务设计者之间的规则博弈。攻击者不需要精通二进制汇编需要的只是对支付流程足够熟悉然后用Burp Suite改几个参数、重放几个请求、并发抢一下就能找到绕过路径。反过来防御方也必须比攻击者更懂自己的业务流程把所有不变量找出来逐一加上校验。1.2 攻击者眼中的“钱”与8类漏洞速览我把支付场景里高频出现的逻辑漏洞归纳成8类下面这张表先给大家一个整体视角。需要注意的是真实攻击中往往不是单点利用而是两三类漏洞组合使用比如用并发漏洞和优惠券漏洞搞出0元单或者用越权和退款漏洞盗刷他人资金。编号漏洞类别核心问题典型后果1价格与参数篡改后端信任客户端提交的金额、数量、折扣1分钱下单、负数金额、0元购2用户与订单越权用订单号或用户ID访问不属于自己的资源查看/操作他人订单、盗刷他人账户3订单状态逻辑绕过状态机校验缺失可跳过或倒退未支付直接发货、重复确认收货4并发与重放缺少幂等控制同一请求可多次生效优惠券重复使用、回调重复入账5优惠与积分规则漏洞规则设计粗糙可组合套利无限领券、积分篡改、负折扣6退款/逆向流程漏洞退款不与原支付单正确绑定超额退款、重复退款、退款不削权7回调通知验签缺失异步通知未验签、未验金额、未防重伪造支付成功通知直接改订单状态8第三方支付集成与配置漏洞沙箱/正式环境混淆、各端参数不一致、签名错误支付卡单、无法调起、被渠道拒付这8类并不是完全平行的第7类回调通知和接口集成是重灾区我遇到过的安全事件里至少有一半最后能追溯到“回调没验签”或者“回调没做幂等”。下面逐类展开重点讲攻击手法和修复要点。2. 高频漏洞第一梯队金额、越权、状态机与并发2.1 金额与参数篡改别相信客户端传上来的价格这是最直观、也最容易修的一种漏洞但仍然在大量系统中反复出现。典型场景是下单接口长这样前端把商品ID、数量、单价、优惠金额、总金额全部POST到后端后端没有回表查商品价格直接用接收到的金额生成订单。攻击者只要把总金额改成0.01甚至改成负数订单就成立了。测试手法也很简单用Burp Suite代理开启拦截正常下单走一遍流程把创建订单的请求拿出来逐个修改这几种字段amount / total_fee / totalPrice 改小比如改成0.01quantity / count 改成负数或小数discount / couponAmount 改成负数currency 或币种字段比如把CNY改成JPY利用汇率差特别注意几点有些系统做了金额校验但只校验了客户端传过来的“单价×数量总价”没校验证后端商品表里的价格攻击者可以把单价改成“后端不存在的历史低价”只要乘出来相等就能绕过。还有的系统把“运费”“税费”也作为可传参数这里同样可以做手脚。正确的修复方式很明确所有与钱相关的数值一律以服务端数据库为准。下单接口只接收商品ID和数量金额由服务端按下单时的商品表价格、优惠规则重新计算。如果要保留前端展示友好性也可以接收金额但必须与后端计算值比对不一致就拒绝。任何客户端可改的字段都不能作为记账依据。2.2 越权操作用户ID、订单号、支付单号的裸奔越权在支付场景里的表现形式非常丰富而且往往危害更大。最常见的是水平越权两个用户A和BA登录后调用“查询订单详情”接口请求里带的是orderId1001攻击者把orderId改成1002响应返回了B的订单。这看起来只是信息泄露但如果这个接口同时支持“取消订单”“申请退款”“重新支付”那我就能取消别人的订单或者用自己的支付方式去支付别人的订单甚至把别人的订单退款退到自己账户里。更隐蔽的越权是通过支付单号。很多系统为了对接第三方支付会生成一个out_trade_no商户订单号或prepay_id这个号有时候是纯数字、可遍历的。如果查单接口、支付结果查询接口、退款接口直接信任这个号码而不校验归属用户攻击者就能操作任意一笔交易。我常用的测试方法是准备两个测试账号A和B各自下一个单然后互通有无用A的登录态去访问B的订单详情、取消B的订单、给B的订单发起退款看有没有被拒绝。还可以抓包对比正常请求和越权请求看后端是否从Session里取用户ID还是从请求参数里取。修复思路分两层。第一层用户身份必须来自服务端会话不能由客户端传参。第二层任何订单、支付单、退款单查询或操作都要追加归属校验SQL里强制带上WHERE user_id 当前用户。不要觉得多写一层判断麻烦这层判断是支付系统的安全底线。2.3 订单状态逻辑绕过先发货后付款或重复确认收货订单状态逻辑漏洞本质上是对状态机的破坏。正常情况下一个订单的生命周期是待支付 - 已支付 - 已发货 - 已完成。如果系统在“待支付”状态下就允许调用“发货”接口或者在“已支付”状态下还能继续发起支付、然后又走一遍退款就会产生资金和货物都对不上的局面。一个经典案例是这样的商品详情页下单后支付收银台在客户端弹出来但有些系统为了用户体验做了“后端自动创建支付单并轮询状态”同时把“确认支付”接口暴露了出来。攻击者在Burp里把这个接口的请求看一遍发现它只校验了订单编号没有校验该订单是否真的完成了第三方支付于是直接调用这个接口订单状态就变成了“已支付”。配合上发货自动化等于0元下单。还有一种是逆向逻辑绕过。订单已经完成了退款但用户还能对订单发起“确认收货”把虚拟权益也领一遍。这属于状态机没有限制单向流转允许倒退回更早的状态或者允许在一个终态上继续执行动作。这一类漏洞的修复不是简单加几个if判断而是要建一个清晰的订单状态机。建议用一个枚举或状态表定义每个状态允许的动作和前置状态。比如只有状态为“待支付”才允许调起支付只有状态为“已支付”才允许发货只有状态为“已发货”才允许确认收货“退款中”“已退款”是终态任何其他操作都不能改变它此外所有状态变更要在同一个事务里更新并使用乐观锁版本号防止并发下两个请求同时把订单从“已支付”改成“已发货”和“已退款”导致一笔订单出现两个终态。2.4 并发请求与重放同一个订单和优惠券被薅穿并发漏洞是支付场景里最容易打、也最容易被忽视的一类。核心问题是系统缺少幂等控制导致同一个请求被同时发多次或者同一份资源被多线程重复消费。举个例子用户下单时使用了“新用户立减50元”优惠券这个优惠券在数据库里是coupon_id statusNORMAL。正常逻辑是下单时把优惠券状态改为USED。但如果没有对优惠券ID加唯一索引或加锁两个线程同时读到NORMAL都判断“可以使用”然后先后改为USED从逻辑上两张订单都占用了同一张优惠券。这个优惠券可能只能使用一次也可能允许重复使用重点在于攻击者可以利用并发把N次核销变成N1次。支付回调的重放问题更致命。第三方支付成功后会异步通知我们服务器的notify_url如果回调处理逻辑是“收到通知 - 验签成功 - 更新订单为已支付 - 给用户加余额”那么只要攻击者截获一次合法通知然后用任何能模拟HTTP请求的工具Burp的Repeater、curl脚本都行反复发送同一份通知每发一次用户余额就加一次。这种情况在真实世界里频繁出现因为很多开发者只做了“所以看状态再处理”比如先查订单是不是已支付不是才更新。但如果第一遍通知处理完后忘了把订单状态改掉重放就能穿透。修复并发重放我一贯推荐三板斧为回调通知、优惠券使用、退款申请这类写操作加唯一索引比如回调流水表里的event_id order_id 唯一重复插入直接报错在处理关键业务之前用分布式锁或数据库悲观锁锁住订单/优惠券行记录防止两个事务同时读取相同状态对客户端重复请求做幂等键同一个业务唯一标识比如订单号在短时间内只允许处理一次。这几种手段不是选一个而是最好都上。唯一索引防重复事件锁防并发状态错乱幂等键防用户误触和重放。3. 第二梯队优惠、退款、回调和第三方集成3.1 优惠券与积分负数金额和套利组合拳优惠和积分是羊毛党最常盯的两块。原因很简单它们的计算规则复杂涉及比例、阈值、互斥、叠加、返还任何一条规则没想清楚都能变成套利工具。经常翻车的情况有这么几种优惠券领取接口没有防重直接用脚本无限领券下单时优惠金额由前端传改成负数后退款时倒赚积分抵现金没有下限积分余额不足也能抵扣抵扣后积分变成负数组合使用多张优惠券叠加上限只校验了单张没校验总和跨店满减一个订单拆成多个子订单用同一张券在多个子订单里同时核销。实操中我会先抓下单请求看哪些字段和优惠相关然后构造一个“总金额0但优惠金额总金额”的请求看系统是拒绝还是生成负数的应付金额。有些系统处理负金额时会“返还”到用户余额里这就是直接被薅钱了。还需要测试并发核销用Burp Intruder同时发送多个携带同一个优惠券ID的请求看返回结果。修复维度也分三层。第一优惠券和积分的使用必须走服务端规则引擎所有优惠金额由后端统一计算不接收客户端的折扣值。第二使用记录要加唯一约束一张券只能在一个订单里核销一次积分扣减记录也要有唯一流水号。第三数值范围校验必须有下限优惠金额最大只能到应付金额不出现负应收。第四对组合优惠做总量校验所有优惠累加不能超过商品总价。3.2 退款流程中的漏洞金额、次数、权限都容易翻车退款是逆向流程但攻击难度比正向支付低很多。多数系统在支付环节做了层层校验到了退款接口反而放飞了。我遇到过几种典型的退款漏洞退款金额由前端传入攻击者把退款数额改成大于实际支付金额系统直接通过并发起退款退款时只校验订单已经支付不校验退款总额是否已超过实付金额一笔订单可以退款多次每次都退全额对虚拟商品用户购买后立即申请退款但系统没有在退款成功后收回虚拟权益相当于免费嫖了一次演唱会直播、一份会员或一套课程调用第三方退款接口时把out_refund_no退款单号改成另一个订单的号做成“同一笔退款单应用到两个订单”的交叉退款。测试退款接口有个小技巧先支付一笔小额订单然后发起退款并抓包修改refund_amount为订单金额的倍数观察是否被拒绝再正常退一次款同一订单再次发起退款看第二笔是否成功最后检查退款被接受时用户虚拟权益有没有被同时冻结。修复上退款金额必须与原支付单的实付金额、已退款金额做累计校验简单写就是本次退款金额 实付金额 - 已退款金额。这笔逻辑要在数据库事务里通过行锁或SQL条件更新实现比如UPDATE 退款流水 SET ... WHERE 累计退款 实付金额如果影响行数为0就拒绝。同时退款成功后必须联动更新权益状态虚拟商品要收回相应的权限或会员时长。3.3 回调通知验签缺失与重放攻击这是支付逻辑漏洞里最“容易出事故”的一个环节我单独拿出来细讲。支付回调的本质是异步通知支付宝、微信都会在用户支付成功后向商户配置的notify_url发送一条HTTP请求里面带有订单号、交易金额、签名等信息。这条通知是外部输入的但很多系统在处理它时表现得像处理内部RPC一样随意。最常见的问题有三个完全不验签只判断字段里有“success”或订单号就更新订单验签逻辑错误比如用错了证书/公钥或者验签时没把需要参与签名的参数取全验签通过后没有校验通知里的金额与订单金额是否一致攻击者伪造一个签名正确的通知但把金额改成任意值。还有一类问题是验签正确但没校验商户号。在多商户平台上攻击者可以用甲商户的支付成功通知带着自己的商户号和订单号打到乙商户的notify_url如果系统只验签不验商户身份乙商户的用户也能收到虚假到账。重放攻击在这里尤其致命。第三方支付为了确保通知送达本身就会重试多次每重试一次后端回调函数就会被调用一次。如果回调处理函数没有把“已处理”和“未处理”区分开那每次重试都会重复执行加余额、发奖励、更新订单等操作。正常情况下的重试已经是安全事故更不用说攻击者手动重放了。正确做法可以总结成四步验签必须使用对应支付渠道的官方SDK或公钥验签参数按渠道规则拼接验签通过后校验商户号、订单号、金额必须与本地订单完全一致处理前查询回调流水表同一通知ID/同一订单已处理过就直接丢弃业务更新加钱、加权益、改状态与“标记回调已处理”要在同一个事务里完成确保要么一起成功要么一起失败。这四步缺一不可。哪怕你第三步幂等做得很好没有第一二步攻击者照样能伪造出“验证通过”的通知——所以顺序也很重要先验签、再验金额、最后查重处理。3.4 用户态签名错误与第三方支付集成的那些坑顺着热搜词里出现的“微信支付提示用户态签名signature错误”我讲讲这类集成问题。严格说这不算攻击漏洞但它经常导致支付功能不可用用户一路折腾最后流失而且很多团队的查错方式也很痛苦。“用户态签名”错误通常发生在我们从小程序或APP调起微信支付的时候。微信支付要求客户端调起支付前后端先生成预支付订单再用预支付交易会话标识prepay_id和其他参数拼一个签名这个签名要参与调起。报signature错误多数是这几种原因后端生成签名时使用的AppID和当前登录小程序/APP的AppID不一致尤其是同时做了小程序和App共用一个商户号但AppID传混了参与签名的参数名写错比如把package写成package_val或者大小写不一致微信官方文档对参数名的要求非常严格签名key用错商户API密钥、API v3密钥、证书序列号搞混不同接口用的密钥不一样忽略了时间戳和随机字符串的生成规则同一个参数在不同的调用里没有更新。排查这类问题我会先把微信支付官方示例代码的参数拼接顺序找出来逐项比对后端输出的签名原串再检查当前请求里的appId是否与后端配置一致。最快的方法是在后端临时打印签名原串将它与微信官方诊断工具生成的签名比对Diff一下就能定位是参数还是密钥问题。还有一个高频问题uniapp打包App支付和微信小程序支付时支付流程和参数是否相同。答案是“后端统一下单是一样但前端调起方式完全不同参数也不同”。小程序支付用wx.requestPayment需要传入timeStamp、nonceStr、package、signType、paySignApp支付用微信开放平台SDK的WXPayTask或者当前客户端环境的拉起方法需要传入appId、partnerId、prepayId、nonceStr、timeStamp、sign。这两套参数的来源字段名都有区别如果直接把小程序的返回数据拿去给App端用大概率调不起支付。更隐蔽的是安卓系统唤醒微信支付以后用户可能又回到APP但APP在前台没有自动刷新订单状态。很多开发者以为支付回调一定能到达后端然后由后端推送消息给APP实际上回调是有延迟的甚至个别情况会丢失。所以正确做法是在APP从后台回到前台时主动调一次后端“查询订单支付结果”接口不要只依赖回调。这也是我在集成排障时反复强调的一点。关于支付宝沙箱测试时要注意沙箱环境和正式环境的“密钥对、网关、AppID”完全不同经常有人把沙箱的公私钥配到正式环境导致下单报错或无法调起。另外微信小程序是否可以加入支付宝支付渠道按照平台规则微信小程序内不允许直接唤起支付宝只能通过“跳转浏览器打开”或引导用户复制链接到支付宝完成支付但很多业务又想要支付宝渠道这就得做H5跳转方案或者引导下载App。在设计支付网关时最好在PRD阶段就把各端渠道写清楚避免开发时在不同平台之间跳来跳去。谷歌支付失败OR-PFGVEM这个错误码通常表示用户账号或付款方式异常比如账号未登录、未绑定有效的付款方式、被风控拦截。它提示了支付失败但没有给出具体原因。我们在做Google Play内购时要结合订单查询接口和用户账号状态进一步判断不能只把这个错误当作业务拒绝否则会把一部分正常用户挡在付费门外。4. 攻防实战从攻击链到防御链我的通杀排查清单4.1 测试前的准备与接口清单梳理支付逻辑测试不是拿起Burp就乱打而是要有章法。建议按下面的步骤来先搞清楚业务流用户从加购到下单、支付、回调、发货、退款哪些环节有接口每个接口的请求参数、上下文状态是什么最好画一张接口调用时序图准备两个以上测试账号分别拥有不同的身份、优惠券、积分和订单方便做越权测试在测试环境开启支付宝沙箱、微信支付测试号或者使用可用回调模拟器不要直接用生产环境刷真实支付用Fiddler或Burp抓包一次完整的正常支付流程把所有请求保留下来作为后续篡改的模板。有一点必须提醒逻辑漏洞测试要严格限定在你拥有权限的系统内进行。找别人系统的漏洞并在未授权情况下利用是违法行为。这篇文章的所有思路都服务于安全评估和自建系统加固不是让你去薅别人羊毛。4.2 八个排查动作与对应测试手法下面把8类漏洞对应的排查动作整理成一张速查表。每次做支付专项测试时我基本就是照着这个表一个一个过的效率很高而且不容易漏。漏洞类型测试动作关注响应与状态金额篡改修改下单/支付的金额、数量、币种、优惠字段是否接受非正常值并生成订单越权用A账号访问B的订单详情、退款、取消接口是否返回完整的B用户数据或操作成功状态绕过直接调用发货、确认收货、关闭订单接口是否在未支付/已退款状态下执行成功并发重放同一下单/核销/回调请求多线程发送是否产生多张订单或多个入账优惠积分重复使用同一券、积分抵现负数、超限叠加是否库存异常或应收为负退款超额退款、重复退款、退款后不削权是否累计退款超过实付、权益未收回回调验签改动回调参数、重放回调、替换商户号是否仍被业务逻辑接受第三方集成切换沙箱/生产配置不同端调起参数比对签名错误、支付卡单、跨端流程不匹配测试时有个小习惯每改一个参数对比一下响应与正常响应的差异然后重新跑一遍正常流程防止误测。实际漏洞往往不是一次改一个字段就能触发的而是要同时改两个字段。比如既要改订单号实现越权又要改退款金额实现超额退款组合拳比单点测试更能碰到真实问题。4.3 修复与加固从代码到架构的几道必须做的“保险”如果让我给一个支付系统做安全设计下面这几道保险是标准配置缺一不可。第一道是“服务端不信任客户端数据”。所有影响资金计算和资格判断的参数能不用客户端传的就不用。商品ID、数量、用户ID可以从会话或服务端缓存取金额由后端统一计算。第二道是“订单状态机严格单向流转”。用状态枚举和动作权限表管理订单流转任何状态变更都必须通过统一的service方法里面先锁行再判断前置状态。不要在控制器里到处写if (order.Status 1)这种散装的判断很容易漏。第三道是“所有资金操作幂等”。订单表、优惠券使用表、退款流水表、回调流水表都要有唯一的业务键用数据库唯一索引作为兜底。同时在高频操作上加分布式锁或redis锁。第四道是“回调与查询对账双保险”。支付结果不要只依赖回调还要定期调用支付宝、微信的订单查询接口做主动对账。这样可以发现回调丢失、回调重放、金额不一致等异常。第五道是“敏感操作日志与风控”。每笔下单、支付、退款都要记录用户ID、设备指纹、IP、金额、订单号并设置阈值告警。比如同一用户秒内多次发起退款或者同一IP批量使用不同用户ID下单都应当被风控规则捕获。4.4 支付集成常见错误速查与解决思路结合前面提到的一些热词把真实开发过程中最容易踩的坑整理一下现象可能原因解决思路微信支付调起时报“用户态签名signature错误”AppID/商户号不匹配、参与签名字段拼错、API密钥用错打印签名原串逐项核对检查appId和密钥类型uniapp打包App支付和微信小程序支付流程参数看起来一样后端统一下单可复用但前端调起参数和SDK不同根据平台判断组装不同的调起数据App端使用openSDK的拉起方式安卓系统从微信返回APP后订单仍显示未支付回调延迟或丢失APP未主动刷新在onShow生命周期里调查询接口主动向服务端获取结果微信小程序想加入支付宝支付渠道平台不允许小程序内直接唤起支付宝做H5跳转或引导复制链接到支付宝考虑在小程序外完成支付gin-vue-admin配置支付宝支付后一直调不起私钥/公钥配置混淆、沙箱与生产网关混用按支付宝官方规范区分商户私钥和支付宝公钥检查网关地址是否为生产环境谷歌支付失败OR-PFGVEM账号状态异常或付款方式无效引导用户检查Google Play账号结合后台订单接口二次确认苹果IAP退款后用户仍能用付费功能退款回调只通知了支付平台没关停应用内权益监听App Store Server通知在退款事件发生后调整用户权限这些坑大多不是算法难度问题而是环境配置和流程设计问题。我建议在做支付模块时先写一份“支付网关设计文档”明确各端的支持渠道、调起方式、回调地址、错误码映射、对账机制再开始编码。否则等联调阶段再回头改成本会高出好几倍。做了这么多年支付安全测试我最大的感受是逻辑漏洞的攻防拼的不是谁漏洞库多而是谁更了解自己的业务流程。在你写代码的时候永远要问一句“这个接口如果被不认识的人直接调用会发生什么”把反问变成习惯支付系统的安全性就会上一个台阶。最后再分享一个小技巧每次发布支付相关代码前用上面那个排查表过一遍成本很低但能避免大多数线上事故。
返回列表