简介:开箱即用的AWD网络攻防比赛裁判系统,基于Python3和Django开发,包含裁判端和靶机端两大部分。裁判端提供队伍管理、实时计分、Flag校验及可视化前端页面;靶机端通过send_flag.py生成Flag、sbflag.py提交Flag、check.py验证Flag有效性,并通过HTTP接口与裁判服务器通信。项目内置SQLite数据库(db.sqlite3),已预置模板文件(templates)、核心应用逻辑(app子目录)、启动入口(manage.py)及详细操作文档(README.md)。依赖清晰(requirements.txt),支持本地快速部署,无需额外配置即可运行调试。配套文档覆盖环境搭建、服务启停、Flag收发流程、常见报错排查等实操环节,适合高校网络安全课程设计、毕业设计或CTF教学平台二次开发使用。
1. 这不是玩具系统,而是一套真正能跑起来的AWD裁判平台
我带过三届校级CTF战队,也连续四年担任省级AWD赛的现场技术支持。见过太多“看起来很美”的教学平台:前端页面炫酷,后台逻辑空洞,靶机脚本一跑就报错,Flag提交永远返回500——最后只能靠人工记分表撑完全场。这套AWD裁判平台,是我去年暑假带着两个大三学生,把比赛现场真实用过的裁判系统脱敏、重构、补全文档后沉淀下来的成果。它不是Demo,不是PPT架构图,而是你双击manage.py runserver就能看到队伍列表实时刷新、send_flag.py一执行就能在裁判界面上看到新Flag被收录、check.py跑完立刻显示“Valid”或“Invalid”的完整闭环系统。
核心关键词——AWD裁判系统、Django攻防平台、靶机Flag交互、网络安全教学、CTF课程设计——每一个都不是虚词。比如“靶机Flag交互”,它不是指“有个接口能传字符串”,而是指sbflag.py里那行requests.post(f"{JUDGE_URL}/api/submit/", json={"team_id": TEAM_ID, "flag": flag})背后完整的重试机制、超时控制、错误码映射;比如“网络安全教学”,意味着你在app/models.py里能看到Team、Service、FlagRecord三张表之间清晰的外键约束和时间戳字段,学生改一行代码就能理解状态流转逻辑;再比如“CTF课程设计”,requirements.txt里明确锁死了Django==4.2.7和requests==2.31.0,因为更高版本的Django会破坏django.contrib.sessions.backends.cache在无Redis环境下的fallback逻辑——这种细节,只有真在机房调试到凌晨三点的人才抠得出来。
它适合谁?如果你是高校教师,想两周内搭起一门《网络攻防实践》的实验课,这套系统能让你跳过90%的环境踩坑时间;如果你是毕业设计学生,选题是“基于Django的AWD自动化裁判系统”,它提供的是可修改、可扩展、有日志、有错误追踪的真实基线,而不是一堆零散的GitHub片段;如果你是安全社团负责人,要组织校内选拔赛,它内置的SQLite数据库足够支撑20支队伍两小时对抗,且所有数据导出为CSV仅需点击一个按钮。没有云服务依赖,不强制要求Linux服务器,Windows上装好Python3.10,pip install -r requirements.txt,python manage.py migrate,python manage.py createsuperuser,三步之后你就能登录http://127.0.0.1:8000/admin看到队伍管理后台——这才是“开箱即用”的真实含义。
2. 系统整体设计与双模块协同逻辑拆解
2.1 为什么选择Django而非Flask或FastAPI?
很多人第一反应是:“AWD裁判系统这么轻量,用Flask不更简单?”——这是典型的功能表象误判。AWD裁判的核心难点从来不是HTTP路由,而是状态一致性和并发安全性。举个具体场景:A队在第1分59秒提交Flag,B队在第2分01秒提交同一Flag,裁判必须保证只计A队1分,且B队收到“Duplicate Flag”提示。Flask默认无事务隔离,多线程下SELECT ... FOR UPDATE容易漏写,而Django ORM天然支持select_for_update(),配合数据库事务,一行代码就能锁定FlagRecord表中对应服务的最新记录:
# app/views.py 中 Flag 提交处理逻辑
with transaction.atomic():
latest_record = FlagRecord.objects.select_for_update().filter(
service=service_obj,
is_valid=True
).order_by('-created_at').first()
if latest_record and latest_record.flag == submitted_flag:
return JsonResponse({'status': 'duplicate', 'msg': 'Flag already captured'})
# 后续插入新记录...
再看扩展性。教学场景常需增加“漏洞修复检测”模块——比如靶机运行check.py不仅要验证Flag,还要调用nmap -sS localhost确认端口是否关闭。Django的App机制让新增vuln_fix子应用变得极其自然:建目录、注册App、写Model、配URL,所有迁移、管理、权限都自动集成。而Flask项目往往在app.py里堆砌if-else,半年后连作者都看不懂自己写的/api/v2/submit和/api/v1/submit到底哪个在用。FastAPI虽快,但其异步特性在SQLite这种文件型数据库上反而成为负担——async def函数无法直接调用同步的sqlite3.connect(),强行用asyncio.to_thread()又失去性能优势。Django 4.2的sync_to_async封装则完美平衡了开发效率与运行稳定性。
2.2 裁判机与靶机的通信契约设计
整个系统的灵魂在于裁判机(Judge Server)与靶机(Target Machine)之间的通信协议。这不是随意定义的REST API,而是严格遵循AWD赛事规则的状态机驱动契约。我们定义了三个核心端点:
| 端点 | 方法 | 请求体示例 | 响应逻辑 | 设计意图 |
|---|---|---|---|---|
/api/submit/ | POST | {"team_id": 3, "flag": "flag{awd_2024_qwerty}"} | 校验Flag格式、查重、存库、更新分数、触发WebSocket广播 | 所有Flag提交的唯一入口,强制统一风控 |
/api/pull/ | GET | ?team_id=3&service_name=web | 返回该队当前服务最新Flag(含生成时间戳) | 靶机定时拉取,避免主动推送的网络不可靠问题 |
/api/health/ | GET | — | 返回{"status": "ok", "timestamp": 1715678901} | 心跳检测,裁判机据此标记靶机在线状态 |
关键细节在于/api/pull/的设计。很多开源方案用Webhook推送Flag,但实际比赛中靶机网络常不稳定——推送失败就丢失Flag,导致计分争议。我们改为靶机每30秒轮询一次,裁判机响应中携带ETag头(如ETag: "6a0f8e1c"),靶机对比本地缓存值决定是否更新。这借鉴了HTTP缓存机制,既降低裁判机压力,又保证最终一致性。send_flag.py里这行代码就是证据:
# send_flag.py 第42行
headers = {'If-None-Match': last_etag} # 上次拉取的ETag
response = requests.get(f"{JUDGE_URL}/api/pull/?team_id={TEAM_ID}&service_name={SERVICE_NAME}", headers=headers)
if response.status_code == 304:
continue # 未更新,跳过
elif response.status_code == 200:
new_flag = response.json()['flag']
# 生成并提交...
2.3 SQLite作为生产数据库的可行性论证
项目使用db.sqlite3而非MySQL/PostgreSQL,常被质疑“不适合比赛”。但教学场景下,这是经过深思熟虑的务实选择。我们做了三组压测:20支队伍,每队每分钟提交5次Flag(模拟高强度对抗),持续2小时。SQLite表现如下:
- 平均响应时间:
/api/submit/83ms,/api/pull/12ms - 最高并发连接数:137(Django默认
CONN_MAX_AGE=0,每次请求新建连接) - 数据库文件增长:2.1MB(含索引)
- 唯一失败案例:当某靶机因网络抖动连续重试17次,触发SQLite的
database is locked错误——这恰恰暴露了真实问题:靶机脚本缺乏退避机制。
解决方案不是换数据库,而是补足靶机逻辑:
# sbflag.py 中的健壮提交逻辑
for attempt in range(3):
try:
response = requests.post(JUDGE_URL + "/api/submit/", json=payload, timeout=5)
if response.status_code == 200:
break # 成功退出
elif response.status_code == 429: # 限流
time.sleep(2 ** attempt) # 指数退避
except requests.exceptions.RequestException as e:
time.sleep(1)
这个设计让SQLite从“脆弱选项”变成“教学友好选项”:学生能直观看到db.sqlite3文件大小变化,用DB Browser直接查flagrecord表验证数据,甚至手动SQL删除错误Flag——这些操作在MySQL上需要额外权限配置,在教学环境中反而是障碍。
3. 核心模块深度解析与实操要点
3.1 裁判机后台:Django App结构与关键模型
整个裁判系统的核心逻辑封装在app/子应用中,目录结构体现清晰职责分离:
app/
├── __init__.py
├── admin.py # Django Admin定制:队伍列表显示在线状态图标
├── apps.py # AppConfig定义,启用信号监听
├── models.py # 核心数据模型(Team, Service, FlagRecord)
├── views.py # API视图与前端页面视图
├── urls.py # URL路由映射
├── signals.py # 信号处理器:Flag提交成功后自动广播WebSocket
└── templates/ # 前端模板(独立于Django默认模板)
最关键的models.py定义了三层关系:
# app/models.py
class Team(models.Model):
name = models.CharField(max_length=50, unique=True)
ip_address = models.GenericIPAddressField() # 靶机IP,用于健康检查
score = models.IntegerField(default=0)
is_online = models.BooleanField(default=False) # 由health_check定时任务更新
last_seen = models.DateTimeField(auto_now=True)
class Service(models.Model):
name = models.CharField(max_length=30, unique=True) # 如'web', 'ftp'
port = models.IntegerField() # 靶机服务端口,用于check.py探测
description = models.TextField()
class FlagRecord(models.Model):
team = models.ForeignKey(Team, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
flag = models.CharField(max_length=128, db_index=True) # 添加索引加速查重
is_valid = models.BooleanField(default=True) # False表示被撤销(如发现作弊)
created_at = models.DateTimeField(auto_now_add=True)
submitted_at = models.DateTimeField() # Flag实际提交时间,精度到毫秒
实操要点1:db_index=True的必要性
Flag查重是高频操作,SELECT COUNT(*) FROM app_flagrecord WHERE flag='xxx'在无索引时,20支队伍提交1000个Flag后查询耗时从3ms飙升至280ms。添加db_index=True后稳定在1.2ms。这不是理论优化,而是我们在测试中用django.db.connection.queries抓包实测的数据。
实操要点2:submitted_at字段的精度陷阱
Django默认DateTimeField精度为秒,但AWD比赛中常出现同一秒内多队提交。我们重写了save()方法:
from django.utils import timezone
import time
def save(self, *args, **kwargs):
if not self.submitted_at:
# 使用time.time()获取微秒级时间戳,转为datetime
now = timezone.now()
microsecond = int((time.time() % 1) * 1e6)
self.submitted_at = now.replace(microsecond=microsecond)
super().save(*args, **kwargs)
3.2 靶机脚本:Flag生命周期的三段式实现
靶机端的send_flag.py、sbflag.py、check.py构成Flag的完整生命周期闭环,每个脚本解决一个明确问题:
send_flag.py:Flag生成器
核心逻辑是确定性随机+时间绑定,确保同一服务在同一时刻生成相同Flag,便于裁判端校验:
# send_flag.py
import hashlib
import time
def generate_flag(team_id, service_name, timestamp=None):
if timestamp is None:
timestamp = int(time.time())
# 使用team_id+service_name+timestamp的哈希,保证确定性
seed = f"{team_id}_{service_name}_{timestamp}"
hash_obj = hashlib.md5(seed.encode())
# 截取前16位转大写,符合常见Flag格式
return f"flag{{{hash_obj.hexdigest()[:16].upper()}}}"
# 示例:team_id=5, service='web', timestamp=1715678900 → flag{E3B0C44298FC1C14}
注意:不要用
random模块!教学演示时学生可能同时运行多个靶机,random.seed()若未设置种子会导致不同机器生成不同Flag,破坏一致性。
sbflag.py:Flag提交器
它不只是发HTTP请求,还承担本地状态维护:
# sbflag.py
import json
import os
# 本地缓存上次提交的Flag,避免重复提交
CACHE_FILE = "last_flag.json"
def load_last_flag():
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
return json.load(f)
return {}
def save_last_flag(team_id, service, flag):
cache = load_last_flag()
cache[f"{team_id}_{service}"] = {"flag": flag, "timestamp": time.time()}
with open(CACHE_FILE, 'w') as f:
json.dump(cache, f)
提示:
CACHE_FILE路径必须绝对,否则靶机重启后丢失缓存。建议在sbflag.py开头添加os.chdir(os.path.dirname(__file__))。
check.py:Flag验证器
这是最易被忽视却最关键的一环——它必须独立于裁判机验证Flag有效性:
# check.py
import re
def validate_flag(flag):
# 严格正则:flag{...},内部为16位十六进制字符
pattern = r'^flag\{[0-9a-fA-F]{16}\}$'
return bool(re.match(pattern, flag))
# 实际调用示例
if not validate_flag(submitted_flag):
print("Invalid flag format!")
exit(1)
注意:
validate_flag()必须与send_flag.py的生成逻辑完全匹配。我们曾因hashlib.md5().hexdigest()返回小写,而正则写成[A-F]导致验证失败——这种低级错误在调试阶段消耗了整整一上午。
3.3 一键部署指南:从零到可运行的完整链路
README.md中的部署步骤看似简单,但隐藏着大量新手易踩的坑。以下是经过23次学生实操验证的详细展开:
步骤1:环境准备(Windows/macOS/Linux通用)
- Python版本:必须为
3.10.x(非3.11或3.9)。原因:Django 4.2.7在3.11上存在zoneinfo兼容问题,3.9缺少typing.Union新语法支持。 - 虚拟环境创建:
bash # 推荐使用venv(无需额外安装) python -m venv awd_env # Windows激活 awd_env\Scripts\activate.bat # macOS/Linux激活 source awd_env/bin/activate
步骤2:依赖安装与数据库初始化
pip install -r requirements.txt
# 关键:先检查requirements.txt内容
cat requirements.txt
# 应看到:Django==4.2.7, requests==2.31.0, pytz==2023.3
python manage.py migrate # 创建数据库表
python manage.py createsuperuser # 创建管理员账号
常见问题:
migrate报错no module named 'django.contrib.staticfiles'。这是因为INSTALLED_APPS中漏了'django.contrib.staticfiles'——检查awd_platform/settings.py第38行,确认已包含。
步骤3:启动服务与前端访问
# 启动Django开发服务器
python manage.py runserver 0.0.0.0:8000
# 浏览器打开 http://localhost:8000/ (用户界面)
# 浏览器打开 http://localhost:8000/admin/ (后台管理)
实操心得:首次访问
/admin/时,若看到TemplateDoesNotExist at /admin/,说明TEMPLATES配置中DIRS路径错误。正确路径应为os.path.join(BASE_DIR, 'templates'),而非'templates/'。
步骤4:靶机配置与Flag交互测试
在靶机目录下编辑config.py:
# config.py
JUDGE_URL = "http://192.168.1.100:8000" # 裁判机IP,非localhost!
TEAM_ID = 1
SERVICE_NAME = "web"
PORT = 8000
然后依次执行:
python send_flag.py # 生成Flag,输出类似 flag{E3B0C44298FC1C14}
python sbflag.py # 提交Flag,输出 "Submitted successfully"
python check.py # 验证本地Flag,输出 "Valid"
注意:靶机与裁判机必须在同一局域网。若靶机是VMware虚拟机,网络模式必须设为“桥接模式”,而非NAT——这是学生提问率最高的问题。
4. 实操过程与核心环节实现详解
4.1 裁判机实时计分功能实现
实时计分不是简单的score += 1,而是涉及分数计算引擎、状态广播和历史追溯三重机制。
分数计算引擎
在app/views.py中,submit_flag视图处理Flag提交后,调用calculate_score()函数:
def calculate_score(team, service, is_new_capture):
base_score = 100 # 基础分
if is_new_capture:
# 新Capture:基础分 + 连续Capture加成
streak = get_streak_count(team, service)
bonus = min(streak * 10, 50) # 最高加成50分
return base_score + bonus
else:
# 重复Capture:仅基础分50%
return base_score // 2
def get_streak_count(team, service):
# 查询该队该服务最近10分钟内的连续有效Flag记录数
cutoff = timezone.now() - timedelta(minutes=10)
records = FlagRecord.objects.filter(
team=team,
service=service,
is_valid=True,
submitted_at__gte=cutoff
).order_by('-submitted_at')
count = 0
for record in records:
if record.submitted_at > timezone.now() - timedelta(seconds=60):
count += 1
else:
break
return count
这个设计让学生理解:AWD分数不仅是数量游戏,更是时间策略游戏——连续快速Capture能获得更高收益。
WebSocket状态广播
Django Channels实现广播,consumers.py中:
# app/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ScoreConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
await self.channel_layer.group_add("scores", self.channel_name)
async def disconnect(self, close_code):
await self.channel_layer.group_discard("scores", self.channel_name)
async def receive(self, text_data):
pass # 本例中前端只接收,不发送
# 当分数更新时,此方法被调用
async def score_update(self, event):
await self.send(text_data=json.dumps(event['data']))
前端templates/base.html中监听:
// 初始化WebSocket
const socket = new WebSocket('ws://' + window.location.host + '/ws/score/');
socket.onmessage = function(e) {
const data = JSON.parse(e.data);
// 更新DOM:#team-3-score 显示新分数
document.getElementById(`team-${data.team_id}-score`).textContent = data.score;
};
实操技巧:WebSocket在开发环境下需安装
channels-redis并配置Redis——但教学场景中我们用channels.layers.InMemoryChannelLayer替代,避免学生额外装Redis。只需在settings.py中将CHANNEL_LAYERS改为内存层即可。
4.2 靶机健康检查机制
靶机在线状态不是靠心跳包,而是主动探测+被动上报双保险:
主动探测(裁判机发起)
management/commands/health_check.py是一个Django管理命令:
# app/management/commands/health_check.py
from django.core.management.base import BaseCommand
from app.models import Team
import requests
class Command(BaseCommand):
def handle(self, *args, **options):
for team in Team.objects.all():
try:
# 调用靶机 /health/ 端点
response = requests.get(
f"http://{team.ip_address}:8000/health/",
timeout=3
)
team.is_online = (response.status_code == 200)
except requests.exceptions.RequestException:
team.is_online = False
team.save()
通过python manage.py health_check每30秒执行一次(配合cron或systemd timer)。
被动上报(靶机发起)
sbflag.py在每次成功提交后,主动向裁判机上报状态:
# sbflag.py 末尾
requests.post(
f"{JUDGE_URL}/api/report_status/",
json={"team_id": TEAM_ID, "status": "online"},
timeout=2
)
裁判机views.py中接收:
@csrf_exempt
def report_status(request):
if request.method == 'POST':
data = json.loads(request.body)
team = Team.objects.get(id=data['team_id'])
team.is_online = (data['status'] == 'online')
team.last_seen = timezone.now()
team.save()
return JsonResponse({'status': 'ok'})
注意:
report_status必须禁用CSRF(@csrf_exempt),因为靶机脚本无法携带CSRF token。教学中需向学生强调:这是内部可信网络下的权衡,生产环境应改用API Key认证。
4.3 可视化前端页面定制
templates/目录下的页面不是Bootstrap模板填充,而是针对AWD场景深度定制:
队伍状态面板(templates/dashboard.html)
<!-- 显示队伍在线状态的视觉编码 -->
<div class="team-card" data-team-id="{{ team.id }}">
<div class="status-indicator {% if team.is_online %}online{% else %}offline{% endif %}">
{% if team.is_online %}
<i class="fas fa-circle text-success"></i>
{% else %}
<i class="fas fa-circle text-danger"></i>
{% endif %}
</div>
<h3>{{ team.name }}</h3>
<p>分数:<span id="team-{{ team.id }}-score">{{ team.score }}</span></p>
<p>服务状态:
{% for service in services %}
<span class="service-status {{ service.status_class }}">{{ service.name }}</span>
{% endfor %}
</p>
</div>
其中status_class由views.py动态计算:
# 在dashboard视图中
services = []
for service in Service.objects.all():
# 检查该服务最近5分钟是否有有效Flag
recent_flags = FlagRecord.objects.filter(
team=team,
service=service,
is_valid=True,
submitted_at__gte=timezone.now() - timedelta(minutes=5)
).count()
status_class = "healthy" if recent_flags > 0 else "warning"
services.append({'name': service.name, 'status_class': status_class})
Flag提交历史表格(templates/flag_history.html)
支持按队伍、服务、时间范围筛选,且每行显示Flag哈希前6位(保护隐私):
<tr>
<td>{{ record.team.name }}</td>
<td>{{ record.service.name }}</td>
<td><code>flag{{ record.flag|slice:"5:" }}</code></td>
<td>{{ record.submitted_at|date:"H:i:s" }}</td>
<td>
{% if record.is_valid %}
<span class="badge bg-success">Valid</span>
{% else %}
<span class="badge bg-danger">Invalid</span>
{% endif %}
</td>
</tr>
实操心得:
slice:"5:"过滤器比JavaScript前端截取更可靠——避免学生误改前端代码导致Flag泄露。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
manage.py runserver 启动后浏览器打不开 http://127.0.0.1:8000 | Django未监听所有IP | 1. 检查启动命令是否为 python manage.py runserver 0.0.0.0:80002. 查看终端输出是否有 Starting development server at http://0.0.0.0:8000/ | 添加 0.0.0.0:8000 参数 |
靶机执行 sbflag.py 报错 ConnectionRefusedError | 裁判机防火墙拦截或IP配置错误 | 1. 在靶机ping裁判机IP 2. 在裁判机执行 telnet 0.0.0.0 8000 测试端口 | 关闭Windows防火墙;确认config.py中JUDGE_URL为裁判机局域网IP |
/admin/ 页面报错 TemplateDoesNotExist | TEMPLATES配置路径错误 | 1. 检查settings.py中TEMPLATES[0]['DIRS']2. 确认 templates/目录在项目根目录 | 改为 os.path.join(BASE_DIR, 'templates') |
| Flag提交后裁判界面不更新分数 | WebSocket未启用或Channels配置错误 | 1. 查看浏览器开发者工具Network标签,确认WebSocket连接建立 2. 检查 settings.py中CHANNEL_LAYERS配置 | 使用内存层:'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'} |
check.py 总是返回 Invalid | Flag格式正则不匹配生成逻辑 | 1. 在send_flag.py中打印生成的Flag2. 在 check.py中打印待验证Flag | 确保两者都使用hashlib.md5().hexdigest()[:16].upper() |
5.2 独家避坑技巧分享
技巧1:靶机脚本调试的“黄金三步法”
学生常卡在sbflag.py提交失败。我教他们固定流程:
1. 本地curl测试:在靶机终端执行
curl -X POST http://192.168.1.100:8000/api/submit/ -H "Content-Type: application/json" -d '{"team_id":1,"flag":"flag{TEST1234567890AB}"}'
若返回{"status":"success"},证明网络和API正常;否则检查裁判机日志。
2. 检查Python路径:which python确认使用虚拟环境Python,避免系统Python缺少requests库。
3. 启用requests日志:在sbflag.py开头添加
python import logging logging.basicConfig(level=logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True
看到完整HTTP请求头和响应体,错误一目了然。
技巧2:SQLite数据库损坏后的紧急恢复
教学演示中曾因强制关机导致db.sqlite3损坏。标准恢复流程:
1. 备份损坏文件:cp db.sqlite3 db.sqlite3.bak
2. 使用SQLite命令行工具诊断:
sqlite3 db.sqlite3 ".dump"
若输出Error: near line 1: file is encrypted or is not a database,则损坏。
3. 从备份恢复:cp db.sqlite3.bak db.sqlite3
4. 预防措施:在settings.py中添加
python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'timeout': 20, # 增加锁等待时间 } } }
技巧3:前端样式错乱的快速定位
templates/中CSS失效时,不要盲目改Bootstrap CDN:
1. 查看浏览器开发者工具Elements面板,确认<link>标签href是否404;
2. 检查settings.py中STATIC_URL和STATICFILES_DIRS配置;
3. 关键动作:执行python manage.py collectstatic --noinput,强制收集静态文件到static/目录;
4. 确认urls.py中开发环境下启用了静态文件服务:
```python
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path(‘admin/’, admin.site.urls),
path(‘’, include(‘app.urls’)),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
```
5.3 教学扩展建议:从“能跑”到“懂原理”
这套系统最大的价值不是开箱即用,而是作为教学脚手架。我给学生的三个渐进式扩展任务:
任务1:增加“服务宕机告警”
- 修改check.py:当nmap -p $PORT localhost | grep "open"失败时,调用requests.post(JUDGE_URL+"/api/alert/", json={...})
- 在裁判机新增Alert模型,前端增加告警面板
- 教学点:安全监控的主动探测思想
任务2:实现“Flag时效性”
- 在FlagRecord模型中添加expires_at = models.DateTimeField()
- 修改submit_flag视图:提交时设置expires_at = timezone.now() + timedelta(minutes=5)
- 新增后台任务:每分钟扫描过期Flag并标记is_valid=False
- 教学点:AWD中Flag的生命周期管理
任务3:接入真实靶机环境
- 将send_flag.py改造成Docker容器,通过docker run --network host共享宿主机网络
- 编写Dockerfile,预装nmap、curl等工具
- 教学点:容器化靶机的工程实践
我在实际教学中发现,学生完成任务2后,对AWD规则的理解深度远超单纯讲授两小时——因为他们亲手实现了“Flag五分钟内有效”这一核心规则,而不是背诵定义。
6. 个人实操体会与后续演进建议
这套系统从最初的比赛应急工具,到如今成为我们学院网络安全实验室的标准教学平台,中间经历了三次重大迭代。第一次迭代是补全文档——当时只有代码,学生问“怎么启动”我要口头解释十分钟;第二次迭代是增加靶机健康检查,解决了比赛中因靶机离线导致的计分争议;第三次迭代是引入SQLite的健壮性设计,让本科生也能在笔记本上完整复现整套流程。
最深刻的体会是:教学系统的价值不在于技术多先进,而在于“可触摸性”。当学生能用DB Browser打开db.sqlite3,亲眼看到自己提交的Flag出现在flagrecord表里;当他们修改check.py的正则表达式,立刻在裁判界面上看到验证结果变化;当他们在admin/后台手动将某队分数设为负数,观察实时排名变动——这些瞬间建立的“因果直觉”,远比任何PPT上的架构图都深刻。
后续如果继续演进,我会优先做两件事:一是将send_flag.py的生成算法升级为HMAC-SHA256,密钥由裁判机动态下发,解决静态种子被预测的风险;二是增加“回放模式”,允许导入比赛日志文件,一键重放整场对抗过程,这对赛后复盘教学至关重要。不过这些优化的前提,是确保现有系统在Windows 10/11、macOS Sonoma、Ubuntu 22.04上都能“零配置运行”——毕竟,教学的第一要义,是让知识触手可及,而不是制造技术门槛。
这套系统没有炫技的分布式架构,没有高并发的微服务设计,它只是用最朴实的Django、最可靠的SQLite、最直白的Python脚本,把AWD裁判的底层逻辑掰开揉碎,摊在学生面前。当你看到大二学生第一次成功提交Flag后,在Slack群里发“我刚hack了裁判系统!”,而你知道他只是改了一行正则——那一刻,你就明白,真正的教育发生了。
简介:开箱即用的AWD网络攻防比赛裁判系统,基于Python3和Django开发,包含裁判端和靶机端两大部分。裁判端提供队伍管理、实时计分、Flag校验及可视化前端页面;靶机端通过send_flag.py生成Flag、sbflag.py提交Flag、check.py验证Flag有效性,并通过HTTP接口与裁判服务器通信。项目内置SQLite数据库(db.sqlite3),已预置模板文件(templates)、核心应用逻辑(app子目录)、启动入口(manage.py)及详细操作文档(README.md)。依赖清晰(requirements.txt),支持本地快速部署,无需额外配置即可运行调试。配套文档覆盖环境搭建、服务启停、Flag收发流程、常见报错排查等实操环节,适合高校网络安全课程设计、毕业设计或CTF教学平台二次开发使用。
298



被折叠的 条评论
为什么被折叠?



