最近尝试对CTFd
进行二次开发,在这里记录一下开发的过程
PS:本文基于CTFd 3.4.3 开发,不同版本可能存在版本兼容问题。
动态计分板插件
用途是在CTF
个人赛上,找寻现有的一些插件,在这个基础上改的。
Usage
在CTFd
的CTFd/plugins
目录下
git clone https://github.com/Ephemeral1y/ctfd-matrix-scoreboard
docker restart ctfd_ctfd_1
一些参数的修改
__init__.py
NumberOfChallenges = 40 # 题目最大数量
if(solve.date == top[0].date):
solvenum = solve.challenge_id
score = score + int(cvalue * 1.1) #一血加成10%
blood[solve.challenge_id].append(1)
elif(solve.date == top[1].date):
solvenum = solve.challenge_id
score = score + int(cvalue * 1.05) #二血加成5%
blood[solve.challenge_id].append(2)
elif(solve.date == top[2].date):
solvenum = solve.challenge_id
score = score + int(cvalue * 1.03) #三血加成3%
blood[solve.challenge_id].append(3)
else:
solvenum = solve.challenge_id
score = score + int(cvalue * 1)
blood[solve.challenge_id].append(0)
目前只支持reverse
,pwn
,web
,misc
,crypto
五个方向的前端显示,有需要的可以自行修改源码
增加用户属性
用途是在注册的时候给用户增加学号、姓名的属性,只有自己和后台能看到,方便用户的管理
数据库配置
这个地方网上涉及的资料不多,着实费了一番功夫
CTFd
使用的是flask
框架,框架使用SQLAlchemy
来访问数据库。
数据库建表的模型是在CTFd/models/__init__.py
中
比如我需要给表users
增加sname
,sid
两个属性,分别代表真实姓名和学号
class Users(db.Model):
__tablename__ = "users"
__table_args__ = (db.UniqueConstraint("id", "oauth_id"), {})
# Core attributes
id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True)
# User names are not constrained to be unique to allow for official/unofficial teams.
name = db.Column(db.String(128))
password = db.Column(db.String(128))
email = db.Column(db.String(128), unique=True)
sname = db.Column(db.String(20)) # 增加姓名
sid = db.Column(db.String(20)) # 增加学号
type = db.Column(db.String(80))
secret = db.Column(db.String(128))
然后在CTFd
的根目录下,找到manager.py
,这是数据库的管理文件
初始化,这个操作会生成一个migrations文件夹,如果已有则无需执行
python manage.py db init
生成建表文件,这个操作会检查CTFd/models/__init__.py
的修改,生成新的建表文件,-m
表示注释,会生成文件名含这个的文件。
SQLAlchemy
对数据库的操作实际上是先删除整个数据库,然后执行建表文件重新生成数据库
PS:后续开发过程中发现CTFd-whale
这个插件安装之后对这个操作会有影响,我的解决方案是暂时下了这个插件,配置好建表语句之后再重新上这个插件。
python manage.py db migrate -m "add_sname_sid"
执行建表文件,在执行这个文件之后数据库即会自动更新。
python manage.py db upgrade
除此之外,以下操作可以查看数据库更新历史
python manage.py db history
以上操作为开发环境下操作,如果是用docker-compose
生成的CTFd
,直接在命令行执行会无法检测到数据库变化
所以如果是用docker-compose
的情况,需要先在docker-compose.yml
中,修改CTFd
容器内部文件系统可写
.:/opt/CTFd:ro
修改为.:/opt/CTFd
然后进入CTFd
的docker
,在内部执行上述操作,生成建表文件即可,最后不要忘记重新修改文件系统为只读。
修改注册界面
在用户注册的时候,在注册选项里增加两个表单,用来填写学号和姓名
CTFd/themes/core/templates/register.html
<div class="form-group">
<b>{{ form.name.label }}</b>
{{ form.name(class="form-control", value=name) }}
<small class="form-text text-muted">
Your username on the site
</small>
</div>
<div class="form-group">
<b>{{ form.sname.label }}</b>
{{ form.sname(class="form-control", value=sname) }}
<small class="form-text text-muted">
Never shown to the public
</small>
</div>
<div class="form-group">
<b>{{ form.sid.label }}</b>
{{ form.sid(class="form-control", value=sid) }}
<small class="form-text text-muted">
Never shown to the public
</small>
</div>
<div class="form-group">
<b>{{ form.email.label }}</b>
{{ form.email(class="form-control", value=email) }}
<small class="form-text text-muted">
Never shown to the public
</small>
</div>
这里只是修改前端显示,还需要后端接收参数进行注册
CTFd/auth.py
接收前端传输数据
if request.method == "POST":
name = request.form.get("name", "").strip()
email_address = request.form.get("email", "").strip().lower()
password = request.form.get("password", "").strip()
sname = request.form.get("sname", "").strip()
sid = request.form.get("sid", "").strip()
将接受的数据插入到列
names = Users.query.add_columns("name","id").filter_by(name=name).first()
emails = (Users.query.add_columns("email", "id").filter_by(email=email_address).first())
sname = Users.query.add_columns("sname","id").filter_by(sname=sname).first()
sid = Users.query.add_columns("sid", "id").filter_by(sid=sid).first()
这里我的理解是将数据插入到一个临时的列中,最终会到底层形成一个sql
插入语句进行插入记录
对于学号,我还加入了一个检验机制,因为学校的学号只有9位和11位的,所以我对输入在后端进行了检测
vaild_number = validators.validate_sid(request.form.get("sid", "").strip())
if not vaild_number:
errors.append("Please enter a vaild student number!")
CTFd/utils/validators
def validate_sid(sid):
if (len(str(sid)) == 11 or len(str(sid)) == 9) and bool(
re.match("\d+", sid)):
return True
else:
return False
创建用户
with app.app_context():
user = Users(name=name,
email=email_address,
password=password,
sname=request.form.get("sname", "").strip(),
sid=request.form.get("sid", "").strip())
增加字段
CTFd/forms/auth.py
def RegistrationForm(*args, **kwargs):
class _RegistrationForm(BaseForm):
name = StringField("User Name", validators=[InputRequired()])
email = EmailField("Email", validators=[InputRequired()])
sname = StringField("Real Name", validators=[InputRequired()])
sid = StringField("Student number", validators=[InputRequired()])
password = PasswordField("Password", validators=[InputRequired()])
submit = SubmitField("Submit")
增加后台修改
用户应当被允许在后台修改自己的信息
增加后台显示表单
CTFd/themes/core/templates/settings.html
<div class="form-group">
<b>{{ form.name.label }}</b>
{{ form.name(class="form-control", value=name) }}
</div>
<div class="form-group">
<b>{{ form.email.label }}</b>
{{ form.email(class="form-control", value=email) }}
</div>
<div class="form-group">
<b>{{ form.sname.label }}</b>
{{ form.sname(class="form-control", value=sname) }}
</div><div class="form-group">
<b>{{ form.sid.label }}</b>
{{ form.sid(class="form-control", value=sid) }}
</div>
<hr>
后台提交数据与数据库绑定
CTFd/views.py
def settings():
infos = get_infos()
errors = get_errors()
user = get_current_user()
name = user.name
email = user.email
sname = user.sname
sid = user.sid
website = user.website
affiliation = user.affiliation
country = user.country
数据渲染到设置界面
return render_template(
"settings.html",
name=name,
email=email,
sname=sname,
sid=sid,
website=website,
affiliation=affiliation,
country=country,
tokens=tokens,
prevent_name_change=prevent_name_change,
infos=infos,
errors=errors,
)
自己的字段
CTFd/forms/self.py
def SettingsForm(*args, **kwargs):
class _SettingsForm(BaseForm):
name = StringField("User Name")
email = StringField("Email")
sname = StringField("Real Name")
sid = StringField("Student Number")
password = PasswordField("Password")
confirm = PasswordField("Current Password")
affiliation = StringField("Affiliation")
website = URLField("Website")
country = SelectField("Country", choices=SELECT_COUNTRIES_LIST)
submit = SubmitField("Submit")
用户的字段
CTFd/forms/users.py
class UserBaseForm(BaseForm):
name = StringField("User Name", validators=[InputRequired()])
email = EmailField("Email", validators=[InputRequired()])
sname = StringField("Real Name", validators=[InputRequired()])
sid = StringField("Student Number", validators=[InputRequired()])
password = PasswordField("Password")
website = StringField("Website")
affiliation = StringField("Affiliation")
country = SelectField("Country", choices=SELECT_COUNTRIES_LIST)
type = SelectField("Type", choices=[("user", "User"), ("admin", "Admin")])
verified = BooleanField("Verified")
hidden = BooleanField("Hidden")
banned = BooleanField("Banned")
submit = SubmitField("Submit")
用户模式里面添加
CTFd/schemas/users.py
class UserSchema(ma.ModelSchema):
class Meta:
model = Users
include_fk = True
dump_only = ("id", "oauth_id", "created", "team_id")
load_only = ("password", )
name = field_for(
Users,
"name",
required=True,
allow_none=False,
validate=[
validate.Length(min=1,
max=128,
error="User names must not be empty")
],
)
email = field_for(
Users,
"email",
allow_none=False,
validate=[
validate.Email(
"Emails must be a properly formatted email address"),
validate.Length(min=1, max=128, error="Emails must not be empty"),
],
)
sname = field_for(
Users,
"sname",
required=True,
allow_none=False,
validate=[
validate.Length(min=1,
max=128,
error="Real name must not be empty")
],
)
sid = field_for(
Users,
"sid",
required=True,
allow_none=False,
validate=[
validate.Length(min=1,
max=128,
error="Student number must not be empty")
],
)
对于不同用户组的显示
views = {
"user": [
"website",
"name",
"country",
"affiliation",
"bracket",
"id",
"oauth_id",
"fields",
"team_id",
],
"self": [
"website",
"name",
"email",
"sname",
"sid",
"country",
"affiliation",
"bracket",
"id",
"oauth_id",
"password",
"fields",
"team_id",
],
"admin": [
"website",
"name",
"sname",
"sid",
"created",
"country",
"banned",
"email",
"affiliation",
"secret",
"bracket",
"hidden",
"id",
"oauth_id",
"password",
"type",
"verified",
"fields",
"team_id",
],
}
即自己跟后台可见,其他用户不可见
后台修改用户的界面显示
CTFd/themes/admin/templates/modals/users/edit.html
<div class="form-group">
{{ form.name.label }}
{{ form.name(class="form-control") }}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
</div>
<div class="form-group">
{{ form.sname.label }}
{{ form.sname(class="form-control") }}
</div>
<div class="form-group">
{{ form.sid.label }}
{{ form.sid(class="form-control") }}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
</div>
后台用户显示
CTFd/themes/admin/templates/users/users.html
<th class="sort-col text-center"><b>ID</b></th>
<th class="sort-col text-center"><b>User</b></th>
<th class="d-md-table-cell d-lg-table-cell sort-col text-center"><b>Email</b></th>
<th class="sort-col text-center"><b>Name</b></th>
<th class="sort-col text-center"><b>Number</b></th>
<th class="sort-col text-center"><b>Country</b></th>
<th class="sort-col text-center px-0"><b>Admin</b></th>
<th class="sort-col text-center px-0"><b>Verified</b></th>
<th class="sort-col text-center px-0"><b>Hidden</b></th>
<th class="sort-col text-center px-0"><b>Banned</b></th>
<td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ user.email }}">
{% if user.email %}
<a href="mailto:{{ user.email }}" target="_blank">{{ user.email | truncate(32) }}</a>
{% endif %}
</td>
<td class="team-id text-center" value="{{ user.sname }}">
<a href="{{ url_for('admin.users_detail', user_id=user.id) }}">
{{ user.sname}}
</a>
</td>
<td class="team-id text-center" value="{{ user.sid }}">
{{ user.sid }}
</td>
<td class="team-country text-center" value="{{ user.country if user.country is not none }}">
<span>
{% if user.country %}
<i class="flag-{{ user.country.lower() }}"></i>
<small>{{ lookup_country_code(user.country) }}</small>
{% endif %}
</span>
</td>
增加题目子类别
基于ctfd-pages-theme
的修改
ctfd-pages-theme
:https://github.com/frankli0324/ctfd-pages-theme
这个想法主要是想要在有分页的基础上给每个页面增加题目的子类别
数据库配置
首先是要给每个challenge
增加一个子类型subcategory
操作方法跟上文相同
CTFd/models/__init__.py
class Challenges(db.Model):
__tablename__ = "challenges"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
connection_info = db.Column(db.Text)
max_attempts = db.Column(db.Integer, default=0)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
subcategory = db.Column(db.String(80)) # 增加子类型
type = db.Column(db.String(80))
state = db.Column(db.String(80), nullable=False, default="visible")
requirements = db.Column(db.JSON)
增加新增题目修改
CTFd/themes/admin/templates/challenges/create.html
<form method="POST" action="{{ script_root }}/admin/challenges/new" enctype="multipart/form-data">
{% block name %}
<div class="form-group">
<label>
Name:<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
{% endblock %}
{% block category %}
<div class="form-group">
<label>
Category:<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
{% endblock %}
{% block subcategory %}
<div class="form-group">
<label>
Subcategory:<br>
<small class="form-text text-muted">
The subcategory of your challenge
</small>
</label>
<input type="text" class="form-control" name="subcategory" placeholder="Enter challenge subcategory">
</div>
{% endblock %}
后台题目显示
CTFd/themes/admin/templates/challenges/challenges.html
<th class="sort-col text-center"><b>ID</b></th>
<th class="sort-col"><b>Name</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col"><b>Category</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col"><b>Subcategory</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>Value</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>Type</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>State</b></th>
<td class="text-center">{{ challenge.id }}</td>
<td><a href="{{ url_for('admin.challenges_detail', challenge_id=challenge.id) }}">{{ challenge.name }}</a></td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.category }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">
{% if challenge.subcategory != "" %}
{{challenge.subcategory}}
{% else %}
None
{% endif %}
</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">{{ challenge.value }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">{{ challenge.type }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">
前端页面修改
修改challenge
的api
/CTFd/api/v1/challenges.py
response.append({
"id":
challenge.id,
"type":
challenge_type.name,
"name":
challenge.name,
"value":
challenge.value,
"solves":
solve_counts.get(challenge.id, solve_count_dfl),
"solved_by_me":
challenge.id in user_solves,
"category":
challenge.category,
"tags":
tag_schema.dump(challenge.tags).data,
"template":
challenge_type.templates["view"],
"script":
challenge_type.scripts["view"],
"subcategory":
challenge.subcategory,
})
这里注意一个问题,子目录未定义会报错。
最后是最复杂的js
部分,实现如下
/CTFd/themes/pages/assets/js/pages/challenges.js
import "./main";
import "bootstrap/js/dist/tab";
import { ezQuery, ezAlert } from "../ezq";
import { htmlEntities } from "../utils";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import $ from "jquery";
import CTFd from "../CTFd";
import config from "../config";
import hljs from "highlight.js";
dayjs.extend(relativeTime);
const api_func = {
teams: x => CTFd.api.get_team_solves({ teamId: x }),
users: x => CTFd.api.get_user_solves({ userId: x })
};
CTFd._internal.challenge = {};
let challenges = [];
let solves = [];
let pages = [];
const loadChal = id => {
const chal = $.grep(challenges, chal => chal.id == id)[0];
if (chal.type === "hidden") {
ezAlert({
title: "Challenge Hidden!",
body: "You haven't unlocked this challenge yet!",
button: "Got it!"
});
return;
}
displayChal(chal);
};
const loadChalByName = name => {
let idx = name.lastIndexOf("-");
let pieces = [name.slice(0, idx), name.slice(idx + 1)];
let id = pieces[1];
const chal = $.grep(challenges, chal => chal.id == id)[0];
displayChal(chal);
};
const displayChal = chal => {
return Promise.all([
CTFd.api.get_challenge({ challengeId: chal.id }),
$.getScript(config.urlRoot + chal.script),
$.get(config.urlRoot + chal.template)
]).then(responses => {
const challenge = CTFd._internal.challenge;
$("#challenge-window").empty();
// Inject challenge data into the plugin
challenge.data = responses[0].data;
// Call preRender function in plugin
challenge.preRender();
// Build HTML from the Jinja response in API
$("#challenge-window").append(responses[0].data.view);
$("#challenge-window #challenge-input").addClass("form-control");
$("#challenge-window #challenge-submit").addClass(
"btn btn-md btn-outline-secondary float-right"
);
let modal = $("#challenge-window").find(".modal-dialog");
if (
window.init.theme_settings &&
window.init.theme_settings.challenge_window_size
) {
switch (window.init.theme_settings.challenge_window_size) {
case "sm":
modal.addClass("modal-sm");
break;
case "lg":
modal.addClass("modal-lg");
break;
case "xl":
modal.addClass("modal-xl");
break;
default:
break;
}
}
$(".challenge-solves").click(function(_event) {
getSolves($("#challenge-id").val());
});
$(".nav-tabs a").click(function(event) {
event.preventDefault();
$(this).tab("show");
});
// Handle modal toggling
$("#challenge-window").on("hide.bs.modal", function(_event) {
$("#challenge-input").removeClass("wrong");
$("#challenge-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
$("#too-fast").slideUp();
});
$(".load-hint").on("click", function(_event) {
loadHint($(this).data("hint-id"));
});
$("#challenge-submit").click(function(event) {
event.preventDefault();
$("#challenge-submit").addClass("disabled-button");
$("#challenge-submit").prop("disabled", true);
CTFd._internal.challenge
.submit()
.then(renderSubmissionResponse)
.then(loadChals)
.then(markSolves);
});
$("#challenge-input").keyup(event => {
if (event.keyCode == 13) {
$("#challenge-submit").click();
}
});
challenge.postRender();
$("#challenge-window")
.find("pre code")
.each(function(_idx) {
hljs.highlightBlock(this);
});
window.location.replace(
window.location.href.split("#")[0] + `#${chal.name}-${chal.id}`
);
$("#challenge-window").modal();
});
};
function renderSubmissionResponse(response) {
const result = response.data;
const result_message = $("#result-message");
const result_notification = $("#result-notification");
const answer_input = $("#challenge-input");
result_notification.removeClass();
result_message.text(result.message);
if (result.status === "authentication_required") {
window.location =
CTFd.config.urlRoot +
"/login?next=" +
CTFd.config.urlRoot +
window.location.pathname +
window.location.hash;
return;
} else if (result.status === "incorrect") {
// Incorrect key
result_notification.addClass(
"alert alert-danger alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.removeClass("correct");
answer_input.addClass("wrong");
setTimeout(function() {
answer_input.removeClass("wrong");
}, 3000);
} else if (result.status === "correct") {
// Challenge Solved
result_notification.addClass(
"alert alert-success alert-dismissable text-center"
);
result_notification.slideDown();
if (
$(".challenge-solves")
.text()
.trim()
) {
// Only try to increment solves if the text isn't hidden
$(".challenge-solves").text(
parseInt(
$(".challenge-solves")
.text()
.split(" ")[0]
) +
1 +
" Solves"
);
}
answer_input.val("");
answer_input.removeClass("wrong");
answer_input.addClass("correct");
} else if (result.status === "already_solved") {
// Challenge already solved
result_notification.addClass(
"alert alert-info alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.addClass("correct");
} else if (result.status === "paused") {
// CTF is paused
result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown();
} else if (result.status === "ratelimited") {
// Keys per minute too high
result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.addClass("too-fast");
setTimeout(function() {
answer_input.removeClass("too-fast");
}, 3000);
}
setTimeout(function() {
$(".alert").slideUp();
$("#challenge-submit").removeClass("disabled-button");
$("#challenge-submit").prop("disabled", false);
}, 3000);
}
function markSolves() {
challenges.map(challenge => {
if (challenge.solved_by_me) {
const btn = $(`button[value="${challenge.id}"]`);
btn.addClass("solved-challenge");
btn.prepend("<i class='fas fa-check corner-button-check'></i>");
}
});
}
async function getSolves(id) {
const data = (await CTFd.api.get_challenge_solves({ challengeId: id })).data;
$(".challenge-solves").text(parseInt(data.length) + " Solves");
const box = $("#challenge-solves-names");
box.empty();
for (let i = 0; i < data.length; i++) {
const id = data[i].account_id;
const name = data[i].name;
const date = dayjs(data[i].date).fromNow();
const account_url = data[i].account_url;
box.append(
'<tr><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(
account_url, id,
htmlEntities(name), date
)
);
}
}
async function loadPages() {
challenges = (await CTFd.api.get_challenge_list()).data;
const pages_board = $('#pages-board');
for (var i of challenges) {
var page = i.category.split('.')[0];
const pageid = page.replace(/ /g, "-").hashCode();
if ($.inArray(page, pages) == -1) {
pages.push(page);
const page_row = $(
'<a ' +
'id="{0}-page-row" class="nav-link" '.format(pageid) +
'data-toggle="pill" role="tab" href="#"' +
'>' + page.slice(0, 15) + "</a>"
);
if (pages.length === 1) page_row.addClass('active');
page_row.on('shown.bs.tab', loadChals);
pages_board.append(page_row);
}
}
loadChals();
}
function loadChals() {
const categories = [];
const subcategories = [];
const $challenges_board = $("#challenges-board");
const current_page_id = $("#pages-board>.active")[0].id;
$challenges_board.empty();
function addCategoryRow(category) {
categories.push(category);
const categoryid = category.replace(/ /g, "-").hashCode();
const categoryrow = $(
"" +
'<div id="{0}-row" class="pt-5">'.format(categoryid) +
'<div class="category-header col-md-12 mb-3">' +
"</div>" +
'<div class="category-challenges col-md-12">' +
'<div class="challenges-row col-md-12"></div>' +
"</div>" +
"</div>"
);
categoryrow
.find(".category-header")
.append($("<h3>" + category + "</h3>"));
$challenges_board.append(categoryrow);
}
function addSubCategoryRow(subcategory) {
subcategories.push(subcategory);
if (subcategory === null) subcategory = "";
const subcategoryid = subcategory.replace(/ /g, "-").hashCode();
const subcategoryrow = $(
"" +
'<div id="{0}-row" class="pt-5">'.format(subcategoryid) +
'<div class="category-header col-md-12 mb-3">' +
"</div>" +
'<div class="category-challenges col-md-12">' +
'<div class="challenges-row col-md-12"></div>' +
"</div>" +
"</div>"
);
subcategoryrow
.find(".category-header")
.append($("<h4>" + subcategory + "</h4>"));
$challenges_board.append(subcategoryrow);
}
// addSubCategoryRow("Linux");
for (let i = 0; i <= challenges.length - 1; i++) {
challenges[i].solves = 0;
var category = challenges[i].category.split('.')[0];
const page = '{0}-page-row'.format(challenges[i]
.category.split('.')[0]
.replace(/ /g, "-").hashCode());
if (page !== current_page_id) continue;
if (category === undefined) category = "";
if ($.inArray(category, categories) == -1)
addCategoryRow(category);
}
for (let i = 0; i <= challenges.length - 1; i++) {
challenges[i].solves = 0;
var subcategory = challenges[i].subcategory;
const page = '{0}-page-row'.format(challenges[i]
.category.split('.')[0]
.replace(/ /g, "-").hashCode());
if (page !== current_page_id) continue;
if (subcategory === undefined || subcategory === null || subcategory === "") subcategory = "none";
if ($.inArray(subcategory, subcategories) == -1) {
if (subcategory != "none") {
addSubCategoryRow(subcategory);
}
}
}
for (let i = 0; i <= challenges.length - 1; i++) {
var subcategory = challenges[i].subcategory;
if (subcategory === undefined || subcategory === null || subcategory === "") subcategory = "none";
if (subcategory != "none") continue;
const chalinfo = challenges[i];
const chalid = chalinfo.name.replace(/ /g, "-").hashCode();
var category = chalinfo.category.split('.')[0];
if (category === undefined) category = "";
const page = '{0}-page-row'.format(challenges[i]
.category.split('.')[0]
.replace(/ /g, "-").hashCode());
if (page !== current_page_id) continue;
const catid = category.replace(/ /g, "-").hashCode();
const chalwrap = $(
"<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid)
);
let chalbutton;
if (solves.indexOf(chalinfo.id) == -1) {
chalbutton = $(
"<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(
chalinfo.id
)
);
} else {
chalbutton = $(
"<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(
chalinfo.id
)
);
}
const chalheader = $("<p>{0}</p>".format(chalinfo.name));
const chalscore = $("<span>{0}</span>".format(chalinfo.value));
for (let j = 0; j < chalinfo.tags.length; j++) {
const tag = "tag-" + chalinfo.tags[j].value.replace(/ /g, "-");
chalwrap.addClass(tag);
}
chalbutton.append(chalheader);
chalbutton.append(chalscore);
chalwrap.append(chalbutton);
$("#" + catid + "-row")
.find(".category-challenges > .challenges-row")
.append(chalwrap);
}
for (let i = 0; i <= challenges.length - 1; i++) {
var subcategory = challenges[i].subcategory;
if (subcategory === undefined || subcategory === null || subcategory === "") subcategory = "none";
if (subcategory == "none") continue;
const subcategoryid = subcategory.replace(/ /g, "-").hashCode();
const chalinfo = challenges[i];
const chalid = chalinfo.name.replace(/ /g, "-").hashCode();
var category = chalinfo.category.split('.')[0];
if (category === undefined) category = "";
const page = '{0}-page-row'.format(challenges[i]
.category.split('.')[0]
.replace(/ /g, "-").hashCode());
if (page !== current_page_id) continue;
const catid = category.replace(/ /g, "-").hashCode();
const chalwrap = $(
"<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid)
);
let chalbutton;
if (solves.indexOf(chalinfo.id) == -1) {
chalbutton = $(
"<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(
chalinfo.id
)
);
} else {
chalbutton = $(
"<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(
chalinfo.id
)
);
}
const chalheader = $("<p>{0}</p>".format(chalinfo.name));
const chalscore = $("<span>{0}</span>".format(chalinfo.value));
for (let j = 0; j < chalinfo.tags.length; j++) {
const tag = "tag-" + chalinfo.tags[j].value.replace(/ /g, "-");
chalwrap.addClass(tag);
}
chalbutton.append(chalheader);
chalbutton.append(chalscore);
chalwrap.append(chalbutton);
$("#" + subcategoryid + "-row")
.find(".category-challenges > .challenges-row")
.append(chalwrap);
}
$(".challenge-button").click(function(_event) {
loadChal(this.value);
getSolves(this.value);
});
markSolves();
}
async function update() {
await loadPages();
markSolves();
}
$(async() => {
await update();
if (window.location.hash.length > 0) {
loadChalByName(decodeURIComponent(window.location.hash.substring(1)));
}
$("#challenge-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#challenge-submit").click();
}
});
$(".nav-tabs a").click(function(event) {
event.preventDefault();
$(this).tab("show");
});
$("#challenge-window").on("hidden.bs.modal", function(_event) {
$(".nav-tabs a:first").tab("show");
history.replaceState("", window.document.title, window.location.pathname);
});
$(".challenge-solves").click(function(_event) {
getSolves($("#challenge-id").val());
});
$("#challenge-window").on("hide.bs.modal", function(_event) {
$("#challenge-input").removeClass("wrong");
$("#challenge-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
$("#too-fast").slideUp();
});
});
setInterval(update, 300000); // Update every 5 minutes.
const displayHint = data => {
ezAlert({
title: "Hint",
body: data.html,
button: "Got it!"
});
};
const displayUnlock = id => {
ezQuery({
title: "Unlock Hint?",
body: "Are you sure you want to open this hint?",
success: () => {
const params = {
target: id,
type: "hints"
};
CTFd.api.post_unlock_list({}, params).then(response => {
if (response.success) {
CTFd.api.get_hint({ hintId: id }).then(response => {
displayHint(response.data);
});
return;
}
ezAlert({
title: "Error",
body: response.errors.score,
button: "Got it!"
});
});
}
});
};
const loadHint = id => {
CTFd.api.get_hint({ hintId: id }).then(response => {
if (response.data.content) {
displayHint(response.data);
return;
}
displayUnlock(id);
});
};
666
师傅,请问你这个ctfd-pages-theme是如何搭建的,我按照git上的做法发现还是不行。想请教一下您。如果可以的话加我QQ 577896795 帮我一下,我也想搭建一个自己的靶场。
天,佬!
佬,你好!
我想问一下为什么我添加字母子类别之后,它还是没有显示呢?
修改完js之后要重新打包js才能正常显示,看一下是不是没有打包
谢谢谢谢谢谢!
大佬,我按照这个方法,弄了之后,docker启动一直重启报错
Waiting for mysql+pymysql://ctfd:ctfd@db to be ready
mysql+pymysql://ctfd:ctfd@db is ready
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
Traceback (most recent call last):
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1283, in _execute_context
self.dialect.do_execute(
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/default.py”, line 590, in do_execute
cursor.execute(statement, parameters)
File “/usr/local/lib/python3.9/site-packages/pymysql/cursors.py”, line 170, in execute
result = self._query(query)
File “/usr/local/lib/python3.9/site-packages/pymysql/cursors.py”, line 328, in _query
conn.query(q)
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 732, in _read_query_result
result.read()
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 1075, in read
first_packet = self.connection._read_packet()
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 684, in _read_packet
packet.check_error()
File “/usr/local/lib/python3.9/site-packages/pymysql/protocol.py”, line 220, in check_error
err.raise_mysql_exception(self._data)
File “/usr/local/lib/python3.9/site-packages/pymysql/err.py”, line 109, in raise_mysql_exception
raise errorclass(errno, errval)
pymysql.err.InternalError: (1054, “Unknown column ‘challenges.subcategory’ in ‘field list'”)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File “/opt/CTFd/manage.py”, line 16, in
app = create_app()
File “/opt/CTFd/CTFd/__init__.py”, line 271, in create_app
update_check(force=True)
File “/opt/CTFd/CTFd/utils/updates/__init__.py”, line 47, in update_check
“challenge_count”: Challenges.query.count(),
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3749, in count
return self.from_self(col).scalar()
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3469, in scalar
ret = self.one()
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3436, in one
ret = self.one_or_none()
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3405, in one_or_none
ret = list(self)
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3481, in __iter__
return self._execute_and_instances(context)
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/orm/query.py”, line 3506, in _execute_and_instances
result = conn.execute(querycontext.statement, self._params)
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1020, in execute
return meth(self, multiparams, params)
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/elements.py”, line 298, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1133, in _execute_clauseelement
ret = self._execute_context(
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1323, in _execute_context
self._handle_dbapi_exception(
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1517, in _handle_dbapi_exception
util.raise_(
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py”, line 178, in raise_
raise exception
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py”, line 1283, in _execute_context
self.dialect.do_execute(
File “/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/default.py”, line 590, in do_execute
cursor.execute(statement, parameters)
File “/usr/local/lib/python3.9/site-packages/pymysql/cursors.py”, line 170, in execute
result = self._query(query)
File “/usr/local/lib/python3.9/site-packages/pymysql/cursors.py”, line 328, in _query
conn.query(q)
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 732, in _read_query_result
result.read()
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 1075, in read
first_packet = self.connection._read_packet()
File “/usr/local/lib/python3.9/site-packages/pymysql/connections.py”, line 684, in _read_packet
packet.check_error()
File “/usr/local/lib/python3.9/site-packages/pymysql/protocol.py”, line 220, in check_error
err.raise_mysql_exception(self._data)
File “/usr/local/lib/python3.9/site-packages/pymysql/err.py”, line 109, in raise_mysql_exception
raise errorclass(errno, errval)
sqlalchemy.exc.InternalError: (pymysql.err.InternalError) (1054, “Unknown column ‘challenges.subcategory’ in ‘field list'”)
[SQL: SELECT count(*) AS count_1
FROM (SELECT challenges.id AS challenges_id, challenges.name AS challenges_name, challenges.description AS challenges_description, challenges.connection_info AS challenges_connection_info, challenges.next_id AS challenges_next_id, challenges.max_attempts AS challenges_max_attempts, challenges.value AS challenges_value, challenges.category AS challenges_category, challenges.subcategory AS challenges_subcategory, challenges.type AS challenges_type, challenges.state AS challenges_state, challenges.requirements AS challenges_requirements
FROM challenges) AS anon_1]
(Background on this error at: http://sqlalche.me/e/2j85)
我弄的是添加子分类
数据库配置之后手动查一下数据库,看一下字段有没有插进去。可能是前面的步骤出了问题,我之前遇到过的问题是ctf-whale的插件安装之后数据库会出问题,当时我的解决方法是先移除这个插件,在所有的数据库修改完毕之后最后安装那个插件。
666
老哥,你的前端是怎么运行的
CTFd本身就是基于flask运行的