API 越权漏洞自动化检测是我最近反复折腾的一个方向。越权漏洞说起来简单,但真要在几十个接口里找出“哪个接口能看别人数据、哪个接口能调管理员功能”,手工点一天也未必能覆盖完整。我最后搭了一套本地组合:Hadrian 负责扫描编排,Vespasian 负责异步执行,crAPI 当靶场,总算把整个流程跑顺了。这篇东西就是把当时的部署过程、配置思路和踩坑记录整理出来,给同样在做 API 安全测试、或者想学习越权漏洞的工程师一个参考。整套环境建议在本地或者授权的测试环境里玩,不要拿去做未授权测试。
1. 为什么用 Hadrian + Vespasian + crAPI 这一套
1.1 API 越权漏洞的本质与测试难点
越权漏洞在 API 里通常分两类:一类是对象级越权,也就是常说的 BOLA(Broken Object Level Authorization),比如订单详情接口写死了/order/123,普通用户把 123 改成 124,就能看到别人的订单;另一类是功能级越权,也就是 BFLA(Broken Function Level Authorization),比如普通用户直接请求管理员删除用户的接口,服务器没有校验角色权限。
这两类漏洞的共同点是:接口本身是正常的,参数和请求格式也都正确,纯粹是“授权逻辑”缺失。所以传统的漏洞扫描器非常难扫出来,因为扫描器只能判断“请求是否报错”“页面是否异常”,没法判断“这个用户有没有权限看这条数据”。手工测倒是能测,但接口一多就非常痛苦,要反复注册账号、切换 token、对比两个用户看到的数据差异,而且越权往往藏在业务逻辑层,黑盒测试很容易漏。
我在 crAPI(一个故意设计成存在大量 API 漏洞的靶场)上试过最土的办法:注册两个账号,手工写脚本逐个接口对比响应。结果发现接口数量超过 30 个之后,脚本里到处是硬编码的 ID、Token、请求头,维护成本极高,换一个接口就要改一次。后来才决定引入专门的自动化检测链路。
1.2 三个工具的分工
这套组合里,三个工具的角色非常清晰:
- crAPI 不是扫描器,它是“猎物”。它是一个模拟真实业务的 API 靶场,有注册登录、车辆管理、社区、优惠券等模块,里面埋了各种越权漏洞。把它当作扫描目标,既安全又可控。
- Hadrian 是“大脑”。它负责定义扫描策略、解析 API 文档、生成越权测试任务。比如它会从 OpenAPI / Swagger 里抽取出所有接口,标记哪些接口带路径参数,哪些接口需要认证,然后决定用什么方式做对象级越权和功能级越权检测。
- Vespasian 是“手脚”。它从队列里拉取 Hadrian 生成的任务,真正去发 HTTP 请求,把响应结果写回存储。把“扫描编排”和“请求执行”拆开,好处是任务可以并行、可以重试、可以横向扩展,扫描一批接口时不会因为单个请求超时卡死整个流程。
实际跑起来就是:Hadrian 先分析目标,把“用 A 用户请求 /user/123,再用 B 用户请求 /user/123,对比响应”这类任务丢到队列里,Vespasian 的 worker 拿到任务后开始请求,最后所有结果汇总回 Hadrian 的报告模块。整个过程不需要写一堆手工脚本,策略变更也只改配置。
1.3 为什么不直接用商业扫描器
可能会有朋友问,市面上商业 API 安全扫描器也不少,为什么折腾这一套?我自己的体验是:商业扫描器在“已知漏洞指纹”上很强,但越权测试需要知道业务上的“资源从属关系”,它很难理解。比如两个接口都不是标准漏洞特征,但响应体里一个返回了用户手机号,一个返回了“无权限”,这种业务语义差异,商业扫描器基本只能靠规则碰运气。
另一个原因是二次开发能力。Hadrian 和 Vespasian 都是开源项目,策略代码在手里,扫描规则可以自己加。比如我想自定义一个“批量赋值”检测规则,直接在 Hadrian 的检测模块里加一个插件就行。这点在内部测试平台里非常关键,因为每个公司的 API 风格和权限模型都不一样,能改代码的扫描器才真正好用。
2. 部署前需要准备的环境
2.1 本地环境与资源建议
我建议准备一台至少 8GB 内存的机器。整套环境要跑 crAPI 的几个后端服务、数据库、消息队列、Hadrian 和 Vespasian 的执行器,全部加起来内存占用非常可观。我自己在 16GB 内存的笔记本上跑是流畅的,但有一次在 4GB 的小机器上硬跑,Vespasian 的 worker 频繁被系统杀掉,日志里全是 OOM。
操作系统方面,Windows、macOS、Linux 都能跑,前提是 Docker 环境正常。如果是在 Linux 服务器上部署,建议用 Docker Engine 配合 Docker Compose 插件;如果在 macOS 或 Windows 上用 Docker Desktop,注意给 Docker 分配足够的内存,不要用默认的 2GB。这里多说一句,如果机器资源紧张,可以先只启动 crAPI 和 Vespasian,Hadrian 先不启动,等需要扫描时再拉起来。
磁盘方面预留 20GB 以上空间,因为要拉取的镜像里包含了各种运行时和数据存储,单个镜像几百 MB 很正常。
2.2 Docker 与 Docker Compose 安装验证
部署之前需要确认 Docker 环境可用。在终端里执行:
docker --version docker compose version如果docker compose没有找到,说明 Compose 插件还没装。现在新版的 Docker Engine 通常自带 Compose v2 插件,旧环境需要单独安装。装完之后先跑一个测试容器:
docker run --rm hello-world看到 “Hello from Docker!” 说明 Docker 本身没问题。要注意一点,如果之前装过旧版 Docker Toolbox 或者一直用老旧的 docker-compose,建议统一升级到 Compose v2,命令是docker compose(中间有空格)而不是docker-compose。新的 compose 文件在解析变量、指定网络、健康检查上都更靠谱,后面部署这套东西会少很多麻烦。
2.3 拉取项目与镜像
三个工具的代码仓库都不一样,建议统一放在同一个工作目录下。我用的是:
mkdir -p ~/api-security-lab cd ~/api-security-lab git clone <crAPI仓库地址> git clone <Hadrian仓库地址> git clone <Vespasian仓库地址>这里我没有写具体仓库地址,因为项目更新比较快,直接去对应项目的 GitHub 首页复制地址最稳妥。拉代码的时候要用 git clone,不要手动下载 zip,后面要切换分支或者拉更新会非常麻烦。
拉完代码之后,先别急着docker compose up。花两分钟看一下每个项目根目录下的 README 和.env.example,确认当前版本需要的环境变量。很多部署问题不是工具本身有 bug,而是版本变了,环境变量名字变了,配置还按旧文档写,结果启动直接报错。我的习惯是先把.env.example复制成.env,再逐个改里面的端口、密码、Token 密钥。
3. crAPI 靶场部署
3.1 启动 crAPI 服务
crAPI 的启动方式基本是一个命令:
cd crAPI cp .env.example .env docker compose up -d第一次启动会拉取一串镜像,时间取决于网速,耐心等就行。启动完成后用docker compose ps看一下容器状态,正常状态应该是所有服务都是Up,没有反复重启的迹象。
crAPI 本身由多个服务组成:API 网关、业务后端、身份认证服务、数据库、邮件服务等。启动后默认 Web 访问端口在8888(具体以当前版本 README 为准),邮件服务可能在另一个端口。如果本地端口有冲突,可以修改 compose 文件里的端口映射,比如把"8888:8888"改成"8889:8888",左边是宿主机端口,右边是容器内端口。这个细节很容易踩坑,改了端口之后,后面 Hadrian 配目标地址也要跟着改。
启动之后浏览器访问http://localhost:8888,能看到注册登录页面就说明 crAPI 起来了。
3.2 注册账号与获取基础配置
crAPI 的注册逻辑和真实业务很接近,要求填邮箱、密码、手机号等。邮箱填入一个看起来真实但不存在的地址就行。注册完之后,crAPI 不会真的发外部邮件,而是把邮件内容显示在它自带的邮件服务里,入口一般在网页右上角或者专门的/mail路径。从这个邮件服务里可以拿到激活链接或者客户端 ID 之类的信息。
越权测试通常需要两个或多个账号,所以至少要注册两个。为了方便识别,我习惯用user1@test.com、user2@test.com这样的邮箱,手机号也填不同的。之后为了避免密码记混,统一用一个测试密码。
这里有个小技巧:注册时把“账号拥有者”和“被越权目标”的数据差异化。比如 user1 的车牌号填 AB-12345,user2 的车牌号填 CD-67890。后面扫描器判断越权时,如果 user1 请求 user2 的资源却返回了 CD-67890,那基本可以确定为对象级越权,而不是因为数据恰好一样导致的误判。
3.3 crAPI 里值得关注的越权场景
crAPI 虽然是靶场,但它设计的接口节奏非常接近真实商业 API。我梳理过的几类典型场景:
- 用户信息接口带主键 ID,修改 ID 可能返回他人资料。
- 车辆控制接口只校验是否登录,不校验车辆归属,A 用户能操作 B 用户的车辆。
- 优惠券、订单、社区帖子等资源接口存在对象级越权。
- 部分管理员接口只做了隐藏,没有做权限校验,普通用户直接请求也能访问。
这些场景非常适合验证 Hadrian 和 Vespasian 的检测能力。我们在本地把这套流程跑通之后,再拿到有授权的真实测试环境里,替换掉目标地址和认证配置,逻辑是一样的。毕竟越权检测的关键不是“用什么工具”,而是“工具能不能理解资源归属关系”。
4. Hadrian 与 Vespasian 部署与配置
4.1 Hadrian 配置解析
Hadrian 启动前需要改配置文件,一般是 YAML 格式。下面这个是我在本地用的配置片段,核心思路是:让 Hadrian 先解析 crAPI 的 OpenAPI 文档,再用两个账号的 Token 做对比测试。
target: base_url: "http://crapi:8888" openapi_path: "/openapi.json" auth: mode: "bearer" login_endpoint: "/auth/login" credential: user1: email: "user1@test.com" password: "Test@123" user2: email: "user2@test.com" password: "Test@123" token_field: "access_token" rotate_user: true scan: concurrency: 10 timeout: 10 checks: - bola - bfla - mass_assignment逐个说下关键项。
target.base_url要特别注意,这里是http://crapi:8888而不是http://localhost:8888。因为 Hadrian 和 crAPI 都在 Docker 网络里,用容器名crapi访问才能走内部网络;如果写 localhost,从 Hadrian 容器里访问的是它自己,会连接拒绝。
auth.rotate_user是越权检测的灵魂。打开之后,Hadrian 会交替使用两个账号的 Token 去请求同一个资源接口,然后对比响应。如果关闭,扫描器就只会用 user1 一个身份,很多越权场景根本测不出来。
scan.concurrency控制并发请求数。第一次跑不要调太高,10 就够。并发太高,crAPI 这种小靶场可能直接拒绝服务,返回一堆 429 或者 500,反而干扰结果判断。
4.2 Vespasian 执行器配置
Vespasian 的配置重点在队列和 worker 数量。它默认需要连一个消息队列,可以是 Redis。如果和 Hadrian 一起部署,确保 Redis 的地址和密码在两边配置成一样的,不然 Hadrian 往队列里写任务,Vespasian 读不到。
Vespasian 的 worker 配置大概长这样:
queue: type: redis host: redis port: 6379 password: "" worker: concurrency: 16 retry_on_failure: true max_retries: 3 timeout: 30worker.concurrency代表同时处理多少个任务。这个值不是越大越好,它受到 CPU、内存、目标接口响应速度的综合影响。在 crAPI 这种本地靶场上,16 足够;如果扫描外部授权目标,可以从 4 开始试探着往上调。
有个细节值得提:timeout和max_retries一定要配。越权检测里经常遇到某个接口响应特别慢,或者偶尔 500,如果没有超时控制,一个任务会一直占着 worker,直到整条队列积压。设置 30 秒超时和 3 次重试,能明显提高扫描稳定性。
4.3 启动并验证服务健康状态
配置改完就可以启动了。我这里假设 Hadrian 和 Vespasian 各自在同一个 compose 文件里,或者被同一个 external 网络管理:
cd hadrian docker compose up -d cd ../vespasian docker compose up -d启动后分别看日志:
docker compose logs -f hadrian docker compose logs -f vespasian-worker健康的日志里应该能看到服务成功连接 Redis、等待任务之类的信息。如果 Vespasian 连不上 Redis,日志里会直接甩出 connection refused,这时候先检查.env里 Redis 的密码和 host 是否跟实际一致。
我这里强烈建议先把 crAPI 和这套扫描链路放到同一个 Docker 网络里。最简单的方式是启动 crAPI 时记住网络名,然后在 Hadrian 和 Vespasian 的 compose 文件里用 external 网络指过去。很多朋友部署完发现 Hadrian 能启动,但扫描时全部连接失败,最后排查下来就是网络隔离问题,容器之间根本 ping 不同。
5. 自动化越权检测的实际操作流程
5.1 配置扫描目标与认证信息
Hadrian 一般都提供 UI 或 REST API 来配置扫描任务。以 UI 为例,进入管理页面后找到 “Targets” 或 “New Scan”,添加目标时填写:
- 目标名称:给这次扫描起个名字,比如
crAPI-local - 目标地址:
http://crapi:8888 - API 文档地址:
http://crapi:8888/openapi.json
认证信息这里有两种做法。一种是在配置里写死账号密码,让扫描器自动去登录获取 Token;另一种是先把两个 Token 手动在页面里填入,扫描器直接使用。我推荐第一种,因为 crAPI 的 Token 有效期有限,自动登录能保证扫描期间 Token 都是新的。不过要注意,登录接口的返回字段要填对,token_field要匹配 crAPI 实际返回 JSON 里的字段名,我见过最典型的问题就是字段名填错了,导致扫描器拿到一个空 Token,所有请求全部 401。
5.2 开始一次扫描
配置完成后,选择要执行的检测项,我只勾选了 BOLA、BFLA 和批量赋值,然后点击启动。Hadrian 会先做一次接口梳理:从 OpenAPI 文档里提取所有 path,标记带路径参数的接口,比如/identity/{user_id}这种,然后把它们作为对象级越权的重点检测对象。
整个扫描过程不需要人盯着。Vespasian 会不断从队列里取任务执行,我这边观察到的典型日志类似:
task user2_request_with_user1_token: /identity/2 task: compare_response_body_by_source task result: high_confidence_bola看到high_confidence_bola这种结果,基本说明发现了一个高置信度的对象级越权漏洞。扫描结束之后,页面会生成一份报告,列出所有检测过的接口,以及每个接口的检测结果。
5.3 报告里的字段与真伪判断
报告里常见的字段包括:
- 请求地址和方法
- 使用的账号身份
- 被访问的具体资源 ID
- 两个账号请求同一资源时的响应状态码
- 响应体长度差异
- 响应体关键字段是否包含不属于当前账号的数据
- 置信度和漏洞类型
看报告时不要只盯着状态码。很多越权不是靠状态码暴露的,比如返回码都是 200,但 user1 的响应体里返回了手机号、真实姓名、车辆 VIN 码,这就是越权。相反,如果两个用户的响应体只有创建时间不同,而业务字段都是空的,那可能是正常数据,不一定算越权。
一个很实用的验证方法:报告里发现可疑接口后,手动用 curl 重放一遍。我从 crAPI 上发现过/identity/user/2越权时,重放命令类似这样:
curl -H "Authorization: Bearer $TOKEN_USER1" http://localhost:8888/identity/user/2 curl -H "Authorization: Bearer $TOKEN_USER2" http://localhost:8888/identity/user/2user1 请求 user2 对应的资源 ID,如果返回的数据里带的是 user2 的邮箱或手机号,基本就能确认问题。这一步不可省,自动化工具的价值在于缩小范围,最终确认还是要靠人。
6. 常见问题与排查实录
6.1 Docker 网络通信问题
我自己遇到最多的问题就是容器之间访问不了。典型现象是 Hadrian 日志里全是dial tcp: lookup crapi: no such host,但 crAPI 明明在运行。
原因通常有两个。第一,Hadrian 和 crAPI 不在同一个 Docker 网络里。解决办法是让两个容器加入同一个网络:
docker network create api-scan-net docker network connect api-scan-net crapi docker network connect api-scan-net hadrian第二,配置文件里写了localhost。容器里的 localhost 指的是容器自己,一定要用服务名或容器名。这个坑我踩了不止一次,现在写配置前都会先确认目标服务在 compose 里的 service name。
6.2 Token 或认证信息不生效
扫描刚开始,所有请求都返回 401,排查思路按顺序来:
- 登录接口地址对不对,crAPI 的登录路径可能是
/auth/login,也可能是/identity/auth/login,要以 OpenAPI 文档为准。 - 返回字段名对不对,crAPI 返回的是
access_token还是data.token?填错了照样 401。 - Token 是否过期,如果扫描时间很长,可以在 Hadrian 里配置自动 refresh,或者改用 Vespasian worker 里的 token 刷新逻辑。
这里有个特殊情况:有些越权漏洞本身就是“不校验 Token”,扫描器如果发现某个接口无论带什么 Token 都返回同样的数据,报告里可能会标记成“认证失效”而不是越权。这两种漏洞要分开上报,因为修复方案不一样,前者是加认证校验,后者是加权限校验。
6.3 误报与漏报的处理策略
自动化工具免不了误报。以 crAPI 为例,有些接口会给未登录用户返回“欢迎页数据”,给已登录用户返回“用户面板数据”,响应体长度差异很大,扫描器可能误以为是越权。
我的经验是调整 Hadrian 的“响应对比策略”。不要只看长度,要设置一个字段黑名单,把timestamp、request_id、session_id这类动态字段排除掉,聚焦在业务数据字段上。然后在报告里按置信度排序,优先人工复核高置信度的结果。漏报的问题更棘手,通常要靠增加测试账号和丰富数据差异来解决。我一开始只注册两个账号,很多越权场景测不到;后来注册了四个不同角色账号,扫描覆盖率明显提升。
6.4 排查速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| hadrian 报 no such host | 容器不在同一网络 / 用错 target 地址 | 配置使用服务名,connect 到同一 docker network |
| vespasian worker 连不上 redis | Redis host/密码不一致 | 检查 compose env,确认 host 和密码 |
| 所有请求 401 | 登录接口或 token_field 配置错误 / token 过期 | 检查 OpenAPI,修正 auth 配置,开启自动登录 |
| 扫描任务积压不执行 | 队列没有 worker / worker 挂掉 | docker compose ps 检查 worker 状态,看日志 |
| 报告大量误报 | 动态字段导致响应对比失真 | 配置字段黑名单,调整置信度阈值 |
| 容器频繁重启 | 内存不足 | 调整 Docker memory 限制,关闭不用的容器 |
| 端口被占用 | 宿主机端口冲突 | 修改 compose 端口映射,注意同步修改 target URL |
这条速查表是我这次部署过程中最常用的东西。很多时候不是工具坏了,而是环境和配置的问题,按这个顺序排查,基本能解决 90% 的问题。
7. 一点个人使用体会
整套组合跑通之后,我把 crAPI 上的越权接口基本上都扫出来了,比自己手工写脚本效率高不少。不过我也得说句实话,自动化检测再怎么方便,也不能完全替代人工分析。工具只能告诉你“这两个响应不一样”,但“为什么不一样、是不是越权、影响范围多大”,还是得人去判断。我现在的习惯是,每周在本地靶场跑一次这套流程,把新增的漏洞场景更新到 crAPI 或者自己的测试应用里,再反过来验证扫描器的能力。另一个实用的小技巧是:正式扫描之前先导入两套业务差异足够大的测试数据,比如一个账号有订单,一个账号没有订单,这样扫描器对不同响应会更加敏感。后续如果想扩展,可以在 Vespasian 里增加埋点,把每次越权请求都记录下来,形成自己的自动化回归用例集,这样以后 API 版本更新时可以直接重放验证。