漏洞思维(3):从允许世界到禁止世界

前言

上一篇,我已经把一条人的业务规则继续拆进了代码世界。

例如:

学生只能修改自己的草稿请假申请

在人脑里,这是一句话。

到了代码世界以后,却要被拆成:

当前是谁?
↓
想做什么?
↓
操作哪个对象?
↓
和对象是什么关系?
↓
对象现在是什么状态?
↓
前面必须发生什么?
↓
服务器根据什么事实做判断?

也就是说,我已经得到了一套:

角色
对象
动作
关系
状态
顺序
信任

这样的业务判断模型。

但很快我又遇到了一个新的问题:

模型已经画出来了,然后呢?漏洞到底怎么从模型里“推”出来?

如果最后我还是:

看到 id 就改

看到 status 就改

看到接口就重放

看到金额就改

那前面的业务建模其实并没有真正改变我的思维方式。

所以这一篇,我只想解决一个问题:

面对一个完全陌生的业务,我能不能不依赖漏洞字典,而是仅仅根据正常业务规则,自己系统地产生值得验证的漏洞假设?

如果能做到这一点,漏洞挖掘就会从:

我记得哪些技巧

慢慢变成:

系统告诉了我哪些规则
↓
我知道哪些条件让规则成立
↓
我故意改变其中一个条件
↓
观察业务不变量是否仍然成立

我现在觉得:

这一步,才是真正从“会建模”走向“会推漏洞”。


第一部分:正常业务只告诉我“允许世界”

一、什么叫“允许世界”

正常产品设计、页面流程和用户说明,通常都在告诉我:

正常情况下,什么事情应该成功。

例如学校请假:

学生创建申请
↓
填写内容
↓
提交
↓
辅导员审核
↓
通过 / 拒绝

再例如:

学生可以修改自己的草稿请假申请。

这句话实际上描述的是一个正常允许组合:

操作者 = 学生 A

动作 = 修改

对象 = 学生 A 的请假申请

关系 = A 是该申请所有者

状态 = 草稿

结果 = 允许

这就是:

允许世界

也就是:

谁
在什么条件下
对什么对象
做什么
应该成功

但是安全测试真正关心的,并不只是:

什么应该成功

而是:

什么事情本来应该失败,程序真的让它失败了吗?


二、业务安全还有另一半:禁止世界

继续看:

学生可以修改自己的草稿请假申请。

正常业务只告诉我:

学生 A
+
修改
+
自己的申请
+
草稿
=
允许

但围绕这个正常组合,其实存在很多理论上应该被拒绝的情况:

学生 A
+
修改
+
学生 B 的申请
+
草稿
=
拒绝

或者:

学生 A
+
修改
+
自己的申请
+
已通过
=
拒绝

甚至:

学生 A
+
审核
+
自己的申请
=
拒绝

这些就是正常产品流程很少主动展示给我的:

禁止世界

所以我现在会把一个业务的安全模型理解成两半:

允许世界
+
禁止世界
=
相对完整的业务安全模型

产品和普通用户最常看到的是:

允许世界

而漏洞研究真正需要补的是:

禁止世界

因为很多漏洞,本质上就是:

一个属于禁止世界的组合,被代码错误地执行成了允许。


第二部分:漏洞假设到底是什么

三、漏洞假设不是“我感觉这里可能有洞”

以前我说:

这里可能有越权

或者:

这里可能能绕过状态

这当然算一种猜测。

但现在我希望自己的漏洞假设更严格一点。

我会把它写成:

漏洞假设
=
一条业务不变量
+
一个被改变的业务条件
+
一个理论上应该被拒绝的操作

例如业务不变量:

学生只能修改自己的请假申请。

我改变:

对象归属

自己
↓
别人

于是得到:

学生 A
是否能够修改
学生 B 的请假申请?

理论结果:

拒绝

这就是一个完整漏洞假设。

它和:

把 id 改一下看看

最大的区别是:

我知道为什么改

我知道改的是哪个业务条件

我知道理论结果应该是什么

我知道什么结果才算规则失效

所以:

“改参数”只是测试动作,“漏洞假设”才是这个动作背后的安全问题。


第三部分:从正常世界推到禁止世界

四、我现在把漏洞推导理解成“变量变异”

上一篇已经得到:

操作者
动作
对象
关系
状态
顺序
信任

这些都是一条业务规则成立时,代码需要判断的条件。

这一篇我要做的事情非常简单:

保持其他条件尽量不变,只改变其中一个条件。

例如正常组合:

学生 A
+
修改
+
学生 A 的请假申请
+
A 是所有者
+
状态 = 草稿
=
允许

现在只改变对象:

学生 A
+
修改
+
学生 B 的请假申请
+
A 不是所有者
+
状态 = 草稿
=
?

或者只改变状态:

学生 A
+
修改
+
自己的请假申请
+
A 是所有者
+
状态 = 已通过
=
?

这特别像做实验。

正常请求是:

控制组

改变一个业务条件以后:

实验组

如果:

合法条件
→ 允许

非法条件
→ 仍然允许

那就说明:

某个本应该参与安全决策的业务条件,没有被正确保证。


五、变量变异法真正改变的,是“为什么允许”的理由

前面已经知道:

保持其他条件尽量不变,只改变其中一个条件。

我现在更愿意把它压成一句话:

漏洞推导,就是把“为什么允许”的其中一个理由拿掉,再看看程序是否还允许。

例如:

学生可以修改自己的草稿申请

真正让这次操作成立的是:

当前是学生
+
操作的是自己的对象
+
所有权关系成立
+
当前状态 = 草稿

所以我真正要做的不是:

随便找一个参数改

而是:

找到一个“允许理由”
↓
让它不再成立
↓
理论结果从“允许”变成“拒绝”
↓
再看服务器是否真的拒绝

到这里,变量变异法的核心就够了。

接下来真正落到 Burp 里,还要解决另一个问题:

我看到的是参数、Cookie、JSON 和 Header,怎么知道它们分别代表哪个业务变量?


第四部分:先把技术字段翻译成业务变量

六、参数只是业务变量在网络请求里的表现形式

做到这里,我已经知道:

漏洞推导
不是随机改参数

而是:

选择一个业务条件
↓
让它不再满足正常条件
↓
观察业务不变量是否仍然成立

但真正落到 Burp 里,我看到的仍然是:

URL
Path
JSON
Cookie
Header
Token
GraphQL Variables

所以这里还要完成一次翻译:

技术字段
↓
业务变量

例如:

orderId
→ 目标订单对象

tenantId
→ 当前租户 / 业务范围

status
→ 对象当前状态

reviewerId
→ 审核关系

price
→ 业务金额事实

SESSION / Token
→ 当前身份上下文

如果我只记:

改 URL 里的 id

那我记住的是参数位置。

但如果我理解成:

改变目标业务对象

那么参数藏在 URL、JSON、Path 还是 GraphQL 里,都不影响我理解自己真正改变了什么。

所以我现在想训练的是:

看到技术字段,先把它翻译成业务变量;再决定这个变量是否值得被改变。


七、从数据包里看见“业务变量”

假设我在自己的测试环境里看到:

GET /api/leave/detail?id=123 HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a

如果:

申请 123
属于学生 A

那么这里的:

id=123

业务上代表:

目标对象 = A 的请假申请

如果后来请求里变成:

GET /api/leave/detail?id=456 HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a

而:

申请 456
属于测试账号 B

那么表面上只是:

123 → 456

但业务上真正改变的是:

目标对象
A 的申请
→
B 的申请

所以我现在不想只记:

改 id

而是先问:

这个字段在业务上代表什么?我真正改变的是哪个业务变量?

这里先把这种“翻译能力”建立起来。

完整的 Hypothesis、Expected 和验证过程,后面的请假案例再正式跑一遍。


第五部分:我可以改变哪些业务变量

八、把第二篇的模型直接变成“假设生成地图”

第二篇给我的模型是:

操作者
对象
动作
关系
状态
顺序
信任

这一篇再补一个:

时间

于是我可以从八个方向生成候选假设:

业务变量 正常世界 改变以后 我真正问的问题
操作者 合法主体 换角色 / 换主体 谁能做?
对象 自己的对象 换另一个对象 能操作谁?
关系 关系成立 关系不成立 为什么有权?
动作 允许动作 换成敏感动作 能做什么?
状态 合法状态 换成另一状态 什么时候能做?
顺序 正常流程 跳步 / 回退 / 重复 前面必须发生什么?
信任 权威事实 让低信任来源影响事实 谁说了算?
时间 单次顺序执行 同时 / 重复 “一次”怎么保证?

把它们翻译成一句话就是:

操作者:
辅导员 → 学生

对象:
自己的申请 → 别人的申请

关系:
负责范围内 → 负责范围外

动作:
查看 → 修改 / 审核

状态:
草稿 → 已通过

顺序:
提交后审核 → 未提交直接审核

信任:
服务端权威事实 → 客户端可影响事实

时间:
顺序执行 → 同时 / 重复

这里的目标只有一个:

从正常业务模型里,系统地产生“理论上应该被拒绝”的候选组合。

这不是一张“每个接口都要全部测试”的清单。

它只是:

假设生成地图

后面的权限、状态机、信任和并发,会分别把这些维度继续放大。


第六部分:完整演示一次——从允许世界到漏洞假设

十、先得到请假业务的正确世界

假设业务是:

学生创建请假
↓
填写信息
↓
上传证明
↓
提交
↓
辅导员审核
↓
通过 / 拒绝

上一篇已经学过怎么建模,这里不重新教学,只快速拿到:

角色:
学生、辅导员

核心对象:
请假申请、附件、审核记录

动作:
创建、查看、修改、提交、审核、下载

关系:
学生拥有请假申请
附件属于请假申请
辅导员负责学生

状态:
草稿、已提交、审核中、已通过、已拒绝

再写业务不变量:

I1:学生只能读取自己的申请。
I2:学生只能修改自己的草稿申请。
I3:学生不能执行审核。
I4:辅导员只能审核自己负责学生的申请。
I5:附件权限不能弱于所属请假申请。
I6:已通过申请不能继续按草稿规则修改。
I7:审核必须发生在提交之后。

到这里,正确世界已经很清楚。

现在开始构造禁止世界。


十一、案例一:改变对象——同一个请求,只换目标对象

正常基线:

GET /api/leave/detail?id=123 HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a

其中:

123 = 学生 A 自己的申请

Expected:

允许

现在授权测试数据中:

456 = 学生 B 的申请

请求变成:

GET /api/leave/detail?id=456 HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a

其他关键条件保持不变:

操作者 = A
动作 = 查看
身份上下文 = A

只有目标对象发生变化。

所以:

Hypothesis:
学生 A 是否能够读取学生 B 的请假申请?

Expected:
拒绝,并且不返回 B 的业务数据

这里真正测试的是:

I1:学生只能读取自己的申请

而不是“id 能不能改”。


十二、案例二:改变状态——请求不变,但业务阶段已经变了

正常情况下:

申请 123
状态 = draft

学生 A 修改:

POST /api/leave/update HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a
Content-Type: application/json

{
  "leaveId": 123,
  "reason": "Updated reason"
}

正常模型:

操作者 = A
对象 = 123
关系 = owner
动作 = 修改
状态 = draft
Expected = 允许

如果同一个申请后来已经合法进入:

status = approved

那么再次执行同一个业务动作时:

操作者不变
对象不变
关系不变
动作不变

只改变:
状态
 draft → approved

于是:

Hypothesis:
已经通过的申请,是否仍然可以被学生按草稿规则修改?

Expected:
拒绝

这里真正测试的是:

I6:已通过申请不能继续按草稿规则修改

十三、案例三:改变关系——角色正确,不代表对象范围正确

正常审核请求:

POST /api/leave/review HTTP/1.1
Host: school.example.com
Cookie: SESSION=counselor_a
Content-Type: application/json

{
  "leaveId": 123,
  "result": "approved"
}

正常情况下:

辅导员 A
↓
负责学生 A
↓
申请 123 属于学生 A

所以:

关系成立
Expected = 允许

现在保持:

角色 = 辅导员
动作 = 审核
请求结构 = 不变

只把目标对象换成:

学生 B 的申请 456

并且:

学生 B 不属于辅导员 A 的负责范围

真正改变的是:

关系
成立 → 不成立

于是:

Hypothesis:
系统只检查“是不是辅导员”,
还是也检查“是不是这个学生的辅导员”?

Expected:
拒绝

这里测试的是:

I4:辅导员只能审核自己负责学生的申请

十四、案例四:改变信任来源——请求字段不等于权威事实

假设某个更新请求里出现:

POST /api/leave/update HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_a
Content-Type: application/json

{
  "leaveId": 123,
  "studentId": 10001,
  "status": "draft",
  "reason": "Updated reason"
}

这里我先不修改任何值,而是先翻译业务语义:

leaveId
→ 目标对象

studentId
→ 对象所有者 / 关联身份事实

status
→ 当前业务状态

reason
→ 用户希望修改的内容

然后问:

reason 可以来自客户端,
因为它表达用户意图。

但 studentId 和 status 呢?

如果它们参与安全判断,
是否应该由服务端重新确认?

所以漏洞假设不是“studentId 改一下”,而是:

H4:服务端是否会重新从权威数据源确认申请所有者和当前状态,而不是把客户端提交的值直接当成业务事实?

Expected:

所有者 / 状态
应由服务端权威事实决定

这就是“改变信任来源”和“修改一个 JSON 字段”之间的区别。


十五、为什么完整演示只选这四个

前面的假设生成表已经覆盖:

操作者
对象
关系
动作
状态
顺序
信任
时间

这里不需要再把八种全部重新讲一遍。

这四个案例分别让我看见:

对象
状态
关系
信任

是怎么从:

业务模型
↓
HTTP 请求
↓
改变一个业务变量
↓
禁止世界
↓
漏洞假设

一路落地的。

其他变量继续套用同一套方法:

先写正常组合,保持其他条件尽量不变,只改变一个业务变量。


第七部分:假设生成以后,先排序,再验证

十六、能想到,不等于现在就值得验证

一个复杂业务可以产生很多候选假设。

所以:

能想到
≠
现在就值得验证

我会优先看四个因素:

影响
可达性
可控性
现有证据

也就是:

先验证影响大、离我近、变量可控、证据强的假设。

这里不需要真的给每条假设算分。

排序的目的只是:

先把最值得验证的假设放到前面

十七、漏洞假设和漏洞结论必须分开

探索阶段可以大胆:

会不会越权?
会不会跳状态?
会不会信任客户端 owner?
会不会附件权限没继承?

但这些都只是:

假设

而:

假设
≠
漏洞事实

所以我会把过程分成两段:

探索阶段

观察
↓
建模
↓
写业务不变量
↓
生成假设

然后:

验证阶段

选择高价值假设
↓
写 Expected
↓
最小化验证
↓
观察实际结果
↓
保存证据

我现在很喜欢一句话:

漏洞思维可以充满猜想,漏洞结论必须证据驱动。


十八、每个假设验证之前,先写 Expected

以前测试时,我容易先发请求。

然后看到:

200 OK

就开始怀疑是不是有问题。

现在我希望顺序反过来。

先写:

Hypothesis:
学生 A 尝试读取学生 B 的请假申请

Expected:
拒绝,并且不返回 B 的业务数据

然后再验证。

最终记录:

Observed:
实际业务结果是什么?

真正判断的是:

Expected
vs
Observed

而不是 HTTP 状态码。

例如:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 403,
  "message": "permission denied"
}

业务上仍然可能是正确拒绝。

真正要观察的是:

本来不允许发生的业务结果,有没有真的发生。


第八部分:用“假设账本”把测试变成研究

十九、每个假设都记录它到底在验证什么

对一个核心对象,我可以建立:

ID 业务不变量 改变变量 漏洞假设 Expected 状态
H1 只能看自己的申请 对象 A 读取 B 拒绝 待验证
H2 已通过不可修改 状态 修改已通过申请 拒绝 待验证
H3 辅导员只能管负责学生 关系 A 审核非负责学生 拒绝 待验证
H4 关键事实由服务端确认 信任 客户端值是否被当成权威事实 服务端重查 待验证

这张表真正帮我固定的是:

为什么测
测的是哪个业务条件
Expected 是什么
已经有什么证据
现在处于什么状态

于是整个过程越来越像:

提出假设
↓
做实验
↓
收集证据
↓
更新模型

而不是:

碰运气

第九部分:把整篇压缩成一个固定流程

二十、我现在生成漏洞假设的流程

以后拿到一个值得深入的核心业务,我希望自己这样走:

第一步:
先正常使用
↓
理解允许世界

第二步:
写业务不变量
↓
知道哪些规则必须始终成立

第三步:
拿出上一篇的业务判断模型
↓
操作者 / 对象 / 动作 / 关系 / 状态 / 顺序 / 信任

第四步:
把 HTTP / API 字段翻译成业务变量

第五步:
选择一个业务变量
↓
其他条件尽量保持不变
↓
让它不再满足正常条件

第六步:
构造禁止世界
↓
这个组合理论上应该被拒绝

第七步:
形成漏洞假设
↓
如果 [某条件改变]
服务器是否仍然允许 [某动作]
从而破坏 [某业务不变量]?

第八步:
按影响、可达性、可控性、证据排序

第九步:
写 Expected

第十步:
最小化验证

第十一步:
记录 Observed

第十二步:
比较 Expected / Observed

第十三步:
更新假设账本和业务模型

这套过程真正训练的是:

从一个正常业务规则中,自己推导出它值得验证的反例。


第十部分:把前三篇放回同一条主线

二十一、WHY → WHAT → HOW

到这里,前三篇的职责已经很清楚:

漏洞思维(1) —— WHY
为什么会产生漏洞?

漏洞思维(2) —— WHAT
代码为了实现业务规则,到底要判断什么?

漏洞思维(3) —— HOW
怎么从这些判断条件里主动推导漏洞假设?

这一篇真正完成的是:

正常允许组合
↓
识别业务变量
↓
改变一个变量
↓
构造禁止组合
↓
Hypothesis
↓
Expected
↓
最小化验证
↓
Observed

权限、状态机、信任、生命周期、规则断层、并发和漏洞链,

后面都会分别深入。

这里不再提前展开。


结语

以前有人问我:

“漏洞到底怎么找?”

我可能会回答:

看到 id 改一下
看到状态测一下
看到流程跳一下
看到一次性业务重放一下

现在我更愿意回答:

先理解正常业务
↓
写出业务不变量
↓
找出业务允许成立依赖的条件
↓
把技术字段翻译成业务变量
↓
只改变其中一个条件
↓
构造理论上应该失败的组合
↓
先写 Expected
↓
再看 Observed

所以真正稳定的能力不是:

我记得多少测试技巧。

而是:

我能不能从一个正常业务规则中,自己推导出它值得验证的反例。

页面会变,接口会变,参数会变,技术栈也会变。

但只要系统存在:

身份
对象
关系
权限
状态
流程
信任
时间

就会存在:

应该允许的世界

和:

应该拒绝的世界

而漏洞研究真正要问的是:

属于禁止世界的那个组合,代码真的拒绝了吗?


附:全文 ASCII 思维流程

┌─────────────────────────────┐
│          正常业务           │
│        “允许世界”           │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│         业务不变量          │
│ 哪些规则必须始终成立?      │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│     技术字段 → 业务变量     │
│                             │
│ id → 对象                   │
│ status → 状态               │
│ token → 身份上下文          │
│ reviewerId → 关系           │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│      找到业务判断变量       │
│                             │
│ 操作者 / 对象 / 动作        │
│ 关系 / 状态 / 顺序          │
│ 信任 / 时间                 │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│       一次改变一个变量      │
│   其他条件尽量保持不变      │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│         构造禁止组合        │
│  “理论上应该被拒绝的世界”  │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│        形成 Hypothesis      │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│         写 Expected         │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│         最小化验证          │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│        记录 Observed        │
└──────────────┬──────────────┘
               │
        ┌──────┴───────┐
        │              │
        ▼              ▼
┌──────────────┐  ┌────────────────┐
│   两者一致   │  │   两者不一致   │
│ 禁止世界拒绝 │  │ 禁止世界被允许 │
│ 规则仍然成立 │  │ 不变量可能失效 │
└──────────────┘  └────────┬───────┘
                            │
                            ▼
                   ┌─────────────────┐
                   │      漏洞       │
                   └─────────────────┘

注:本文讨论的是授权 SRC / 安全测试中的漏洞分析与建模方法。漏洞假设可以大胆提出,但实际验证必须遵守项目范围、账号权限和最小影响原则;假设不等于漏洞,最终结论必须建立在可复现证据之上。

posted @ 2026-09-04 18:29  0xMouise  阅读(3)  评论(0)    收藏  举报