
1. 项目概述从“退款零元购”看支付业务逻辑漏洞最近在SRC安全应急响应中心的实战挖掘中遇到一类非常典型且危害巨大的漏洞——“退款导致零元购”支付漏洞。这个标题听起来可能有点拗口但说白了就是攻击者利用业务系统中的退款逻辑缺陷在支付成功后申请退款但退款后商品或服务并未被收回最终实现了“不花钱买东西”或“低价买高价物”的效果。这可不是简单的“薅羊毛”而是实打实的业务逻辑漏洞直接导致企业资金损失和资产流失是SRC中高价值漏洞的常客。这类漏洞的核心往往不在于复杂的代码执行或权限绕过而在于对业务流程理解的深度。开发者在设计“支付-发货-退款”这个闭环时如果对各环节的状态校验、数据一致性以及逆向流程的处理考虑不周就会留下致命的安全隐患。对于刚入门SRC挖掘的朋友来说支付逻辑漏洞是一个绝佳的切入点它不需要你掌握多么高深的二进制或代码审计技巧更需要的是耐心、细心和对业务场景的模拟推演能力。接下来我将结合实战经验为你拆解这类漏洞的成因、挖掘思路、测试方法以及修复建议希望能帮你打开支付业务安全测试的大门。2. 漏洞原理深度剖析状态机混乱与数据不同步要理解“退款零元购”我们必须先理解一个正常的在线支付流程应该是什么样的状态流转。一个健壮的支付系统本质上是一个严谨的状态机。2.1 理想状态下的支付闭环在一个设计完善的系统中订单、支付单、物流单或虚拟商品发放记录之间应该保持强一致性。其核心状态流转大致如下订单创建用户下单生成订单状态为“待支付”。支付发起用户选择支付方式系统生成支付流水号订单状态可能变为“支付中”。支付成功回调支付渠道如支付宝、微信支付通知系统支付成功。系统验证回调签名和金额无误后将订单状态更新为“已支付”同时触发后续业务逻辑如扣减库存、生成发货单、发放会员权益等。履约对于实物商品进入“已发货”状态对于虚拟商品或服务直接标记为“已完成”或“已使用”。退款申请用户在允许的退款周期内发起退款。退款审核与执行系统检查订单状态是否已发货/已使用、退款规则等。审核通过后调用支付渠道接口进行资金原路退回。退款成功回调支付渠道通知退款成功。系统必须将订单状态更新为“已退款”并执行逆向业务逻辑例如回滚库存、标记权益失效、关闭已发货订单的物流等。闭环完成订单生命周期结束。2.2 漏洞产生的核心逻辑缺陷“退款零元购”漏洞就爆发在上述流程的第7步——“退款成功回调”的处理上。系统在这里犯了两个关键错误缺陷一状态更新与逆向业务逻辑执行不同步。这是最常见的问题。系统在收到退款成功回调后仅仅将订单的“支付状态”更新为“已退款”或者只是在数据库的order表里写了一条退款记录但没有触发或完整执行与之关联的逆向业务逻辑。对于实物商品订单状态变成了“已退款”但之前生成的“发货单”状态依然是“已发货”或“运输中”仓库和物流系统并不知道这笔订单已经退款商品照常配送给用户。对于虚拟商品卡券、会员、积分等退款后用户账户里收到的卡券、激活的会员权益、到账的积分依然有效没有被系统回收或标记作废。对于服务类商品课程、预约退款后用户依然可以访问已购买的视频课程或者享受已预约的服务。缺陷二缺乏最终状态校验或校验逻辑可被绕过。某些系统会在用户尝试使用商品或服务时如核销券码、观看视频、下载文件做一次“订单是否有效”的校验。但如果这个校验逻辑不严谨就可能被绕过。案例系统校验时只检查订单的“支付状态”是否为“已支付”但退款后支付状态可能被更新为“部分退款”或一个独立的状态字段而主订单状态仍是“已完成”。如果校验逻辑只依赖主状态漏洞就产生了。更隐蔽的案例退款操作和权益回收操作不是原子性的。可能在极短的时间窗口内退款已完成资金已退回但回收权益的定时任务还未执行。攻击者利用这个时间差快速消费掉权益如兑换成其他不易回收的资产从而达成“零元购”。简单来说漏洞的本质是资金流退款和业务流发货/发放在逆向过程中脱钩了。系统没有将“退款”这个金融操作与“收回已提供的商品或服务”这个业务操作进行强制绑定。3. 实战挖掘方法论从黑盒到灰盒的测试思路挖掘这类漏洞不能靠瞎点需要有清晰的测试思路。我通常将其分为三个阶段信息收集、业务流程梳理、漏洞探测与验证。3.1 信息收集与业务理解这是所有测试的第一步也是最关键的一步。你需要把自己当成一个真正的用户甚至是一个产品经理去理解这个业务。枚举业务类型目标网站或APP主要卖什么是实物百货、生鲜、虚拟商品代金券、软件激活码、数字内容电子书、在线课程还是服务酒店预订、家政服务不同类型的商品其发货、核销、退款逻辑差异巨大。寻找退款入口在“我的订单”页面仔细寻找退款、售后、申请开票等入口。注意不同状态的订单待发货、已发货、已完成所展示的按钮可能不同。阅读退款规则仔细阅读网站公示的退款规则或用户协议。重点关注哪些商品支持退款退款周期是多久7天无理由退款处理时长是多久退款路径是什么原路退回还是退余额特殊商品如生鲜、定制商品的退款政策是什么这些规则本身可能就隐藏着逻辑冲突。3.2 业务流程梳理与状态映射在正式测试前我习惯画一张简单的状态流转图哪怕只是在纸上画草图。正向流程走查正常完成一次购买支付用Burp Suite或浏览器开发者工具抓取所有请求。重点关注订单创建接口返回的订单号order_id、支付单号pay_id。支付回调接口支付成功后前端或后端调用了哪个接口来更新订单状态这个接口的参数是什么履约接口支付成功后是哪个接口触发了发货或发放权益例如是否有/api/order/deliver、/api/coupon/grant这样的接口。逆向流程走查发起一次正常的退款申请同样抓包分析。退款申请接口提交了哪些参数order_id,refund_reason,refund_amount等。退款状态查询接口退款进度如何查询最关键的一步退款成功回调接口。这通常是支付平台异步通知商户服务器的接口如支付宝的/notify/refund。你需要尝试在测试环境中模拟或分析这个回调的处理逻辑。有时商家会提供一个同步的退款结果查询接口供前端轮询其内部逻辑可能与回调处理逻辑类似。状态字段猜测通过观察不同页面的订单状态展示以及接口返回的JSON数据猜测数据库中可能存在的状态字段。常见字段名如status,pay_status,refund_status,delivery_status,use_status。理解这些字段的组合关系。3.3 漏洞探测与验证技巧有了前面的铺垫我们就可以进行针对性的测试了。测试的核心思想是在退款成功后检查商品或服务的有效性是否被同步回收。技巧一时间差攻击测试适用于退款处理和业务回收非原子操作的系统。购买一个可即时交付的虚拟商品如优惠券。发起退款申请。在退款申请提交后、退款成功前或退款成功但系统未及时处理回收逻辑的瞬间尝试急速使用该商品。例如立刻用优惠券下单买另一个东西或者将卡券赠送给另一个账号。观察结果如果优惠券被成功使用且后续退款也成功了那么漏洞存在。技巧二状态不一致性测试这是最主流的测试方法。购买并完成履约购买一个商品确保其已发货实物显示快递单号或已到账虚拟商品已存入账户。发起退款在允许的退款期内发起全额退款申请。如果是平台自动审核退款等待退款成功如果是人工审核尝试寻找审核逻辑漏洞如重复提交退款申请、修改退款金额参数等使其通过。验证资产留存实物检查订单详情发货信息和物流跟踪是否依然有效尝试联系客服修改收货地址如果还能改说明订单仍被视作有效。虚拟商品登录账户检查卡券包、会员有效期、积分余额看退款对应的资产是否还在且可用。尝试使用它。数字内容尝试再次下载已购的电子书或继续观看已购的课程视频。交叉状态校验如果步骤3发现资产依然有效再次检查订单的各个状态字段。可能pay_statusREFUNDED支付状态已退款但delivery_statusSHIPPED发货状态已发货且order_statusCOMPLETED订单状态已完成。这种状态组合就是漏洞的铁证。技巧三绕过最终校验测试针对那些在使用环节有校验的系统。完成购买和退款资产未被回收。在尝试使用资产时如核销券码抓取校验接口的请求。分析该接口校验了哪些参数。通常会是order_id、user_id、token。尝试修改请求比如将order_id替换成一个未退款的正常订单ID或者分析其校验逻辑是否只依赖了有缺陷的状态字段。如果校验逻辑在客户端前端JS可以通过修改本地JS或直接发送构造的请求来绕过。注意所有测试应在获得授权的测试环境如SRC提供的测试沙箱或对自己账户进行操作。严禁对未授权的生产环境进行测试。4. 漏洞案例场景化复现与拆解为了让理解更透彻我们虚构几个典型的场景并拆解其漏洞点。请注意以下案例均为教学演示融合了多种常见漏洞模式。4.1 案例一电商平台优惠券“退款永流传”场景描述某电商平台用户购买一张“满100减20”的优惠券支付10元。购买后券立即发放到用户账户。用户申请退款10元原路退回但账户中的优惠券依然存在且可使用。漏洞复现步骤用户A支付10元购买优惠券C。平台接口/api/coupon/buy处理购买逻辑在coupon表生成一条记录status为active可用order_id关联到支付订单O1。用户A在“我的订单”页面对订单O1发起退款。调用接口/api/order/refund/apply。平台审核通过或自动通过调用支付渠道退款接口。支付渠道异步通知平台退款成功调用平台的回调接口/notify/pay/refund。漏洞点/notify/pay/refund接口的处理伪代码如下def refund_notify_handler(pay_order_id, refund_amount): # 1. 验证签名略 # 2. 更新支付订单状态 pay_order PayOrder.get(idpay_order_id) pay_order.status REFUNDED pay_order.save() # 3. 找到对应的业务订单 biz_order Order.get(pay_order_idpay_order_id) biz_order.refund_status SUCCESS biz_order.save() # 4. 发送退款成功消息可能给用户发短信 send_message(biz_order.user_id, 您的订单已退款) # 缺失的关键步骤没有回收优惠券 # coupon Coupon.get(order_idbiz_order.id) # coupon.status INVALID # 应将券状态置为失效 # coupon.save() return success结果用户A的支付记录显示已退款订单状态也显示已退款。但查询用户A的优惠券列表券C的状态仍是active。用户A可以使用券C进行消费实现“零元购”。漏洞根因退款回调处理函数只完成了金融状态支付订单、业务订单的退款状态的更新完全遗漏了与这笔订单绑定的实际业务资产优惠券的回收操作。业务模块发券和支付模块退款之间没有通过事件驱动或事务进行强关联。4.2 案例二在线教育课程“退款任我行”场景描述用户购买一门付费视频课程支付后即可观看。用户申请退款后钱款退回但课程访问权限未被关闭用户依然可以观看全部视频。漏洞复现步骤用户B支付199元购买课程《SRC挖掘实战》。平台调用/api/course/access/grant在user_course_access表中添加一条记录user_idB, course_id123, access1。用户B申请退款。平台处理退款成功。漏洞点退款处理逻辑中可能只关闭了订单或者仅在一个“课程订单关系表”里标记了退款但没有去更新最关键的用户课程访问权限表user_course_access。更隐蔽的一种情况权限校验逻辑有缺陷。用户每次点击播放视频时前端会调用/api/course/123/play接口。该接口的后端校验逻辑可能是def check_course_access(user_id, course_id): order Order.query.filter_by(user_iduser_id, course_idcourse_id, pay_statusPAID).first() if order: return True # 有已支付的订单允许访问 return False退款后订单的pay_status被更新为REFUNDED。但上述校验逻辑是查找pay_statusPAID的订单自然找不到于是返回False。等等那用户不是不能访问了吗这里可能还有另一个逻辑系统为了性能可能在用户第一次成功购买时就在Redis或内存中缓存了用户的课程访问权限列表并设置一个很长的过期时间比如7天。退款操作只更新了数据库没有清除或更新这个缓存。导致在缓存过期前用户凭缓存里的权限列表依然可以畅通无阻。漏洞根因多数据源状态不一致。涉及数据库、缓存、甚至文件系统等多个存储位置时退款操作没有保证所有相关数据状态的原子性更新。缓存成了“法外之地”。4.3 案例三联运游戏币“退款双丰收”场景描述在手游中用户通过应用内购买IAP充值游戏币。用户向手机应用商店如苹果App Store申请退款苹果同意退款并将钱退给用户。但游戏服务器没有收到有效的退款通知或处理逻辑有误导致用户充值的游戏币没有被扣除。漏洞复现步骤玩家C在游戏内购买价值648元的宝石包。游戏客户端调用苹果IAP接口支付成功。苹果向游戏服务器发送一个“购买收据”receipt。游戏服务器验证收据有效后向玩家C的游戏账户增加6480宝石。玩家C联系苹果客服声称“孩子误操作”申请退款。苹果经过审核同意退款款项退回玩家C的苹果账户。关键点苹果会向游戏服务器发送一个“退款通知”吗答案是不会主动发送。苹果的服务器不会主动推送退款消息给游戏开发商。游戏开发商需要自己定期通过苹果提供的“服务器通知”Server-to-Server NotificationsV2接口或主动查询收据状态包含cancellation_date字段来获取退款信息。如果游戏服务器没有实现或正确配置这个监听/查询机制它就永远不知道这笔订单已经退款了。漏洞结果玩家C的钱拿回来了但6480宝石还留在游戏账户里实现了“零元购”。玩家可以用这些宝石在游戏内交易市场购买稀有道具再卖给其他玩家变现造成游戏经济系统失衡。漏洞根因对第三方支付渠道的退款机制理解不透彻、对接不完整。很多开发团队只实现了支付成功回调的验证忽略了退款通知的接收和处理。这属于跨系统协同中的信息同步失败。5. 防御方案与安全开发建议知道了漏洞怎么产生的修复和预防的思路就清晰了。核心原则是将退款视为一个需要原子性执行多个子操作的分布式事务。5.1 设计层面的防御状态机驱动严格定义订单、支付、资产商品/权益的状态机并明确状态转换的条件和伴随动作。任何状态变更尤其是“退款成功”都必须触发定义好的后续动作链。事件驱动架构采用消息队列如RabbitMQ, Kafka进行解耦。当“退款成功”事件发布后相关的业务监听器如“库存回滚监听器”、“权益回收监听器”、“物流拦截监听器”各自消费该事件执行自己的逆操作。这确保了即使某个监听器处理失败事件也不会丢失可以重试。事务性补偿TCC模式对于核心交易可采用TCCTry-Confirm-Cancel模式。Try支付时预占库存、生成待发放的权益记录状态为“预占”。Confirm支付成功确认预占的资源将权益记录状态改为“有效”。Cancel退款时执行Cancel操作释放预占资源如果还在预占状态或回收已确认的资源如果已生效并将权益记录状态改为“失效”。对账与稽核建立每日对账机制。将支付系统的退款流水与业务系统的资产变更流水进行比对。如果发现“退款成功但资产未回收”的异常记录立即告警并启动人工或自动化的修复流程。这是最后一道也是非常重要的防线。5.2 编码层面的防御原子操作在退款回调处理的核心函数中将“更新订单退款状态”和“回收资产”放在同一个数据库事务中。确保二者同时成功或同时失败。Transactional(rollbackFor Exception.class) public void handleRefundSuccess(String orderId) { // 1. 更新订单状态为已退款 orderService.updateStatusToRefunded(orderId); // 2. 查询订单关联的所有资产商品、券、权益 ListAsset assets assetService.findAssetsByOrder(orderId); // 3. 遍历回收所有资产 for (Asset asset : assets) { assetService.revokeAsset(asset.getId(), REFUND); } // 4. 记录审计日志 auditLogService.logRefund(orderId, assets); // 事务提交以上所有数据库操作要么全成功要么全回滚 }清除缓存任何导致资产失效的操作如退款、过期、管理员操作都必须同步清理相关的用户权限缓存、资产列表缓存。最终校验加固在资产使用的最终校验环节不要依赖单一状态字段。应进行联合查询确保订单的支付状态、退款状态、资产本身的状态都符合使用条件。-- 校验用户是否有权使用某张优惠券 SELECT COUNT(*) FROM coupon c JOIN order o ON c.order_id o.id WHERE c.id ? AND c.user_id ? AND c.status ACTIVE AND o.pay_status PAID AND o.refund_status NO_REFUND;完善第三方支付对接对接支付渠道时必须同时处理支付通知和退款通知。对于苹果IAP、谷歌支付等必须按照官方最新文档实现服务器对服务器通知的接收和验证或实现定时的收据状态查询任务。5.3 测试层面的验证安全测试和QA测试应在流程中覆盖退款场景。正向用例验证正常退款后资产确实被回收券不可用、课程无法访问、订单物流显示取消。边界用例部分退款后剩余金额对应的资产权限是否准确退款处理中状态为“审核中”资产是否应被暂时冻结多次发起退款申请重复点击是否会导致资产被多次回收或状态异常网络超时、服务重启等异常情况下退款处理逻辑的幂等性如何同一笔退款通知可能被多次调用对账验证在测试环境运行对账脚本验证其是否能准确发现人工构造的“退款未回收资产”的异常数据。6. 常见问题与排查技巧实录在实际的漏洞挖掘和修复过程中会遇到各种各样的问题。这里分享一些常见的坑和排查思路。Q1测试时发现退款后资产确实没了但不确定是漏洞不存在还是我的测试方法不对A1首先确认你的测试资产是“立即生效”型的。比如测试一个需要管理员审核后才发放的实体卡券退款后卡券没发这不能算漏洞。其次检查资产回收的时机。有些系统是“延迟回收”比如退款成功后由一个每小时运行一次的定时任务去批量回收资产。你需要等待足够的时间或者尝试在定时任务运行前急速使用资产。最后使用不同的账号、不同的商品类型多测试几次有些漏洞可能只在特定条件下触发。Q2抓包时找不到退款成功后的业务回调接口怎么办A2有几种可能。第一退款处理是同步的在退款申请接口的同一个请求响应周期内就完成了所有状态更新和资产回收你需要仔细分析这个接口的响应数据流。第二回调接口的路径可能比较隐蔽或者使用了内部域名你无法直接拦截。这时可以尝试在退款申请后立即进行一系列资产操作如尝试使用券并抓取这些操作的请求观察其响应。如果返回“资产无效”或“订单已退款”说明系统做了校验如果成功则可能漏洞存在。你也可以通过查看前端JS代码搜索refund、notify、callback等关键词寻找线索。Q3遇到需要人工审核的退款怎么办在SRC测试中总不能真的让客服给我退款吧A3这是SRC测试中的一个现实难题。有几个思路1)寻找测试环境/沙箱很多大型SRC平台会提供完整的测试环境里面的退款可能是自动通过的。2)测试“极速退款”或“闪电退款”业务一些平台对信用良好的用户提供退款先行垫付服务这本质上是系统自动审核通过可以尝试成为这类用户。3)挖掘审核逻辑漏洞如果必须经过人工审核界面可以尝试寻找审核逻辑的漏洞。例如审核接口是否直接依赖前端传入的参数如audit_result是否存在越权访问普通用户能否访问审核列表但这些属于其他漏洞类型需谨慎测试并明确上报。切记绝对不要尝试攻击或绕过生产环境的审核流程。Q4我怀疑有漏洞但无法100%复现如何编写高质量的漏洞报告A4SRC审核人员喜欢清晰、可复现的报告。即使不能100%稳定复现你也可以详细记录步骤按时间顺序记录你的所有操作、输入的数据、观察到的现象。提供完整数据流附上关键的HTTP请求和响应数据脱敏后用箭头图或文字说明你的推理逻辑。阐明漏洞原理结合你的分析说明你认为的系统逻辑缺陷在哪里。例如“贵系统在处理退款回调时更新了order表的refund_status但没有调用asset_revoke服务导致用户资产残留。”说明潜在影响量化影响。例如“利用此漏洞攻击者可以零成本获取付费会员权益假设会员月费30元每日有100次此类退款每月可能造成约9万元的直接损失。”提出修复建议给出具体的修复方向如“建议在handleRefundNotify函数中加入对user_coupon表的更新操作”。Q5开发说“这是产品设计如此退款后允许用户保留资产作为补偿”怎么判断是不是真需求A5这确实可能是一种商业策略比如“无忧退款”体验。但作为安全人员你需要判断第一策略是否公开透明在用户退款时是否有明确提示“退款后您仍可继续使用本次购买的商品”如果没有提示那就是漏洞。第二策略是否可控是否所有商品都这样高价值商品也这样吗这可能导致严重的资损。第三策略是否被滥用可以通过数据来分析是否有异常用户频繁购买-退款。将你的分析和风险点反馈给产品和业务方由他们做出最终决策。你的职责是指出不受控的资损风险。