Skip to content

Commit 36c83b5

Browse files
authored
1.2.0 (CTFd#627)
* Changing to a new plugin oriented challenge type plugin and fixing extra width on admin chal description * Add window.challenge.submit, renderSubmissionResponse, and csrf_nonce * Update admin side renderer calls * Updating to Flask 1.0 and adding files for flask run * Adding a preliminary case-insensitive key * Adding case insensitive keys * Adding CTF Logo * Reducing the amount of team information shown on the main page * Add better base64 helpers * Switch from button to badge * Rudimentary solve checking from admin panel * Refine admin chals solves view & fix PEP8 * Compare base64 encoded data with bytestring * Removing need to urlencode/urldecode in base64 wrappers * Adding decorator documentation * Randomly order tests & add test for case_insensitive flags * Add regex flag case_insensitive test * Add tests for /admin/chal/1/solves and ctf_logo
1 parent 9c812ad commit 36c83b5

40 files changed

+700
-434
lines changed

.flaskenv

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_ENV=development
2+
FLASK_RUN_PORT=4000

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ before_script:
1515
- psql -c 'create database ctfd;' -U postgres
1616
script:
1717
- pep8 --ignore E501,E712 CTFd/ tests/
18-
- nosetests -d
18+
- nosetests -v -d --with-randomly
1919
after_success:
2020
- codecov

CTFd/admin/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ def admin_config():
147147
utils.set_config("mail_username", None)
148148
utils.set_config("mail_password", None)
149149

150+
if request.files.get('ctf_logo', None):
151+
ctf_logo = request.files['ctf_logo']
152+
file_id, file_loc = utils.upload_file(ctf_logo, None)
153+
utils.set_config("ctf_logo", file_loc)
154+
elif request.form.get('ctf_logo') == '':
155+
utils.set_config("ctf_logo", None)
156+
150157
utils.set_config("ctf_name", request.form.get('ctf_name', None))
151158
utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
152159
utils.set_config('css', request.form.get('css', None))
@@ -176,6 +183,7 @@ def admin_config():
176183
cache.clear()
177184

178185
ctf_name = utils.get_config('ctf_name')
186+
ctf_logo = utils.get_config('ctf_logo')
179187
ctf_theme = utils.get_config('ctf_theme')
180188
hide_scores = utils.get_config('hide_scores')
181189
css = utils.get_config('css')
@@ -216,6 +224,7 @@ def admin_config():
216224
return render_template(
217225
'admin/config.html',
218226
ctf_name=ctf_name,
227+
ctf_logo=ctf_logo,
219228
ctf_theme_config=ctf_theme,
220229
css=css,
221230
start=start,

CTFd/admin/challenges.py

+13
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ def admin_chal_detail(chalid):
100100
return jsonify(data)
101101

102102

103+
@admin_challenges.route('/admin/chal/<int:chalid>/solves', methods=['GET'])
104+
@admins_only
105+
def admin_chal_solves(chalid):
106+
response = {'teams': []}
107+
if utils.hide_scores():
108+
return jsonify(response)
109+
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid).order_by(
110+
Solves.date.asc())
111+
for solve in solves:
112+
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
113+
return jsonify(response)
114+
115+
103116
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
104117
@admins_only
105118
def admin_tags(chalid):

CTFd/admin/teams.py

+15-18
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def admin_create_team():
5757
affiliation = request.form.get('affiliation', None)
5858
country = request.form.get('country', None)
5959

60+
admin_user = True if request.form.get('admin', None) == 'on' else False
61+
verified = True if request.form.get('verified', None) == 'on' else False
62+
hidden = True if request.form.get('hidden', None) == 'on' else False
63+
6064
errors = []
6165

6266
if not name:
@@ -92,6 +96,10 @@ def admin_create_team():
9296
team.affiliation = affiliation
9397
team.country = country
9498

99+
team.admin = admin_user
100+
team.verified = verified
101+
team.hidden = hidden
102+
95103
db.session.add(team)
96104
db.session.commit()
97105
db.session.close()
@@ -119,31 +127,17 @@ def admin_team(teamid):
119127
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
120128
place=place, wrong_keys=wrong_keys, awards=awards)
121129
elif request.method == 'POST':
122-
admin_user = request.form.get('admin', None)
123-
if admin_user:
124-
admin_user = True if admin_user == 'true' else False
125-
user.admin = admin_user
126-
# Set user.banned to hide admins from scoreboard
127-
user.banned = admin_user
128-
db.session.commit()
129-
db.session.close()
130-
return jsonify({'data': ['success']})
131-
132-
verified = request.form.get('verified', None)
133-
if verified:
134-
verified = True if verified == 'true' else False
135-
user.verified = verified
136-
db.session.commit()
137-
db.session.close()
138-
return jsonify({'data': ['success']})
139-
140130
name = request.form.get('name', None)
141131
password = request.form.get('password', None)
142132
email = request.form.get('email', None)
143133
website = request.form.get('website', None)
144134
affiliation = request.form.get('affiliation', None)
145135
country = request.form.get('country', None)
146136

137+
admin_user = True if request.form.get('admin', None) == 'on' else False
138+
verified = True if request.form.get('verified', None) == 'on' else False
139+
hidden = True if request.form.get('hidden', None) == 'on' else False
140+
147141
errors = []
148142

149143
if email:
@@ -177,6 +171,9 @@ def admin_team(teamid):
177171
user.website = website
178172
user.affiliation = affiliation
179173
user.country = country
174+
user.admin = admin_user
175+
user.verified = verified
176+
user.banned = hidden
180177
db.session.commit()
181178
db.session.close()
182179
return jsonify({'data': ['success']})

CTFd/auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def confirm_user(data=None):
2929
if data and request.method == "GET":
3030
try:
3131
s = TimedSerializer(app.config['SECRET_KEY'])
32-
email = s.loads(utils.base64decode(data, urldecode=True), max_age=1800)
32+
email = s.loads(utils.base64decode(data), max_age=1800)
3333
except BadTimeSignature:
3434
return render_template('confirm.html', errors=['Your confirmation link has expired'])
3535
except (BadSignature, TypeError, base64.binascii.Error):
@@ -86,7 +86,7 @@ def reset_password(data=None):
8686
if data is not None:
8787
try:
8888
s = TimedSerializer(app.config['SECRET_KEY'])
89-
name = s.loads(utils.base64decode(data, urldecode=True), max_age=1800)
89+
name = s.loads(utils.base64decode(data), max_age=1800)
9090
except BadTimeSignature:
9191
return render_template('reset_password.html', errors=['Your link has expired'])
9292
except (BadSignature, TypeError, base64.binascii.Error):

CTFd/plugins/challenges/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def attempt(chal, request):
146146
provided_key = request.form['key'].strip()
147147
chal_keys = Keys.query.filter_by(chal=chal.id).all()
148148
for chal_key in chal_keys:
149-
if get_key_class(chal_key.type).compare(chal_key.flag, provided_key):
149+
if get_key_class(chal_key.type).compare(chal_key, provided_key):
150150
return True, 'Correct'
151151
return False, 'Incorrect'
152152

Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
// Markdown Preview
22
$('#desc-edit').on('shown.bs.tab', function (event) {
3-
var md = window.markdownit({
4-
html: true,
5-
});
6-
if (event.target.hash == '#desc-preview'){
3+
if (event.target.hash == '#desc-preview') {
74
var editor_value = $('#desc-editor').val();
85
$(event.target.hash).html(
9-
md.render(editor_value)
6+
window.challenge.render(editor_value)
107
);
118
}
129
});
1310
$('#new-desc-edit').on('shown.bs.tab', function (event) {
14-
var md = window.markdownit({
15-
html: true,
16-
});
17-
if (event.target.hash == '#new-desc-preview'){
11+
if (event.target.hash == '#new-desc-preview') {
1812
var editor_value = $('#new-desc-editor').val();
1913
$(event.target.hash).html(
20-
md.render(editor_value)
14+
window.challenge.render(editor_value)
2115
);
2216
}
2317
});
24-
$("#solve-attempts-checkbox").change(function() {
25-
if(this.checked) {
18+
$("#solve-attempts-checkbox").change(function () {
19+
if (this.checked) {
2620
$('#solve-attempts-input').show();
2721
} else {
2822
$('#solve-attempts-input').hide();
2923
$('#max_attempts').val('');
3024
}
3125
});
3226

33-
$(document).ready(function(){
27+
$(document).ready(function () {
3428
$('[data-toggle="tooltip"]').tooltip();
3529
});
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
1-
$('#submit-key').unbind('click');
2-
$('#submit-key').click(function (e) {
3-
e.preventDefault();
4-
submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val())
1+
window.challenge.renderer = new markdownit({
2+
html: true,
53
});
64

7-
$("#answer-input").keyup(function(event){
8-
if(event.keyCode == 13){
9-
$("#submit-key").click();
10-
}
11-
});
5+
window.challenge.preRender = function(){
6+
7+
};
8+
9+
window.challenge.render = function(markdown){
10+
return window.challenge.renderer.render(markdown);
11+
};
12+
1213

13-
$(".input-field").bind({
14-
focus: function() {
15-
$(this).parent().addClass('input--filled' );
16-
$label = $(this).siblings(".input-label");
17-
},
18-
blur: function() {
19-
if ($(this).val() === '') {
20-
$(this).parent().removeClass('input--filled' );
21-
$label = $(this).siblings(".input-label");
22-
$label.removeClass('input--hide' );
23-
}
14+
window.challenge.postRender = function(){
15+
16+
};
17+
18+
19+
window.challenge.submit = function(cb, preview){
20+
var chal_id = $('#chal-id').val();
21+
var answer = $('#answer-input').val();
22+
var nonce = $('#nonce').val();
23+
24+
var url = "/chal/";
25+
if (preview) {
26+
url = "/admin/chal/";
2427
}
25-
});
28+
29+
$.post(script_root + url + chal_id, {
30+
key: answer,
31+
nonce: nonce
32+
}, function (data) {
33+
cb(data);
34+
});
35+
};

CTFd/plugins/challenges/assets/standard-challenge-update.js

+4-10
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,18 @@ $('#limit_max_attempts').change(function() {
1818

1919
// Markdown Preview
2020
$('#desc-edit').on('shown.bs.tab', function (event) {
21-
var md = window.markdownit({
22-
html: true,
23-
});
24-
if (event.target.hash == '#desc-preview'){
21+
if (event.target.hash == '#desc-preview') {
2522
var editor_value = $('#desc-editor').val();
2623
$(event.target.hash).html(
27-
md.render(editor_value)
24+
window.challenge.render(editor_value)
2825
);
2926
}
3027
});
3128
$('#new-desc-edit').on('shown.bs.tab', function (event) {
32-
var md = window.markdownit({
33-
html: true,
34-
});
35-
if (event.target.hash == '#new-desc-preview'){
29+
if (event.target.hash == '#new-desc-preview') {
3630
var editor_value = $('#new-desc-editor').val();
3731
$(event.target.hash).html(
38-
md.render(editor_value)
32+
window.challenge.render(editor_value)
3933
);
4034
}
4135
});

CTFd/plugins/keys/__init__.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,20 @@ class CTFdStaticKey(BaseKey):
2424
}
2525

2626
@staticmethod
27-
def compare(saved, provided):
27+
def compare(chal_key_obj, provided):
28+
saved = chal_key_obj.flag
29+
data = chal_key_obj.data
30+
2831
if len(saved) != len(provided):
2932
return False
3033
result = 0
31-
for x, y in zip(saved, provided):
32-
result |= ord(x) ^ ord(y)
34+
35+
if data == "case_insensitive":
36+
for x, y in zip(saved.lower(), provided.lower()):
37+
result |= ord(x) ^ ord(y)
38+
else:
39+
for x, y in zip(saved, provided):
40+
result |= ord(x) ^ ord(y)
3341
return result == 0
3442

3543

@@ -42,8 +50,15 @@ class CTFdRegexKey(BaseKey):
4250
}
4351

4452
@staticmethod
45-
def compare(saved, provided):
46-
res = re.match(saved, provided, re.IGNORECASE)
53+
def compare(chal_key_obj, provided):
54+
saved = chal_key_obj.flag
55+
data = chal_key_obj.data
56+
57+
if data == "case_insensitive":
58+
res = re.match(saved, provided, re.IGNORECASE)
59+
else:
60+
res = re.match(saved, provided)
61+
4762
return res and res.group() == provided
4863

4964

Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
<label for="create-key-regex" class="control-label">Enter Regex Key Data</label>
2-
<input type="text" id="create-key-regex" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
2+
<input type="text" id="create-key-regex" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
3+
<div class="form-check form-check-inline">
4+
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive">
5+
<label class="form-check-label" for="keydata">Case Insensitive</label>
6+
</div>
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
<div class="modal-dialog">
2-
<div class="modal-content">
3-
<div class="modal-header text-center">
4-
<div class="container">
5-
<div class="row">
6-
<div class="col-md-12">
7-
<h3 class="text-center">Regex Key</h3>
8-
</div>
9-
</div>
10-
</div>
11-
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
12-
<span aria-hidden="true">&times;</span>
13-
</button>
14-
</div>
15-
<div class="modal-body">
16-
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
17-
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
18-
<input type="hidden" id="key-type" name="key_type" value="regex">
19-
<input type="hidden" id="key-id">
20-
<hr>
21-
<div class="form-group">
22-
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
23-
<button id="submit-keys" class="btn btn-success float-right">Update</button>
24-
</div>
25-
</form>
26-
</div>
27-
</div>
2+
<div class="modal-content">
3+
<div class="modal-header text-center">
4+
<div class="container">
5+
<div class="row">
6+
<div class="col-md-12">
7+
<h3 class="text-center">Regex Key</h3>
8+
</div>
9+
</div>
10+
</div>
11+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
12+
<span aria-hidden="true">&times;</span>
13+
</button>
14+
</div>
15+
<div class="modal-body">
16+
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
17+
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
18+
<div class="form-check form-check-inline">
19+
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
20+
{% if data %}checked{% endif %}>
21+
<label class="form-check-label" for="keydata">Case Insensitive</label>
22+
</div>
23+
<input type="hidden" id="key-type" name="key_type" value="regex">
24+
<input type="hidden" id="key-id">
25+
<hr>
26+
<div class="form-group">
27+
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
28+
<button id="submit-keys" class="btn btn-success float-right">Update</button>
29+
</div>
30+
</form>
31+
</div>
32+
</div>
2833
</div>

0 commit comments

Comments
 (0)