漏洞思维(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 / 安全测试中的漏洞分析与建模方法。漏洞假设可以大胆提出,但实际验证必须遵守项目范围、账号权限和最小影响原则;假设不等于漏洞,最终结论必须建立在可复现证据之上。

浙公网安备 33010602011771号