文章目录
  1. 起因
  2. 架构
  3. 构思
  4. 比赛规则
    1. Web界面
  5. 分数统计服务
  6. 安全部署
  7. 后记

Syclover线下赛平台开发小记

起因

今年SCTF的决赛举办这段时间正碰上回家于是没有参与,回学校的时候比赛马上就要开始了。临近考试周,师傅们准备考试还要出题布置环境,非常辛苦。但万万没想到比赛当天因为比赛平台的原因大家干瞪眼了一上午!到了中午比赛才打起来。于是就有了写一个自己的线下赛平台的想法。一直到了假期都没人搞,我想最近实习工作比较轻松,趁着自主学习的时候写个线下赛平台吧,于是和刘师傅一拍即合,前前后后写了接近一个月平台终于基本完工了,记录一下一些东西。

架构

平台使用Django编写,考虑到表比较少,连接关系并不复杂,就用了自带的Sqlite作为持久层。

构思

由于是两个人写平台,就想着两个人分开写不同的部分,两个部分通过接口通信,各自有各自的数据库和表,最后拼起来就完成了。最初的想法是将攻防平台分为两个部分:Web界面(包括登录、管理员后台、选手比赛界面)和分数统计服务(Flag分发、选手分数的修改)。

最初的构想

比赛规则

比赛规则的两个特征是:

  1. 比赛的总分不变(除非管理员加分),分数在各个队伍之间流动
  2. 获得Flag的方式是比赛机器curl flag机

因为采用0和博弈,分数也是动态的,所以并不会提交一个flag分数就立刻增加(但是比赛界面会看到成功攻击的信息),而是发送到分数统计服务。分数统计服务不仅接受选手提交的Flag,还会接受Check机的结果、管理员后台给选手加分扣分数据,将他们统一计算,新的一轮开始时更新分数。

由于获得Flag的方法是curl flag机,因此我们做出的前提是一个比赛可以有多个web题,但是只有一个flag。在比赛规则方面想要改变这个情况需要让不同的web题目有不同的机器,但是设计之初并没有如此考虑,因此这是一个设计的缺陷。

Web界面

Django自带一个管理员工具,可以对数据库做增删改查,极大地缩短了管理员后台编写的时间。在保留默认管理员界面的基础上,增加了向刘师傅的API接口提供队伍信息和管理员直接扣除选手分数的功能。这里要解释一下这两个功能,因为比赛分数的核心部分是分数统计服务,因此与队伍相关的大多数数据保存在刘师傅的表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 刘师傅的队伍表
class TeamScoreProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
numberOfTeam = models.IntegerField()
numberOfRounds = models.IntegerField(default=80)
totalScorePerRound = models.FloatField()
teamName = models.CharField(max_length=128, unique=True)
teamToken = models.CharField(max_length=128)
totalScore = models.FloatField(default=10000.0)
compareWithLastRound = models.FloatField(default=0.0)
webGet = models.FloatField(default=0)
pwnGet = models.FloatField(default=0)
checkGet = models.FloatField(default=0)
webLoss = models.FloatField(default=0)
pwnLoss = models.FloatField(default=0)
checkLoss = models.FloatField(default=0)
webip = models.TextField(default="127.0.0.1")
pwnip = models.TextField(default="127.0.0.1")
sumOfcheck = models.IntegerField(default=0)
checkip = models.TextField(default="0.0.0.0") #被check题目的ip
tempScore = models.FloatField(default=0.0)

但是队伍的名称和密码都保存在Web界面所管辖的数据库中,因此需要将我创建的队伍信息发送给刘师傅,他扩展与队伍相关的信息。

1
2
3
4
5
6
7
8
9
10
11
# 我的队伍表
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
question = models.ManyToManyField("Question")
class Meta:
verbose_name = "队伍" # 给模型起一个可读的名字
verbose_name_plural = "队伍"
def __str__(self):
return self.user.username

将队伍信息分成两部分写,通过API通讯,这样的好处是在沟通清楚API后只需要把API需要的数据处理好发送到指定API即可,不需要关心刘师傅如何处理这些数据,减少了很多没有必要的沟通时间。

分数统计服务

本以为这部分刘师傅会写的比较快,结果是比我慢了好几天才搞定,而且修改了好几次API。再写这种合作的平台时API一定要充分的考虑好,尽量不要修改。

安全部署

首先要考虑到基本的安全问题,首先排除一些复杂的漏洞,XXE,SSRF一类的,因为平台没有这样的功能,其他剩下XSS,SQL注入,CSRF。得益于Django自带对于这些漏洞的防御,按照规范编写代码并不需要考虑更多。另外根据DJango的安全部署,关闭Debug模式,开启多个和安全相关的头部。

接下来是平台安全,平台暂时没有内测,Django本身并不打算搭建在其他的Web容器中,而是独立运行。选手的登录密码会做的比较长,复杂性也会比较高。

最后,考虑到选手在内网中做嗅探的可能,有两种解决方法,比赛环境的路由映射或者比赛平台HTTPS。最后选择了HTTPS。内网的HTTPS是可以做自签名证书的,但需要选手添加信任,不太优雅,于是在Let’s encrypt申请了免费的SSL证书。Let’s encrype申请指向内网的ip需要一个域名,并且通过dns认证(修改TXT为一个指定值来确保你是域名所有者)。

后记

由于是尚未内测的初代版本,后期打算长期更新迭代,重构也是有可能的。现在心得并不多,如果后面发生一些比较糟心的事情,处理完再来补充:D

支持一下
扫一扫,支持forsigner