NUAACTF靶场搭建总结

这次NUAACTF面向所有高校参赛,因此使用了ctfd的whale插件来实现动态flag,然后又对ctfd进行一些魔改,实现校外校内榜分开排名的功能。

部署参考文章 ctfd使用ctfd-whale动态靶机插件搭建靶场指南 | VaalaCat

修改好的代码推到了 Asuri-Team/NUAA-CTfd (github.com)

0x00 引言

第六届南京航空航天大学网络攻防大赛

AsuriCTF / NUAACTF 2021

承办单位:由南京航空航天大学信息化处、南京航空航天大学教务处、共青团南京航空航天大学委员会指导,南京航空航天大学计算机科学与技术学院承办,Asuri信息安全战队,南京航空航天大学学生网络安全与信息技术协会协办,奇安信科技集团股份有限公司独家赞助。

活动对象:南京航空航天大学全体学生,校外对信息安全感兴趣的同学。

报名时间:2021年11月22日0:00-12月6日12:00

比赛时间:2021年12月11日13:00-18:00(最后实际是到 19:00)

作为这次NUAACTF的举办者,有一说一办比赛是真的烦。拉赞助,协调学校场地,做宣传等焦头烂额。而且最后因为疫情缘故不得不改成了线上,而且最后还有很多没做到位的地方。但想想去年这个时候,我们就是与miao师傅在校赛上相识,从此到深圳,郑州一起快乐比赛。自己也是从校赛入门,一起和朱师傅共同学习PWN方向。如果没有校赛,没有曹师傅,朱师傅,帆哥哥,我可能也不会接触到ctf,也没有动力继续走下去。希望校赛能越办越好,大家都能从校赛里获得快乐,提升技术,找到志同道合的伙伴一起进步。

0x01 针对校赛对ctfd进行的二次开发

ScoreBoard

校内外分类

基于注册时填写的Affiliation字段进行分类,这里只对填写值为NUAA的人员判定为校内人员,其他都是校外人员。注意此字段因为我魔改时候的bug,即使校外人员注册也不要填为空,不然会在查询时索引不到,最好设置一个默认初始值。

下面是具体更改:

首先在/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 查询方式:

增加参数

1
2
@cache.memoize(timeout=60)
def get_standings(count=None, admin=False, fields=None, request=0):

分情况查询

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
else:
if request == 0:
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
Model.affiliation.label("affiliation"),
sumscores.columns.score,
*fields,
)
.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 == 1:
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
Model.affiliation.label("affiliation"),
sumscores.columns.score,
*fields,
)
.join(sumscores, Model.id == sumscores.columns.account_id)
.filter(Model.banned == False, Model.hidden == False, Model.affiliation == "NUAA")
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
elif request == 2:
standings_query = (
db.session.query(
Model.id.label("account_id"),
Model.oauth_id.label("oauth_id"),
Model.name.label("name"),
Model.affiliation.label("affiliation"),
sumscores.columns.score,
*fields,
)
.join(sumscores, Model.id == sumscores.columns.account_id)
.filter(Model.banned == False, Model.hidden == False, Model.affiliation != "NUAA")
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)

默认总榜是0,校内是1,校外是2

然后在/CTFd/scoredboard.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@scoreboard.route("/scoreboard")
@check_score_visibility
def listing():
infos = get_infos()

if config.is_scoreboard_frozen():
infos.append("Scoreboard has been frozen")

if is_admin() is True and scores_visible() is False:
infos.append("Scores are not currently visible to users")
clear_standings()
standings = get_standings()
return render_template("scoreboard.html", standings=standings, infos=infos)

@scoreboard.route("/scoreboard/1")
@check_score_visibility
def listing1():
infos = get_infos()

if config.is_scoreboard_frozen():
infos.append("Scoreboard has been frozen")

if is_admin() is True and scores_visible() is False:
infos.append("Scores are not currently visible to users")
clear_standings()
standings = get_standings(None, False, request=1)
return render_template(
"scoreboard.html",
standings=standings,
infos=infos
)


@scoreboard.route("/scoreboard/2")
@check_score_visibility
def listing2():
infos = get_infos()
if config.is_scoreboard_frozen():
infos.append("Scoreboard has been frozen")
if is_admin() is True and scores_visible() is False:
infos.append("Scores are not currently visible to users")
clear_standings()
standings = get_standings(None, False, request=2)
return render_template(
"scoreboard.html",
standings=standings,
infos=infos
)

计分图版

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

1
from flask import request

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@scoreboard_namespace.route("/top/<count>")
@scoreboard_namespace.param("count", "How many top teams to return")
class ScoreboardDetail(Resource):
@check_account_visibility
@check_score_visibility
@cache.cached(timeout=60, key_prefix=make_cache_key)
def get(self, count):
clear_standings()
response = {}
if "/scoreboard/1" in request.headers['Referer']:
board_type = 1
elif "/scoreboard/2" in request.headers['Referer']:
board_type = 2
else:
board_type = 0

standings = get_standings(count=count, request=board_type)

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

一些还没来的及实现的功能

  • 前三血自动播报

  • 在解题面板区分校内和校外

    ##

0x02 赛后反思

题目难度

最后pwn题只有两道题目有解,感觉后面出题可以效仿中科大,不必拘泥于ctf形式,还是以简单有趣为主。让更多的人有参与感,能学到东西。题面上可以多给提示

服务器运维

  • 采用4c8g服务器,1000Mb带宽,刚开始带宽开小了导致平台非常卡。
  • 题目与平台都在一个服务器上,cpu负载很高,下次可以分布式部署
-------------本文结束感谢您的阅读-------------
+ +