Yashmko/charon
GitHub: Yashmko/charon
Charon 是一款基于 LLM 语义推理与流量重放的 IDOR/BOLA 授权漏洞检测工具,通过自动推断资源所有权模型来替代传统盲目的 ID 暴力替换。
Stars: 0 | Forks: 0
# Charon — 语义化 IDOR 与授权检测工具
停止猜测 ID。开始推理所有权关系。



## 目录
- [问题所在](#the-problem)
- [相关工具](#related-tools)
- [工作原理](#how-it-works)
- [阶段分解](#stage-breakdown)
- [授权 DNA 与漂移检测](#authorization-dna--drift-detection)
- [数据模型](#data-model)
- [计划中的 CLI](#planned-cli)
- [技术栈](#tech-stack)
- [项目结构](#project-structure)
- [构建路线图](#build-roadmap)
- [OWASP 映射](#owasp-mapping)
- [未来愿景 (v2)](#future-vision-v2)
- [范围与道德](#scope--ethics)
- [状态](#status)
## 问题所在
大多数 IDOR/BOLA 扫描工具都采用同样简单粗暴的方式:接收类似 `GET /api/orders/123` 的请求,将 ID 替换为 `122`、`124` 或其他用户的值,然后检查响应是否发生变化。这种暴力破解方式根本不理解 endpoint 的具体作用,也不知道谁应该拥有该资源。这不仅会产生大量噪音,还会遗漏依赖于角色或关系的授权逻辑(例如,经理可以查看下属的数据,但不能查看其他经理的),并且无法区分正确脱敏的响应与真实的数据泄露。
Charon 用推理代替猜测。它根据 API 规范和真实流量构建资源所有权模型,生成授权假设(“用户 B 绝不应该看到用户 A 的发票”),并且只有在响应确实违反了某项假设时,才会将其报告为漏洞。
## 相关工具
像 **Autorize** 和 **AuthMatrix** 这样的 Burp 插件已经能够实现跨账号请求重放和响应差异比对了——这种机制并不新鲜,提前了解这一点很有价值。它们做不到的是推理:它们只能告诉你响应发生了变化,却无法判断这种变化是否真的是一次数据泄露,而且所有权/角色规则必须由人工提前配置好,工具才能使用。Charon 的差异化优势在更高一层——它从流量中推断所有权和策略模型,而不是依靠手工配置;它通过语义进行漏洞分诊,而不仅仅是依靠状态码。
## 工作原理
```
┌─────────────────────┐
│ Input capture │ API spec + multi-account traffic
└──────────┬──────────┘
│
┌──────────▼───────────┐
│ Ownership modeling │ LLM maps fields → resource owners
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Hypothesis + test gen│ Crafts cross-account test cases
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Live replay execution│ Fires requests with swapped tokens
└──────────┬───────────┘
│
┌──────────▼────────────┐
│ Semantic triage+report│ Maps findings to OWASP API Top 10
└───────────────────────┘
```
包含五个阶段,每个阶段都处理上一阶段的输出。每个阶段都可以独立测试——你可以针对已保存的抓包记录运行所有权建模,而无需访问实际的目标环境,这对于 CI 流程以及安全地迭代启发式算法非常重要。
## 阶段分解
### 1. 输入抓取
**目的:** 收集 API 在至少两个不同账号下实际运行情况的真实数据,以便后续阶段能够基于真实流量进行推理,而不是单凭规范进行盲目猜测。
**输入:** OpenAPI/Swagger 规范(如果没有正式规范,也可以是 Postman 集合),以及每个测试账号在正常使用时的代理抓包记录(Burp/ZAP/mitmproxy)——包括登录、浏览、创建/读取/更新一些资源。
**每笔交易记录的内容:**
| 字段 | 描述 |
|---|---|
| `account_label` | 发起请求的测试账号(例如 `userA`, `userB`, `admin`) |
| `method` | HTTP 动词 |
| `raw_path` | 抓取到的字面路径,例如 `/api/orders/8821` |
| `path_template` | 标准化路由,例如 `/api/orders/{id}` —— 使工具能够跨账号和不同 ID 将请求归为“同一个 endpoint” |
| `query_params` | 解析后的键值对 |
| `headers` | 完整的 header 集合;auth header/cookie 会被保留(供后续重放使用),其他所有内容也会保留用作上下文参考 |
| `body` | 解析后的 JSON/form body(如果存在) |
| `response.status` | HTTP 状态码 |
| `response.headers` | 响应头 |
| `response.body` | 解析后的响应体 |
| `latency_ms` | 响应时间——后续在发现基于时间的权限检查差异时偶尔会用到 |
**资源引用提取:** 对于每一笔交易,抓取层会扫描路径片段、query params 和 body 字段,寻找任何看起来像标识符的内容——UUID、数字 ID、slug——并将其记录为 `resource_ref`:它来自哪个字段、它的字面值,以及它出现在哪个账号的会话中。由于 `userA` 的会话自然会引用 `userA` 自己的资源,这正是下一阶段所需的原始信号:“这个 ID 属于这个账号,对应这个 endpoint 模板。”
**为什么需要两个以上的账号:** 单个账号的流量只能显示该用户被允许看到的内容。跨账号抓取使得“B 的 token 是否暴露了 A 的资源”成为一个可测试的、具体的问题,而不是盲目猜测。
### 2. 所有权建模
**目的:** 将抓取到的原始流量转化为资源所有权图——对于每个 endpoint 模板,它涉及哪种资源类型,哪个字段是所有权标识符,以及它预期具有什么样的访问范围(仅限所有者、受角色控制、公开)。
**过程:** LLM 会读取 OpenAPI 规范描述、路径模板以及响应字段名称/结构(由第 1 阶段提供),并对所有权语义进行推理。响应中名为 `user_id` 或 `owner_id` 的字段,或者像 `/users/{id}/invoices` 这样的路径,都是强烈的信号。模型还会参考 REST 约定——对 `/admin/*` 的 `DELETE` 请求意味着即使没有明确的规范注解,也存在角色访问控制。
**输出:** 生成一个图,其中每个节点代表一种资源类型(`order`, `invoice`, `profile`),并包含指向与其相关联的 endpoint 的边,以及针对每个节点推断出的所有权规则——此外,还会为每个角色生成一份通俗易懂的策略声明,例如:“访客只能访问公开资源”、“客服支持可以读取但不能编辑”、“管理员绕过租户限制”。以可读的形式展示推断出的策略(而不仅仅是作为内部图),正是让第 5 阶段能够标记出既定策略与观察到的行为之间的矛盾的关键,而不仅仅是发现孤立的异常响应。
### 3. 假设与测试生成
**目的:** 将所有权图转化为具体的、可证伪的声明,然后将每个声明转换为实际的请求。
**假设示例:** “使用 `userB` 身份验证对 `/invoices/{id}` 发起的请求,在 `{id}` 属于 `userA` 的情况下,应当被拒绝(403/404)。”
**测试生成:** 对于每一个假设,构建一个请求模板,该模板接收一个真实抓取到的请求,并在保留原始资源 ID 的同时替换其 auth 上下文(token/cookie)——这就生成了实际的跨账号测试用例。涵盖 BOLA(对象级别——B 能否读取/写入 A 的特定资源)、BFLA(功能级别——非管理员账号能否触发仅限管理员的操作),以及属性级别的测试——除了仅测试完整对象的 GET 替换外,还会针对另一个账号的资源生成单字段的 PATCH/PUT 请求,因为只写部分字段的访问权限是一个常见的漏洞,只读测试会完全漏掉它。
### 4. 实时重放执行
**目的:** 向实时目标实际触发生成的测试用例。
**过程:** 轻量级重放引擎接收每个测试用例,将 `Authorization` header 或会话 cookie 替换为另一个账号的凭据,发送请求,并将响应与原始(同账号)响应一起记录以供比较。系统会按账号处理 session/token 的刷新,因此长时间的测试运行不会因为认证过期而失败。
### 5. 语义分诊与报告
**目的:** 将真实漏洞与噪音区分开来。并非所有非 403 的响应都是漏洞——有些 endpoint 对于错误的账号会正确地返回空数据或脱敏数据。
**过程:** 响应体会与第 2 阶段的所有权图进行比对。只有当响应确实包含了在语义上属于其他账号的字段——姓名、电子邮件、地址、余额,以及任何被标记为所有者范围的字段——时,才会触发漏洞报告,而不是仅仅因为状态码不是 403 就报警。每个漏洞都会获得一个置信度分数和一行解释。
**影响范围:** 一旦确认漏洞,Charon 就会查询所有权图以寻找同级节点——触及相同资源类型或匹配相同路由模板族的其他 endpoint——并自动针对它们重新运行相同的跨账号测试。在 `/invoices/{id}` 上确认的一个泄露,会自动触发对 `/invoices/{id}/export`、`/invoices/{id}/attachments` 以及共享该资源类型的任何其他路径的检查,而不需要依赖人工去想到并手动测试它们。
**根本原因聚类:** 在报告输出中,共享相同潜在原因的漏洞——例如 `InvoiceController` 中的每个 endpoint 都缺少相同的所有权检查——将被归并在一个根本原因下,而不是作为单独的警报列出。一份写着“1 个根本原因,4 个受影响的 endpoint”的报告是开发者真正会去修复的东西;而“4 个漏洞”只会被塞进待办事项中然后被遗忘。
**“为什么这个没有被拦截?”:** 每个漏洞报告都包含一个简短的前后对比——该角色预期的推断策略是什么,实际观察到的情况是什么,以及可能的原因。在构建此功能时需要牢记一个诚实的事实:Charon 是一个黑盒工具,它永远看不到目标的源代码,所以这是一种推断出的假设,而不是追踪的执行日志——它无法字面意义上看到“所有权检查的 middleware 没有运行”。它能够切实做到的是将策略与行为进行比较,并指出最可能的漏洞,尤其是当影响范围显示相同的漏洞在整个路由族中反复出现时:
```
Expected (from inferred policy): guest -> invoice: forbidden
Observed: guest token -> GET /invoices/8821 -> 200, returned owner_id + email + amount
Likely cause: ownership check missing or misconfigured for this resource —
repeats across 4 sibling endpoints in the same route family, suggesting the
gap is in shared handler logic rather than one-off endpoint code.
```
这依然具有极高的可操作性——“这个漏洞在于共享逻辑,这是证据”——同时无需声称 Charon 实际上并不具备的、对服务器内部逻辑的可见性。
**证据级别的输出:** 每个漏洞都附带原始(同账号)请求、替换账号后的请求、结构化的响应差异,以及一个最小复现步骤——即仍然能触发该泄露的最小请求。推理过程本身作为一条显式的逻辑链被存储(资源所有者、请求者身份、泄露的内容、推断策略预测的内容),而不是一句简单的置信度评语,这样审核人员就可以逐步验证它,而不是仅仅盲目相信一个数字。
**输出:** 映射到 OWASP API Security Top 10 的结构化漏洞报告——主要针对 **API1:2023 Broken Object Level Authorization** 和 **API5:2023 Broken Function Level Authorization**——可以直接放入渗透测试报告中。
## 授权 DNA 与漂移检测
第 2 阶段已经在内部构建了策略模型。将其序列化可以为你提供超越单次运行的实用价值:一份应用程序推断授权规则的可读快照,以及两个不同时间点快照之间的差异对比。
```
resource: invoice
owner: customer_id
permissions:
owner: [read, download]
accountant: [read, update]
admin: ["*"]
forbidden:
guest: ["*"]
```
在重构或重新部署后再次运行 Charon,并将新的快照与上一次的快照进行对比:
```
- guest -> read invoice ❌
+ guest -> read invoice ✅
```
这是一种机制,而不是三个独立的功能——对策略图进行快照、存储,并将下一次的快照与它进行对比。这种单一的对比引擎涵盖了人类可读的应用程序授权规范、重构静默扩大访问权限时的漂移检测,以及(配合未来愿景中提到的 CI 集成)与特定部署绑定的回归测试。
## 数据模型
抓取的流量和发现的漏洞存储在 SQLite 中(在这种规模下不需要更重的数据库)。
```
CREATE TABLE captured_requests (
id INTEGER PRIMARY KEY,
account_label TEXT NOT NULL,
method TEXT NOT NULL,
raw_path TEXT NOT NULL,
path_template TEXT NOT NULL,
query_params TEXT, -- JSON
headers TEXT, -- JSON
body TEXT, -- JSON or raw text
captured_at TIMESTAMP
);
CREATE TABLE captured_responses (
id INTEGER PRIMARY KEY,
request_id INTEGER REFERENCES captured_requests(id),
status_code INTEGER,
headers TEXT, -- JSON
body TEXT, -- JSON or raw text
latency_ms INTEGER
);
CREATE TABLE resource_refs (
id INTEGER PRIMARY KEY,
request_id INTEGER REFERENCES captured_requests(id),
field_location TEXT, -- 'path' | 'query' | 'body'
field_name TEXT,
value TEXT,
inferred_type TEXT -- 'uuid' | 'int' | 'slug'
);
CREATE TABLE ownership_graph (
id INTEGER PRIMARY KEY,
resource_type TEXT NOT NULL,
path_template TEXT NOT NULL,
owner_field TEXT,
scope TEXT -- 'owner_only' | 'role_gated' | 'public'
);
CREATE TABLE findings (
id INTEGER PRIMARY KEY,
hypothesis TEXT NOT NULL,
original_request_id INTEGER REFERENCES captured_requests(id), -- same-account baseline
swapped_request_id INTEGER REFERENCES captured_requests(id), -- cross-account replay
leaked_fields TEXT, -- JSON list
response_diff TEXT, -- JSON: field-level diff between baseline and swapped response
confidence REAL,
owasp_class TEXT, -- 'API1' | 'API5' | ...
explanation TEXT,
repro_steps TEXT, -- minimal reproduction
sibling_of INTEGER REFERENCES findings(id) -- set when surfaced via blast radius
);
```
已抓取交易的示例,及其存储格式:
```
{
"account_label": "userB",
"method": "GET",
"raw_path": "/api/invoices/8821",
"path_template": "/api/invoices/{id}",
"headers": { "Authorization": "Bearer " },
"response": {
"status_code": 200,
"body": { "id": 8821, "owner_id": "userA", "amount": 4200, "email": "usera@example.com" }
},
"resource_refs": [
{ "field_location": "path", "field_name": "id", "value": "8821", "inferred_type": "int" }
]
}
```
这一条记录——userB 的 token,userA 的发票 ID,包含 userA 电子邮件的响应——正是构建漏洞报告的原始素材。
证据级别漏洞报告示例,其在报告中的呈现形式:
```
{
"hypothesis": "userB should not be able to read userA's invoice",
"owasp_class": "API1",
"confidence": 0.93,
"leaked_fields": ["email", "amount", "owner_id"],
"explanation": "userB's token returned userA's invoice including PII fields tagged owner-scoped in the ownership graph",
"evidence": {
"baseline_request": "GET /api/invoices/8821 (as userA) -> 200",
"swapped_request": "GET /api/invoices/8821 (as userB) -> 200",
"response_diff": "swapped response identical to baseline; no redaction applied"
},
"repro_steps": "Authenticate as userB, GET /api/invoices/8821 (an ID owned by userA)",
"sibling_of": null
}
```
## 计划中的 CLI
目标接口(尚未实现——这是设计目标):
```
# 步骤 1:通过 local proxy 捕获每个账户的流量
charon capture --account userA --proxy :8080
charon capture --account userB --proxy :8080
charon capture --account admin --proxy :8080
# 步骤 2:根据 spec + captures 构建 ownership graph
charon model --spec openapi.json --captures ./captures.db
# 步骤 3:生成 hypotheses + test cases
charon generate --captures ./captures.db --out ./testcases.json
# 步骤 4:针对 live target 进行重放
charon run --testcases ./testcases.json --target https://staging.example.com
# 步骤 5:triage + 报告
charon report --format markdown --out ./findings.md
```
## 技术栈
- **语言:** Python 3.11+(实现调用 LLM、重放 HTTP 的 CLI 工具的最快途径)
- **HTTP 重放:** `httpx`
- **存储:** SQLite(无基础设施开销,便携的单文件数据库)
- **LLM 调用:** Groq API,用于所有权建模和分诊分类步骤——这些调用是大量小型、低延迟的请求,而不是一次大型的内容生成,这正是 Groq 推理速度的强项
- **规范解析:** `openapi-spec-validator` / `prance` 用于 OpenAPI 解析
- **运行环境:** GitHub Codespaces 或任何云 shell——这里没有任何地方需要本地计算,因此在低内存硬件上也能流畅运行
## 项目结构
```
charon/
├── capture/ # proxy listener, traffic recorder
├── modeling/ # ownership graph builder (LLM calls)
├── generate/ # hypothesis + test case generation
├── replay/ # live request execution engine
├── triage/ # semantic response classification + scoring
├── report/ # OWASP-mapped markdown/JSON report output
├── db/ # SQLite schema + migrations
├── cli.py # entrypoint
└── tests/
```
## 构建路线图
旨在快速获得可供演示的版本,随后再逐步深化。
**第 1 周 — 抓取与重放骨架**
基于代理的单账号抓取,SQLite 存储,手动跨账号放(硬编码 token 替换)。目前不需要 AI。目标:证明你可以将一个请求记录为 userA,并成功地以 userB 的身份针对真实的(已授权的)目标进行重放。
**第 2 周 — 所有权建模**
将抓取的数据和 OpenAPI 规范输入给 LLM 调用,生成所有权图。通过手动对比几个你已经知道答案的 endpoint 来验证它(你已经从 Tirth.com 获得了这些信息)。
**第 3 周 — 假设生成与自动化重放**
自动将图转化为测试用例,连接重放引擎以一次性运行所有测试。
**第 4 周 — 语义分诊与报告**
添加响应分类步骤,对漏洞进行评分而不是直接输出原始数据,生成映射到 OWASP 的报告输出。
每周结束时都会有一个能够实现端到端运行的东西,哪怕它还很粗糙——这就是让它在任何时候都具备演示价值的原因,而不是变成一堆半成品。
## OWASP 映射
| 漏洞类型 | OWASP API Security Top 10 (2023) |
|---|---|
| 对象级访问绕过 (IDOR) | API1 — Broken Object Level Authorization |
| 功能级访问绕过 | API5 — Broken Function Level Authorization |
| 响应中的过度数据暴露 | API3 — Broken Object Property Level Authorization |
## 未来愿景 (v2)
上面提到的 v1 范围——输入抓取、包含策略推断的所有权建模、包含属性级别用例的假设/测试生成、重放、带有影响范围的语义分诊——是特意设计为可在一个月左右完成的。以下想法被明确排除在 v1 范围之外,记录在此是为了证明我们的雄心,同时不会让第一次构建变成一个无法收尾的庞大工程。
**更深入的授权面**
- 工作流/状态机测试——对多步骤流程(邀请 → 接受 → 升级 → 取消)进行建模,并生成针对状态转换滥用的测试用例
- 时间授权——检查在角色降级、移出组织或会话失效后,访问权限是否确实被撤销,而不仅仅是在某个单一时间点进行检查
**身份与关系建模**
- 超越简单所有权的完整身份图:用户 → 团队 → 组织 → 项目,委托访问,继承权限——解答诸如“Team Red 外部的人能否访问 Repo X”这样的问题
**多协议覆盖**
- 感知 GraphQL schema 的所有权推断(嵌套对象边,节点级 auth)
- 文件/对象存储检查——签名 URL,S3/Firebase 存储桶,CDN 资源,导出 endpoint
- 跨 REST、GraphQL、WebSockets 和后台任务的跨服务身份关联
**攻击叙事层**
- 权限提升链探测器——将孤立的漏洞链接成一条完整的攻击路径(这是之前在规划此项目时提出的漏洞链合成器的想法;一旦所有权图和漏洞存储库建立起来,它自然就能完美融入)
- 角色引擎——自动预配合成账号(已挂起、未验证、订阅过期、外部协作者)并以每个身份重放完整的测试套件
- 业务影响风险评分——将漏洞表示为跨租户/账号的估计影响范围,而不仅仅是一个严重性标签
**工具集成**
- Burp Suite 插件,使实时抓取的流量直接流入 Charon
- CI 集成的回归测试——将授权 DNA 差异对比(现在在 v1 中)接入 CI,使其在每次部署时运行,并将任何漂移与导致该漂移的提交绑定,而不是手动运行
- 组织定义的策略 DSL——允许测试人员声明自定义规则(“账单对象绝不应跨账号暴露 PII”),并直接据此验证应用程序,将 Charon 从一个检测器转变为一个验证器
这里的每一项本身都是一项需要数周甚至数月才能完成的严谨工作。在 v1 发布并可供演示后,选择其中一项作为 v2 的里程碑——不要一次启动多个项目。
## 范围与道德
该工具会跨账号边界触发经过身份验证的请求——它是一种主动测试工具,而不是被动的扫描器。请仅在您拥有明确书面授权进行测试的系统上运行它。在没有明确测试时段和回滚计划的情况下,请勿将其指向生产系统,因为跨账号写入(如果您将其扩展到 GET 请求之外)可能会修改真实数据。
## 状态
设计阶段。上述架构和数据模型已最终确定;实现阶段将遵循构建路线图。本 README 将随着每个阶段的发布而更新。
标签:API安全, C2, CISA项目, DLL 劫持, IDOR检测, JSON输出, Web安全, 大语言模型, 蓝队分析, 逆向工具