四道内部 CTF 的共同根因:API/Web 鉴权割裂与 RBAC 继承风险

导读

一次经过授权的内部 CTF,一共四道题:读取受限数据、完成受限写操作、识别高权限身份风险、分析角色继承造成的权限扩大。题目表面不同,最后却指向同一件事:系统拥有两套入口,却没有一套统一的授权规则

本文只保留权限模型、排查方法和整改思路。真实域名、人员、账号、凭证、内部路径、业务 ID、请求命令和可直接复用的利用细节均已删除或泛化,请勿将这些思路用于未授权系统。

四题其实是一条权限链

靶场同时提供 Web 端和 API 端。两边认证方式不同,权限节点也分别维护,但最终访问的是同一批业务数据。

入口 身份载体 授权方式 主要风险
Web 端 Session 页面或控制器节点 页面被拦,不代表数据真的不可访问
API 端 Token API 路由节点 同一业务对象可能套用另一份权限规则
1
2
3
普通用户
├─ Web Session → Web RBAC ─┐
└─ API Token → API RBAC ─┴→ 同一业务数据

如果 Web RBACAPI RBAC 分别配置、分别上线、分别回归,就会产生最危险的错觉:前端页面返回“无权限”,团队便认为功能已经安全;实际上,另一条 API 路径可能仍然开放。

第一题:页面被拦,数据却能从另一入口读取

第一道题的现象是 Web 页面拒绝访问,但同一业务对象在 API 端存在等价查询能力。真正的问题不是“页面能不能绕”,而是:

  1. 同一资源被两个入口暴露;
  2. 两套入口使用不同权限节点;
  3. API 只验证“是否登录”或路由权限,没有验证用户能否访问这个业务对象。

这类问题不能靠隐藏菜单、跳转登录页或返回特定状态码修复。正确做法是把授权条件落到资源层:

1
2
3
canReadTask(user, task)
= hasRoutePermission(user, "task:read")
AND belongsToAuthorizedProject(user, task.projectId)

Web 和 API 都调用同一个资源授权规则,入口差异只负责认证,不负责决定业务权限。

第二题:读权限和写权限没有形成闭环

第二道题从“能看见数据”进一步走到了“能够创建并改变状态”。这说明系统不仅存在读越权,写接口也缺少同等级别的校验。

写接口至少需要同时回答四个问题:

  • 当前身份能否访问目标项目?
  • 当前身份能否执行这个动作?
  • 请求中的资源 ID 是否属于该身份的授权范围?
  • 状态变化是否符合业务状态机?

只检查 Token 有效、路由存在或参数齐全,都不足以证明这次写入合法。尤其是“更新状态”类接口,服务端必须自己判断允许的前后状态,不能直接相信客户端传入的完成比例或状态值。

第三题:列表接口不该返回身份材料

第三道题暴露的是数据最小化问题:普通业务列表返回了远超页面展示需要的字段,其中包含认证相关材料和可用于身份冒用的信息。

这是典型的“对象序列化过度”:

1
2
3
4
5
6
7
8
9
// 风险做法:数据库整行直接返回
return json($userModel->select());

// 更安全:显式定义公开 DTO
return json($users->visible([
'id',
'display_name',
'department_name',
]));

密码摘要、盐、初始密码、API Token、手机号和邮箱都不应因为“前端暂时没显示”就留在响应里。浏览器开发者工具、代理日志、错误平台和缓存都可能把这些字段继续扩散。

一旦发现此类问题,修复顺序应该是:

  1. 立即下线敏感字段;
  2. 轮换已经暴露的 Token 和会话;
  3. 检查访问日志,确认泄露范围;
  4. 使用专用 DTO 或响应白名单,避免再次整行返回。

第四题:岗位继承让一次改动影响一群人

最后一道题的核心是 RBAC 继承关系。系统的权限不是直接绑定用户,而是经过岗位与角色组逐层传递:

1
用户 → 岗位 → 角色组 → 权限规则

这种模型便于批量管理,但也放大了错误配置的影响。如果一个普通岗位被关联到高权限角色组,那么该岗位下的所有用户都会同步获得权限。

风险不在于“岗位关联功能存在”,而在于高权限变更缺少保护:

  • 高权限角色可以被普通编辑表单直接选择;
  • 变更前不展示受影响用户数量;
  • 没有二次确认或更强身份校验;
  • 没有独立审批与不可抵赖审计;
  • 超级权限和普通业务角色共用同一套批量继承机制。

更稳妥的设计是把超级权限从普通岗位继承中拆开,采用单独授权、双人审批、短时有效和完整审计。批量角色变更前,还应计算并展示影响范围。

根因总结

四道题分别触发了不同现象,但根因可以压缩为四类:

现象 根因 防守措施
Web 拒绝,API 可读 多入口策略不一致 统一资源级授权
普通身份可写 只校验路由,不校验对象与动作 对象级授权 + 状态机
列表泄露身份材料 数据模型直接序列化 DTO 白名单 + 凭证轮换
改岗位导致批量提权 RBAC 继承缺少高权限边界 超级权限独立管理 + 影响面审计

另外,演练还发现了遗留密码摘要和可恢复初始密码的风险。无论算法叠加多少次,快速摘要都不能替代现代密码哈希;密码应使用 Argon2id、bcrypt 等带成本参数的方案,初始密码也不应以可恢复形式保存。

一份可以直接复用的检查清单

接口层

  • 同一资源是否同时存在 Web、API、移动端或旧版本入口?
  • 各入口是否调用同一份资源授权逻辑?
  • 详情、列表、导出、创建、编辑、删除是否分别校验?
  • 是否校验“当前用户与目标对象的关系”,而不只是登录状态?

数据层

  • 响应是否使用字段白名单?
  • 用户表、日志和导出文件是否包含 Token、密码材料或隐私字段?
  • 凭证泄露后是否具备批量吊销和轮换能力?

RBAC

  • 超级角色是否能被普通岗位继承?
  • 角色变更是否展示影响人数?
  • 高权限操作是否需要二次认证、审批和审计?
  • 删除菜单后,对应后端接口是否仍能访问?

回归验证

  • 用最低权限账号跑完整的读写矩阵;
  • 对同一资源分别测试所有入口;
  • 测试跨项目、跨部门和跨租户的对象 ID;
  • 权限变更后验证受影响用户,而不只验证操作者本人;
  • 对敏感响应字段设置自动化契约测试。

结语

这次四题通关,最有价值的不是记住某个接口或请求写法,而是建立一套排查顺序:

先画入口,再画身份;先找资源,再查授权;最后沿角色继承计算影响范围。

当系统存在多套入口、历史接口和复杂 RBAC 时,“页面提示无权限”只是开始。真正可靠的安全边界,必须落在统一、可测试、默认拒绝的服务端授权规则上。