ctfhive/ctfhive

GitHub: ctfhive/ctfhive

CTFHive 是一个开源的自托管 CTF 赛事管理平台,为主办方提供从注册、题目管理到评分和实时记分板的完整竞赛基础设施。

Stars: 0 | Forks: 0

CTFHive logo

CTFHive Public

Open-source CTF event application for self-hosted competitions.

Hosted CTFHive

## 概述 CTFHive Public 是 CTFHive 背后的开源 CTF 赛事应用程序。它是一个独立的 Web 应用,用于举办 Capture the Flag 赛事,包含注册、团队、题目导入、附件分发、提交、评分、实时记分板以及后台管理操作。 本仓库面向希望自行部署公开 CTF 应用的主办方。[https://www.ctfhive.us/](https://www.ctfhive.us/) 上的托管版 CTFHive 控制面板是托管部署、托管工作流、产品信息和定价的公共入口。 题目托管有意设于本应用外部。主办方运行他们自己的题目基础设施,并将每个题目附加到一个 URL、命令或连接说明,类似于 CTFd。 ## 包含内容 - 选手注册、登录、邮箱验证和密码重置流程 - 个人和团队参与 - 管理员题目 CRUD 和 YAML 题目导入 - 静态题目附件和按团队生成的构建产物 - 每个题目上的远程目标元数据 - Flag 提交、防作弊信号、分数事件和记分板冻结 - 用于赛事名称、赛事时间窗口、注册、团队规模和 UI 的管理员设置 - 本地开发支持 SQLite,部署支持 PostgreSQL - 在 Redis 可用时使用基于 Redis 的缓存/速率限制,并提供本地回退 - 包含应用、PostgreSQL、Redis 和 nginx 的 Docker Compose 技术栈 ## 快速开始 要求: - Python 3.13 - `uv` - Redis 对于本地开发是可选的 ``` cp .env.example .env uv sync --all-extras uv run flask --app wsgi:app ctfhive seed --file data/example_challenges.yml uv run flask --app wsgi:app run --debug --port 5000 ``` 打开 `http://localhost:5000`。 开发环境的引导管理员默认为: - 用户名:`admin` - 密码:`ChangeMeAdmin!2026` 首次登录会强制更改密码。在进行任何共享操作之前,请先更新 `.env`。 ## Docker Compose 包含的 Compose 技术栈会启动带有 PostgreSQL、Redis 和 nginx 的公共 Web 应用。 ``` cp .env.example .env docker compose up --build ``` 打开 `http://localhost:8080`。 持久化的本地目录: - `data/` 用于存储生成的生产环境密钥 - `challenges/` 用于存储导入的题目工作区和生成的附件 - `uploads/` 用于存储管理员上传的文件 使用以下命令检查有效的 Compose 配置: ``` docker compose config ``` ## 应用程序工作原理 CTFHive Public 是一个 Flask 应用程序,后端由 SQLAlchemy 模型、服务端渲染模板以及用于 flag、题目工作区、邮件、设置、分析和审计事件的服务模块提供支持。 | 组件 | 职责 | | --- | --- | | Flask 应用 | 路由、模板、表单、会话和请求处理。 | | SQL 数据库 | 用户、团队、题目、flag、提交、解答、设置和审计事件。 | | Redis | 可选的缓存、速率限制后端和 flag 镜像。 | | 题目工作区 | 从导入的题目清单生成的本地文件。 | | 管理 UI | 赛事设置、题目管理、用户、团队、评分和审计审查。 | | CLI | 数据填充、导出、健康检查、测试运行和管理员创建。 | 典型的参与者流程: 1. 访客注册或登录。 2. 应用将用户关联到个人主体或团队主体。 3. 题目板列出可见的题目。 4. 参与者打开题目,下载附件,并使用显示的远程目标信息。 5. 参与者提交 flag。 6. Flag 服务针对特定于主体的题目 flag 验证提交的值。 7. 如果提交正确,将记录解答和分数事件。 8. 记分板根据数据库和缓存进行更新。 使用 `ADMIN_KEY`、主体的团队密钥和题目 ID 为每个主体和题目派生 Flag。这意味着同一题目可以针对不同的个人用户或团队具有不同的有效 flag。 ## 配置 CTFHive Public 通过环境变量和可编辑的数据库设置进行配置。环境变量在启动时读取。应用启动后,可以通过 Web UI 更改管理员设置。 ### 核心环境变量 | 变量 | 用途 | | --- | --- | | `APP_ENV` | `development`、`testing` 或 `production`。 | | `SITE_URL` | 用于生成的链接和电子邮件的公共基础 URL。 | | `CTF_NAME` | 首次启动时植入管理员设置的赛事名称。 | | `CTF_DESCRIPTION` | 首次启动时植入管理员设置的赛事标语。 | | `DEFAULT_FLAG_PREFIX` | 当题目未覆盖时使用的默认 flag 前缀。 | | `EVENT_STARTS_AT` | 植入设置的 ISO 8601 赛事开始时间。 | | `EVENT_ENDS_AT` | 植入设置的 ISO 8601 赛事结束时间。 | | `CHALLENGES_ROOT` | 导入的题目工作区的文件系统路径。 | | `MAX_CONTENT_LENGTH` | 上传的最大请求正文大小。 | ### 数据存储 | 变量 | 用途 | | --- | --- | | `DATABASE_URL` | SQLAlchemy 数据库 URL。本地可使用 SQLite;部署的赛事推荐使用 PostgreSQL。 | | `REDIS_URL` | 用于缓存和 flag 镜像操作的 Redis URL。 | | `RATELIMIT_STORAGE_URI` | 速率限制存储后端。在生产环境中请使用 Redis。 | ### 密钥 | 变量 | 用途 | | --- | --- | | `SECRET_KEY` | Flask 会话签名密钥。 | | `ADMIN_KEY` | 用于按团队派生 flag 和审计链签名的密钥材料。 | | `ENCRYPTION_KEY` | 用于加密数据库字段的密钥材料。 | | `KEY_DIR` | 未显式设置密钥时,生产环境可在此目录持久化生成的密钥。 | 对于生产环境,请自行设置强密钥,或允许生产环境配置生成并将其持久化到 `KEY_DIR` 下。请将 `KEY_DIR` 保留在持久化存储上。 ### 管理员引导 | 变量 | 用途 | | --- | --- | | `ADMIN_USERNAME` | 默认管理员用户名。 | | `ADMIN_PASSWORD` | 默认管理员密码。 | | `ADMIN_EMAIL` | 默认管理员电子邮件地址。 | | `BOOTSTRAP_ADMIN_IN_PRODUCTION` | 仅当您有意在生产环境中进行首次启动管理员创建时,才设置为 `true`。 | 植入的管理员在使用应用之前将被强制更改默认密码。 ### 安全和代理设置 | 变量 | 用途 | | --- | --- | | `SESSION_COOKIE_SECURE` | 通过 HTTPS 提供服务时设置为 `true`。 | | `SESSION_COOKIE_SAMESITE` | Cookie SameSite 策略,通常为 `Lax`。 | | `TRUST_PROXY` | 在 nginx 或其他受信任的反向代理后运行时启用。 | | `SECURE_HEADERS_ENABLED` | 启用默认安全标头。 | | `ENABLE_HSTS` | 启用 HSTS 标头。仅在 HTTPS 正常工作后使用。 | ### 电子邮件 | 变量 | 用途 | | --- | --- | | `EMAIL_VERIFICATION_ENABLED` | 启用账户验证和密码重置邮件流程。 | | `MAILTRAP_API_KEY` | Mailtrap 发送 API 密钥。留空则禁用外发邮件。 | | `MAIL_SENDER` | 赛事邮件的发件人地址。 | | `MAIL_REPLY_TO` | 用于回复的主办方地址。 | 如果启用了电子邮件但未配置发送密钥,应用将记录日志并跳过邮件发送,而不会导致注册失败。 ### Docker Compose 覆盖 Compose 技术栈会读取 `.env` 并覆盖容器网络的数据库/缓存设置。 | 变量 | 用途 | | --- | --- | | `COMPOSE_SITE_URL` | Compose 部署的公共 URL,默认为 `http://localhost:8080`。 | | `COMPOSE_SESSION_COOKIE_SECURE` | 特定于 Compose 的安全 cookie 设置。 | | `COMPOSE_TRUST_PROXY` | 特定于 Compose 的代理信任设置。 | ## 添加题目 CTFHive Public 支持两种题目工作流: - 在管理 UI 中创建或编辑题目。 - 从 YAML 导入一个或多个题目。 CTFHive 存储题目元数据、附件、flag、解答状态和远程连接详情。它不运行远程题目服务。 ### 管理 UI 工作流 1. 以管理员身份登录。 2. 打开 `Admin -> Challenges`。 3. 选择 `New Challenge`。 4. 填写标题、slug、类别、描述、积分、flag 前缀和状态。 5. 对于远程服务题目,启用 `Remote service challenge`。 6. 添加连接标签、目标 URL 和连接说明。 7. 附上任何参与者文件。 8. 保存题目。 起草时将状态设置为 `Hidden`,当参与者应该看到题目时设置为 `Visible`。 ### YAML 导入 使用以下命令导入 YAML 文件: ``` uv run flask --app wsgi:app ctfhive seed --file data/example_challenges.yml ``` 一个文件可以包含一个题目或顶级的 `challenges:` 列表。 ### 远程题目示例 ``` ctf_version: 1 slug: remote-web title: Remote Web category_slug: web description_md: | Visit the target and find the flag. points: 150 status: visible is_dynamic: true connection_label: Web Target connection_url: https://example.org/challenge connection_info: | This target is managed by the organizer. Replace this URL with your event infrastructure before publishing. files: - name: notes.txt inject_flag: false contents: | Enumerate the web app and submit the flag in the scoreboard. ``` ### 题目清单字段 | 字段 | 必填 | 用途 | | --- | --- | --- | | `ctf_version` | 是 | 清单版本。使用 `1`。 | | `slug` | 是 | 稳定的 URL 和导入标识符。 | | `title` | 是 | 显示名称。 | | `category_slug` | 是 | 诸如 `web`、`crypto`、`pwn`、`rev`、`forensics`、`misc`、`osint` 或 `blockchain` 之类的类别。 | | `description_md` | 否 | 在题目页面上显示的 Markdown。 | | `points` | 是 | 分值。 | | `flag_prefix` | 否 | 可选的按题目的 flag 前缀。留空则使用 `DEFAULT_FLAG_PREFIX`。 | | `status` | 否 | `hidden` 或 `visible`。默认为 `hidden`。 | | `is_dynamic` | 否 | 在 UI 中将题目标记为远程服务题目。 | | `connection_label` | 否 | 显示在目标详情上方的标签,例如 `Web Target` 或 `SSH Target`。 | | `connection_url` | 否 | 目标 URL。HTTP/HTTPS URL 将渲染为可打开的链接。 | | `connection_info` | 否 | 命令、凭据、说明或重置指令。 | | `files` | 否 | 静态或生成的参与者文件。 | 未知字段将被拒绝,这样拼写错误就不会静默地改变题目行为。 ### 文件和附件 文件可以是内联的: ``` files: - name: README.txt inject_flag: false contents: | Read this before starting. ``` 或者可以引用暂存在清单旁边的文件: ``` files: - path: handouts/source.py name: source.py inject_flag: false ``` `inject_flag: true` 会在生成打包文件时,将已知的占位符字符串替换为参与者派生的 flag。对于绝不应包含真实 flag 的公开源代码文件,请使用 `inject_flag: false`。 使用相同的 `slug` 重新导入清单会更新现有题目,而不是创建重复项。 使用以下命令导出当前题目元数据: ``` uv run flask --app wsgi:app ctfhive export-challenges --output challenges_export.yml ``` ## 注入的 Flag、远程 Docker 目标和生成的静态输出 CTFHive Public 会验证按主体的 flag。个人选手或团队会为每个题目获得一个派生的 flag,并且提交的 flag 必须与该派生值匹配。公共应用本身不会将密钥注入到已经运行的外部服务中。它将 flag 注入到生成/下载的题目文件中。 使用以下模式之一: - 远程 Docker 题目:生成按团队的 `target.env`、compose 文件或配置文件,供 Docker 服务作为 `FLAG` 使用。 - 静态生成输出题目:在文件生成时运行 `chal.py`,并写入按团队的谜题文件,例如 `challenge.txt` 或 `challenge.json`。 - 静态附件题目:重写已声明附件文件中的占位符,而无需运行 `chal.py`。 ### Flag 注入工作原理 当参与者点击 `Generate Challenge Files` 时,CTFHive 将: 1. 将 `CHALLENGES_ROOT//` 复制到临时工作目录。 2. 将参与者派生的 flag 写入 `flag.txt`。 3. 除非该文件被标记为 `inject_flag: false`,否则重写文件中的 flag 占位符。 4. 仅当清单声明了 `output_file` 或将文件标记为 `generated: true` 时才运行 `chal.py`。 5. 将声明输出的文件复制到 `CHALLENGES_ROOT/.generated//principal-/`。 `flag.txt` 是内部文件。不要将 `flag.txt` 列在 `files` 中;schema 会拒绝它。生成器脚本应读取该文件并写入安全的面向参与者的构建产物。 识别的占位符值包括: ``` FLAG{placeholder} FLAG{test} FLAG{example} CTFHIVE{placeholder} CTFHIVE{test} CTFHIVE{example} FORGE{placeholder} FORGE{test} FORGE{example} ``` CTFHive 还会使用活动前缀重写 flag 形式的值,例如当 `DEFAULT_FLAG_PREFIX=FLAG` 时的 `FLAG{anything}`。 ### 工作区目录结构 导入的题目会实体化在 `CHALLENGES_ROOT` 下。 ``` challenges/ remote-docker/ remote-docker.yml # manifest written by import chal.py # generator; private if not listed in files Dockerfile # optional participant handout app.py # optional participant handout flag.txt # internal; auto-created/rewritten by CTFHive .generated/ remote-docker/ principal-12/ target.env .generation.json ``` 如果在 YAML 中使用 `path:` 条目,则引用的文件必须在导入运行时已存在于 `CHALLENGES_ROOT/<>/` 内。对于完全可移植的 YAML,请使用内联的 `contents:` 条目。 ### 情况 1:带有生成的 Flag 环境变量文件的远程 Docker 题目 当远程或参与者运行的 Docker 容器从环境变量中读取 flag 时使用此项。 推荐私有生成器的目录结构: ``` challenges/ remote-docker/ remote-docker.yml chal.py Dockerfile app.py ``` 在由 `CHALLENGES_ROOT` 配置的相同路径下创建该目录,然后从那里导入清单: ``` mkdir -p challenges/remote-docker # 将 remote-docker.yml、chal.py、Dockerfile 和 app.py 放入该目录中 uv run flask --app wsgi:app ctfhive seed --file challenges/remote-docker/remote-docker.yml ``` `remote-docker.yml`: ``` ctf_version: 1 slug: remote-docker title: Remote Docker category_slug: web description_md: | Build and run the Docker challenge. The generated target.env file contains the per-team FLAG value expected by the scoreboard. points: 200 status: visible is_dynamic: true connection_label: Docker Target connection_url: http://localhost:8081 connection_info: | Generate your challenge files, then run: docker build -t remote-docker . docker run --rm --env-file target.env -p 8081:8080 remote-docker output_file: target.env files: - name: Dockerfile path: Dockerfile inject_flag: false - name: app.py path: app.py inject_flag: false - name: target.env generated: true ``` `chal.py`: ``` from pathlib import Path flag = Path("flag.txt").read_text(encoding="utf-8").strip() Path("target.env").write_text(f"FLAG={flag}\n", encoding="utf-8") ``` Docker 题目内的 `app.py` 示例: ``` import os from http.server import BaseHTTPRequestHandler, HTTPServer FLAG = os.environ.get("FLAG", "FLAG{missing}") class Handler(BaseHTTPRequestHandler): def do_GET(self): body = f"flag: {FLAG}\n".encode() self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() ``` `Dockerfile` 示例: ``` FROM python:3.13-slim WORKDIR /app COPY app.py . EXPOSE 8080 CMD ["python", "app.py"] ``` 参与者流程: 1. 打开题目页面。 2. 点击 `Generate Challenge Files`。 3. 下载 `Dockerfile`、`app.py` 和 `target.env`。 4. 使用 `--env-file target.env` 构建/运行容器。 5. 将显示的 flag 提交到 CTFHive。 主办方托管的 Docker 流程: 1. 为每个个人选手或团队创建一个容器实例。 2. 将该主体生成的 `FLAG` 值传递给容器环境。 3. 将可访问的 URL 或命令放入 `connection_url` / `connection_info` 中。 4. 确保服务返回参与者提交的相同 flag。 除非您同时更改验证逻辑,否则不要对所有团队使用一个共享的固定 flag。CTFHive Public 需要按主体派生的 flag。 ### 情况 2:仅 YAML 的远程 Docker 题目 如果您想要一个没有预存文件的单一 YAML 文件,请将附件内容内联。这对于小型示例很方便。 ``` ctf_version: 1 slug: yaml-only-docker title: YAML Only Docker category_slug: web description_md: Build the Docker service and use the generated env file. points: 150 status: visible is_dynamic: true connection_label: Local Docker connection_url: http://localhost:8081 output_file: target.env files: - name: Dockerfile inject_flag: false contents: | FROM python:3.13-slim WORKDIR /app COPY app.py . EXPOSE 8080 CMD ["python", "app.py"] - name: app.py inject_flag: false contents: | import os from http.server import BaseHTTPRequestHandler, HTTPServer FLAG = os.environ.get("FLAG", "FLAG{missing}") class Handler(BaseHTTPRequestHandler): def do_GET(self): body = f"flag: {FLAG}\n".encode() self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() - name: chal.py inject_flag: false contents: | from pathlib import Path flag = Path("flag.txt").read_text(encoding="utf-8").strip() Path("target.env").write_text(f"FLAG={flag}\n", encoding="utf-8") - name: target.env generated: true ``` 重要提示:由于 `chal.py` 列在 `files` 中,它会被视为已声明的参与者文件。对于真正的赛事,如果您不想发布生成器源代码,请首选上面的私有生成器结构。 ### 情况 3:静态生成输出题目 当没有远程服务时使用此项。参与者下载生成的谜题构建产物并提交派生的 flag。 目录结构: ``` challenges/ runtime-static/ runtime-static.yml chal.py flag.txt # internal; auto-created/rewritten by CTFHive ``` `runtime-static.yml`: ``` ctf_version: 1 slug: runtime-static title: Runtime Static category_slug: crypto description_md: | Generate your personalized challenge file and recover the flag. points: 100 status: visible is_dynamic: false output_file: challenge.txt files: - name: challenge.txt generated: true ``` `chal.py`: ``` from pathlib import Path flag = Path("flag.txt").read_text(encoding="utf-8").strip() encoded = flag.encode("utf-8").hex() Path("challenge.txt").write_text( "Decode this hex string and submit the result:\n" f"{encoded}\n", encoding="utf-8", ) ``` 生成输出题目的规则: - `output_file` 必须与 `chal.py` 写入的文件相匹配。 - `chal.py` 在运行时,其当前工作目录设置为题目工作区副本。 - `chal.py` 必须在 20 秒内完成并退出且代码为 `0`。 - 生成的文件将复制到 `CHALLENGES_ROOT/.generated//principal-/`。 - 不要将原始 flag 打印到 stdout 或将其存储在额外的未声明文件中。 ### 情况 4:带有占位符注入的静态附件 当可下载的附件本身需要包含参与者派生的 flag 时,在非常小的题目中使用此项。不要设置 `output_file`,也不要将任何文件标记为 `generated: true`;在该模式下 `chal.py` 不会运行。 ``` ctf_version: 1 slug: injected-handout title: Injected Handout category_slug: misc description_md: Generate and read your handout. points: 50 status: visible is_dynamic: false files: - name: README.txt inject_flag: true contents: | Your flag is FLAG{placeholder} ``` 对于绝不能包含答案的公开源代码、攻略、Dockerfile 或任何文件,请设置 `inject_flag: false`。 ## 编辑设置 应用启动后,大部分赛事设置都可以从管理 UI 进行更改。 1. 以管理员身份登录。 2. 打开 `Admin -> Settings`。 3. 编辑字段。 4. 保存设置。 设置存储在数据库中。环境变量会在首次启动时植入初始值,但之后保存的管理员设置优先。 | 设置 | 用途 | | --- | --- | | Site / Event Name | 显示在导航、页面和电子邮件中。 | | Site Tagline | 简短的公开赛事描述。 | | Event Start | ISO 8601 开始时间。 | | Event End | ISO 8601 结束时间。 | | Max Team Size | 一个团队允许的最大用户数。 | | Allow Solo Play | 允许用户在不加入团队的情况下参赛。 | | Allow Solo-to-Team Switching | 允许个人用户稍后加入或创建团队。 | | Registration Open | 允许注册新账户。 | | Default Background | 新访客的默认动画背景。 | 使用具有时区意识的赛事时间窗口值,例如: ``` 2026-07-01T12:00:00+00:00 ``` 其他管理页面可处理题目管理、用户管理、团队、记分板冻结、手动调整分数、解答分析和审计事件。 ## 托管层级 CTFHive Public 是自托管的开源应用程序,不受托管计划的限制。托管的 CTFHive 控制面板为希望使用 CTFHive 运营的部署工作流的主办方提供托管层级。 托管控制面板:[https://www.ctfhive.us/](https://www.ctfhive.us/) 定价和限制可能会发生变化。请查看托管控制面板获取最新详情。以下层级曾于 2026 年 6 月 29 日公开列出。 | 层级 | 价格 | 年度 | 列出容量 | 最适合 | | --- | --- | --- | --- | --- | | Solo | `$25/mo` | `$250/yr` | 最多 128 名参与者,32 支队伍 | 个人训练和单机赛事。 | | Standard | `$65/mo` | `$650/yr` | 最多 256 名参与者,64 支队伍 | 俱乐部、研讨会以及拥有完整团队赛的大学 CTF。 | | Pro | `$125/mo` | `$1,250/yr` | 最多 512 名参与者,128 支队伍 | 更大规模的团队竞赛和更高的并发题目容量。 | | Enterprise | 联系方式 | 定制 | 最多 1024 名参与者,256 支队伍 | 全公司或多赛事培训,提供 SSO 和定制支持。 | 这些层级适用于托管的 CTFHive 产品,而不适用于自托管的 CTFHive Public 运行时行为。自托管的公共应用没有计划限制。 ## 生产环境注意事项 - 发布后保持 `SECRET_KEY`、`ADMIN_KEY` 和 `ENCRYPTION_KEY` 稳定。 - 在生产环境中使用 Redis 进行缓存和速率限制。 - 使用 HTTPS 并设置 `SESSION_COOKIE_SECURE=true`。 - 仅在受信任的反向代理后才启用 `TRUST_PROXY=true`。 - 将 `KEY_DIR`、数据库存储、题目工作区和上传保留在持久化存储上。 - 在赛事开始前和期间备份数据库和持久化目录。 - 如果开启了电子邮件验证,请在开放注册前测试邮件发送情况。 - 单独运行远程题目基础设施,并使用 `connection_url` 或 `connection_info` 从每个题目中进行链接。 ## CLI 参考 ``` # 导入 challenge YAML uv run flask --app wsgi:app ctfhive seed --file data/example_challenges.yml # 预览导入而不提交 uv run flask --app wsgi:app ctfhive seed --file data/example_challenges.yml --dry-run # 导出 challenge uv run flask --app wsgi:app ctfhive export-challenges --output challenges_export.yml # 检查 app 健康状态 uv run flask --app wsgi:app ctfhive health # 创建管理员用户 uv run flask --app wsgi:app ctfhive create-admin # 重置数据库 uv run flask --app wsgi:app ctfhive reset-db --confirm ``` ## 测试 ``` uv run pytest ``` 专项检查: ``` uv run pytest tests/test_public_edition.py -q uv run pytest tests/test_importer.py tests/test_static_handout.py -q uv run pytest tests/test_auth.py tests/test_team.py tests/test_scoreboard.py -q ``` 可选的本地检查: ``` uv run python -m compileall -q ctfapp docker compose config ```
标签:CTF平台, Docker, Flask, PostgreSQL, Python, 安全防御评估, 搜索引擎查询, 无后门, 测试用例, 请求拦截, 赛事系统, 逆向工具