CTFd平台针对校赛的二次开发

增加学号属性

数据库配置

sqlite 数据库,也就是 ctf.db 的文件里,添加一个列名为 sid 的字段:

我是用图形管理工具添加的,命令的话参考:

1
alter table users add column sid varchar(20) default 0;
  1. 字段需要设置默认值,不然 flask 会报错
  2. 字段位置不能在中间的某个位置,只能放置在最后面(默认是最后)

html 文件配置

/CTFd/CTFd/themes/core/templates/register.html 中添加 html 表单数据,这里主要是注意 name 的值,因为是 html 的 form 表单使用这个字段名来取值

8tnn2V.png

auth.py 文件配置

/CTFd/CTFd/auth.py 中的 register 函数中添加

1
sid = Users.query.add_columns('sid','id').filter_by(sid=request.form['sid']).first()

8tnMKU.png

sid = request.form.get("sid", "").strip() 取学号,然后在 validators 里写一个验证学号格式的函数:

1
2
3
4
5
6
7
def validate_sid(sid):
if sid == "wctf2020": # 针对校外
return True
elif len(sid) == 12 and sid.isdigit() and sid.startswith("201"):
return True
else:
return False

函数位置:/CTFd/CTFd/utils/validators/__init__.py

接着还是在 auth.py ,创建用户的时候写上 sid

8tneCq.png

models 类中的配置

/CTFd/CTFd/models/__init__.py 中的 Users 类中添加一个表单名:

1
sid = db.Column(db.String(20))

8tnuvT.png

admin面板中users的学号显示

文件地址 /CTFd/CTFd/themes/admin/templates/users/users.html

这里把国家的一列用来显示学号

8tnQrF.png

在过滤的下拉框增加学号的搜索选项:

8tnm80.png

然后在表中输出学号:

8tnlb4.png

效果

8tn3VJ.png

Scoreboard

校内外分类

基于学号来进行分类,首先在 /CTFd/CTFd/themes/core/templates/scoreboard.html 增加下拉框:

1
2
3
4
5
6
<select class="form-control custom-select w-10" onchange="top.location.href=this.value">
<option value="/">排名方式</option>
<option value="/scoreboard">总排名</option>
<option value="/scoreboard/1">校内排名</option>
<option value="/scoreboard/2">校外排名</option>
</select>

更改 /CTFd/utils/scores/__init__.py 里的 get_standings 查询方式:

增加参数

8tn8a9.png

分情况查询

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
else:
if request == "total":
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
sumscores.columns.score,
)
.join(sumscores, Model.id == sumscores.columns.account_id)
.filter(Model.banned == False, Model.hidden == False)
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
elif request == "wust":
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
sumscores.columns.score,
)
.join(sumscores, Model.id == sumscores.columns.account_id)
.filter(Model.banned == False, Model.hidden == False, Model.sid != "wctf2020")
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
elif request == "others":
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
sumscores.columns.score,
)
.join(sumscores, Model.id == sumscores.columns.account_id)
.filter(Model.banned == False, Model.hidden == False, Model.sid == "wctf2020")
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)

默认是总榜(0),1为校内,2为校外。

然后在 /CTFd/scoreboard.py 添加路由:

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
@scoreboard.route("/scoreboard")
@check_score_visibility
@cache.cached(timeout=60, key_prefix=make_cache_key)
def listing():
standings = get_standings(None, False, request=0)
return render_template(
"scoreboard.html",
standings=standings,
score_frozen=config.is_scoreboard_frozen(),
)

@scoreboard.route("/scoreboard/1")
@check_score_visibility
@cache.cached(timeout=60, key_prefix=make_cache_key)
def listing1():
standings = get_standings(None, False, request=1)
return render_template(
"scoreboard.html",
standings=standings,
score_frozen=config.is_scoreboard_frozen(),
)

@scoreboard.route("/scoreboard/2")
@check_score_visibility
@cache.cached(timeout=60, key_prefix=make_cache_key)
def listing2():
standings = get_standings(None, False, request=2)
return render_template(
"scoreboard.html",
standings=standings,
score_frozen=config.is_scoreboard_frozen(),
)

计分板图表

/CTFd/CTFd/api/v1/scoreboard.py,对接口请求时的 url 进行分类,先添加 request

1
from flask import request

然后对接口 @scoreboard_namespace.route("/top/<count>") 里进行修改,添加

1
2
3
4
5
6
if "/scoreboard/1" in request.headers['Referer']:
board_type = 1
elif "/scoreboard/2" in request.headers['Referer']:
board_type = 2
else:
board_type = 0

然后将 get_standings 的参数改成 standings = get_standings(count=count, request=board_type)

8tnG5R.png

前三血自动播报

/CTFd/CTFd/api/v1/challenges.py

8tnYP1.png

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
# auto-broadcast
if app.config['AUTO_BROADCAST']:
solve_count = Solves.query.filter_by(challenge_id=challenge_id).count()
blood_number = ""
if solve_count == 1:
blood_number = "first"
elif solve_count == 2:
blood_number = "second"
elif solve_count == 3:
blood_number = "third"
else:
pass
if blood_number:
broad = {
'title':"Congratulations!",
'content':"User [{0}] got the {1} blood of [{2}]".format(user_name, blood_number, challenge_name)
}
schema = NotificationSchema()
broad_result = schema.load(broad)

db.session.add(broad_result.data)
db.session.commit()

# Grab additional settings
broad["type"] = "toast"
broad["sound"] = False
broad["blood"] = blood_number

app.events_manager.publish(data=broad, type="notification")

return {
"success": True,
"data": {"status": "correct", "message": message},
}

然后在 /CTFd/config.py 里增加 AUTO_BROADCAST = True

提示音修改

将准备的三杀的音效放进 /CTFd/CTFd/themes/core/static/sounds,然后在 /CTFd/CTFd/themes/core/static/js/events.js 修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const howl = new Howl({
src: [
root + "/themes/core/static/sounds/notification.webm",
root + "/themes/core/static/sounds/notification.mp3"
]
});
const howl_f = new Howl({
src: [
root + "/themes/core/static/sounds/first.webm",
root + "/themes/core/static/sounds/first.mp3"
]
});
const howl_s = new Howl({
src: [
root + "/themes/core/static/sounds/second.webm",
root + "/themes/core/static/sounds/second.mp3"
]
});
const howl_t = new Howl({
src: [
root + "/themes/core/static/sounds/third.webm",
root + "/themes/core/static/sounds/third.mp3"
]
});

然后添加:

1
2
3
4
5
6
7
8
9
switch (data.blood) {
case "first":
howl_f.play(); break;
case "second":
howl_s.play(); break;
case "third":
howl_t.play(); break;
default: break;
}

8tnt8x.png

然后全局搜索,将带有 notification.webm 的所有 min.js 全部按照这个格式修改(大概20+个文件),还要注意混淆:root -> edata.blood -> e.blood

题目solves显示

将solves的用户筛选只显示前三,并且增加小图标:

8tnN26.png

增加一个列,在 /themes/core/static/js/pages/challenges.min.js

8tndKO.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (var n = 0; n < 3; n++) {
var s = t[n].account_id,
a = t[n].name,
i = (0, d.default)(t[n].date).local().fromNow(),
l = t[n].account_url;
if(n == 0){
rk = '<img src="themes/core/static/img/1.png" width="33" height="33">';
}else if(n == 1){
rk = '<img src="themes/core/static/img/2.png" width="27" height="27">';
}else{
rk = '<img src="themes/core/static/img/3.png" width="15" height="15">';
}
o.append('<tr><td>{4}</td><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(l, s, (0, r.htmlEntities)(a), i, rk))
}

CTFd平台针对校赛的二次开发
https://52hertz.tech/2020/03/15/CTFd_second_develop/
作者
Ustin1an
发布于
2020年3月15日
许可协议