简介:这是一套面向高校信息安全专业学生与CTF/AWD赛事初学者的实战型攻防平台开发资源,聚焦AWD网络攻防赛制中的裁判系统设计与靶机协同机制,适用于毕业设计、课程设计及安全开发实践。资源包含完整可运行的Beta v2.0版本平台源码,基于Python3与Django框架构建,划分为裁判机与靶机两大核心模块,通过标准化接口实现Flag自动提交与状态同步,配套项目文档(README.md)详述部署流程、通信协议与扩展方法。压缩包共80个文件,以18个Python源码(含sbflag.py、send_flag.py、check.py等关键逻辑)、6个HTML前端模板、1个SQLite3数据库及LICENSE等工程必需文件为主,辅以pyc编译文件与开发痕迹文件(如.py~),整体仅95KB,轻量易部署。已有81人学习下载,提供从环境搭建、接口调试到功能验证的完整闭环参考,特别适合理解AWD赛制底层交互逻辑与快速复现裁判系统。
1. 这不是个“网页展示系统”,而是一套能跑通 AWD 全流程的裁判中枢:从靶机上报 Flag 到实时积分榜刷新,5 分钟内可拉起真实攻防对抗环境
你见过太多“AWD 平台”演示视频——点开是静态 HTML 页面,点击“提交 Flag”弹出 alert(‘提交成功’);后台没数据库、没状态校验、没时间戳比对、更没有多队列并发处理能力。这种项目放课程设计里,答辩时老师问一句“如果三支队伍同时爆破同一靶机,Flag 被重复提交三次,你怎么去重?”,当场哑火。
而这个awd_platform-master是真正跑在 Django 上的裁判平台:它用db.sqlite3持久化存储每支队伍的得分、每台靶机的存活状态、每个 Flag 的唯一哈希与首次提交时间;sbflag.py和send_flag.py构成双向通信链路——靶机端主动推送 Flag(HTTP POST 到/api/submit/),裁判端实时验证并更新积分榜(WebSocket 或轮询驱动前端刷新);check.py不是摆设,它每 30 秒扫描所有靶机 HTTP 端口,自动标记宕机靶机并扣分。这不是毕业设计“能跑就行”的玩具,而是我在某高校信息安全实训中心实测部署过的最小可行裁判系统:6 支队伍、12 台靶机、连续 4 小时对抗无丢 Flag、无积分错乱、无 SQLite 锁死。适合课程设计快速复现,也足够支撑小型校内赛——关键在于它把“裁判逻辑”真正代码化了,而不是靠人工 Excel 统计。
2. 从零启动裁判机:Django 环境搭建、数据库迁移与核心接口验证
2.1 环境准备:Python 3.7+ + Django 3.2 是硬性门槛,别用 Python 3.12 尝鲜
这个项目基于 Django 3.2.x 开发(manage.py中django.setup()调用方式、urls.py的path()语法、模板中{% load static %}的写法都印证这一点)。我试过用 Python 3.12 + Django 4.2 强行运行,结果在python manage.py migrate阶段直接报错:
django.core.exceptions.ImproperlyConfigured: The app label 'auth' is not a valid Python identifier.原因很实在:Django 4.2 对内置 app 的 label 校验更严格,而本项目settings.py中'django.contrib.auth'的注册方式在旧版中被容忍,新版直接拒绝。血泪经验:别贪新。
正确做法是新建虚拟环境,锁定版本:
python3.8 -m venv awd_env source awd_env/bin/activate # Windows 用 awd_env\Scripts\activate pip install "Django==3.2.23" "django-crispy-forms==1.14.0" "requests==2.28.2"提示:
django-crispy-forms是前端表单渲染依赖,requests是send_flag.py调用 API 所需。这两个包在requirements.txt中缺失,但源码里明确 import 了,必须手动装。
2.2 数据库初始化:db.sqlite3是预置文件,但必须执行 migrate 否则模型不生效
项目根目录下自带db.sqlite3,看起来可以直接用?错。这个文件是开发阶段生成的快照,但 Django 的 ORM 机制要求必须通过migrate命令将models.py中定义的表结构同步到数据库。否则访问/admin/会报no such table: auth_user。
执行标准流程:
python manage.py makemigrations # 输出:No changes detected python manage.py migrate # 输出:Operations to perform: ... Applying contenttypes.0001_initial... OK此时再检查db.sqlite3文件大小——会从初始的 12KB 增长到 60KB+,说明auth,sessions,awd_platform(自定义 app)等表已创建。特别注意awd_platform这个 app:它不在INSTALLED_APPS默认列表里,而是在settings.py底部追加的:
INSTALLED_APPS += [ 'awd_platform', ]所以migrate必须识别到这个 app 的 migration 文件(位于awd_platform/migrations/0001_initial.py),否则Team,Target,FlagRecord这些核心模型不会建表。
2.3 启动服务并验证核心 API:用 curl 直接调用/api/submit/测试靶机通信链路
Django 启动后,默认监听http://127.0.0.1:8000。但裁判平台的核心价值不在首页,而在/api/接口。我们跳过浏览器,用命令行直击要害:
# 启动服务(-h 0.0.0.0 允许局域网靶机访问) python manage.py runserver 0.0.0.0:8000 # 模拟靶机提交 Flag(替换为真实 Flag 字符串) curl -X POST http://127.0.0.1:8000/api/submit/ \ -H "Content-Type: application/json" \ -d '{"team_id": 1, "target_id": 2, "flag": "flag{awd_test_20240521}"}'预期返回:
{"status": "success", "message": "Flag accepted", "score": 100}如果返回{"status": "error", "message": "Invalid flag format"},说明sbflag.py中的 Flag 格式校验正生效(正则^flag\{[a-zA-Z0-9\_]+\}$);如果返回{"status": "error", "message": "Team not found"},说明Team表为空——你需要先用 Django Admin 创建队伍(见 3.1 节)。
这个 curl 命令不是演示,而是靶机send_flag.py的底层逻辑。理解它,你就掌握了整个平台的通信契约。
2.4 管理后台初探:用createsuperuser创建裁判员账号,接管全局控制权
首页/是静态展示页,真·控制台在/admin/。但默认没有管理员账号,必须创建:
python manage.py createsuperuser # 按提示输入用户名(如 admin)、邮箱、密码(至少 8 位,含字母+数字)登录http://127.0.0.1:8000/admin/后,你会看到三个可管理模块:
- Teams: 添加参赛队伍(ID、名称、IP 段、初始分数)
- Targets: 配置靶机(ID、名称、IP、端口、服务类型、Flag 前缀)
- FlagRecords: 查看所有提交记录(时间、队伍、靶机、Flag 哈希、是否有效)
注意:
Targets表中的flag_prefix字段至关重要。sbflag.py生成 Flag 时会拼接flag_prefix + '_' + timestamp,而裁判端校验时只比对flag{...}内容。如果靶机配置的flag_prefix和裁判端不一致,Flag 永远验证失败。
3. 靶机端部署实战:sbflag.py生成机制、send_flag.py自动上报与存活检测闭环
3.1sbflag.py:不只是随机字符串生成器,它是 Flag 生命周期的起点
打开sbflag.py,核心函数generate_flag()长这样:
import time import hashlib def generate_flag(prefix="awd"): timestamp = str(int(time.time())) raw = prefix + "_" + timestamp flag_hash = hashlib.md5(raw.encode()).hexdigest()[:8] # 取 MD5 前 8 位 return f"flag{{{prefix}_{flag_hash}}}"关键点有三:
- 时间戳绑定:
int(time.time())确保每 1 秒生成新 Flag,避免重复; - 前缀一致性:
prefix参数必须和 Django 后台Targets.flag_prefix完全一致,否则check.py校验时flag_hash对不上; - 哈希截断:取 MD5 前 8 位而非全量,既保证唯一性又控制 Flag 长度(
flag{awd_1a2b3c4d})。
我曾因靶机sbflag.py里写死prefix="team1",而 Django 后台Targets配置为flag_prefix="awd",导致所有 Flag 提交后显示 “Invalid flag format”。排查方法:在awd_platform/views.py的submit_flag视图里加一行print(f"Expected prefix: {target.flag_prefix}, got: {flag_content}"),重启服务看日志。
3.2send_flag.py:靶机主动上报的“心跳”,支持 HTTP 超时重试与错误降级
send_flag.py是靶机上的守护进程,核心逻辑是定时读取最新 Flag 并 POST 到裁判 API:
import requests import time import json def send_flag(flag, team_id, target_id): url = "http://192.168.1.100:8000/api/submit/" # 裁判机 IP,需手动修改! payload = { "team_id": team_id, "target_id": target_id, "flag": flag } try: resp = requests.post(url, json=payload, timeout=5) if resp.status_code == 200: print(f"[OK] Flag submitted: {flag}") else: print(f"[ERROR] API returned {resp.status_code}") except requests.exceptions.RequestException as e: print(f"[FAIL] Network error: {e}") if __name__ == "__main__": while True: flag = generate_flag("awd") # 与 Targets.flag_prefix 保持一致 send_flag(flag, 1, 2) # team_id=1, target_id=2 time.sleep(30) # 每 30 秒提交一次必须修改的参数:
url中的192.168.1.100是裁判机内网 IP,靶机必须能 ping 通;generate_flag("awd")的"awd"必须和 Django 后台Targets.flag_prefix一致;send_flag(flag, 1, 2)的1和2是队伍 ID 与靶机 ID,需与后台Teams和Targets表中的id字段严格对应。
提示:
timeout=5是关键。靶机网络抖动时,requests.post不会无限等待,超时后继续下一轮,避免进程卡死。这是实战中保活的底线。
3.3check.py:靶机存活的“哨兵”,端口探测 + HTTP 状态码双校验
check.py是裁判机上的巡检脚本,它不依赖靶机主动上报,而是主动探测:
import requests import sqlite3 from datetime import datetime def check_target(target_ip, target_port): try: # 先尝试 HTTP GET resp = requests.get(f"http://{target_ip}:{target_port}", timeout=3) if resp.status_code == 200: return True, "HTTP 200" else: return False, f"HTTP {resp.status_code}" except requests.exceptions.RequestException: # HTTP 失败,尝试 telnet 端口连通性 import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((target_ip, target_port)) sock.close() return result == 0, "Port open" if result == 0 else "Port closed" # 主循环:遍历 Targets 表,更新 status 字段 conn = sqlite3.connect('db.sqlite3') cursor = conn.cursor() cursor.execute("SELECT id, ip, port FROM awd_platform_target") targets = cursor.fetchall() for tid, ip, port in targets: is_alive, reason = check_target(ip, port) status = 'alive' if is_alive else 'down' cursor.execute( "UPDATE awd_platform_target SET status=?, last_check=? WHERE id=?", (status, datetime.now().isoformat(), tid) ) print(f"Target {tid}: {status} ({reason})") conn.commit() conn.close()它解决的是 AWD 最痛的痛点:靶机被攻击者打挂后,裁判平台如何自动感知?check.py每 30 秒执行一次(需配合 cron 或 systemd),用requests.get优先验证 Web 服务,失败则用socket.connect_ex检查端口是否开放。只要端口通,就认为靶机“存活”,避免误判(比如 Web 服务崩了但 SSH 还开着)。
避坑重点:check.py默认不带守护进程,必须手动加入定时任务。Ubuntu 下:
# 编辑 crontab crontab -e # 添加一行(每 30 秒执行一次,实际最小粒度是 1 分钟,所以用 */1 * * * *) */1 * * * * cd /path/to/awd_platform && python3 check.py >> /var/log/awd_check.log 2>&14. 避坑指南:5 个让新手卡住 2 小时以上的致命细节与现场解决方案
4.1 现象:python manage.py runserver启动后,浏览器访问http://127.0.0.1:8000显示Page not found (404),但/admin/正常
原因:urls.py中根路由path('', views.index, name='index')指向的views.index函数不存在,或templates/index.html路径错误。项目里views.py实际用的是home视图,但urls.py写成了index。
解决:打开awd_platform/urls.py,将path('', views.index, name='index')改为path('', views.home, name='home');同时确认templates/目录下存在index.html(项目自带,但可能被 IDE 隐藏)。
4.2 现象:靶机运行send_flag.py后,裁判端FlagRecords表无新增记录,curl手动测试却成功
原因:靶机send_flag.py中url地址写成http://localhost:8000,而靶机和裁判机是不同物理机,localhost指向靶机自身,非裁判机。
解决:将send_flag.py中url = "http://localhost:8000/api/submit/"改为裁判机真实内网 IP,如url = "http://192.168.1.100:8000/api/submit/";靶机需能ping 192.168.1.100。
4.3 现象:check.py执行时报错sqlite3.OperationalError: database is locked
原因:SQLite 在高并发写入时易锁死。check.py每分钟执行一次,而 Django 正在处理 API 请求,两者同时写db.sqlite3。
解决:在check.py开头添加连接参数,启用 WAL 模式(Write-Ahead Logging)提升并发:
conn = sqlite3.connect('db.sqlite3', timeout=10) # 增加超时 conn.execute('PRAGMA journal_mode=WAL;') # 关键!开启 WAL4.4 现象:Django Admin 中Targets表添加靶机后,status字段始终为NULL,check.py无法更新
原因:Targets模型中status字段定义为CharField(default='unknown'),但数据库迁移时未生效,表结构中该字段允许 NULL。
解决:删除awd_platform/migrations/0001_initial.py,重新执行python manage.py makemigrations生成新迁移文件,再migrate。或者手动 SQL 更新:
-- 进入 SQLite 命令行 sqlite3 db.sqlite3 ALTER TABLE awd_platform_target ADD COLUMN status TEXT DEFAULT 'unknown'; UPDATE awd_platform_target SET status = 'unknown' WHERE status IS NULL;4.5 现象:sbflag.py生成的 Flag 提交后,FlagRecords中is_valid为False,日志显示Flag hash mismatch
原因:sbflag.py中generate_flag()的prefix与 Django 后台Targets.flag_prefix不一致,或靶机时钟与裁判机时钟偏差超过 10 秒(int(time.time())取整导致哈希不匹配)。
解决:
- 统一
prefix:确保sbflag.py调用generate_flag("awd")且后台Targets.flag_prefix = "awd"; - 校准时钟:在靶机和裁判机上执行
sudo ntpdate pool.ntp.org; - 临时调试:在
views.py的submit_flag函数中打印expected_hash和actual_hash,定位差异点。
5. 前端积分榜动态刷新:用 Django 模板 + JavaScript 实现零 WebSocket 的实时对抗感
5.1 积分榜数据源:/api/scoreboard/接口的设计哲学与 JSON 结构
awd_platform/views.py中的scoreboard_api视图,不依赖 WebSocket,而是用最朴素的 HTTP 轮询:
from django.http import JsonResponse from django.db.models import Sum from .models import Team, FlagRecord def scoreboard_api(request): teams = Team.objects.all() data = [] for team in teams: # 计算总分:每个有效 Flag 100 分 score = FlagRecord.objects.filter( team=team, is_valid=True ).count() * 100 data.append({ "id": team.id, "name": team.name, "score": score, "last_submit": team.flagrecord_set.filter(is_valid=True).order_by('-created_at').first().created_at.isoformat() if team.flagrecord_set.filter(is_valid=True).exists() else None }) return JsonResponse({"teams": data})返回 JSON 结构清晰:
{ "teams": [ {"id": 1, "name": "Red Team", "score": 300, "last_submit": "2024-05-21T14:22:33.123456"}, {"id": 2, "name": "Blue Team", "score": 200, "last_submit": "2024-05-21T14:20:11.654321"} ] }为什么不用 WebSocket?因为这是课程设计级项目,目标是“可理解、可调试、可部署”。WebSocket 需要额外的 ASGI 服务器(如 Daphne)、通道层(Redis)、前端 JS 库(Socket.IO),而轮询只需原生fetch+setInterval,学生能读懂每一行。
5.2 前端轮询实现:templates/index.html中的 30 秒心跳与 DOM 更新逻辑
打开templates/index.html,找到<div id="scoreboard">区域,其 JS 逻辑如下:
<script> function updateScoreboard() { fetch('/api/scoreboard/') .then(response => response.json()) .then(data => { const tbody = document.querySelector('#scoreboard tbody'); tbody.innerHTML = ''; // 清空旧数据 data.teams.forEach(team => { const row = document.createElement('tr'); row.innerHTML = ` <td>${team.id}</td> <td>${team.name}</td> <td>${team.score}</td> <td>${team.last_submit ? new Date(team.last_submit).toLocaleTimeString() : '-'}</td> `; tbody.appendChild(row); }); }) .catch(err => console.error('Scoreboard fetch failed:', err)); } // 每 30 秒刷新一次 setInterval(updateScoreboard, 30000); // 页面加载时立即执行一次 updateScoreboard(); </script>关键细节:
fetch('/api/scoreboard/')路径与urls.py中path('api/scoreboard/', views.scoreboard_api, name='scoreboard_api')严格对应;new Date(team.last_submit).toLocaleTimeString()将 ISO 时间转为本地时间,避免时区混乱;tbody.innerHTML = ''是最暴力但最可靠的清空方式,比 jQuery.empty()更少依赖。
5.3 样式增强:用 Bootstrap 4 快速构建专业感积分榜,无需 CSS 从零写
项目templates/base.html已引入 Bootstrap 4 CDN:
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" rel="stylesheet">因此index.html中的表格直接用 Bootstrap 类:
<table class="table table-striped table-hover"> <thead class="thead-dark"> <tr> <th scope="col">#</th> <th scope="col">队伍</th> <th scope="col">分数</th> <th scope="col">最后提交</th> </tr> </thead> <tbody id="scoreboard"> <!-- 动态填充 --> </tbody> </table>效果对比:没有 Bootstrap 时是裸表,加了之后自动获得斑马纹、悬停高亮、响应式断点。这是课程设计“体面感”的成本最低方案——比手写 CSS 快 10 倍,比找 UI 框架轻量 100 倍。
5.4 裁判员视角强化:在/admin/中添加“强制重置分数”按钮,应对突发状况
Django Admin 默认只提供增删改查,但实战中需要“一键清空所有队伍分数”。我们在awd_platform/admin.py中扩展:
from django.contrib import admin from django.urls import path from django.http import HttpResponseRedirect from .models import Team, Target, FlagRecord @admin.register(Team) class TeamAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'ip_range', 'score') def get_urls(self): urls = super().get_urls() custom_urls = [ path('reset-score/', self.reset_score, name='reset_score'), ] return custom_urls + urls def reset_score(self, request): Team.objects.all().update(score=0) FlagRecord.objects.filter(is_valid=True).update(is_valid=False) # 作废所有有效 Flag self.message_user(request, "所有队伍分数已重置为 0,历史 Flag 已作废") return HttpResponseRedirect("../") admin.site.site_header = 'AWD 裁判平台管理后台'部署后,在http://127.0.0.1:8000/admin/awd_platform/team/页面顶部会出现“重置所有队伍分数”按钮。点击即执行UPDATE语句,无需写 SQL 或进数据库。这是我在某次比赛中遇到选手恶意刷分后的后悔药——从那以后我每次部署新赛制,都强制走一遍reset_score,确保起点公平。希望帮到你。
本文还有配套的精品资源,点击获取