CTFd 题解收集插件他来啦

CTFd 题解收集插件他来啦

IShirai_KurokoI

这不是在准备办比赛嘛,寻思着要是每次都邮件手动收集有点太麻烦了,于是干脆写一个插件直接让选手们在平台上上传。
大致思路就是在题目页面插入一个上传按钮,然后点击跳转上传页面。

20231227203027

20231227203039

怎么插入呢?由于是以插件的形式加入的,我们要用到wrap的后处理功能。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def insert_tags(page):
# 没有开启直接返回
if not get_config('writeup:enabled'):
return page

# 没开始之前不显示上传wp
if ctf_started() is False:
if current_user.is_admin() is False:
return page

if isinstance(page, etree._ElementTree):
root = page
else:
try:
root = etree.fromstring(page, etree.HTMLParser())
except:
# 我们无法解析它(例如,它是一个 Response 对象),所以只需将它传递过去
return page

try:
language = "en"
try:
language = request.cookies.get("Scr1wCTFdLanguage", "en")
except:
pass

inserted = False
for window in root.xpath('/html/body/main/div[@id="challenge-window"]'):
if language == "zh":
link = etree.Element(
'button',
attrib={
'class': 'btn btn-md btn-primary btn-outlined float-right',
'style': 'position: sticky; top: 80px; margin-top:40px; float: right;margin-right:30px; '
'z-index: 999;',
'onclick': 'window.open("/plugins/writeup/upload")',
}
)
link.text = "上传题解"
window.addnext(link)
else:
link = etree.Element(
'button',
attrib={
'class': 'btn btn-md btn-primary btn-outlined float-right',
'style': 'position: sticky; top: 90px; margin-top:40px; float: right;margin-right:30px; '
'z-index: 999;',
'onclick': 'window.open("/plugins/writeup/upload")',
}
)
div1 = etree.SubElement(link, 'div')
div1.text = 'Upload'
div2 = etree.SubElement(link, 'div')
div2.text = 'Writeup'
window.addnext(link)
inserted = True

if not inserted:
log_simple("writeup", "[{date}] [Writeup] 页面插入元素失败,未找到对应元素。")

# fix some weird padding
if language == "zh":
return etree.tostring(root, encoding="unicode", method='html').replace("<span class=\"d-lg-none"
"\">注销</span>", "注销")
else:
return etree.tostring(root, encoding="unicode", method='html').replace("<span class=\"d-lg-none"
"\">Logout</span>", "Logout")
except Exception as e:
log_simple("writeup", "[{date}] [Writeup] 页面插入元素失败:{e}", e=e)
return page

def insert_tags_decorator(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
return insert_tags(view_func(*args, **kwargs))

return wrapper

app.view_functions['challenges.listing'] = insert_tags_decorator(app.view_functions['challenges.listing'])

前端简单编写一个拖拽上传如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
{% extends "base.html" %}

{% block content %}
<div class="jumbotron" style="height: 220px;">
<div class="container">
<h1><b>{{ 'Writeup Upload' if en else '题解上传' }}</b></h1>
</div>
</div>
<div class="container" style="height: 228px;">
<div class="form-group" id="drop">
<p>{{"You can upload your writeup file below, only support pdf file!" if en else "您可以在下面上传题解文件,只支持PDF!"}}</p>
<div class="drop-area" ondragover="event.preventDefault()" ondrop="handleDrop(event)"
style="border: 2px dashed #ccc;padding: 20px;text-align: center;height: 250px;display: flex;align-items: center;justify-content: center;">
<div class="centered-content"
style="display: flex;flex-direction: column;align-items: center;justify-content: center;height: 100%;">
<h2>{{"Drag and drop the writeup file here to upload" if en else "将题解文件拖拽至此处上传"}}</h2>
</div>
</div>
</div>
</div>
{% endblock %}

{% block scripts %}
<script>
function handleDrop(event) {
event.preventDefault();
var files = event.dataTransfer.files;
var progressBarContainer = document.getElementById("drop");
var file = files[0];
var progressBar = createProgressBar(progressBarContainer);
var fileNameElement = document.createElement('div');
fileNameElement.classList.add('file-name');
fileNameElement.textContent = file.name;
fileNameElement.style = "margin-bottom: 5px;"
progressBarContainer.appendChild(fileNameElement);
uploadFile(file, progressBar, fileNameElement);
}

function createProgressBar(progressBarContainer) {
var progressBar = document.createElement('div');
progressBar.classList.add('progress-bar');
progressBar.style = "width: 100%;background-color: #f5f5f5;border-radius: 4px;overflow: hidden;margin-bottom: 10px;margin-top: 5px;"

var progress = document.createElement('div');
progress.classList.add('progress');
progress.style = "width: 0;height: 20px;background-color: #4caf50;transition: width 0.3s ease-in-out;"

progressBar.appendChild(progress);
progressBarContainer.appendChild(progressBar);

return progressBar;
}

function updateProgress(progressBar, percent) {
progressBar.getElementsByClassName('progress')[0].style.width = percent + '%';
}

function uploadFile(file, progressBar, fileNameElement) {
var formData = new FormData();
formData.append('writeup', file);
formData.append("nonce", init.csrfNonce);

$.ajax({
url: '/plugins/writeup/upload',
type: 'POST',
headers: {
"Accept": "application/json; charset=utf-8"
},
data: formData,
processData: false,
contentType: false,
xhr: function () {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', function (event) {
if (event.lengthComputable) {
var percent = Math.round((event.loaded / event.total) * 100);
progressBar.getElementsByClassName('progress')[0].style.width = percent + '%';
}
}, false);
return xhr;
},
success: function (response) {
fileNameElement.remove();
progressBar.remove();
var e = new Object;
e.title = "{{'Upload success!' if en else '上传成功!'}}";
e.body = "{{'Writeup upload success!' if en else '题解上传完成!'}}";
CTFd.ui.ezq.ezToast(e)
},
error: function (xhr, status, error) {
fileNameElement.remove();
progressBar.remove();
var e = new Object;
e.title = "{{'Upload fail!' if en else '上传失败!'}}";
e.body = JSON.parse(xhr.responseText).message;
e.button="{{'Got it' if en else '知道了'}}";
CTFd.ui.ezq.ezAlert(e)
}
});
}
</script>
{% endblock %}

可能到这就有人有疑问了,你这说着只接受pdf,你也没做校验啊。我说先别急,你在一个ctf平台上搞前端校验嘛难道23333,校验在后端呢。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def is_pdf(file):
try:
original_position = file.tell()
# 使用BytesIO将文件内容读取到内存中
file_content = BytesIO(file.read())
file.seek(original_position)
pdf_reader = PdfReader(file_content)
# 判断PDF文件是否能成功读取
len(pdf_reader.pages)
return True
except Exception as e:
return False

@page_blueprint.route("/upload", methods=['GET', 'POST'])
@authed_only
def UserUpload():
if request.method == "POST":
# 没开始之前不许上传wp
if ctf_started() is False:
if current_user.is_admin() is False:
return redirect(url_for("challenges.listing"))

if get_config("writeup:enabled"):
filename_for_store = ""
user = current_user.get_current_user()
try:
filename_for_store = get_config("writeup:name").format(user=user)
except Exception as e:
log_simple("writeup", "[{date}] [Writeup] 用户上传writeup时格式化名称出错,请检查后台文件名配置!:{e}",
e=str(e))
return {
'success': False,
'message': '后端处理失败,请联系管理员!'
}, 500
upload_folder = os.path.join(
os.path.normpath(app.root_path), app.config.get("UPLOAD_FOLDER")
)
writeup_folder = os.path.join(upload_folder, "writeups")
os.makedirs(writeup_folder, exist_ok=True)

# 检查文件是否存在于请求中
if 'writeup' not in request.files:
return {
'success': False,
'message': '题解文件不存在'
}, 400
file = request.files['writeup']
# 如果用户未选择文件,浏览器也可能提交一个空的 part
if file.filename == '':
return {
'success': False,
'message': '题解文件为空'
}, 400

if file:
try:
if not is_pdf(file):
log_simple("writeup", "[{date}] [Writeup] pdf校验失败,可能用户{name}上传的是恶意文件!",
name=user.name)
return {'success': False, 'message': "文件未通过校验!"}, 400
except:
log_simple("writeup", "[{date}] [Writeup] pdf校验失败,可能用户{name}上传的是恶意文件!",
name=user.name)
return {'success': False, 'message': "文件未通过校验!"}, 400
try:
filepath = os.path.join(writeup_folder, filename_for_store)
file.save(filepath)
log_simple("writeup", "[{date}] [Writeup] 用户{name}成功上传了writeup:[{filename}]。",
name=user.name,
filename=file.filename)
except Exception as e:
log_simple("writeup", "[{date}] [Writeup] 用户{name}上传writeup:[{filename}]时出错{e}",
name=user.name,
filename=file.filename,
e=str(e))
return {'success': False, 'message': "上传失败"}, 500

return {'success': True, 'message': "上传成功"}, 200
else:
return redirect(url_for("challenges.listing"))
else:
# 没开始之前不许上传wp
if ctf_started() is False:
if current_user.is_admin() is False:
return redirect(url_for("challenges.listing"))

if get_config("writeup:enabled"):
return render_template("writeup_upload.html")
else:
return redirect(url_for("challenges.listing"))

app.register_blueprint(page_blueprint)

校验文件类型我直接用读取的方法,如果读都读不了绝对不是对的,然后如果在上传时间之外访问是会被重定向回去的。

至于什么文件名称什么的,我直接 强!制!格!式!化!而!且!不!解!析!:

20231227203846

这里的意思是如果是团队模式就用user.team获取团队对象。

可以单独下载也可以打包下载:

20231227204838

项目地址:https://github.com/IShiraiKurokoI/CTFd-writeup-plugin
PS:我们Scr1w战队二次开发的CTFd整合版地址:https://github.com/dlut-sss/CTFD-Public
完活,下机!

  • 标题: CTFd 题解收集插件他来啦
  • 作者: IShirai_KurokoI
  • 创建于 : 2023-12-27 00:00:00
  • 更新于 : 2023-12-27 21:11:57
  • 链接: https://ishiraikurokoi.top/2023-12-27-CTFd-Writeup-Plugin/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
CTFd 题解收集插件他来啦