* 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
selenium-screenshot-testing
Kevin Chung 2018-05-03 18:04:39 -04:00 committed by GitHub
parent 9c812ad52e
commit 36c83b59bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 695 additions and 429 deletions

2
.flaskenv Normal file
View File

@ -0,0 +1,2 @@
FLASK_ENV=development
FLASK_RUN_PORT=4000

View File

@ -15,6 +15,6 @@ before_script:
- psql -c 'create database ctfd;' -U postgres - psql -c 'create database ctfd;' -U postgres
script: script:
- pep8 --ignore E501,E712 CTFd/ tests/ - pep8 --ignore E501,E712 CTFd/ tests/
- nosetests -d - nosetests -v -d --with-randomly
after_success: after_success:
- codecov - codecov

View File

@ -147,6 +147,13 @@ def admin_config():
utils.set_config("mail_username", None) utils.set_config("mail_username", None)
utils.set_config("mail_password", None) utils.set_config("mail_password", None)
if request.files.get('ctf_logo', None):
ctf_logo = request.files['ctf_logo']
file_id, file_loc = utils.upload_file(ctf_logo, None)
utils.set_config("ctf_logo", file_loc)
elif request.form.get('ctf_logo') == '':
utils.set_config("ctf_logo", None)
utils.set_config("ctf_name", request.form.get('ctf_name', None)) utils.set_config("ctf_name", request.form.get('ctf_name', None))
utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
utils.set_config('css', request.form.get('css', None)) utils.set_config('css', request.form.get('css', None))
@ -176,6 +183,7 @@ def admin_config():
cache.clear() cache.clear()
ctf_name = utils.get_config('ctf_name') ctf_name = utils.get_config('ctf_name')
ctf_logo = utils.get_config('ctf_logo')
ctf_theme = utils.get_config('ctf_theme') ctf_theme = utils.get_config('ctf_theme')
hide_scores = utils.get_config('hide_scores') hide_scores = utils.get_config('hide_scores')
css = utils.get_config('css') css = utils.get_config('css')
@ -216,6 +224,7 @@ def admin_config():
return render_template( return render_template(
'admin/config.html', 'admin/config.html',
ctf_name=ctf_name, ctf_name=ctf_name,
ctf_logo=ctf_logo,
ctf_theme_config=ctf_theme, ctf_theme_config=ctf_theme,
css=css, css=css,
start=start, start=start,

View File

@ -100,6 +100,19 @@ def admin_chal_detail(chalid):
return jsonify(data) return jsonify(data)
@admin_challenges.route('/admin/chal/<int:chalid>/solves', methods=['GET'])
@admins_only
def admin_chal_solves(chalid):
response = {'teams': []}
if utils.hide_scores():
return jsonify(response)
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid).order_by(
Solves.date.asc())
for solve in solves:
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
return jsonify(response)
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST']) @admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_tags(chalid): def admin_tags(chalid):

View File

@ -57,6 +57,10 @@ def admin_create_team():
affiliation = request.form.get('affiliation', None) affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None) country = request.form.get('country', None)
admin_user = True if request.form.get('admin', None) == 'on' else False
verified = True if request.form.get('verified', None) == 'on' else False
hidden = True if request.form.get('hidden', None) == 'on' else False
errors = [] errors = []
if not name: if not name:
@ -92,6 +96,10 @@ def admin_create_team():
team.affiliation = affiliation team.affiliation = affiliation
team.country = country team.country = country
team.admin = admin_user
team.verified = verified
team.hidden = hidden
db.session.add(team) db.session.add(team)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@ -119,24 +127,6 @@ def admin_team(teamid):
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing, return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
place=place, wrong_keys=wrong_keys, awards=awards) place=place, wrong_keys=wrong_keys, awards=awards)
elif request.method == 'POST': elif request.method == 'POST':
admin_user = request.form.get('admin', None)
if admin_user:
admin_user = True if admin_user == 'true' else False
user.admin = admin_user
# Set user.banned to hide admins from scoreboard
user.banned = admin_user
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
verified = request.form.get('verified', None)
if verified:
verified = True if verified == 'true' else False
user.verified = verified
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
name = request.form.get('name', None) name = request.form.get('name', None)
password = request.form.get('password', None) password = request.form.get('password', None)
email = request.form.get('email', None) email = request.form.get('email', None)
@ -144,6 +134,10 @@ def admin_team(teamid):
affiliation = request.form.get('affiliation', None) affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None) country = request.form.get('country', None)
admin_user = True if request.form.get('admin', None) == 'on' else False
verified = True if request.form.get('verified', None) == 'on' else False
hidden = True if request.form.get('hidden', None) == 'on' else False
errors = [] errors = []
if email: if email:
@ -177,6 +171,9 @@ def admin_team(teamid):
user.website = website user.website = website
user.affiliation = affiliation user.affiliation = affiliation
user.country = country user.country = country
user.admin = admin_user
user.verified = verified
user.banned = hidden
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return jsonify({'data': ['success']}) return jsonify({'data': ['success']})

View File

@ -29,7 +29,7 @@ def confirm_user(data=None):
if data and request.method == "GET": if data and request.method == "GET":
try: try:
s = TimedSerializer(app.config['SECRET_KEY']) s = TimedSerializer(app.config['SECRET_KEY'])
email = s.loads(utils.base64decode(data, urldecode=True), max_age=1800) email = s.loads(utils.base64decode(data), max_age=1800)
except BadTimeSignature: except BadTimeSignature:
return render_template('confirm.html', errors=['Your confirmation link has expired']) return render_template('confirm.html', errors=['Your confirmation link has expired'])
except (BadSignature, TypeError, base64.binascii.Error): except (BadSignature, TypeError, base64.binascii.Error):
@ -86,7 +86,7 @@ def reset_password(data=None):
if data is not None: if data is not None:
try: try:
s = TimedSerializer(app.config['SECRET_KEY']) s = TimedSerializer(app.config['SECRET_KEY'])
name = s.loads(utils.base64decode(data, urldecode=True), max_age=1800) name = s.loads(utils.base64decode(data), max_age=1800)
except BadTimeSignature: except BadTimeSignature:
return render_template('reset_password.html', errors=['Your link has expired']) return render_template('reset_password.html', errors=['Your link has expired'])
except (BadSignature, TypeError, base64.binascii.Error): except (BadSignature, TypeError, base64.binascii.Error):

View File

@ -146,7 +146,7 @@ class CTFdStandardChallenge(BaseChallenge):
provided_key = request.form['key'].strip() provided_key = request.form['key'].strip()
chal_keys = Keys.query.filter_by(chal=chal.id).all() chal_keys = Keys.query.filter_by(chal=chal.id).all()
for chal_key in chal_keys: for chal_key in chal_keys:
if get_key_class(chal_key.type).compare(chal_key.flag, provided_key): if get_key_class(chal_key.type).compare(chal_key, provided_key):
return True, 'Correct' return True, 'Correct'
return False, 'Incorrect' return False, 'Incorrect'

View File

@ -1,23 +1,17 @@
// Markdown Preview // Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) { $('#desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#desc-preview') { if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val(); var editor_value = $('#desc-editor').val();
$(event.target.hash).html( $(event.target.hash).html(
md.render(editor_value) window.challenge.render(editor_value)
); );
} }
}); });
$('#new-desc-edit').on('shown.bs.tab', function (event) { $('#new-desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#new-desc-preview') { if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val(); var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html( $(event.target.hash).html(
md.render(editor_value) window.challenge.render(editor_value)
); );
} }
}); });

View File

@ -1,25 +1,35 @@
$('#submit-key').unbind('click'); window.challenge.renderer = new markdownit({
$('#submit-key').click(function (e) { html: true,
e.preventDefault();
submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val())
}); });
$("#answer-input").keyup(function(event){ window.challenge.preRender = function(){
if(event.keyCode == 13){
$("#submit-key").click();
}
});
$(".input-field").bind({ };
focus: function() {
$(this).parent().addClass('input--filled' ); window.challenge.render = function(markdown){
$label = $(this).siblings(".input-label"); return window.challenge.renderer.render(markdown);
}, };
blur: function() {
if ($(this).val() === '') {
$(this).parent().removeClass('input--filled' ); window.challenge.postRender = function(){
$label = $(this).siblings(".input-label");
$label.removeClass('input--hide' ); };
}
window.challenge.submit = function(cb, preview){
var chal_id = $('#chal-id').val();
var answer = $('#answer-input').val();
var nonce = $('#nonce').val();
var url = "/chal/";
if (preview) {
url = "/admin/chal/";
} }
$.post(script_root + url + chal_id, {
key: answer,
nonce: nonce
}, function (data) {
cb(data);
}); });
};

View File

@ -18,24 +18,18 @@ $('#limit_max_attempts').change(function() {
// Markdown Preview // Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) { $('#desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#desc-preview') { if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val(); var editor_value = $('#desc-editor').val();
$(event.target.hash).html( $(event.target.hash).html(
md.render(editor_value) window.challenge.render(editor_value)
); );
} }
}); });
$('#new-desc-edit').on('shown.bs.tab', function (event) { $('#new-desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#new-desc-preview') { if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val(); var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html( $(event.target.hash).html(
md.render(editor_value) window.challenge.render(editor_value)
); );
} }
}); });

View File

@ -24,10 +24,18 @@ class CTFdStaticKey(BaseKey):
} }
@staticmethod @staticmethod
def compare(saved, provided): def compare(chal_key_obj, provided):
saved = chal_key_obj.flag
data = chal_key_obj.data
if len(saved) != len(provided): if len(saved) != len(provided):
return False return False
result = 0 result = 0
if data == "case_insensitive":
for x, y in zip(saved.lower(), provided.lower()):
result |= ord(x) ^ ord(y)
else:
for x, y in zip(saved, provided): for x, y in zip(saved, provided):
result |= ord(x) ^ ord(y) result |= ord(x) ^ ord(y)
return result == 0 return result == 0
@ -42,8 +50,15 @@ class CTFdRegexKey(BaseKey):
} }
@staticmethod @staticmethod
def compare(saved, provided): def compare(chal_key_obj, provided):
saved = chal_key_obj.flag
data = chal_key_obj.data
if data == "case_insensitive":
res = re.match(saved, provided, re.IGNORECASE) res = re.match(saved, provided, re.IGNORECASE)
else:
res = re.match(saved, provided)
return res and res.group() == provided return res and res.group() == provided

View File

@ -1,2 +1,6 @@
<label for="create-key-regex" class="control-label">Enter Regex Key Data</label> <label for="create-key-regex" class="control-label">Enter Regex Key Data</label>
<input type="text" id="create-key-regex" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data"> <input type="text" id="create-key-regex" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive">
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>

View File

@ -15,6 +15,11 @@
<div class="modal-body"> <div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}"> <form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data"> <input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
{% if data %}checked{% endif %}>
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>
<input type="hidden" id="key-type" name="key_type" value="regex"> <input type="hidden" id="key-type" name="key_type" value="regex">
<input type="hidden" id="key-id"> <input type="hidden" id="key-id">
<hr> <hr>

View File

@ -1,2 +1,6 @@
<label for="create-key-static" class="control-label">Enter Static Key Data</label> <label for="create-key-static" class="control-label">Enter Static Key Data</label>
<input type="text" id="create-key-static" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data"> <input type="text" id="create-key-static" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive">
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>

View File

@ -14,7 +14,13 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{ id }}"> <form method="POST" action="{{ script_root }}/admin/keys/{{ id }}">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data"> <input type="text" id="key-data" class="form-control" name="key" value="{{ key }}"
placeholder="Enter static key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
{% if data %}checked{% endif %}>
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>
<input type="hidden" id="key-type" name="key_type" value="static"> <input type="hidden" id="key-type" name="key_type" value="static">
<input type="hidden" id="key-id"> <input type="hidden" id="key-id">
<hr> <hr>

View File

@ -4,8 +4,6 @@ pre {
} }
.chal-desc { .chal-desc {
padding-left: 30px;
padding-right: 30px;
font-size: 14px; font-size: 14px;
} }

View File

@ -1,6 +1,10 @@
$.ajaxSetup({ cache: false }); $.ajaxSetup({ cache: false });
window.challenge = new Object();
function load_chal_template(challenge){ function load_chal_template(challenge){
$.getScript(script_root + challenge.scripts.modal, function () {
console.log('loaded renderer');
$.get(script_root + challenge.templates.create, function (template_data) { $.get(script_root + challenge.templates.create, function (template_data) {
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
$("#create-chal-entry-div").html(template.render({'nonce': nonce, 'script_root': script_root})); $("#create-chal-entry-div").html(template.render({'nonce': nonce, 'script_root': script_root}));
@ -8,6 +12,7 @@ function load_chal_template(challenge){
console.log('loaded'); console.log('loaded');
}); });
}); });
});
} }
$.get(script_root + '/admin/chal_types', function(data){ $.get(script_root + '/admin/chal_types', function(data){

View File

@ -1,18 +1,17 @@
var challenges = {}; var challenges = {};
window.challenge = new Object();
function load_chal_template(id, success_cb){ function load_chal_template(id, success_cb){
var obj = $.grep(challenges['game'], function (e) { $.get(script_root + "/admin/chal/" + id, function (obj) {
return e.id == id; $.getScript(script_root + obj.type_data.scripts.modal, function () {
})[0]; console.log('loaded renderer');
$.get(script_root + "/admin/chal/" + id, function (challenge_data) {
$.get(script_root + obj.type_data.templates.update, function (template_data) { $.get(script_root + obj.type_data.templates.update, function (template_data) {
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
challenge_data['nonce'] = $('#nonce').val(); obj['nonce'] = $('#nonce').val();
challenge_data['script_root'] = script_root; obj['script_root'] = script_root;
$("#update-modals-entry-div").html(template.render(challenge_data)); $("#update-modals-entry-div").html(template.render(obj));
$.ajax({ $.ajax({
url: script_root + obj.type_data.scripts.update, url: script_root + obj.type_data.scripts.update,
@ -22,6 +21,7 @@ function load_chal_template(id, success_cb){
}); });
}); });
}); });
});
} }
function load_challenge_preview(id){ function load_challenge_preview(id){
@ -30,55 +30,63 @@ function load_challenge_preview(id){
function render_challenge_preview(chal_id){ function render_challenge_preview(chal_id){
var preview_window = $('#challenge-preview'); var preview_window = $('#challenge-preview');
var md = window.markdownit({ $.get(script_root + "/admin/chal/" + chal_id, function(obj){
html: true, $.getScript(script_root + obj.type_data.scripts.modal, function () {
}); console.log('loaded renderer');
$.get(script_root + "/admin/chal/" + chal_id, function(challenge_data){
$.get(script_root + challenge_data.type_data.templates.modal, function (template_data) { $.get(script_root + obj.type_data.templates.modal, function (template_data) {
preview_window.empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
challenge_data['description'] = md.render(challenge_data['description']); window.challenge.preRender()
challenge_data['script_root'] = script_root;
var challenge = template.render(challenge_data); obj['description'] = window.challenge.render(obj['description']);
obj['script_root'] = script_root;
preview_window.append(challenge); var challenge = template.render(obj);
preview_window.html(challenge);
$('#submit-key').click(function (e) {
e.preventDefault();
$('#submit-key').addClass("disabled-button");
$('#submit-key').prop('disabled', true);
window.challenge.submit(function (data) {
renderSubmissionResponse(data)
}, preview=true);
});
$("#answer-input").keyup(function (event) {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
window.challenge.postRender()
$.getScript(script_root + challenge_data.type_data.scripts.modal, function () {
preview_window.modal(); preview_window.modal();
}); });
}); });
}); });
} }
function loadchals(cb){
$.post(script_root + "/admin/chals", {
'nonce': $('#nonce').val()
}, function (data) {
var categories = [];
challenges = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) { function loadsolves(id) {
if ($.inArray(challenges['game'][i].category, categories) == -1) { $.get(script_root + '/admin/chal/' + id + '/solves', function (data) {
categories.push(challenges['game'][i].category) var teams = data['teams'];
} var box = $('#challenge-solves-body');
} var modal = $('#challenge-solves-modal')
box.empty();
if (cb) { for (var i = 0; i < teams.length; i++) {
cb(); var id = teams[i].id;
var name = teams[i].name;
var date = moment(teams[i].date).local().format('MMMM Do, h:mm:ss A');
box.append('<tr><td><a href="team/{0}">{1}</td><td><small>{2}</small></td></tr>'.format(id, htmlentities(name), date));
} }
modal.modal();
}); });
} }
loadchals(function(){
$('.edit-challenge').click(function (e) {
var id = $(this).attr('chal-id');
load_chal_template(id, function () {
openchal(id);
});
});
});
function loadhint(hintid) { function loadhint(hintid) {
var md = window.markdownit({ var md = window.markdownit({
@ -107,12 +115,7 @@ function loadhint(hintid) {
}); });
} }
function submitkey(chal, key, nonce){ function renderSubmissionResponse(data, cb) {
$.post(script_root + "/admin/chal/" + chal, {
key: key,
nonce: nonce
}, function (data) {
console.log(data);
var result = $.parseJSON(JSON.stringify(data)); var result = $.parseJSON(JSON.stringify(data));
var result_message = $('#result-message'); var result_message = $('#result-message');
@ -164,7 +167,10 @@ function submitkey(chal, key, nonce){
$('#submit-key').removeClass("disabled-button"); $('#submit-key').removeClass("disabled-button");
$('#submit-key').prop('disabled', false); $('#submit-key').prop('disabled', false);
}, 3000); }, 3000);
});
if (cb) {
cb(result);
}
} }
$(document).ready(function () { $(document).ready(function () {
@ -194,9 +200,24 @@ $(document).ready(function () {
}); });
}); });
$('.edit-challenge').click(function (e) {
var id = $(this).attr('chal-id');
load_chal_template(id, function () {
openchal(id);
});
});
$('.preview-challenge').click(function (e) { $('.preview-challenge').click(function (e) {
var chal_id = $(this).attr('chal-id'); var chal_id = $(this).attr('chal-id');
load_challenge_preview(chal_id); load_challenge_preview(chal_id);
}); });
$('.stats-challenge').click(function (e) {
var chal_id = $(this).attr('chal-id');
var title = $(this).attr('title') || $(this).attr('data-original-title');
$('#challenge-solves-title').text(title);
loadsolves(chal_id);
});
}); });

View File

@ -17,13 +17,9 @@ function load_edit_key_modal(key_id, key_type_name) {
} }
function create_key(chal, key, key_type) { function create_key(chal, chal_data) {
$.post(script_root + "/admin/keys", { chal_data.push({name: 'nonce', value: $('#nonce').val()});
chal: chal, $.post(script_root + "/admin/keys", chal_data, function (data) {
key: key,
key_type: key_type,
nonce: $('#nonce').val()
}, function (data) {
if (data == "1"){ if (data == "1"){
loadkeys(chal); loadkeys(chal);
$("#create-keys").modal('toggle'); $("#create-keys").modal('toggle');
@ -73,17 +69,12 @@ function deletekey(key_id){
} }
function updatekey(){ function updatekey(){
var edit_key_modal = $('#edit-keys form').serializeArray();
var key_id = $('#key-id').val(); var key_id = $('#key-id').val();
var chal = $("#update-keys").attr('chal-id'); var chal = $("#update-keys").attr('chal-id');
var key_data = $('#key-data').val();
var key_type = $('#key-type').val(); $.post(script_root + '/admin/keys/'+key_id, edit_key_modal, function(data){
var nonce = $('#nonce').val();
$.post(script_root + '/admin/keys/'+key_id, {
'chal':chal,
'key':key_data,
'key_type': key_type,
'nonce': nonce
}, function(data){
if (data == "1") { if (data == "1") {
loadkeys(chal); loadkeys(chal);
$('#edit-keys').modal('toggle'); $('#edit-keys').modal('toggle');
@ -132,9 +123,13 @@ $(document).ready(function () {
$('#create-keys-submit').click(function (e) { $('#create-keys-submit').click(function (e) {
e.preventDefault(); e.preventDefault();
var chal_data = $('#create-keys-entry-div :input').serializeArray();
var chalid = $("#update-keys").attr('chal-id'); var chalid = $("#update-keys").attr('chal-id');
var key_data = $('#create-keys').find('input[name=key]').val(); chal_data.push({name: 'chal', value: chalid});
var key_type = $('#create-keys-select').val(); var key_type = $('#create-keys-select').val();
create_key(chalid, key_data, key_type); chal_data.push({name: 'key_type', value: key_type});
create_key(chalid, chal_data);
}); });
}); });

View File

@ -19,6 +19,7 @@
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/nunjucks.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/nunjucks.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var script_root = "{{ request.script_root }}"; var script_root = "{{ request.script_root }}";
var csrf_nonce = "{{ nonce }}";
</script> </script>
{% block stylesheets %} {% endblock %} {% block stylesheets %} {% endblock %}
</head> </head>

View File

@ -7,6 +7,7 @@
{% block content %} {% block content %}
{% include "admin/modals/challenges/challenges.html" %} {% include "admin/modals/challenges/challenges.html" %}
{% include "admin/modals/challenges/challenges-solves.html" %}
{% include "admin/modals/tags/tags.html" %} {% include "admin/modals/tags/tags.html" %}
{% include "admin/modals/files/files.html" %} {% include "admin/modals/files/files.html" %}
@ -57,9 +58,9 @@
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.type }}</td> <td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.type }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"> <td class="d-none d-md-table-cell d-lg-table-cell text-center">
{% if challenge.hidden %} {% if challenge.hidden %}
<button class="btn-sm btn-danger" type="submit" disabled>Hidden</button> <span class="badge badge-danger">hidden</span>
{% else %} {% else %}
<button class="btn-sm btn-success" type="submit" disabled>Visible</button> <span class="badge badge-success">visible</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@ -75,6 +76,13 @@
<i class="btn-fa fas fa-file-alt" aria-hidden="true" chal-id="{{ challenge.id }}"></i> <i class="btn-fa fas fa-file-alt" aria-hidden="true" chal-id="{{ challenge.id }}"></i>
</span> </span>
<span class="stats-challenge" data-toggle="tooltip"
data-placement="top" chal-id="{{ challenge.id }}"
title="{{ challenge.name }} solves">
<i class="btn-fa fas fa-chart-bar" aria-hidden="true"
chal-id="{{ challenge.id }}"></i>
</span>
<span>&nbsp; &nbsp;</span> <span>&nbsp; &nbsp;</span>
<span class="edit-keys" data-toggle="tooltip" data-placement="top" <span class="edit-keys" data-toggle="tooltip" data-placement="top"

View File

@ -28,7 +28,7 @@
</ul> </ul>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<form method="POST" autocomplete="off" class="w-100"> <form method="POST" enctype="multipart/form-data" autocomplete="off" class="w-100">
{% for error in errors %} {% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert"> <div class="alert alert-danger alert-dismissable" role="alert">
<span class="sr-only">Error:</span> <span class="sr-only">Error:</span>
@ -43,7 +43,31 @@
<div role="tabpanel" class="tab-pane active" id="appearance-section"> <div role="tabpanel" class="tab-pane active" id="appearance-section">
<div class="form-group"> <div class="form-group">
<label for="ctf_name">CTF Name:</label> <label for="ctf_name">CTF Name:</label>
<input class="form-control" id='ctf_name' name='ctf_name' type='text' placeholder="CTF Name" {% if ctf_name is defined and ctf_name != None %}value="{{ ctf_name }}"{% endif %}> <input class="form-control" id='ctf_name' name='ctf_name' type='text' placeholder="CTF Name"
{% if ctf_name is defined and ctf_name != None %}value="{{ ctf_name }}"{% endif %}>
</div>
<div class="form-group">
<label for="ctf_logo">CTF Logo:</label>
{% if ctf_logo %}
<div class="d-block py-3">
<img id="ctf_logo_preview" class="img-responsive ctf_logo" src="{{ request.script_root }}/files/{{ ctf_logo }}"
height="25">
<button type="button" class="btn-sm btn-danger float-right" onclick="
$('#ctf_logo').attr('type', 'text');
$('#ctf_logo').attr('placeholder', 'Logo will be removed on next update');
$('#ctf_logo').attr('value', '');
$('#ctf_logo').attr('readonly', 'true');
$('#ctf_logo_preview').css('visibility', 'hidden');
">
Remove Logo
</button>
</div>
{% endif %}
<input class="form-control" id='ctf_logo' name='ctf_logo' type='file' placeholder="CTF Logo"
{% if ctf_logo is defined and ctf_logo != None %}value="{{ ctf_logo }}"{% endif %}>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -0,0 +1,38 @@
<div id="challenge-solves-modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 id="challenge-solves-title" class="text-center">Solves</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="challenge-solves-body">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,2 +1,3 @@
<div id="challenge-preview" class="modal fade" tabindex="-1"> <div id="challenge-preview" class="modal fade" tabindex="-1">
</div> </div>

View File

@ -82,6 +82,20 @@
<input type="text" class="form-control" name="country" id="country" <input type="text" class="form-control" name="country" id="country"
placeholder="Enter country"> placeholder="Enter country">
</div> </div>
<div class="form-group">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="admin" id="admin-checkbox">
<label class="form-check-label" for="admin-checkbox">Admin</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="verified" id="verified-checkbox">
<label class="form-check-label" for="verified-checkbox">Verified</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="hidden" id="hidden-checkbox">
<label class="form-check-label" for="hidden-checkbox">Hidden</label>
</div>
</div>
<div id="results"> <div id="results">
</div> </div>
@ -133,16 +147,18 @@
</td> </td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Email</b> <td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Email</b>
</td> </td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Website</b> <td class="d-none"><b>Website</b>
</td> </td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Affiliation</b> <td class="d-none"><b>Affiliation</b>
</td> </td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Country</b> <td class="d-none"><b>Country</b>
</td> </td>
<td class="text-center"><b>Admin</b> <td class="text-center"><b>Admin</b>
</td> </td>
<td class="text-center"><b>Verified</b> <td class="text-center"><b>Verified</b>
</td> </td>
<td class="text-center"><b>Hidden</b>
</td>
<td class="text-center"><b>Settings</b> <td class="text-center"><b>Settings</b>
</td> </td>
</tr> </tr>
@ -154,7 +170,7 @@
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a> <td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td> </td>
<td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ team.email }}">{{ team.email | truncate(32) }}</td> <td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
<td class="team-website d-none d-md-table-cell d-lg-table-cell text-center"> <td class="team-website d-none text-center">
{% if team.website %} {% if team.website %}
<a href="{{ team.website }}" target="_blank"> <a href="{{ team.website }}" target="_blank">
<i class="btn-fa fas fa-external-link-alt" data-toggle="tooltip" data-placement="top" <i class="btn-fa fas fa-external-link-alt" data-toggle="tooltip" data-placement="top"
@ -162,21 +178,26 @@
</a> </a>
{% endif %} {% endif %}
</td> </td>
<td class="team-affiliation d-none d-md-table-cell d-lg-table-cell" value="{{ team.affiliation if team.affiliation is not none }}"> <td class="team-affiliation d-none" value="{{ team.affiliation if team.affiliation is not none }}">
<span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span> <span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
</td> </td>
<td class="team-country d-none d-md-table-cell d-lg-table-cell" value="{{ team.country if team.country is not none }}"> <td class="team-country d-none" value="{{ team.country if team.country is not none }}">
<span>{% if team.country %}{{ team.country }}{% endif %}</span> <span>{% if team.country %}{{ team.country }}{% endif %}</span>
</td> </td>
<td class="team-admin"> <td class="team-admin d-none d-md-table-cell d-lg-table-cell text-center" value="{{ team.admin }}">
<div class="center-block checkbox text-center"> {% if team.admin %}
<input type="checkbox" {% if team.admin %}checked{% endif %}> <span class="badge badge-primary">admin</span>
</div> {% endif %}
</td> </td>
<td class="team-verified"> <td class="team-verified d-none d-md-table-cell d-lg-table-cell text-center" value="{{ team.verified }}">
<div class="center-block checkbox text-center"> {% if team.verified %}
<input type="checkbox" {% if team.verified %}checked{% endif %}> <span class="badge badge-success">verified</span>
</div> {% endif %}
</td>
<td class="team-hidden d-none d-md-table-cell d-lg-table-cell text-center" value="{{ team.banned }}">
{% if team.banned %}
<span class="badge badge-danger">hidden</span>
{% endif %}
</td> </td>
<td class="text-center"><span> <td class="text-center"><span>
<span class="edit-team" data-toggle="tooltip" data-placement="top" <span class="edit-team" data-toggle="tooltip" data-placement="top"
@ -222,7 +243,7 @@
<script> <script>
var nonce = "{{ nonce }}"; var nonce = "{{ nonce }}";
function load_update_modal(id, name, email, website, affiliation, country){ function load_update_modal(id, name, email, website, affiliation, country, admin, verified, hidden){
var modal_form = $('#update-user-modal form'); var modal_form = $('#update-user-modal form');
modal_form.find('input[name=name]').val(name); modal_form.find('input[name=name]').val(name);
@ -233,6 +254,10 @@ function load_update_modal(id, name, email, website, affiliation, country){
modal_form.find('input[name=country]').val(country); modal_form.find('input[name=country]').val(country);
modal_form.find('input[name=password]').val(''); modal_form.find('input[name=password]').val('');
modal_form.find('input[name=admin]').prop('checked', admin);
modal_form.find('input[name=verified]').prop('checked', verified);
modal_form.find('input[name=hidden]').prop('checked', hidden);
if (id == 'new'){ if (id == 'new'){
$('#update-user-modal .modal-action').text('Create Team'); $('#update-user-modal .modal-action').text('Create Team');
} else { } else {
@ -280,24 +305,6 @@ $(document).ready(function () {
}) })
}); });
$('.team-admin input').on('change', function () {
var elem = $(this).parent().parent().parent();
var id = elem.find('.team-id').text().trim();
var admin = $(this).prop('checked');
console.log(admin);
$.post('{{ request.script_root }}/admin/team/' + id, {'admin': admin, 'nonce': nonce});
});
$('.team-verified input').on('change', function () {
var elem = $(this).parent().parent().parent();
var id = elem.find('.team-id').text().trim();
var verified = $(this).prop('checked');
console.log(verified);
$.post('{{ request.script_root }}/admin/team/' + id, {'verified': verified, 'nonce': nonce});
});
$('#send-user-email').click(function (e) { $('#send-user-email').click(function (e) {
e.preventDefault(); e.preventDefault();
var id = $('#email-user input[name="id"]').val(); var id = $('#email-user input[name="id"]').val();
@ -329,11 +336,15 @@ $(document).ready(function () {
var affiliation = elem.find('.team-affiliation').attr('value') || ''; var affiliation = elem.find('.team-affiliation').attr('value') || '';
var country = elem.find('.team-country').attr('value') || ''; var country = elem.find('.team-country').attr('value') || '';
load_update_modal(id, name, email, website, affiliation, country); var admin = elem.find('.team-admin').attr('value') == 'True' || false;
var verified = elem.find('.team-verified').attr('value') == 'True' || false;
var hidden = elem.find('.team-hidden').attr('value') == 'True' || false;
load_update_modal(id, name, email, website, affiliation, country, admin, verified, hidden);
}); });
$('.create-team').click(function () { $('.create-team').click(function () {
load_update_modal('new', '', '', '', '', ''); load_update_modal('new', '', '', '', '', '', false, false, false);
}); });
$('.delete-team').click(function () { $('.delete-team').click(function () {

View File

@ -2,6 +2,8 @@ var challenges;
var user_solves = []; var user_solves = [];
var templates = {}; var templates = {};
window.challenge = new Object();
function loadchal(id) { function loadchal(id) {
var obj = $.grep(challenges['game'], function (e) { var obj = $.grep(challenges['game'], function (e) {
return e.id == id; return e.id == id;
@ -20,26 +22,25 @@ function loadchalbyname(chalname) {
function updateChalWindow(obj) { function updateChalWindow(obj) {
$.get(script_root + "/chals/" + obj.id, function(challenge_data){ $.get(script_root + "/chals/" + obj.id, function(challenge_data){
$.getScript(script_root + obj.script, function(){
$.get(script_root + obj.template, function (template_data) { $.get(script_root + obj.template, function (template_data) {
$('#chal-window').empty(); $('#chal-window').empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
var solves = obj.solves == 1 ? " Solve" : " Solves"; var solves = obj.solves == 1 ? " Solve" : " Solves";
var solves = obj.solves + solves; var solves = obj.solves + solves;
var nonce = $('#nonce').val(); var nonce = $('#nonce').val();
var md = window.markdownit({ window.challenge.preRender();
html: true,
});
challenge_data['description'] = md.render(challenge_data['description']); challenge_data['description'] = window.challenge.render(challenge_data['description']);
challenge_data['script_root'] = script_root; challenge_data['script_root'] = script_root;
challenge_data['solves'] = solves; challenge_data['solves'] = solves;
$('#chal-window').append(template.render(challenge_data)); $('#chal-window').append(template.render(challenge_data));
$.getScript(script_root + obj.script,
function () {
// Handle Solves tab
$('.chal-solves').click(function (e) { $('.chal-solves').click(function (e) {
getsolves($('#chal-id').val()) getsolves($('#chal-id').val())
}); });
@ -58,9 +59,36 @@ function updateChalWindow(obj) {
$("#too-fast").slideUp(); $("#too-fast").slideUp();
}); });
// $('pre code').each(function(i, block) { $('#submit-key').click(function (e) {
// hljs.highlightBlock(block); e.preventDefault();
// }); $('#submit-key').addClass("disabled-button");
$('#submit-key').prop('disabled', true);
window.challenge.submit(function (data) {
renderSubmissionResponse(data)
});
});
$("#answer-input").keyup(function (event) {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
$(".input-field").bind({
focus: function () {
$(this).parent().addClass('input--filled');
$label = $(this).siblings(".input-label");
},
blur: function () {
if ($(this).val() === '') {
$(this).parent().removeClass('input--filled');
$label = $(this).siblings(".input-label");
$label.removeClass('input--hide');
}
}
});
window.challenge.postRender();
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name); window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
$('#chal-window').modal(); $('#chal-window').modal();
@ -76,13 +104,7 @@ $("#answer-input").keyup(function(event){
}); });
function submitkey(chal, key, nonce, cb) { function renderSubmissionResponse(data, cb){
$('#submit-key').addClass("disabled-button");
$('#submit-key').prop('disabled', true);
$.post(script_root + "/chal/" + chal, {
key: key,
nonce: nonce
}, function (data) {
var result = $.parseJSON(JSON.stringify(data)); var result = $.parseJSON(JSON.stringify(data));
var result_message = $('#result-message'); var result_message = $('#result-message');
@ -141,7 +163,6 @@ function submitkey(chal, key, nonce, cb) {
if (cb) { if (cb) {
cb(result); cb(result);
} }
})
} }
function marksolves(cb) { function marksolves(cb) {

View File

@ -28,12 +28,19 @@
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/nunjucks.min.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/nunjucks.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var script_root = "{{ request.script_root }}"; var script_root = "{{ request.script_root }}";
var csrf_nonce = "{{ nonce }}"
</script> </script>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<div class="container"> <div class="container">
<a href="{{ request.script_root }}/" class="navbar-brand">{{ ctf_name() }}</a> <a href="{{ request.script_root }}/" class="navbar-brand">
{% if ctf_logo() %}
<img class="img-responsive ctf_logo" src="{{ request.script_root }}/files/{{ ctf_logo() }}" height="25" alt="{{ ctf_name() }}">
{% else %}
{{ ctf_name() }}
{% endif %}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars" <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars"
aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>

View File

@ -150,6 +150,7 @@ def init_utils(app):
app.jinja_env.globals.update(can_register=can_register) app.jinja_env.globals.update(can_register=can_register)
app.jinja_env.globals.update(can_send_mail=can_send_mail) app.jinja_env.globals.update(can_send_mail=can_send_mail)
app.jinja_env.globals.update(ctf_name=ctf_name) app.jinja_env.globals.update(ctf_name=ctf_name)
app.jinja_env.globals.update(ctf_logo=ctf_logo)
app.jinja_env.globals.update(ctf_theme=ctf_theme) app.jinja_env.globals.update(ctf_theme=ctf_theme)
app.jinja_env.globals.update(get_configurable_plugins=get_configurable_plugins) app.jinja_env.globals.update(get_configurable_plugins=get_configurable_plugins)
app.jinja_env.globals.update(get_registered_scripts=get_registered_scripts) app.jinja_env.globals.update(get_registered_scripts=get_registered_scripts)
@ -211,6 +212,11 @@ def ctf_name():
return name if name else 'CTFd' return name if name else 'CTFd'
@cache.memoize()
def ctf_logo():
return get_config('ctf_logo')
@cache.memoize() @cache.memoize()
def ctf_theme(): def ctf_theme():
theme = get_config('ctf_theme') theme = get_config('ctf_theme')
@ -661,7 +667,7 @@ def verify_email(addr):
text = """Please click the following link to confirm your email address for {ctf_name}: {url}/{token}""".format( text = """Please click the following link to confirm your email address for {ctf_name}: {url}/{token}""".format(
ctf_name=get_config('ctf_name'), ctf_name=get_config('ctf_name'),
url=url_for('auth.confirm_user', _external=True), url=url_for('auth.confirm_user', _external=True),
token=base64encode(token, urlencode=True) token=base64encode(token)
) )
sendmail(addr, text) sendmail(addr, text)
@ -673,7 +679,7 @@ def forgot_password(email, team_name):
{0}/{1} {0}/{1}
""".format(url_for('auth.reset_password', _external=True), base64encode(token, urlencode=True)) """.format(url_for('auth.reset_password', _external=True), base64encode(token))
sendmail(email, text) sendmail(email, text)
@ -700,35 +706,30 @@ def sha512(string):
return hashlib.sha512(string).hexdigest() return hashlib.sha512(string).hexdigest()
def base64encode(s, urlencode=False): def base64encode(s):
if six.PY3 and isinstance(s, six.string_types): if six.PY3 and isinstance(s, six.string_types):
s = s.encode('utf-8') s = s.encode('utf-8')
else: else:
# Python 2 support because the base64 module doesnt like unicode # Python 2 support because the base64 module doesnt like unicode
s = str(s) s = str(s)
encoded = base64.urlsafe_b64encode(s) encoded = base64.urlsafe_b64encode(s).rstrip(b'\n=')
if six.PY3: if six.PY3:
try: try:
encoded = encoded.decode('utf-8') encoded = encoded.decode('utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if urlencode:
encoded = quote(encoded)
return encoded return encoded
def base64decode(s, urldecode=False): def base64decode(s):
if urldecode:
s = unquote(s)
if six.PY3 and isinstance(s, six.string_types): if six.PY3 and isinstance(s, six.string_types):
s = s.encode('utf-8') s = s.encode('utf-8')
else: else:
# Python 2 support because the base64 module doesnt like unicode # Python 2 support because the base64 module doesnt like unicode
s = str(s) s = str(s)
decoded = base64.urlsafe_b64decode(s) decoded = base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b'='))
if six.PY3: if six.PY3:
try: try:
decoded = decoded.decode('utf-8') decoded = decoded.decode('utf-8')

View File

@ -4,11 +4,11 @@ import functools
def during_ctf_time_only(f): def during_ctf_time_only(f):
''' """
Decorator to restrict an endpoint to only be seen during a CTF Decorator to restrict an endpoint to only be seen during a CTF
:param f: :param f:
:return: :return:
''' """
@functools.wraps(f) @functools.wraps(f)
def during_ctf_time_only_wrapper(*args, **kwargs): def during_ctf_time_only_wrapper(*args, **kwargs):
if utils.ctftime() or utils.is_admin(): if utils.ctftime() or utils.is_admin():
@ -28,6 +28,11 @@ def during_ctf_time_only(f):
def require_verified_emails(f): def require_verified_emails(f):
"""
Decorator to restrict an endpoint to users with confirmed active email addresses
:param f:
:return:
"""
@functools.wraps(f) @functools.wraps(f)
def require_verified_emails_wrapper(*args, **kwargs): def require_verified_emails_wrapper(*args, **kwargs):
if utils.get_config('verify_emails'): if utils.get_config('verify_emails'):
@ -39,6 +44,11 @@ def require_verified_emails(f):
def viewable_without_authentication(status_code=None): def viewable_without_authentication(status_code=None):
"""
Decorator that allows users to view the specified endpoint if viewing challenges without authentication is enabled
:param status_code:
:return:
"""
def viewable_without_authentication_decorator(f): def viewable_without_authentication_decorator(f):
@functools.wraps(f) @functools.wraps(f)
def viewable_without_authentication_wrapper(*args, **kwargs): def viewable_without_authentication_wrapper(*args, **kwargs):
@ -55,6 +65,11 @@ def viewable_without_authentication(status_code=None):
def authed_only(f): def authed_only(f):
"""
Decorator that requires the user to be authenticated
:param f:
:return:
"""
@functools.wraps(f) @functools.wraps(f)
def authed_only_wrapper(*args, **kwargs): def authed_only_wrapper(*args, **kwargs):
if session.get('id'): if session.get('id'):
@ -66,6 +81,11 @@ def authed_only(f):
def admins_only(f): def admins_only(f):
"""
Decorator that requires the user to be authenticated and an admin
:param f:
:return:
"""
@functools.wraps(f) @functools.wraps(f)
def admins_only_wrapper(*args, **kwargs): def admins_only_wrapper(*args, **kwargs):
if session.get('admin'): if session.get('admin'):

View File

View File

@ -6,4 +6,6 @@ rednose>=1.1.1
pep8>=1.7.0 pep8>=1.7.0
freezegun>=0.3.9 freezegun>=0.3.9
psycopg2>=2.7.3.1 psycopg2>=2.7.3.1
psycopg2-binary>=2.7.3.1
codecov>=2.0.9 codecov>=2.0.9
nose-randomly>=1.2.5

View File

@ -1,4 +1,4 @@
Flask==0.12.2 Flask==1.0.0
Flask-SQLAlchemy==2.3.2 Flask-SQLAlchemy==2.3.2
Flask-Session==0.3.1 Flask-Session==0.3.1
Flask-Caching==1.3.3 Flask-Caching==1.3.3
@ -19,3 +19,4 @@ netaddr==0.7.19
redis==2.10.6 redis==2.10.6
datafreeze==0.1.0 datafreeze==0.1.0
gevent==1.2.2 gevent==1.2.2
python-dotenv==0.8.2

View File

@ -16,6 +16,7 @@ def test_admin_post_config_values():
data = { data = {
'nonce': sess.get('nonce'), 'nonce': sess.get('nonce'),
'ctf_name': 'CTFd', 'ctf_name': 'CTFd',
'ctf_logo': '',
'ctf_theme': 'core', 'ctf_theme': 'core',
'workshop_mode': 'on', 'workshop_mode': 'on',
'paused': 'on', 'paused': 'on',
@ -59,6 +60,7 @@ def test_admin_post_config_values():
result = { result = {
'ctf_name': 'CTFd', 'ctf_name': 'CTFd',
'ctf_logo': None,
'ctf_theme': 'core', 'ctf_theme': 'core',
'workshop_mode': True, 'workshop_mode': True,
'paused': True, 'paused': True,

View File

@ -208,7 +208,7 @@ def test_admins_can_delete_challenges():
def test_admins_can_delete_challenges_with_extras(): def test_admins_can_delete_challenges_with_extras():
""""Test that admins can delete challenges that have a hint""" """Test that admins can delete challenges that have a hint"""
app = create_ctfd() app = create_ctfd()
with app.app_context(): with app.app_context():
client = login_as_user(app, name="admin", password="password") client = login_as_user(app, name="admin", password="password")
@ -274,6 +274,24 @@ def test_admin_chal_detail_returns_proper_data():
destroy_ctfd(app) destroy_ctfd(app)
def test_admin_load_chal_solves():
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
chal1 = gen_challenge(app.db)
flag1 = gen_flag(app.db, chal=chal1.id, flag='flag')
chal1_id = chal1.id
gen_solve(app.db, teamid=1, chalid=chal1_id)
r = client.get('/admin/chal/1/solves')
data = r.get_data(as_text=True)
assert json.loads(data)
destroy_ctfd(app)
def test_admins_can_create_teams(): def test_admins_can_create_teams():
'''Test that admins can create new teams''' '''Test that admins can create new teams'''
app = create_ctfd() app = create_ctfd()

View File

@ -117,8 +117,10 @@ def gen_file(db, chal, location):
return f return f
def gen_flag(db, chal, flag='flag', key_type='static'): def gen_flag(db, chal, flag='flag', key_type='static', data=None):
key = Keys(chal, flag, key_type) key = Keys(chal, flag, key_type)
if data:
key.data = data
db.session.add(key) db.session.add(key)
db.session.commit() db.session.commit()
return key return key

View File

@ -63,19 +63,14 @@ def test_base64encode():
if six.PY2: if six.PY2:
assert base64encode('abc123') == 'YWJjMTIz' assert base64encode('abc123') == 'YWJjMTIz'
assert base64encode(unicode('abc123')) == 'YWJjMTIz' assert base64encode(unicode('abc123')) == 'YWJjMTIz'
assert base64encode(unicode('"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4'), urlencode=True) == 'InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ%3D' assert base64encode(unicode('"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4')) == 'InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ'
assert base64encode('user+user@ctfd.io') == 'dXNlcit1c2VyQGN0ZmQuaW8=' assert base64encode('user+user@ctfd.io') == 'dXNlcit1c2VyQGN0ZmQuaW8'
assert base64encode('user+user@ctfd.io', urlencode=True) == 'dXNlcit1c2VyQGN0ZmQuaW8%3D' assert base64encode('😆') == '8J-Yhg'
assert base64encode('😆') == '8J-Yhg=='
assert base64encode('😆', urlencode=True) == '8J-Yhg%3D%3D'
else: else:
assert base64encode('abc123') == 'YWJjMTIz' assert base64encode('abc123') == 'YWJjMTIz'
assert base64encode('abc123') == 'YWJjMTIz' assert base64encode('"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4') == 'InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ'
assert base64encode('"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4', urlencode=True) == 'InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ%3D' assert base64encode('user+user@ctfd.io') == 'dXNlcit1c2VyQGN0ZmQuaW8'
assert base64encode('user+user@ctfd.io') == 'dXNlcit1c2VyQGN0ZmQuaW8=' assert base64encode('😆') == '8J-Yhg'
assert base64encode('user+user@ctfd.io', urlencode=True) == 'dXNlcit1c2VyQGN0ZmQuaW8%3D'
assert base64encode('😆') == '8J-Yhg=='
assert base64encode('😆', urlencode=True) == '8J-Yhg%3D%3D'
def test_base64decode(): def test_base64decode():
@ -83,17 +78,13 @@ def test_base64decode():
if six.PY2: if six.PY2:
assert base64decode('YWJjMTIz') == 'abc123' assert base64decode('YWJjMTIz') == 'abc123'
assert base64decode(unicode('YWJjMTIz')) == 'abc123' assert base64decode(unicode('YWJjMTIz')) == 'abc123'
assert base64decode(unicode('InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ%3D'), urldecode=True) == '"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4' assert base64decode(unicode('InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ')) == '"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4'
assert base64decode('8J-Yhg==') == '😆' assert base64decode('8J-Yhg') == '😆'
assert base64decode('8J-Yhg%3D%3D', urldecode=True) == '😆'
else: else:
assert base64decode('YWJjMTIz') == 'abc123' assert base64decode('YWJjMTIz') == 'abc123'
assert base64decode('YWJjMTIz') == 'abc123' assert base64decode('InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ') == '"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4'
assert base64decode('InRlc3RAbWFpbGluYXRvci5jb20iLkRHeGVvQS5sQ3NzVTNNMlF1QmZvaE8tRnRkZ0RRTEtiVTQ%3D', urldecode=True) == '"test@mailinator.com".DGxeoA.lCssU3M2QuBfohO-FtdgDQLKbU4' assert base64decode('dXNlcit1c2VyQGN0ZmQuaW8') == 'user+user@ctfd.io'
assert base64decode('dXNlcit1c2VyQGN0ZmQuaW8=') == 'user+user@ctfd.io' assert base64decode('8J-Yhg') == '😆'
assert base64decode('dXNlcit1c2VyQGN0ZmQuaW8%3D', urldecode=True) == 'user+user@ctfd.io'
assert base64decode('8J-Yhg==') == '😆'
assert base64decode('8J-Yhg%3D%3D', urldecode=True) == '😆'
def test_override_template(): def test_override_template():
@ -251,7 +242,7 @@ def test_verify_email(mock_smtp):
# This is currently not actually validated # This is currently not actually validated
msg = ("Please click the following link to confirm" msg = ("Please click the following link to confirm"
" your email address for CTFd:" " your email address for CTFd:"
" http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc%3D") " http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc")
ctf_name = get_config('ctf_name') ctf_name = get_config('ctf_name')
email_msg = MIMEText(msg) email_msg = MIMEText(msg)

View File

@ -118,7 +118,7 @@ def test_expired_confirmation_links():
client = login_as_user(app, name="user", password="password") client = login_as_user(app, name="user", password="password")
# user@user.com "2012-01-14 03:21:34" # user@user.com "2012-01-14 03:21:34"
confirm_link = 'http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc%3D' confirm_link = 'http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc'
r = client.get(confirm_link) r = client.get(confirm_link)
assert "Your confirmation link has expired" in r.get_data(as_text=True) assert "Your confirmation link has expired" in r.get_data(as_text=True)
@ -137,7 +137,7 @@ def test_invalid_confirmation_links():
client = login_as_user(app, name="user", password="password") client = login_as_user(app, name="user", password="password")
# user@user.com "2012-01-14 03:21:34" # user@user.com "2012-01-14 03:21:34"
confirm_link = 'http://localhost/confirm/a8375iyu<script>alert(1)<script>hn3048wueorighkgnsfg%3D%3D' confirm_link = 'http://localhost/confirm/a8375iyu<script>alert(1)<script>hn3048wueorighkgnsfg'
r = client.get(confirm_link) r = client.get(confirm_link)
assert "Your confirmation token is invalid" in r.get_data(as_text=True) assert "Your confirmation token is invalid" in r.get_data(as_text=True)
@ -180,7 +180,7 @@ def test_invalid_reset_password_link():
with app.test_client() as client: with app.test_client() as client:
# user@user.com "2012-01-14 03:21:34" # user@user.com "2012-01-14 03:21:34"
forgot_link = 'http://localhost/reset_password/5678ytfghjiu876tyfg<>hvbnmkoi9u87y6trdfcgvhbnm,lp09iujmk%3D' forgot_link = 'http://localhost/reset_password/5678ytfghjiu876tyfg<>hvbnmkoi9u87y6trdfcgvhbnm,lp09iujmk'
r = client.get(forgot_link) r = client.get(forgot_link)
assert "Your reset token is invalid" in r.get_data(as_text=True) assert "Your reset token is invalid" in r.get_data(as_text=True)

View File

@ -132,6 +132,46 @@ def test_submitting_correct_flag():
destroy_ctfd(app) destroy_ctfd(app)
def test_submitting_correct_static_case_insensitive_flag():
"""Test that correct static flags are correct if the static flag is marked case_insensitive"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
chal = gen_challenge(app.db)
flag = gen_flag(app.db, chal=chal.id, flag='flag', data="case_insensitive")
with client.session_transaction() as sess:
data = {
"key": 'FLAG',
"nonce": sess.get('nonce')
}
r = client.post('/chal/{}'.format(chal.id), data=data)
assert r.status_code == 200
resp = json.loads(r.data.decode('utf8'))
assert resp.get('status') == 1 and resp.get('message') == "Correct"
destroy_ctfd(app)
def test_submitting_correct_regex_case_insensitive_flag():
"""Test that correct regex flags are correct if the regex flag is marked case_insensitive"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
chal = gen_challenge(app.db)
flag = gen_flag(app.db, chal=chal.id, key_type='regex', flag='flag', data="case_insensitive")
with client.session_transaction() as sess:
data = {
"key": 'FLAG',
"nonce": sess.get('nonce')
}
r = client.post('/chal/{}'.format(chal.id), data=data)
assert r.status_code == 200
resp = json.loads(r.data.decode('utf8'))
assert resp.get('status') == 1 and resp.get('message') == "Correct"
destroy_ctfd(app)
def test_submitting_incorrect_flag(): def test_submitting_incorrect_flag():
"""Test that incorrect flags are incorrect""" """Test that incorrect flags are incorrect"""
app = create_ctfd() app = create_ctfd()

View File

@ -567,7 +567,7 @@ def test_user_can_confirm_email(mock_smtp):
assert r.location == "http://localhost/confirm" # We got redirected to /confirm assert r.location == "http://localhost/confirm" # We got redirected to /confirm
# Use precalculated confirmation secret # Use precalculated confirmation secret
r = client.get('http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc%3D') r = client.get('http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc')
assert r.location == 'http://localhost/challenges' assert r.location == 'http://localhost/challenges'
# The team is now verified # The team is now verified

6
wsgi.py Normal file
View File

@ -0,0 +1,6 @@
from CTFd import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True, threaded=True, host="127.0.0.1", port=4000)