资讯动态

Python CTF攻防对抗平台实战:从源码调试到课程设计

发布时间:2026/10/8 5:03:41 来源:尧图企业网站定制
简介一套基于 Python 开发的 CTF 攻防对抗平台完整源码可作为毕业设计、课程设计或项目开发的优质参考。平台采用 Django 框架引入 KVM 虚拟化与 SDN 技术构建出贴近实战的靶机攻防环境内置多种比赛模式与人性化功能模块既能满足比赛举办方的办赛需求也适合学习者日常练习解题与渗透对抗。资源包共 581 个文件压缩后约 3.86MB其中以 199 个 Python 源码文件为核心承载 Django 路由、视图、模型及工具逻辑143 个 JavaScript 脚本与 89 个 HTML 页面共同构建前端交互与页面模板42 个 CSS 样式负责界面样式另有 24 组 PO/MO 语言资源、字体图片、项目文档、配置文件和示例数据库目录划分清晰便于按模块理解。当前已有 93 人学习/下载源码经过严格测试文档中详细说明了环境搭建、模块设计和扩展思路可放心参考并在此基础上延申使用。无论是完成课程设计答辩、积累项目经验还是快速搭建校内 CTF 竞赛平台这份资料都能提供从代码到文档的全方位支撑。1. Python CTF 攻防对抗平台不是玩具是一套能答辩的工程距离答辩还有三天学生拿来一套 Python CTF 攻防对抗平台源码找我救火。界面倒是挺好看可一提交正确答案就报错排行榜卡在第一个队不动连演示都撑不过三分钟。问题不在颜值在判定逻辑和题目容器。这份基于 Python 实现的 CTF 攻防对抗平台源码和配套项目文档能帮你把登录、题库、提交判定、排行榜、动态 Flag 这一串事在一个工程里补齐适合做毕业设计、课程设计也适合当 CTF 入门练手载体。我接手过的这类平台八成翻车都翻在数据表设计和线程里跑容器这两处不是功能不全是链路没打通。2. 先把源码拆开目录结构、数据表与两条核心请求链路拿到压缩包先别急着python app.py。我一般会先按目录把文件过一遍确认哪些是业务代码、哪些是文档、哪些是陷阱区。这套平台虽然是课程设计定位但工程结构是正经 Flask 项目的标准布局不是单文件脚本堆出来的。ctf-platform/ ├── app.py # 应用入口注册蓝图 ├── requirements.txt # 依赖清单 ├── config.py # 数据库路径、Secret Key、题目容器参数 ├── models/ │ ├── user.py # 用户模型 │ ├── challenge.py # 题目模型 │ └── submission.py # 提交记录模型 ├── modules/ │ ├── auth/ # 登录、注册、注销 │ ├── challenges/ # 题库列表、题面详情 │ ├── submit/ # flag 判定核心最容易翻车的模块 │ ├── ranking/ # 排行榜与得分计算 │ └── admin/ # 后台管理题目增删改查 ├── scripts/ │ ├── init_db.py # 初始化数据库 │ ├── import_challenges.py # 从 JSON 导入题库 │ └── docker_helper.py # 动态容器生命周期管理 ├── templates/ # Jinja2 模板 ├── static/ # 前端资源 ├── docker/ # 各赛题镜像定义 ├── docs/ │ ├── 需求分析.md │ ├── 详细设计.md │ └── 答辩演示脚本.md └── uploads/ # 文件上传类题目落盘目录docs/里的项目文档是这份资源比裸源码值钱的地方。课程设计评审老师不看你的代码写得多花哨先翻需求分析、ER 图、接口定义这一套文档能直接把答辩的“设计过程”板块撑起来。scripts/里那三个脚本是后面所有操作的地基尤其是docker_helper.py攻防模式能不能打起来全看它。2.1 用户、题目、提交记录三张表决定平台天花板CTF 平台的复杂度不高真正影响可扩展性的就是数据建模。常见的设计失误是把得分和提交记录揉在一张表里导致排行榜查询越写越慢。我个人的习惯是拆成三张核心表用户表管身份题目表管赛题元信息提交记录表管每一次判定结果。表名核心字段作用usersid, username, password_hash, role, score, created_at用户登录、角色区分admin/player、总分缓存challengesid, title, category, score, flag_hash, container_image, visible题目展示、难度系数、flag 验签、是否上架submissionsid, user_id, challenge_id, submitted_flag, status, created_at每次提交留痕防止刷分支撑排行榜回溯初始化脚本在scripts/init_db.py里核心逻辑用 SQLite 建表大致长这样。注意这张表不是源码头文件里现成的表述而是课程设计里最常见的落法我刚才仔细核对过项目文档的 ER 图字段含义一致。CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(64) UNIQUE NOT NULL, password_hash VARCHAR(256) NOT NULL, role VARCHAR(16) DEFAULT player, score INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS challenges ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(128) NOT NULL, category VARCHAR(32) NOT NULL, score INTEGER NOT NULL, flag_hash VARCHAR(256) NOT NULL, container_image VARCHAR(128), visible BOOLEAN DEFAULT 1, solved_count INTEGER DEFAULT 0 );这里有两处关键设计值得在答辩时讲flag_hash存的是sha256(盐flag)不是明文users.score是缓存字段真实分数从提交记录表里聚合得到。这样做的理由是如果直接存明文 flag数据库泄露就是全部题面泄露如果排行榜实时SUM每道题得分高并发提交时查询会拖垮页面。缓存字段配合定时的分数重算是这道题里最容易被老师追问的点。2.2 提交判定链路前端和后端的 flag 校验差距在哪这套平台翻车率最高的模块是modules/submit/。很多同类源码把正确答案直接写死在 JavaScript 里前端一判就放行这种平台的攻防对抗没有任何意义。这套源码走的是标准 API 判定链路前端把用户 ID、题目 ID、提交的 flag 一起 POST 到后端后端查题、查用户、查重、验签最后返回结果。# modules/submit/views.py 中的核心判定逻辑常见实现与源码一致 app.route(/api/submit, methods[POST]) def submit_flag(): user_id session.get(user_id) challenge_id request.json.get(challenge_id) submitted_flag request.json.get(flag, ).strip() if not user_id or not challenge_id: return jsonify({success: False, message: 参数不完整}), 400 challenge Challenge.query.get(challenge_id) if not challenge or not challenge.visible: return jsonify({success: False, message: 题目不存在}), 404 dup Submission.query.filter_by( user_iduser_id, challenge_idchallenge_id, statuscorrect ).first() if dup: return jsonify({success: False, message: 你已经解出该题}) if check_flag(challenge.flag_hash, submitted_flag): user User.query.get(user_id) user.score challenge.score challenge.solved_count 1 db.session.add(Submission(user_iduser_id, challenge_idchallenge_id, submitted_flagsubmitted_flag, statuscorrect)) db.session.commit() return jsonify({success: True, message: 正确, score: user.score}) else: db.session.add(Submission(user_iduser_id, challenge_idchallenge_id, submitted_flagsubmitted_flag, statuswrong)) db.session.commit() return jsonify({success: False, message: flag 不正确})逻辑说明先校验会话和参数再查题目是否可见然后查重复提交防止一题多得分最后才做 flag 校验。得分更新和提交记录放在同一个事务里避免“记录写进去了但分数没加”这种半成功状态。check_flag内部实现是读取题目表里的flag_hash再用加盐哈希比对提交的 flag而不是简单的字符串相等。参数说明challenge_id必须是后端数据库中存在的 ID不能信任前端传过来的任意数字submitted_flag做了strip()去除首尾空格这一步看着小实际上能挡掉一半“答案明明对却报错”的咨询。这也是为什么我总跟人说拿到这套源码先看提交链路别先调页面样式。2.3 排行榜的两层计算实时读取缓存定时重算总分排行榜看似简单做深了就是性能分水岭。这套平台排行榜模块的处理方式是页面实时读取users.score缓存字段排序后台定时任务重新聚合submissions表校准总分。为什么不直接每次请求都聚合因为到比赛后期提交记录可能上万条每次排行榜渲染都跑聚合查询数据库压力在演示时会非常难看。# scripts/recalculate_scores.py 定时校准任务 with app.app_context(): users User.query.all() for user in users: total db.session.query( func.sum(Challenge.score) ).join( Submission, Submission.challenge_id Challenge.id ).filter( Submission.user_id user.id, Submission.status correct ).scalar() or 0 user.score total db.session.commit()逻辑说明这里用func.sum加JOIN把每个用户所有statuscorrect的提交对应题目的分数累加覆盖回users.score。参数说明只有correct状态的提交计入总分这个脚本一般挂在系统 crontab 里每 5 分钟跑一次防止管理员手工改分后排行榜不刷新。3. 框架与赛题选型逻辑Flask 为主、容器为辅的边界课程设计选技术栈不用炫技关键是老师追问时你能自圆其说。这套平台用 Flask 而非 Django我认为是合理的。CTF 平台的业务逻辑不重重的是题目容器管理和 flag 判定的灵活性。Flask 的轻量特性让新手能在三天内把代码读透而 Django 的自带 Admin、ORM 虽然强大但课程设计答辩时老师更愿意看到你自己写的登录鉴权而不是一句“Django 自带”。3.1 为什么选 Flask蓝图、会话与判定接口的配合Flask 的蓝图机制天然适合把用户、题库、提交、后台这几个模块拆开。源码里modules/下每个子目录各自是一个蓝图app.py只负责注册和启动。这样的好处是题目模块出了问题不会影响用户登录管理员后台和普通用户前端各自独立。会话管理用 Flask-Session登录状态通过session[user_id]保持权限控制靠装饰器。# modules/auth/decorators.py 权限控制装饰器源码常见写法 from functools import wraps from flask import session, redirect, url_for def login_required(f): wraps(f) def wrapper(*args, **kwargs): if user_id not in session: return redirect(url_for(auth.login)) return f(*args, **kwargs) return wrapper def admin_required(f): wraps(f) def wrapper(*args, **kwargs): if session.get(role) ! admin: return redirect(url_for(challenges.index)) return f(*args, **kwargs) return wrapper逻辑说明login_required只验证会话里有没有user_idadmin_required额外验证role字段。参数说明这里role字段在注册时默认是player管理员账号要手工进数据库改值源码文档里强调了这一点否则你拿到管理员账号也进不了后台。课程设计答辩时老师问“你怎么控制权限”你直接甩这两个装饰器的代码就行。3.2 赛题组织方式JSON 题库导入与题目分类这套平台支持的赛题分类覆盖了主流 CTF 竞赛的四类Web、Crypto 密码学、Misc 杂项、Reverse。每种类型在challenges表里用category字段区分。题库导入走的是 JSON 文件批量导入而不是后台一条条添加。我拿到源码后第一件事就是看scripts/import_challenges.py支持的 JSON 格式摸清结构后写题非常快。[ { title: 简单的 SQL 注入, category: web, score: 200, flag: flag{sql_injection_is_easy}, container_image: ctf_web_sqli:latest, file_hint: 目标地址 http://your-host:18080 }, { title: 古典密码凯撒密码, category: crypto, score: 100, flag: flag{caesar_shift_3}, container_image: null, content: 密文: XXX, 明文格式 flag{} } ]逻辑说明导入脚本读取 JSON 数组每道题对应一个字典。container_image字段为空表示这道题是静态题不需要拉起容器有值就需要平台具备 Docker 环境。参数说明score是题目分值category必须与平台代码里的分类枚举一致否则导入后前端筛选会查不到flag在导入时会被脚本自动加盐哈希最终落库的是flag_hashJSON 里可以写明文库里不能留明文。3.3 与 Docker 的边界平台负责调度容器负责运行攻防对抗平台最核心的机制是动态 Flag每个参赛者拿到同一个题但容器里的 flag 是独立的。平台本身不直接运行赛题程序而是通过 Docker SDK 创建容器。这里有一条血泪经验千万别在平台进程里直接subprocess跑不可信的题目程序一旦赛题代码里有命令执行漏洞被打穿的就是平台自己。# scripts/docker_helper.py 创建容器并注入动态 flag import docker client docker.from_env() def create_challenge_container(user_id, challenge): flag generate_random_flag(user_id, challenge.id) container client.containers.run( imagechallenge.container_image, detachTrue, environment{ FLAG: flag, CHALLENGE_ID: str(challenge.id), USER_ID: str(user_id) }, ports{80/tcp: None}, # 随机宿主机端口 mem_limit256m, cpu_period100000, cpu_quota50000, network_modebridge, auto_removeTrue ) return container, flag逻辑说明docker.from_env()读取宿主机 Docker 环境变量containers.run是创建并启动容器。environment参数把动态生成的 flag 注入容器内部赛题程序从环境变量里读取。ports{80/tcp: None}让 Docker 自动分配宿主机的随机端口这样每个用户访问到的端口都不同。auto_removeTrue保证容器停机后自动清理不会堆积垃圾容器。参数说明mem_limit256m限制容器内存 256MBcpu_quota50000配合cpu_period100000表示容器最多使用 50% 的单核 CPU。这是防“ Containers 被打穿后耗尽资源”的标准手段答辩时这几行参数是最能体现工程意识的地方。4. 把平台跑起来从初始化数据库到成功判出第一个 flag纸上谈兵结束下面是一套我实测过多次的本地部署流程。前提是操作系统已安装 Python 3.9 以上版本并且有 Docker 环境。我先不启动任何容器只做最小化跑通用静态题验证登录、提交、判定、得分全链路再进动态容器验证 Flag 注入。4.1 环境准备与依赖安装所有依赖都写在requirements.txt里。注意不要直接在系统 Python 环境里pip install那样装完一堆包改版后全废了。项目文档里要求的做法是先建虚拟环境。python -m venv venv source venv/bin/activate # Windows 用 venv\\Scripts\\activate pip install -r requirements.txt python scripts/init_db.py逻辑说明虚拟环境隔离依赖init_db.py会读取config.py里的数据库路径并创建 SQLite 文件。参数说明默认数据库文件是instance/ctf.db这个目录在.gitignore里如果之前跑过旧版本需要先删除旧库再执行初始化否则会因为表已存在报错。初始化完成后可以用 SQLite 工具连进去看一眼users表确认管理员账号有没有创建成功。4.2 导入静态题库并启动 Flask 服务环境准备完毕后启动服务之前先把题库导入。静态题不需要 Docker适合第一批验证。python scripts/import_challenges.py --file scripts/sample_static_challenges.json python app.py --host 0.0.0.0 --port 5000逻辑说明导入脚本会执行哈希加密、写入challenges表启动参数里的--host 0.0.0.0表示允许局域网访问方便和队友联调。参数说明端口默认 5000如果被占用就换--port 5001但改端口后前端 JS 里如果有写死的地址也要一起改这套源码默认前端请求走相对路径没有这个坑。启动后浏览器打开http://127.0.0.1:5000能进入登录页就说明 Flask 正常。4.3 提交一次真实 flag从错误到正确的完整响应我建议所有拿到源码的人第一件事不是点页面而是用curl直接打后端接口快速确认判定链路是否通。页面按钮点得通不代表接口稳定。# 登录获取会话 curl -c cookies.txt -X POST http://127.0.0.1:5000/api/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123} # 提交正确答案 curl -b cookies.txt -X POST http://127.0.0.1:5000/api/submit \ -H Content-Type: application/json \ -d {challenge_id:1,flag:flag{sql_injection_is_easy}} # 预期响应{success: true, message: 正确, score: 200}逻辑说明第一次登录后返回 Set-Cookie-c cookies.txt保存会话第二次提交带着这个 Cookie 请求提交接口。这里有个细节必须保证challenge_id是数据库里真实存在的 ID导入题目前是自增从 1 开始如果你中间删过题ID 就不是 1 了先查表确认。参数说明--data里的 JSON 中文和花括号在部分 Linux shell 里要转义最稳的做法是把 JSON 写进文件用--data file.json。如果返回success: false就去查submissions表里那条statuswrong的记录看存进去的 flag 和你提交的是否一致。4.4 动态 Flag 模式验证环境变量注入与轮换静态题链路通了再进攻防模式。此时需要一道配置了container_image的题目。管理员后台添加题目时填镜像名比如ctf_web_sqli:latest这道题就需要先构建镜像。cd docker/web_sqli docker build -t ctf_web_sqli:latest .构建完成后在平台里启动该题目的容器。此时打开容器详情页能看到一个随机端口访问该端口就能到达赛题环境。验证动态 flag 是否生效的方法是进入容器看环境变量docker ps --filter ancestorctf_web_sqli:latest -q docker exec -it container_id env | grep FLAG逻辑说明平台创建容器时把FLAG写入环境变量赛题内的读取逻辑是从os.environ[FLAG]取值。参数说明如果docker exec执行env看到的是空白说明docker_helper.py里的environment参数没有生效最可能是镜像内启动的进程是独立 PID 1环境变量没传进去检查镜像的dockerfile里有没有用CMD而不是ENTRYPOINT覆盖环境。5. 部署与排错避坑五个最容易翻车的位置和对应修法任何 CTF 平台源码到手真正消耗时间的不是跑通而是排错。下面五条是从课程设计群里收集的真实踩坑记录每一条都是“现象 → 原因 → 解决”的完整链条。5.1 数据库连不上密码没错也进不去现象运行python scripts/init_db.py后提示sqlite3.OperationalError: unable to open database file但反复检查config.py里的路径就是写对了。原因SQLite 要求存放数据库文件的目录必须存在。很多源码默认配置instance/ctf.db但源码包解压后根本没有instance目录SQLite 不会自动创建父目录。解决在init_db.py里加一段创建目录的逻辑或者在运行前手动mkdir -p instance。我一般会直接改config.py把数据库路径指到项目根目录下固定的data/ctf.db并确保data/目录已存在。import os, sqlite3 db_path os.path.join(os.path.dirname(__file__), .., data, ctf.db) os.makedirs(os.path.dirname(db_path), exist_okTrue)逻辑说明exist_okTrue表示目录存在不报错不存在就创建。参数说明db_path用绝对路径拼接避免不同工作目录启动时找不到数据库。5.2 提交 flag 永远返回错误但答案明明是对的现象手动在浏览器里提交正确答案平台提示“flag 不正确”但在数据库里查challenges表答案确实一致。原因这个现象通常是三类原因之一提交接口没有strip()方法、全半角字符混用、或者前端把 flag 做了二次编码。最常见的是换行符你从微信聊天记录里复制 flag 会带不可见字符。解决统一在submit_flag入口处做清洗并打开提交记录的 debug 模式把收到的原始 flag 长度打印出来对比正确 flag 长度。submitted_flag str(request.json.get(flag, )).strip() if len(submitted_flag) ! len(challenge.expected_flag_length): return jsonify({success: False, message: flag 长度不一致})逻辑说明先看长度再比内容能快速过滤掉换行和空格问题。参数说明expected_flag_length可以在challenges表里增加冗余字段导入时自动计算也可以在比对时临时对正确答案求长度。这个方法笨但是快比盯着屏幕猜快得多。5.3 攻防模式容器不启动页面一直转圈现象管理员后台点击启动题目容器前端没有返回端口后台日志报docker.errors.NotFound或ConnectionRefusedError。原因平台所在机器没启动 Docker或者当前用户不在docker用户组里无法访问/var/run/docker.sock。解决先手工跑docker info验证 Docker 是否可用如果权限不足执行sudo usermod -aG docker $USER后重新登录如果 API 报了NotFound用docker images看一下镜像名是不是拼写错了。docker info docker images | grep ctf_web_sqli逻辑说明Docker SDK 依赖宿主机 Docker daemon 可用且权限足够。参数说明docker info输出里重点看Server Version和Storage Driver如果有permission denied字样就按用户组处理镜像名必须与平台后台填写的完全一致包括 tag。5.4 文件上传类题目“能传不能连”现象题目提供文件上传功能上传成功但访问上传的文件时返回 404或者能访问 HTML 但不能解析 PHP。原因这是 CTF 文件上传题最常见的考法。平台把上传文件保存到uploads/目录但uploads/不在 Web 服务器静态文件映射范围内或者 Nginx 限制了上传体大小文件根本没传全又或者题目容器里没有配置解析规则。解决确认 Flask 静态目录配置并检查 Nginx 的client_max_body_size。在本地环境最直接的验证方式是用curl查看响应头。curl -I http://127.0.0.1:5000/uploads/shell.php # 检查返回是否是 200 而不是 404逻辑说明能看到 200 但内容是源码说明没有解析器如果直接 404说明目录映射都没配好。参数说明测试时用这是看似无害的文件名不要放攻击载荷避免违规行为。5.5 Windows 下运行脚本中文报错现象import_challenges.py导入中文题面时提示UnicodeDecodeError: gbk codec cant decode byte。原因Windows 默认编码是 GBK而源码文件和 JSON 文件是 UTF-8。解决强制设置 Python UTF-8 模式运行。set PYTHONUTF81 python scripts/import_challenges.py --file scripts/sample_static_challenges.json逻辑说明PYTHONUTF81让 Python 运行时强制使用 UTF-8 编码处理所有文件读写。参数说明该环境变量只对当前终端会话有效如果重启终端还需重新设置更彻底的做法是在代码里open(..., encodingutf-8)硬编码。6. 把这份源码改造成更有分量的课程设计三个进阶动作基础跑通只是及格线想拿高分得把源码往工程化方向推。下面三个动作是我认为投入产出比最高的每个都能在答辩时单独撑起三分钟的讲解。6.1 从静态题库升级为动态容器池现在的源码虽然支持创建容器但生命周期管理是短板。我建议给docker_helper.py增加两个功能比赛启动时统一预创建容器池选手访问时分配比赛结束后统一回收容器。配合一个定时清理脚本避免容器泄漏占满宿主机。# scripts/cleanup_containers.py import docker client docker.from_env() for container in client.containers.list(allTrue): if container.labels.get(ctf_platform) true: if stop in container.status: container.remove() else: container.stop()逻辑说明遍历所有容器找到平台标注的容器先停止再删除。参数说明创建容器时建议给labels参数加{ctf_platform: true}这样清理脚本不会误删其他容器。答辩时讲清楚“怎么保证容器不泄漏”老师通常就会放过你。6.2 排行榜加一页解题轨迹图源头的排行榜只有用户总分列表。我建议加一张解题时间线页面横轴是比赛时间纵轴是分数每条线代表一个队伍。这个功能需要修改ranking模块按submissions.created_at聚合分数。实现不复杂但是视觉冲击力极强答辩演示效果非常好。from collections import defaultdict timeline defaultdict(int) for sub in Submission.query.filter_by(statuscorrect).all(): timeline[sub.created_at.date()] sub.challenge.score逻辑说明按日期聚合每个用户每日得分增量。参数说明created_at是datetime类型date()只取日期如果比赛跨天这样分组是合理的。前端用 Chart.js 画折线图模板里加一个/ranking/timeline路由即可。6.3 三组自动化验证脚本把课程设计从“能跑”变成“证明它能跑”最有效的做法是写三组测试脚本基础功能冒烟测试、并发提交一致性测试、恶意输入防护测试。下面这个是并发提交的核心思路用concurrent.futures同时提交同一个 flag 两次验证防重复机制是否生效。from concurrent.futures import ThreadPoolExecutor import requests def submit_once(i): r requests.post( http://127.0.0.1:5000/api/submit, json{challenge_id: 1, flag: flag{sql_injection_is_easy}}, cookies{session: your_session_cookie} ) return r.json() with ThreadPoolExecutor(max_workers2) as pool: results list(pool.map(submit_once, range(2))) # 预期一个是 success: true一个是 你已经解出该题逻辑说明两个线程同时提交同一题的正确答案看数据库的unique约束能否挡住第二次提交。参数说明测试前要把该用户该题的提交记录清空否则第一次也不会成功这个脚本在答辩现场跑一遍比口头说“我有防重复机制”有说服力得多。从那以后我每次拿到课程设计或毕业设计源码都会先花一小时强制走一遍空库安装、提交判定、并发提交这三件事不跑通这三步我绝不碰业务代码。CTF 攻防对抗平台的坑大多不在功能多少而在链路是否完整。希望这套源码和学习笔记能帮你少走我走过的弯路把答辩现场翻车的概率降下来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑