SUCTF五校联合招新赛已经过去几天了。现在从主办方的角度来写写我第一次办比赛整个过程吧…

首先本次比赛是江苏高校信息安全联盟SU内东南大学、南京航空航天大学、南京理工大学、三江学院、金陵科技学院五校联合举办的队内招新赛,原本计划是出题给萌新做的,结果到后面是题目越来越难2333…感觉内部出题也形成了一定的竞争,然后结果就不提了,变成了大佬屠榜赛。

数据

本次比赛由于最后放的调查问卷,而且收集到的调查问卷远远达到签到题得分人数,姑且用来做个大概的分析吧。

首先是本次参赛人数,注册人数达到了463个队,最后调查问卷填写人数为81个队。

izikeH.md.png
izikeH.md.png

五个学校的学校数据就不分析了,其中其他学校占到了242个队,基本变成了半公开赛…但是由于还是想保护萌新,就没有变成完全的公开赛。否则我担心会被日穿…

第一天开赛的情况,由于docker环境没配置好,导致有一题pwn直接被打穿了,还被改了flag,大师傅们真的tql…

年级所占比例:

比赛难度调查统计:

时间安排调查统计:

比赛题目调查统计:


比赛评分调查统计(满分5分):

以上数据均由比赛调查问卷数据统计得出。

整个比赛历时7天,从2018/11/7 10:00:00 CST – 2018/11/14 10:00:00 CST,比赛期间无暂停,总共放出题目40道题,其中pwn类7道,web类14道,rev类8道,misc类11道。并没有无人做出的题目。

Preparation

起初要搞SUCTF联合招新其实在开学初貌似就在SU内部群就提出来了,因为之前2016年也有过SUCTF类似的联合招新赛,当时徐院长还是作为美女客服的时候,当时我还对着徐院长的头像一脸懵逼的时候。

结果拖到了10月底这样然后才真正搞起来,拉了小群然后开始计划出题,我也就才去拉赞助。于是乎,长亭、赛宁双方这边我谈的都不是很顺利,主要也是由于第一次拉赞助,没有写proposal,进度就一直拖慢了挺多的,而且跟白师傅那边要机器也没沟通顺利,平台自己也并没有很及时地搭建起来。然后在这一切的原因之下,导致了比赛只能往后推了两天,由原计划的11/5推到了11/7,由于自己也是第一次改CTFd,改的也比较慢,于是乎我记得直到11/7日凌晨我依然还在改平台。

然后11/5晚上这样整合题目的时候,由于各个师傅用出题模版理解不太一样,也怪自己没有让各位师傅开个小会啥的强调一下出题模板,导致整合题目的时候还有一堆格式不对的题目模板,也统统全部都让出题人规范了,也耗费了白师傅不少精力去改这些题目。

这里主要记录下我这边改CTFd的经历吧,我看网络上也并没有详细的更改CTFd的流程。

CTFd

首先明确一下自己的需求:

  • 在注册界面提供学校的下拉框选择
  • 在得分榜显示学校
  • 更改主页添加赞助商信息

虽然只是这简简单单的几个需求,也把我这个不太熟悉flask的人折腾了好一阵子。
我的操作环境在Ubuntu 16.04下完成的。
首先参照CTFd官方仓库文档进行安装,这几步也比较简单。

接着我们可以直接现在CTFd/themes/core/templates/register.html中加入对应的片段,对应的option对应的value可以是中文。

1
2
3
<select class="form-control input-filled-valid" name="school" id="school-input">
<option value="xx大学">xx大学</option>
</select>

其次我们需要在models.py中找到对应的Class Teams,这里初始化了队伍的一些信息,所以我们这里再加上一个school成员就好

1
school = db.Column(db.String(128))

之后初始化的时候,我一直对于C的多重构造一直耿耿于怀,想着这里初始化Teams的时候也根据参数的个数不同使用不同的构造方法,这样可以不用修改原来是用的构造方法,查阅资料发现可以这么写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time

class Date:
"""方法一:使用类方法"""
# Primary constructor
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day

# Alternate constructor
@classmethod
def today(cls):
t = time.localtime()
return cls(t.tm_year, t.tm_mon, t.tm_mday)

虽然可以这么写,但是我在弄排行榜的时候,第一次访问有新队伍的排行榜总会报错。

1
'unicode' object has no attribute 'label'

注册队伍老是不能把学校写进sqlite,这就很烦了。而且第一次注册的时候老是报错,结果最后询问了东大的师傅,他们的写法是在models.pyclass Teams加入school成员后,在auth.py下的register方法中,这么写

1
2
3
4
5
6
7
8
9
10
11
school = request.form['school']     #获取school参数
...
if len(errors) > 0:
return render_template('register.html',errors=errors,name=request.form['name'],email=request.form['email'],password=request.form['password'])
else:
with app.app_context():
team = Teams(name, email.lower(), password)
team.school = shcool #直接让school参数赋值给school成员变量
db.session.add(team)
db.session.commit()
db.session.flush()

使用原来的构造方法,这个问题就解决了…2333
解决了注册的问题,排行榜的显示也就比较简单了,首先改CTFd/themes/core/templates/scoreboard.html中的模版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="scoreboard" class="row">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<td scope="col" width="10px"><b>Place</b></td>
<td scope="col"><b>Team</b></td>
<td scope="col"><b>School</b></td> //加入School表头
<td scope="col"><b>Score</b></td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<th scope="row" class="text-center">{{ loop.index }}</th>
<td><a href="{{ request.script_root }}/team/{{ team.teamid }}">{{ team.name | truncate(50) }}</a></td>
<td>{{ team.school }}</td> //加入team.school内容
<td>{{ team.score }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

接下来把就去scoreboard.py获取team.school,通过大概的猜测以及推断,我们可以带改知道从这里修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if admin:
standings_query = db.session.query(
Teams.id.label('teamid'),
Teams.name.label('name'),
Teams.school.label('school'),
Teams.banned, sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Teams.id.label('teamid'),
Teams.name.label('name'),
Teams.school.label('school'),
sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)

这样scoreboard基本就改完了。接着我们再去管理员的Teams界面加入学校名方便查队伍的时候知道是哪个学校的。同样,先改CTFd/themes/admin/templates/teams.html

1
2
3
4
5
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td>
<td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
<td class="team-id d-md-table-cell d-lg-table-cell" value="{{ team.school }}">{{ team.school }}</td>

这样基本就完成了,接下来我们修改首页,直接配置好之后去Admin面板通过编辑Pages来操作会更加方便。(一开始我直接在views.py中修改index,这个是要被写进数据库的pages中的,我一边改一边update数据库,现在想起来简直蠢爆。

到这里我们自定义CTFd基本就完成了。(有些功能仍然还会报错,例如导出比赛功能,因为Teams多了一个字段school,所以很大一部分其他报错,虽然目前我只遇到这个功能会报错,基本都是因为自定义增加了这个字段导致的,去看看源码把相应缺少的加上基本就能解决了。

Enviroment

本次采用的配置是使用nginxmysqluwsgi来配置ctfd,配置环境在ubuntu serve 18.04下,安装过程就不提了。网上教程很多。

这里说一下nginx的配置:

1
2
3
4
5
6
7
8
server  {
listen 80;
server_name Your domain;
location / {
include uwsgi_params;
uwsgi_pass unix:/tmp/uwsgi.sock;
}
}

然后需要把CTFd连接数据库的方法改成mysql,我们可以看到CTFd/config.py

1
2
DATABASE_URL = os.environ.get(
'DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))

所以我们需要在环境中定义DATABASE_URL,参考它给出的格式

1
e.g. mysql+pymysql://root:<YOUR_PASSWORD_HERE>@localhost/ctfd

但是我们最好不要用root使用mysql,而且如果我当时用root用户的话,直接报错了

1
ERROR 1698 (28000): Access denied for user 'root'@'localhost'

参考MySQL ERROR 1698 (28000) 错误,是因为我的root用户的plugin没有配置好,而且用root用户连接mysql不太安全,所以我们这里创建了一个ctfd的数据库用户,并给予它ctfd数据库的所有权力。

1
2
3
4
5
6
7
CREATE USER 'ctfd'@'localhost' IDENTIFIED BY 'Your password';     //创建用户

select user, plugin from mysql.user; //查看当前所有数据库用户的plugin

update mysql.user set plugin='mysql_native_password' where user='ctfd'; //更改ctfd用户的plugin,可以使用密码登录ctfd用户

GRANT ALL ON ctfd.* TO 'ctfd'@'localhost'; //给予ctfd用户ctfd数据库的权力

虽然我们可以直接在bash

1
export DATABASE_URL=mysql+pymysql://ctfd:Your [email protected]/ctfd

但是这样不太优雅,我们可以新建一个uwsgi.ini中写入

1
2
3
4
5
6
7
[uwsgi]

# Where you've put CTFD

chdir = /your/dir/CTFd

env = "DATABASE_URL=mysql+pymysql://ctfd:Your [email protected]/ctfd"

这样会比较好。这样,连接数据库的操作就基本完成了。

然后为了方便运维,可以使用Supervisoruwsgi进行管理操作。贴一下当时的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[unix_http_server]
file = /var/run/supervisor.sock
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; (default is no username (open server))
;password=123 ; (default is no password (open server))

[inet_http_server] ; inet (TCP) server disabled by default
port=9001 ; (ip_address:port specifier, *:port for ;all iface)
username=admin ; (default is no username (open server))
password=suctf_new_2018 ; (default is no password (open server))

[supervisord]
;logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
;修改为 /var/log 目录,避免被系统删除
logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log)
; 日志文件多大时进行分割
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
; 最多保留多少份日志文件
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)

pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
;设置启动supervisord的用户,一般情况下不要轻易用root用户来启动,除非你真的确定要这么做
;user=chrism ; (default is current user, required if root)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP)

[supervisorctl]
; 必须和'unix_http_server'里面的设定匹配
;serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;修改为 /home/supervisor 目录,避免被系统删除
serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
username=admin ; should be same as http_username if set
password=suctf_new_2018 ; should be same as http_password if set

[program:uwsgi]
command=uwsgi -s /tmp/uwsgi.sock --chmod-socket=666 -w 'CTFd:create_app()'
directory=/xxx/CTFd
autostart = true
startsecs = 5
redirect_stderr = true
;这对这个program的log的配置,上面的logfile_maxbytes是supervisord本身的log配置
stdout_logfile_maxbytes = 20MB
stdoiut_logfile_backups = 20
stdout_logfile = /var/log/supervisord/uwsgi.log

具体可以参考:
Python 进程管理工具 Supervisor 使用教程
用Supervisord管理Python进程
Python 进程管理工具 Supervisor 使用教程
【已解决】supervisor去启动gunicorn的Flask出错:supervisor couldn’t setuid to 0 Can’t drop privilege as nonroot user

最后白师傅通过做成了系统服务来管理的uwsgi,这里留到NUAACTF再来更吧。

PS:今天中午12:28刚刚发现CTFd更新到了2.0,可能稍有不同。后续做完NUAACTF我会再更一下。