NUSGreyhats/ctfd-team-token-plugin
GitHub: NUSGreyhats/ctfd-team-token-plugin
CTFd 插件,为每个队伍的每个挑战签发唯一 token,并供外部挑战后端通过专用 API 安全解析身份。
Stars: 0 | Forks: 0
# CTFd Team Token 插件
CTFd 挑战类型插件,为每个 `(team, challenge)` 签发**基于 DB 的唯一 token**,并替换挑战描述中的 `{TEAM_TOKEN}`。外部服务通过特定于插件的服务器到服务器 API 解析 token。
请参阅 [docs/PRD.md](docs/PRD.md) 了解需求和验收标准。
## 架构概述
该插件有两条运行时路径:CTFd 为玩家渲染 Team Token 挑战,以及受信任的外部挑战后端通过 CTFd 解析玩家提供的 token。外部服务永远不会收到 CTFd 的 admin token;它们只接收插件 API 密钥。
挑战查看请求路径:
```
flowchart LR
Player["Team member browser"]
ViewAPI["CTFd challenge API"]
ChallengeType["TeamTokenChallenge"]
Helpers["tokens.py helpers"]
Database["CTFd database"]
Player -->|view challenge| ViewAPI
ViewAPI --> ChallengeType
ChallengeType --> Helpers
Helpers --> Database
```
Token 解析请求路径:
```
flowchart LR
Backend["External challenge backend"]
ResolveAPI["CTFd resolve API"]
Settings["plugin config"]
Helpers["tokens.py helpers"]
Database["CTFd database"]
Backend -->|resolve token| ResolveAPI
ResolveAPI --> Settings
ResolveAPI --> Helpers
Helpers --> Database
```
Token 持久化规则:
```
flowchart TB
Team["team_id"]
Challenge["challenge_id"]
Scope["Scope: team and challenge"]
TokenRow["team_tokens row"]
TokenValue["Opaque tt_ token"]
Resolve["resolve_token()"]
Solves["solves table"]
Response["team, challenge, solved"]
Team --> Scope
Challenge --> Scope
Scope -->|UNIQUE pair| TokenRow
TokenRow -->|UNIQUE token| TokenValue
TokenValue --> Resolve
Solves -->|solved state| Resolve
Resolve --> Response
```
玩家挑战视图:
```
sequenceDiagram
participant Team as Team member
participant CTFd as CTFd challenge API
participant Plugin as Team Token plugin
participant DB as CTFd database
Team->>CTFd: Open team_token challenge
CTFd->>Plugin: read challenge and render fields
Plugin->>DB: Find token by team and challenge
alt Token row exists
DB-->>Plugin: Existing tt_ token
else First team view
Plugin->>DB: Insert team_tokens row
DB-->>Plugin: New tt_ token
end
Plugin-->>CTFd: Description and connection info with token
CTFd-->>Team: Challenge modal with team token
```
外部挑战解析流程:
```
sequenceDiagram
participant Browser as Player browser
participant Backend as External challenge backend
participant API as CTFd resolve API
participant Plugin as Team Token plugin
participant DB as CTFd database
Browser->>Backend: Send X-Team-Token
Backend->>API: GET /resolve with bearer secret
API->>API: Check enabled flag and secret
API->>Plugin: resolve_token(token)
Plugin->>DB: Read token, team, and solve rows
DB-->>Plugin: Metadata, solve state, or no match
Plugin-->>API: Resolve result
API-->>Backend: HTTP status and JSON
Backend-->>Browser: Unlock or reject
```
解析结果:
```
flowchart TB
Request["Resolve request"]
Enabled{"API enabled"}
Secret{"Bearer secret valid"}
Token{"Token found"}
Disabled["503 resolve_disabled"]
Unauthorized["401 unauthorized"]
Invalid["200 valid false"]
Valid["200 valid true with solved flag"]
Request --> Enabled
Enabled -->|no| Disabled
Enabled -->|yes| Secret
Secret -->|no| Unauthorized
Secret -->|yes| Token
Token -->|no| Invalid
Token -->|yes| Valid
```
## 快速开始 (Docker)
在 repo 根目录下:
```
./scripts/dev-up.sh
python scripts/acceptance_tests.py
```
或手动操作:
```
cd docker
docker compose up -d --build
docker compose --profile seed run --rm seed
python ../scripts/acceptance_tests.py
```
服务:
| 服务 | URL |
|---------|-----|
| CTFd | http://localhost:8000 |
| 示例外部挑战 | http://localhost:5001 |
| 插件配置 | http://localhost:8000/admin/config → **Team Token** 选项卡 |
默认凭据:
| 账户 | 密码 | 团队 |
|---------|----------|------|
| `admin` | `Password123!` | — |
| `alpha` | `Password123!` | TeamAlpha |
| `beta` | `Password123!` | TeamBeta |
开发环境插件 API 密钥(服务器到服务器):`dev-plugin-secret-for-testing`
## 在现有的 CTFd 上安装
克隆到 CTFd 的插件目录中(文件夹名称必须为 `team-token`):
```
git clone https://github.com/NUSGreyhats/ctfd-team-token-plugin.git CTFd/plugins/team-token
```
重启 CTFd,然后打开 **Admin → Configuration → Team Token** 查看 resolve API 密钥。
仅用于开发的路径(`docker/`、`scripts/`、`example-challenge/`、`docs/`)与插件文件一起位于 repo 根目录;CTFd 会忽略它们。
## 管理员工作流
1. 创建一个类型为 **team_token** 的挑战。
2. 在描述中的任意位置放置 `{TEAM_TOKEN}`。
3. 添加一个普通的静态 flag。
4. 仅与受信任的挑战后端共享插件 API 密钥。
示例描述:
```
Your token: `{TEAM_TOKEN}`
curl -H "X-Team-Token: {TEAM_TOKEN}" https://your-challenge.example/start
```
## 解析 API
```
GET /plugins/team-token/api/v1/resolve?token=tt_...
Authorization: Bearer
```
响应:
```
{"valid": true, "team_id": 1, "team_name": "TeamAlpha", "challenge_id": 3, "solved": false, "solved_at": null}
{"valid": false}
```
在 **Admin → Configuration → Team Token** 下配置密钥并启用/禁用 API。
resolve endpoint 是一个**路径**,而不是完整的 URL:
```
GET {CTFD_BASE_URL}/plugins/team-token/api/v1/resolve?token=tt_...
Authorization: Bearer {team_token_plugin_secret}
```
使用您的外部挑战服务器可以访问的任何 base URL(在 Docker 内部使用 `http://ctfd:8000`,在生产环境中使用您的公共 hostname)。CTFd 无法替您选择。
## KOTH 排行榜
该插件还公开了一个只读的 KOTH 排行榜,访问地址为:
```
GET /koth
GET /plugins/team-token/api/v1/koth
```
它从 CTFd 的 awards 中派生出排行榜行,并且不会创建或更新任何数据库行。该页面每 5 秒从 JSON endpoint 刷新一次表格数据,而无需重新加载整个页面。排行榜读取结果会在每个 CTFd 进程中缓存 2 秒,以减少打开的浏览器标签页带来的重复数据库操作。每个公开榜单仅返回排名前 10 的参与者,并跳过那些不再指向可见 team 或 user 的 awards。默认情况下,它包含 category、name 或 description 看起来与 KOTH 相关的 awards,以及诸如 `koth-score-v1` 或 `rubikscube-score-v1` 的分数标记描述。
使用逗号分隔的 `category` 查询参数过滤一个或多个 award 类别:
```
/koth?category=watchdog-koth
/plugins/team-token/api/v1/koth?category=watchdog-koth,my-long-snake-koth
```
为了便于稳定解析,分数提交者应在 award 描述中包含一个 metric:
```
koth-score-v1 challenge=watchdog-koth cycles=344 score=1000
```
也支持诸如 `Best watchdog run: 344 cycles` 这样的描述。
## 示例外部挑战
`example-challenge/` 服务演示了预期的流程:
1. 玩家从 CTFd 复制 `{TEAM_TOKEN}`。
2. 浏览器通过 `X-Team-Token` header 将其发送到 `POST /api/enter`。
3. 后端使用**插件密钥**(而不是 CTFd admin token)调用 CTFd resolve API。
4. 一旦 team 被识别且 `solved` 为 false,UI 即会解锁。
在 seeding 之后尝试:
```
curl -H "Authorization: Bearer dev-plugin-secret-for-testing" \
"http://localhost:8000/plugins/team-token/api/v1/resolve?token=YOUR_TEAM_TOKEN"
```
## 插件布局
repo 根目录就是 CTFd 插件(用于 `git clone` 安装的标准布局):
```
team-token/ # clone target: CTFd/plugins/team-token
├── __init__.py
├── api.py
├── challenge.py
├── config.json
├── models.py
├── tokens.py
├── migrations/
├── assets/
├── templates/
├── docker/ # local dev stack only
├── scripts/
├── example-challenge/
└── docs/
```
## 开发说明
- Token 是在首次查看挑战时延迟创建的。
- 编辑挑战的管理员仍然会看到原始的 `{TEAM_TOKEN}` 占位符。
- MVP 仅针对 **team 模式**。
- 如果在本地使用,Docker CTFd 数据卷会在 `docker/.data/` 下被 gitignore。
## 验收测试
在 stack 运行并 seeding 之后:
```
CTFD_RESOLVE_URL=http://localhost:8000/plugins/team-token/api/v1/resolve \
CTFD_TEAM_TOKEN_PLUGIN_SECRET=dev-plugin-secret-for-testing \
python scripts/acceptance_tests.py
```
涵盖 PRD 测试 T1–T5、T6/T7 和 T8。
标签:API, CTFd插件, Python, Syscall, Web开发, 令牌管理, 无后门, 请求拦截, 逆向工具