漏洞思维(1):从人的业务规则,到代码世界里的失效假设
前言
前两篇写信息搜集的时候,我慢慢想通了一件事:
互联网上的资产不是凭空出现的,而是业务为了运行,被迫留下来的投影。
企业要让用户访问,就需要域名;要让浏览器信任,就需要证书;要让系统协作,就需要 API、回调、DNS;要持续运营,就会有后台、监控、文档、测试环境。
所以后来我做信息搜集的时候,已经不太喜欢问:
“还有什么工具可以跑?”
而更喜欢问:
“这个业务为了跑起来,必须留下什么?”
但是当我真正开始挖漏洞以后,我发现自己又遇到了几乎一样的问题。
很多教程会告诉我:
- 越权要改 ID;
- 支付漏洞要改金额;
- 验证码要看能不能重复使用;
- 密码重置要看流程能不能绕;
- 文件上传要看后端有没有校验;
- SQL 注入要找用户输入进入 SQL 的位置。
这些东西当然有用。
但它们仍然让我产生一种熟悉的别扭感:
我知道怎么测试,却不知道为什么我要这么测试。
为什么改 ID 会产生越权?
为什么重复请求可能产生优惠券漏洞?
为什么跳过第二步直接调用第三步,有时候就能绕过业务限制?
为什么前端明明限制了一个按钮,直接请求接口以后限制却消失了?
如果我只记:
“看到 ID 就改一下。”
那么我仍然是在背技巧。
我真正想知道的是:
一个正常业务,究竟是在什么地方开始变成漏洞的?
于是我开始换一个角度。
这一次,不从“攻击者能做什么”开始。
而从:
业务本来想保证什么?
开始。
第一部分:先建立“人的业务世界”
这一部分只回答一件事:
在人看来,这个系统正确的时候应该是什么样?
先把现实目标、业务规则和业务不变量想清楚,后面才有标准去判断代码到底有没有做错。
一、先把“业务”这个词说清楚
写到这里,我发现其实还有一个词必须先讲清楚:
什么叫业务?
以前我一听“业务”这个词,会觉得它很虚。
好像是产品经理、运营、公司领导才会说的东西。
但后来我慢慢发现,如果我连“业务”是什么都没有真正理解,后面讲:
业务规则
业务不变量
业务流程
业务漏洞
其实都会变成几个听起来很高级、但脑子里没有具体画面的词。
所以我后来给“业务”下了一个很简单的定义:
业务,就是一个组织为了完成某个现实目标,让不同的人按照一定规则去操作某些对象的一整套过程。
比如学校为什么要做一个请假系统?
不是因为学校想做:
一个请假页面
也不是因为学校想做:
几个 POST 接口
学校真正想解决的是:
学生临时不能上课
↓
需要提出请假
↓
学校需要知道请假的原因和时间
↓
对应老师 / 辅导员需要审核
↓
审核通过以后
这次缺勤才被认为是合理的
这整件事情,才叫:
请假业务
所以:
页面
按钮
接口
数据库
都不是业务本身。
它们只是:
程序为了把现实业务搬到计算机里而做出来的实现。
再比如订单系统。
它真正的业务也不是:
订单列表页
订单详情页
支付接口
退款接口
真正的业务是:
用户挑选商品
↓
确认购买
↓
系统确定商品和价格
↓
用户付款
↓
商家履约
↓
用户收货
↓
特殊情况下退款 / 售后
所以以后我看一个系统,我会先把技术界面拿掉。
不先看:
URL 是什么
参数是什么
接口叫什么
而先问:
这个系统到底在帮谁完成什么现实中的事情?
因为只要回答了这个问题,后面的角色、对象和规则就会慢慢出现。
例如请假业务:
谁?
学生、辅导员
操作什么?
请假申请
做什么?
创建、提交、审核、通过、拒绝
为什么要这样做?
为了让学校能够判断一次缺勤是否合理
于是我发现,一个业务最少可以拆成四样东西:
人
+
对象
+
动作
+
规则
也可以把它理解成:
谁
↓
对什么东西
↓
做什么
↓
在什么规则下做
比如:
学生
↓
操作自己的请假申请
↓
提交
↓
提交以后等待辅导员审核
这里就已经开始出现业务规则了。
所以我现在会把:
业务
理解成:
人按照规则操作对象,最终完成一个现实目标。
而所谓:
业务系统
其实就是程序把这套现实关系搬进了计算机。
这也是为什么漏洞最终一定会回到“业务规则”。
因为攻击者真正突破的,通常不是:
一个页面
一个按钮
一个参数
而是:
现实业务原本规定
某个人
在某种条件下
只能对某个对象
做某些事情
程序却没有把这条规则完整地保证下来。
所以后面再看到:
业务规则
我脑子里不会再把它理解成一个抽象词。
我会先想到:
这个系统到底在帮谁做什么?
为了让这件事正常成立,
有哪些规则必须一直成立?
从这里开始,才真正进入漏洞思维。
二、漏洞到底是什么
以前我理解漏洞,容易把它想成:
程序写错了。
后来我觉得这个解释太宽了。
因为程序里每天都有 Bug,但不是每个 Bug 都是安全漏洞。
按钮错位是 Bug。
页面颜色不对是 Bug。
计算结果偶尔显示错误也是 Bug。
但安全漏洞通常有一个更明显的特点:
系统原本想维持的一条安全规则,被用户突破了。
比如学校成绩系统。
它真正的业务规则并不是:
学生可以查询成绩。
完整规则其实是:
学生可以查询自己的成绩,但不能查询其他学生的成绩。
这里面其实存在一条隐藏规则:
用户 A
只能访问
属于用户 A 的成绩
如果有一天:
用户 A
可以访问
属于用户 B 的成绩
那么真正发生的事情不是:
参数被修改了。
而是:
系统原本应该维持的业务约束被破坏了。
这就是我现在理解漏洞的第一句话:
漏洞,本质上是系统应该维持的安全约束,在某种输入、身份、状态或流程下失效了。
三、业务不是由页面组成的,而是由规则组成的
站在普通用户角度看一个系统,我们看到的是页面:
登录
首页
成绩查询
修改资料
提交申请
退出登录
但是站在业务人员角度,这套系统其实是一堆规则。
比如学校系统。
1. 登录
业务规则:
只有能够证明自己身份的人
才能进入对应账号
2. 查询成绩
业务规则:
学生只能查看自己的成绩
教师可以查看自己负责班级的成绩
管理员可以查看全部成绩
3. 修改成绩
业务规则:
学生不能修改成绩
普通教师只能修改自己课程的成绩
特定管理员拥有更高权限
4. 提交申请
业务规则:
只有满足条件的学生才能申请
同一申请不能无限重复提交
提交之后进入待审核状态
审核完成后不能随意重新修改
突然会发现:
一个系统真正复杂的地方,不是有多少页面,而是有多少规则。
页面只是规则的外壳。
API 是规则的执行入口。
数据库则负责保存规则运行之后产生的状态。
所以以后看一个功能,我认为第一个问题不应该是:
这里有什么参数?
而应该是:
这个功能在业务上想保证什么?
四、把必须始终成立的约束叫作业务不变量
前一节讲“业务规则”,我理解的是:
正常业务应该怎样运行。
例如谁可以做什么、在什么条件下允许做、流程应该怎么往下走。
但是继续往下,我开始接触到一个非常有用的概念:
不变量。
业务不变量不是简单等于“业务不能做什么”。
更准确地说,它描述的是:
正常业务运行过程中,无论用户怎么操作,都必须始终成立的约束。
为了做安全分析,我经常会把它换一个角度问:
什么事情绝对不能发生?
比如:
学生永远不能查看其他学生的私人信息。
这是一条不变量。
一张只能使用一次的优惠券,不能被成功使用两次。
也是。
订单支付金额必须等于服务器计算出的订单金额。
也是。
密码重置只能修改已经通过身份验证的那个账号。
也是。
普通用户不能获得管理员权限。
还是。
于是漏洞测试突然变得特别清楚。
我不再首先问:
有没有越权?
而是先写出:
应该永远成立:
A 只能访问 A 的数据。
然后故意寻找:
有没有一种情况,
能让这个规则不成立?
漏洞挖掘,其实开始变成了一种:
寻找业务不变量反例的过程。
五、为什么漏洞分析之前要先理解业务
前面一路追下来,我先得到了一个很重要的东西:
业务目标
↓
业务规则
↓
业务不变量
也就是说,我开始知道:
在人看来,这个系统“正确的时候”应该是什么样。
这也是为什么业务分析有价值。
它不是为了让我变成产品经理,而是为了先给漏洞判断找到一个标准。
如果我连:
什么应该允许
什么必须拒绝
什么约束必须始终成立
都不知道,
那后面就算看到接口行为异常,我也很难判断它到底是不是安全问题。
但到这里还只解决了一半。
因为业务规则最终不能只存在于人的理解里。
真正执行这些规则的,是代码。
所以接下来我要继续追:
人脑里的业务规则,到底是怎么被翻译成程序判断的?
第二部分:进入“代码的业务世界”
人的业务世界已经告诉我“正确答案应该是什么”。
接下来我要追的是:
代码到底靠什么,把这些业务规则真正执行出来?
六、业务规则最终必须被翻译成代码
但这里还有一层特别重要。
业务人员说:
一人只能领取一次优惠券。
计算机根本听不懂。
开发必须把它翻译成代码。
可能变成:
收到领取请求
↓
检查用户有没有领取过
↓
没有
↓
发放优惠券
↓
记录“已经领取”
业务说:
学生只能看自己的成绩。
开发可能翻译成:
用户请求成绩
↓
获取 studentId
↓
查询数据库
↓
返回成绩
问题就出现在这里。
业务规则是人类语言,而最终执行规则的是代码。
从:
业务想要什么
变成:
程序具体怎么判断
中间需要经过一次“翻译”。
而安全漏洞,经常就出现在这个翻译过程中。
这里是全文的分界:从“人的业务世界”进入“代码的业务世界”
写到这里,我发现前面的内容其实一直在回答:
人认为这个系统应该怎么工作?
也就是:
人的业务世界
现实目标
↓
业务规则
↓
业务不变量
例如:
学生只能查看自己的成绩
在人看来,这句话非常自然。
但计算机不能直接执行“只能查看自己的成绩”。
开发必须把它拆成程序真正能够判断的问题:
当前用户是谁?
↓
请求的是哪份成绩?
↓
这份成绩属于谁?
↓
当前用户和目标成绩是什么关系?
↓
允许 / 拒绝
所以从这里开始,我的问题发生了变化。
前面我在问:
正确世界应该是什么样?
现在我要问:
代码到底靠什么维持这个正确世界?
可以把两边理解成:
人的业务世界
→ 告诉我“正确结果应该是什么”
代码的业务世界
→ 真正决定“这次请求到底允不允许”
这也是整篇文章真正进入代码层的地方。
后面我要继续追的,不再只是业务本身,而是:
代码需要知道哪些事实?
这些事实从哪里来?
程序相信哪些事实?
程序为什么认为这些判断已经足够?
这些假设能不能被用户打破?
所以我现在会把这个分界记成一句话:
业务不变量告诉我系统必须维持什么;代码实现告诉我系统靠什么去维持它。
到这里,我又把“找漏洞”这件事想得更具体了一点。
以前我会觉得:
找漏洞
=
找代码哪里写错了
现在我觉得更准确的理解应该是:
找漏洞,就是进入代码世界,检查开发为了实现人的业务规则,所写出来的事实获取、条件判断和状态操作,是否真的满足了业务不变量这个安全标准。
例如人的业务世界规定:
学生只能查看自己的成绩
我先把它转换成业务不变量:
学生 A
绝对不能查看
学生 B 的成绩
接下来进入代码世界。
代码为了实现这条规则,必须完成类似这样的过程:
获取当前用户身份
↓
确定目标成绩对象
↓
读取目标成绩属于谁
↓
比较当前用户和资源所有者
↓
允许 / 拒绝
于是我真正要检查的就不是:
这里有没有一个 ID 可以改
而是:
代码为了维持这条业务不变量
到底做了哪些判断?
这些判断够不够?
判断依赖的事实从哪里来?
有没有漏掉必要条件?
最终代码行为是否真的符合业务不变量?
所以两边真正连接起来的是:
人的业务世界
↓
提出业务规则
↓
提炼业务不变量
↓
================
代码的业务世界
↓
获取事实
↓
条件判断
↓
状态 / 业务动作
↓
================
再用业务不变量检查结果
如果:
业务不变量要求:
A 不能看 B
代码实际也做到:
A → B
拒绝
说明:
代码实现
符合
业务不变量
如果代码实际变成:
A → B
允许
那么:
代码实现
没有维持住
业务不变量
↓
人的业务逻辑
≠
代码实际逻辑
↓
漏洞
所以我现在会把业务不变量理解成:
连接人的业务世界和代码世界的一把“安全尺子”。
人的业务世界告诉我:
什么结果才是正确的
代码世界告诉我:
程序实际上是怎么做的
业务不变量负责:
拿这两个结果进行对照
而漏洞,就是这种对照中出现的:
安全相关的不一致
七、代码为了实现规则,必须相信一些东西
再往下追,我发现一个特别重要的东西:
信任。
假设系统收到:
studentId=20260001
服务器必须决定:
这个 studentId 能不能相信?
订单接口收到:
price=100
服务器也必须判断:
这个价格是谁算出来的?
权限接口收到:
role=user
服务器必须知道:
角色信息来自数据库,还是用户自己提交的?
每一个业务功能背后,其实都有很多类似的问题:
我相信谁告诉我的用户身份?
我相信谁告诉我的价格?
我相信谁告诉我的订单状态?
我相信谁告诉我的资源属于谁?
我相信谁告诉我这一步已经完成?
我相信谁告诉我验证码已经验证?
而这里就出现了我认为漏洞思维里特别核心的一句话:
安全问题往往不是“有没有检查”,而是“系统把什么当成了可信事实”。
这正是前面“人的业务世界”和“代码业务世界”开始产生具体差异的地方。
人的业务世界只会说:
学生只能查看自己的成绩
而代码真正要做的是判断:
你是谁?
这份成绩是谁的?
我凭什么相信这两个事实?
所以“信任”不是一个突然出现的新概念。
它是在继续回答:
代码到底靠什么维持业务不变量?
八、漏洞经常来自错误的隐含假设
继续往下想,我发现很多漏洞背后都有一句开发人员没有真正说出口的话。
比如:
用户应该不会修改这个参数。
或者:
用户必须先经过前面的页面才能来到这里。
或者:
这个接口只有前端会调用。
或者:
正常情况下一个人不会同时点击两次。
或者:
前端已经限制最大金额了。
或者:
普通用户看不到管理员按钮,所以不会调用管理员接口。
这些东西,我现在习惯叫:
隐含假设。
系统设计者觉得它理所当然成立,于是没有真正用服务器端规则去保证。
攻击者做的事情,很多时候特别简单:
故意让这个假设不成立。
所以漏洞形成的完整链条就出来了:
业务目标
↓
业务规则
↓
程序实现
↓
程序依赖某个假设
↓
这个假设实际上可以被用户影响
↓
攻击者让假设失效
↓
业务规则被突破
↓
漏洞产生
这条链,是我认为理解漏洞最重要的一条链。
因为它把全文重新闭合了:
人的业务世界定义规则
↓
业务不变量给出安全标准
↓
代码尝试实现这些标准
↓
代码依赖事实和假设
↓
用户让其中一个假设失效
↓
代码世界偏离人的业务世界
↓
业务不变量被破坏
↓
漏洞
第三部分:把整篇文章收回来
到这里不再增加新概念,只做两件事:
- 把前三篇放回同一条学习主线;
- 用我自己的话,把这一篇从头到尾再串一次。
九、把前三篇放回同一条主线
写到这里,我发现前三篇其实一直在追同一个东西:
为什么?
第一篇问:
企业为什么会暴露资产?
因为业务要运行。
第二篇问:
为什么能够通过公开信息发现这些资产?
因为业务运行一定会留下痕迹。
而这一篇继续问:
为什么正常业务会变成漏洞?
因为业务规则最终必须被程序实现,而程序又必须依赖身份、对象、状态、数据、顺序和信任去做判断。
只要其中某个本应成立的条件没有被真正保证,业务不变量就可能失效。
所以这三篇真正训练的从来不是:
工具
漏洞名称
Payload
固定测试动作
而是不断追:
这个东西为什么存在?
系统为什么这样判断?
它凭什么相信这个事实?
这条规则到底靠什么保证?
我越来越觉得:
理解系统为什么能够正常成立,比单纯记住“这里应该试什么”更重要。
十、最后,我用自己的理解把整篇文章再串一次
我现在理解,所谓业务,其实就是人为了实现某个现实目标、解决某个现实问题而设计出来的一套流程和规则。
比如:
学生查成绩
用户下单
申请审核
优惠券领取
密码重置
这些东西首先存在于:
人的需求
制度
产品设计
业务人员和开发人员的理解
但是程序本身根本不懂:
学生只能看自己的成绩
一张优惠券只能使用一次
没有审核通过的申请不能直接生效
这些都是人的业务语言。
所以开发人员必须把:
人的业务世界
翻译成:
代码的业务世界
也就是说,真正把业务执行出来的不是人的想法,而是代码。
所以我现在也明白了,为什么做漏洞分析之前要先理解业务。
因为如果我连:
这个系统正常情况下到底应该怎么工作
都不知道,
那我就没有办法判断:
程序实际做出来的结果
到底是不是错的
所以我先通过业务分析去找:
核心业务对象
再找:
围绕这个对象
哪些规则绝对不能被破坏
这些规则就是:
业务不变量
业务规则更多是在告诉我:
正常情况下
谁可以做什么
而业务不变量是在告诉我:
无论用户怎么操作
哪些安全约束都必须一直成立
例如:
A 不能查看 B 的私人数据
一张只能使用一次的优惠券
不能成功使用两次
未审核通过的申请
不能直接生效
这样我就先拿到了:
业务世界里的“正确答案”
接下来才真正开始进入代码世界。
开发为了实现:
学生只能查看自己的成绩
代码就必须知道:
当前用户是谁?
目标成绩属于谁?
当前用户和这份成绩是什么关系?
为了实现:
订单必须按照正确金额支付
代码就必须知道:
真正的订单金额是多少?
为了实现:
验证码只能验证指定账号
代码就必须知道:
这个验证结果到底属于哪个账号?
所以代码为了维持业务不变量,必须依赖很多事实:
身份
对象归属
状态
金额
验证结果
流程是否完成
这时候就会出现一个非常关键的问题:
代码到底把什么当成了可信事实?这些事实是谁告诉它的?
如果本来应该由服务器确认的事实,
却直接相信了客户端,
例如:
studentId
price
role
status
verified
那么人的业务世界里原本认为理所当然成立的规则,
在代码世界里可能根本没有被真正保证。
再往下就是:
隐含假设
开发可能认为:
用户不会修改这个参数
用户一定会按照页面顺序操作
前端看不到管理员按钮
普通用户就不会调用管理员接口
正常用户不会同时发送两次请求
这些在人脑里听起来都很合理。
但如果代码没有真正保证,
那么攻击者做的事情其实就是:
故意让这些假设不成立。
所以整篇文章最后就可以串成:
现实业务目标
↓
业务规则
↓
业务不变量
↓
开发把规则翻译成代码
↓
代码依赖身份、对象、状态、顺序、数据和信任做判断
↓
某个条件被遗漏
或者某个事实被错误信任
↓
人的业务逻辑
≠
代码真正执行的业务逻辑
↓
业务不变量被破坏
↓
漏洞出现
所以我现在再看开头那句话:
漏洞,本质上是系统应该维持的安全约束,在某种输入、身份、状态或流程下失效了。
就能真正理解它了。
如果从最下面看:
代码层
是漏洞真正发生的具体位置。
如果往上一层看:
业务不变量层
是被破坏的安全约束。
如果继续往最上面看:
人的业务世界
就是为什么这条约束本来必须存在。
所以我现在理解:
业务分析告诉我“正确世界应该是什么样”;代码分析告诉我“程序实际上靠什么维持这个世界”;漏洞分析,就是寻找这两个世界之间能够让业务不变量失效的差异。
我做业务分析,不是为了把自己变成产品经理。
而是为了先知道:
系统本来应该保证什么
然后再去问:
代码真的保证了吗?
我现在觉得,这才是整篇文章真正想讲清楚的东西。
结语
如果让我最后只留下一个公式,我会写成:
业务目标
↓
业务规则
↓
业务不变量
↓
程序实现
↓
身份 / 对象 / 状态 / 顺序 / 数据 / 信任
↓
隐含假设
↓
假设失效
↓
不变量被破坏
↓
漏洞
所以下一次面对一个陌生系统,我不想先问:
“这里能打什么漏洞?”
我更想先问:
这个系统究竟在努力保证哪些事情始终成立、哪些事情永远不能发生?
然后再问:
代码凭什么认为这些规则一定不会被突破?
我现在认为,真正的漏洞思维,就是从这两个问题开始的。
注:在 SRC 场景中,理解系统可以无限深入,但实际验证必须始终限制在明确授权的资产、账号、功能和测试规则之内。思维可以往外推,测试不能越界。
最终总图:全文漏洞思维 ASCII 流程
┌──────────────────────────────┐
│ 现实业务目标 │
│ 这个系统为什么存在? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 业务规则 │
│ 正常情况下应该怎么运行? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 业务不变量 │
│ 哪些安全约束必须始终成立? │
│ 哪些事情绝对不能发生? │
└──────────────┬───────────────┘
│
│ 作为“安全标准”
▼
════════════════════════════════
从人的业务世界
进入代码业务世界
════════════════════════════════
│
▼
┌──────────────────────────────┐
│ 程序具体实现 │
│ 为了实现规则,代码需要什么? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 获取事实 │
│ │
│ 当前用户是谁? │
│ 目标对象是谁的? │
│ 当前状态是什么? │
│ 金额是多少? │
│ 前一步是否完成? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 信任来源 │
│ │
│ 这些事实是谁告诉服务器的? │
│ 客户端?数据库?Session? │
│ 服务端计算结果? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 条件判断 │
│ │
│ 身份是否正确? │
│ 对象是否属于当前用户? │
│ 状态是否允许? │
│ 顺序是否正确? │
│ 权限关系是否成立? │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 执行业务动作 │
│ │
│ 允许 / 拒绝 │
│ 查询 / 修改 │
│ 状态转换 │
│ 发券 / 扣款 / 审核 │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 用业务不变量进行对照 │
│ │
│ 代码实际行为 │
│ VS │
│ 人的业务世界要求 │
└──────────────┬───────────────┘
│
┌──────┴──────┐
│ │
▼ ▼
┌───────────────┐ ┌────────────────┐
│ 两者一致 │ │ 两者不一致 │
│ │ │ │
│ 规则被正确维持│ │ 安全约束可能失效│
└───────────────┘ └────────┬───────┘
│
▼
┌────────────────────┐
│ 继续追失效原因 │
│ │
│ 条件遗漏? │
│ 错误信任? │
│ 状态判断缺失? │
│ 流程假设错误? │
│ 权限关系遗漏? │
│ 并发 / 时间问题? │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ 假设失效 │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ 业务不变量被破坏 │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ 漏洞 │
└────────────────────┘
如果再把整张图压缩成一句话:
业务分析
→ 告诉我“正确世界应该是什么样”
业务不变量
→ 给我一把衡量安全的尺子
代码分析
→ 告诉我程序实际上靠什么维持这个世界
漏洞分析
→ 找到代码实际行为和业务不变量之间的安全差异

浙公网安备 33010602011771号