扫描器永远扫不出来的那一类洞:业务逻辑漏洞怎么挖

admin 2026-09-28 05:05:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入探讨扫描器难以发现的业务逻辑漏洞挖掘方法。核心观点是工具只能发现实现与规范不符的问题,而逻辑漏洞源于设计缺陷,需通过理解业务流程来挖掘。文章提出四问法测试每个步骤,涵盖状态机、越权、并发等关键场景,并强调报告需量化危害。提供了可操作的方法论和实战经验,对渗透测试人员具有较高参考价值。 综合评分: 88 文章分类: 渗透测试,漏洞分析,红队,实战经验


扫描器永远扫不出来的那一类洞:业务逻辑漏洞怎么挖

原创

Sink Sink

船山信安

2026年9月27日 18:06 湖南

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

漏洞挖掘 · VULNHUNT

扫描器永远扫不出来的那一类洞:业务逻辑漏洞怎么挖

逻辑漏洞 · 写给习惯用工具扫洞的人

工具能给你的答案,是「哪些接口可能写错了」。业务逻辑漏洞的答案是「哪段流程想错了」。前者的单位是接口,后者的单位是流程,而流程是跨接口的。这篇讲的不是 Payload,是把一个业务拆开、然后逐个环节发问的方法。

01工具查的是另一件事

扫描器跑完三百个接口,报了七个中危。点开一看,四个是缺安全响应头,两个是版本信息泄露,剩下一个它觉得某个参数像注入点,其实那里早就用了预编译。

这不怪工具。它干的是另一件事。

工具能验证的是「实现与规范不符」。响应头该有而没有,规范里写了;版本号该藏而露着,规范里写了;参数进了拼接,模式匹配能认出来。这些都有现成的对照物,机器最擅长的就是拿现成的东西做比对。

业务逻辑漏洞没有对照物。

退款接口没校验这笔订单是否已经退过,连点十次,退了十次。这十个请求,每一个格式都合法,参数都在取值范围内,签名都对,令牌也没过期。工具看十遍,看到的都是十个正常请求。

它错在哪儿?错在这个流程允许了「一笔订单退十次」这件事。

看清这个区别:不是代码写错了,是流程本来就这么设计的。字段类型对,边界值对,鉴权也对,唯独顺序和次数没人管。

所以它不是技术偏差,是设计偏差。

这也是为什么它没有特征签名。注入有语法特征,跨站有标签特征,越权至少还有个可以替换的 ID。逻辑漏洞什么特征都没有,它长得跟正常流量一模一样。

OWASP Top 10 在 2021 年把「不安全设计」单列成 A04,官方对它的说明里有一句很要紧的话:这类问题没有哪一段具体的错误代码可以指给你看。CWE 那边给它留的编号是 CWE-840,业务逻辑错误。

我刚开始做的时候不信这套。总觉得是规则没开全、字典没换对、爬虫没爬深。后来花了两个晚上,把某个电商的优惠券流程自己在纸上画了一遍,才承认一件事:工具扫不出来,不是因为它不够强,是因为它压根不知道什么叫「一张券只能用一次」。

后来我给自己定了条规矩:扫完之后,单独列一张清单,写「哪些地方是工具根本没法下判断的」。这张清单通常不长,十来项,但真出东西的往往就在里面。

02前提不是 Payload,是懂它在干什么

有一项功课绕不过去:读懂这个业务。

不是读代码,是读业务。具体读四样东西。

角色与权限矩阵。系统里有几种身份,每种身份能碰哪些对象。别只看菜单,菜单里藏起来的按钮,接口往往还在,只是前端不给你点。画成一张表:行是角色,列是操作。这张表不必一次画全,先把看得见的三种角色填进去,剩下的在测试过程中往里补。补的过程本身就在发现问题——你总会补出一个文档里没写过的角色。

状态机。一张订单从创建到关闭有几种状态,谁能把它从 A 挪到 B。这一条最容易出事,因为它通常是散落在各个接口里的,全系统没有一处集中定义。

金额和数量在哪儿算。前端传过来的总价,服务端信不信?优惠券减免是服务端重算的,还是直接采信前端那个数字?改一下请求里的 price 字段看结果变不变,答案就有了。

异步与补偿。支付回调、发货通知、定时任务、失败重试。这些入口常常没有登录态,因为作者把它们当成「系统内部调用」,而内部调用是不需要验身份的。

四样里最难受的是状态机那一栏。如果你问了两个人、翻了两份文档,都说不清一张订单到底有几种状态,那基本可以确定:写这个功能的人当时也没说清。说不清的状态机,几乎一定存在能跳过去的路径。

黑盒下怎么读?手段很土:把功能从头到尾用一遍,边用边抓包,把每个请求按业务动作重命名,然后按时间排序。

我习惯给每个请求标四个字段:谁发起的、改了什么、前置状态是什么、后置状态是什么。

标完你就会发现一件事:有些请求的前置状态那栏,是空的。

那不是你忘了填,是它真的没有检查。

异步那一栏我吃过亏。支付回调会重试,这是设计上的正常行为。可不少系统的回调处理并不幂等,重试三次就发了三次货——因为发货动作写在回调里,而回调只判断了「这笔支付是否成功」,没判断「这笔订单是否已经发过」。

读懂业务这件事没法加速。你在一个系统上待的小时数,直接决定了你能问出什么水平的问题。这句话不好听,但它是逻辑漏洞这一行的入场费。

03三个反复出事的位置

读得够多之后,你会发现出事的位置高度集中。

状态可被跳过,或者逆向跳回去。订单没支付直接发货,是最直白的一种。更常见的是倒着走:已经完成的订单被改回待支付,再走一次正常的取消流程,退款就出去了。判断方法很机械——找出所有会改状态的接口,看它有没有检查「当前状态是否允许这次迁移」。

步骤可被重放,或者乱序。退款接口连点十次退十次,是重放。优惠券领了退、退了领,是重放套着重放。还有顺序问题:先领券后下单,和先下单后领券,两条路径走的可能是两套完全不同的校验,其中一套漏了「这张券已经用过了」。

校验在一处,执行在另一处。商品详情页写着限购五件,前端也挡住了第六件,接口不管。短信验证码前端做了六十秒倒计时,接口没做频率限制,把请求重放一次就能一直发。

顺序问题还有个变种:两个接口都能办成同一件事,其中一个做了校验,另一个没做。用户在页面上永远点不到那个没做校验的,但它就在那儿,等着被拼进请求里。

第三类值得单独记一句:问题不在有没有校验,在校验和执行的距离。校验在前端,执行在后端;校验在 A 服务,执行在 B 服务。只要这两处不是同一个地方,中间就有一条缝。

示意伪代码:一次状态迁移,该问的两个问题

def change_status(order_id, target):

    order = load(order_id)

    # ① 当前状态允许迁移到 target 吗?

    if not allowed(order.status, target):

        return reject(“非法状态迁移”)

    # ② 发起这次迁移的人,有资格迁吗?

    if not permitted(current_user, order, target):

        return reject(“无权执行”)

    order.status = target

    save(order)

这段伪代码里,真正要盯的是两个 if 在不在。很多实现只留了第二个——验了身份,没验状态。因为身份是所有接口都要做的,有中间件管;状态是这一个接口才要操心的,没人管。

// 示意伪代码:校验在一处,执行在另一处

func ApplyCoupon(req) {

    // 入口:只查了这张券是否存在

    coupon := findCoupon(req.CouponID)

    if coupon == nil {

        return ErrNotFound

    }

    // 执行:核销发生在另一个服务里

    // 那里会不会再查一次 coupon.Used,取决于它自己的实现

    return couponService.Consume(req.CouponID, req.UserID)

}

写这段代码的人没有偷懒,他在入口做了校验。他只是默认下游会再查一遍,而下游默认上游已经查过了。两个人都没错,两个人都没查。

这种「上下游互相以为对方查了」的情形,在服务拆得比较细的系统里格外常见。拆得越细,缝越多。

04四问法:对每一步发四个问题

方法本身很朴素。把流程拆成步骤图,横向排开,每一步是一个接口调用。画完之后,对每一步问四句话。

步骤的边界怎么定?我的标准是:以服务端状态发生变化为一步。纯查询不算,前端跳个页面不算。一张订单的图通常只有五到八步,够画了。

图上还要补两类隐藏步骤:定时任务触发的,和第三方回调触发的。它们不在你的点击路径里,但它们会改状态。漏掉这两类,四问就问不全。

| | | | — | — | | 问什么 | 怎么试 / 出问题时长什么样 | | 跳过它会怎样 | 不调第 3 步,直接调第 4 步。成功了,说明这一步的前置检查是空的 | | 重来一次会怎样 | 把同一个请求原样重放十次。十次都成功,就是次数没被约束 | | 换身份执行会怎样 | 拿另一个账号的令牌重放,或者退掉登录态重放。看它返回 401 还是照常返回数据 | | 两个并发会怎样 | 同一时刻发两个一模一样的请求。看余额、库存、名额有没有被扣两次 |

四问不是并列的,试起来的成本差得很远。

「重来一次」最便宜,抓包工具里右键重放,三十秒出结果,所以我先问它。「跳过」要贵一些,你得先知道完整步骤序列,所以必须先把图画出来。「换身份」要准备两个账号。「并发」需要工具配合,成本最高,放到末尾。

命中率又是另一回事。我的体感从高到低是:重来一次、换身份、跳过、并发。

并发排在末位,但一旦命中,危害往往最大。所以它不能因为难试就不问。

还有一句补充:不是每一步都要问满四句,是每一个写操作要问满四句。只读接口问一句「换身份」就够了。

四问法有个副作用,我觉得比四问本身更值钱:它逼着你把业务真真切切走一遍。很多人挖不到逻辑漏洞,不是不会问,是从下单到售后这条完整链路,他自己一次都没走完过。

05并发:最容易漏掉的一类

同一时刻进来两个请求,读到了同一份余额。

账户里一百块。两个提现请求同时到,各自读到一百,各自判断余额充足,各自扣掉一百,各自写回零。两次提现都成功,账户支出两百,余额显示为零。

根源就一句话:读取、判断、写入这三步之间没有排他性。它不是原子操作,中间那段空隙就是竞态窗口。

为什么最容易被忽略?因为你手工点不出来。

人手点击的间隔是几百毫秒,服务端处理一个请求是几毫秒。你永远碰不到那个窗口,除非刻意去制造它。所以它是「看不见」,不是「不存在」。

这一节我只讲怎么判断它可能存在,不讲怎么打。

三个信号,看请求和响应的形状就能看出来。

涉及余额、库存、次数、配额、名额的写操作。只要有一个数字会被减,就值得放进候选清单。

接口有「先查后写」的语义。先查余额再扣款,先查库存再占座,先查券有没有用过再核销。单次请求看不出来,得从返回结构和响应时间去推断。

业务明确写了「限量」「限一次」「先到先得」。这句话本身就是设计者的意图声明,而意图和实现之间经常有距离。凡是写了限量的地方,都去想一句:这个「限」是在哪一行代码里生效的。

补偿机制还会把这类问题放大。异步任务失败重试本身是好事,但如果重试没有幂等设计,一次网络抖动就变成两次核销。日志里看到的是「重试成功」,业务上看到的是多出去的那一份。

危害要写成规模,不是写成个案。一个限量一百份的活动最终核销了一百三十份,和多领了一份,是两件事:前者是平台成本失控,后者是一个人占便宜。

末了说边界。这类验证请在自己搭的环境里做,线上的真实资金系统不要碰。这不是姿态问题,是你在生产环境里没有回滚能力,一次误操作的代价远超一个漏洞的赏金。

06越权的三层,与黑盒下的规律

水平越权是同角色看别人的数据,垂直越权是低权限干高权限的事,未授权是连身份都不需要。名词三分钟就能背下来,难的是黑盒下你怎么知道该往哪儿试。

你见不到权限模型,只能从接口的形状去推。四条规律。

ID 的规律。订单号、用户编号、文件标识,如果是自增数字或者带时间戳,替换它就有意义。如果是随机串,成本会高很多——但很多系统只在创建时随机,查询接口仍然接受老格式。

参数的对称性。创建接口让你传 owner_id,编辑接口大概率也认 owner_id。列表返回了 tenant_id,详情接口大概率也吃 tenant_id。一个接口里出现过的身份相关字段,值得在它所有兄弟接口里都试一遍。

批量接口。能一次操作多个对象的接口往往做批量校验,而批量校验容易只校验第一个。传十个 id,把第一个换成自己的,剩下九个换成别人的。

猜得出来的路径。水平越权靠换 ID,垂直越权靠猜路径和改角色字段。管理端的路径前缀、接口名里的 admin、返回体里那个多出来的字段,都是线索。至于未授权,最朴素的做法是退出登录,把刚才的请求原样重放一遍。

这四条里,第二条的性价比最高。因为它不需要你理解业务,只需要你注意到:同一个字段,在这个系统里出现过。

测的顺序也有讲究:先测未授权,再测垂直,水平越权放在收尾。理由是成本和严重性倒挂——未授权最好测也最严重,水平越权最难测,也最依赖具体的 ID 设计。

07写成厂商会认的危害,以及心态账

「可导致任意用户数据泄露」这句话,厂商看过一百遍,它不产生任何行动。

换成这样:把订单 ID 从 1 遍历到 10000,能拿到一万条订单记录,字段包含收件人姓名、手机号和收货地址,按该站公开的日均单量推算,全量约 XX 万条。

差别在哪儿?前者是形容词,后者是量词。

逻辑漏洞尤其需要这一步,因为它的危害从来不是「能不能」,而是「能多少」。一个可以退十次的洞,危害取决于单次退多少、一天能退几次、有多少人会这么干。

我写报告固定三段:可达路径(几个步骤,每步需要什么身份)、规模化推演(单账号能放大到几倍,全站有多少账号符合条件)、损失口径(钱、数据条数、名额)。

第三段最容易被省掉,也最影响定级。写「可导致资金损失」是一句空话;写「单账号单次可多退 199 元,站内近三十天有订单的账号约若干万个」,厂商那边当天就能排优先级。

然后是心态账。

逻辑漏洞不适合批量。它来自对一个系统长时间的理解——你得知道它为什么这么设计,才看得出它在哪儿没想清楚。

所以别在批量扫荡里期待它。一天扫二十个站,你找到的是二十个站的共性问题;一个站待两周,你找到的是这个站自己的问题。后者才是逻辑漏洞。

这话不好听,我知道。但「效率低」和「没有」是两回事。

我自己算过一笔账:花两周挖到一个逻辑漏洞,和花两天扫出三个低危,哪个更划算,取决于你要什么。要积分,扫;要手艺,挖。两个都要的时候,人会很拧巴,这是常态。

折中的办法是:挑三到五个业务最复杂的系统长期盯着,其余的用工具过一遍就走。逻辑漏洞的产出从来不是均匀分布的,它集中在你真正理解的那一两个系统里。


ONE LINE        工具找的是写错的代码,你要找的是想错的流程。


参考来源

· OWASP Web Security Testing Guide(WSTG,OWASP 官方 Web 安全测试指南,v4.2,2021 年起持续更新):其中对身份认证、授权、业务逻辑等测试项的组织方式,是本文四问法的参照框架

· OWASP Top 10(2021):A01 Broken Access Control(失效的访问控制)覆盖越权类问题,A04 Insecure Design(不安全设计)覆盖设计层面的缺陷,官方对 A04 的说明明确指出这类问题无法指向某一段具体代码

· MITRE CWE(Common Weakness Enumeration,1999 年起持续维护):CWE-840 Business Logic Errors(业务逻辑错误)及其子条目,把「流程允许了一件不该允许的事」纳入了标准化编号体系

· PTES(Penetration Testing Execution Standard,2011 年发布,2014 年更新):渗透测试执行标准中的威胁建模与漏洞分析两个阶段,强调先建立业务威胁模型再进入验证环节


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:船山信安 Sink Sink《扫描器永远扫不出来的那一类洞:业务逻辑漏洞怎么挖》

谈谈汽车嵌入式软件 网络安全文章

谈谈汽车嵌入式软件

文章总结: 本文介绍汽车嵌入式软件的定义、特点与分类,阐述其与非嵌入式软件的区别,并详细说明汽车嵌入式软件方向,包括软件架构、刷写、底层驱动、硬件抽象层、软件集
评论:0   参与:  0