Testing branch (#211)

* Extracting key checking logic to make it more extensible

* Add missing keys __init__ file

* Adding logging access and errors to Dockerfile

* Use template inheritance for page.html (#198)

* Fix exception on cofirmation screen (#202)

When a user attempts to confirm an e-mail address, an exception is thrown because the db session is closed prior to logging.

The line db.session.close() has to move after the logging, otherwise the team parameters from the orm object are discarded and an exception is thrown.

Closing the session after logging, fixes the issue.

* Adding custom key types for challenges

* Separating out admin.py, adding challenge types

* Don't let truncate affect edit modal

* File uploads no longer refresh page (#207)

Closes (#180)

* Fixing missing import

* Fixing mistake in flag JSON response

* Removing compare_digest to support Python 2.7.6

* Fixing inconsistencies in standard challenge modal

* Passing submission input over to template js

* Handling cases where data can't be found in the DOM better

* Don't refresh modal if it's just a refresh operation

* Fixing solving challenges while scoreboard is public

Induce a redirect to make user login

* Adding missing js file and fixing migration

* Fixing some visual glitches and streamlining challenge creation
selenium-screenshot-testing
Kevin Chung 2017-02-24 21:46:25 -05:00 committed by GitHub
parent 2e3df14764
commit fdb2c34d88
47 changed files with 2245 additions and 1313 deletions

1
.gitignore vendored
View File

@ -60,6 +60,5 @@ target/
.idea/
CTFd/static/uploads
CTFd/uploads
CTFd/plugins
.data/
.ctfd_secret_key

View File

@ -4,7 +4,7 @@ from distutils.version import StrictVersion
from flask import Flask
from jinja2 import FileSystemLoader
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import OperationalError
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy_utils import database_exists, create_database
from utils import get_config, set_config, cache, migrate, migrate_upgrade
@ -68,7 +68,7 @@ def create_app(config='CTFd.config.Config'):
from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard
from CTFd.auth import auth
from CTFd.admin import admin
from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_containers, admin_keys, admin_teams
from CTFd.utils import init_utils, init_errors, init_logs
init_utils(app)
@ -79,7 +79,15 @@ def create_app(config='CTFd.config.Config'):
app.register_blueprint(challenges)
app.register_blueprint(scoreboard)
app.register_blueprint(auth)
app.register_blueprint(admin)
app.register_blueprint(admin_statistics)
app.register_blueprint(admin_challenges)
app.register_blueprint(admin_teams)
app.register_blueprint(admin_scoreboard)
app.register_blueprint(admin_keys)
app.register_blueprint(admin_containers)
app.register_blueprint(admin_pages)
from CTFd.plugins import init_plugins

View File

@ -1,842 +0,0 @@
import hashlib
import json
import os
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
admin = Blueprint('admin', __name__)
@admin.route('/admin', methods=['GET'])
def admin_view():
if is_admin():
return redirect(url_for('admin.admin_graphs'))
return redirect(url_for('auth.login'))
@admin.route('/admin/graphs')
@admins_only
def admin_graphs():
return render_template('admin/graphs.html')
@admin.route('/admin/config', methods=['GET', 'POST'])
@admins_only
def admin_config():
if request.method == "POST":
start = None
end = None
if request.form.get('start'):
start = int(request.form['start'])
if request.form.get('end'):
end = int(request.form['end'])
try:
view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None))
view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None))
prevent_registration = bool(request.form.get('prevent_registration', None))
prevent_name_change = bool(request.form.get('prevent_name_change', None))
view_after_ctf = bool(request.form.get('view_after_ctf', None))
verify_emails = bool(request.form.get('verify_emails', None))
mail_tls = bool(request.form.get('mail_tls', None))
mail_ssl = bool(request.form.get('mail_ssl', None))
except (ValueError, TypeError):
view_challenges_unregistered = None
view_scoreboard_if_authed = None
prevent_registration = None
prevent_name_change = None
view_after_ctf = None
verify_emails = None
mail_tls = None
mail_ssl = None
finally:
view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered)
view_scoreboard_if_authed = set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
prevent_registration = set_config('prevent_registration', prevent_registration)
prevent_name_change = set_config('prevent_name_change', prevent_name_change)
view_after_ctf = set_config('view_after_ctf', view_after_ctf)
verify_emails = set_config('verify_emails', verify_emails)
mail_tls = set_config('mail_tls', mail_tls)
mail_ssl = set_config('mail_ssl', mail_ssl)
mail_server = set_config("mail_server", request.form.get('mail_server', None))
mail_port = set_config("mail_port", request.form.get('mail_port', None))
mail_username = set_config("mail_username", request.form.get('mail_username', None))
mail_password = set_config("mail_password", request.form.get('mail_password', None))
ctf_name = set_config("ctf_name", request.form.get('ctf_name', None))
ctf_theme = set_config("ctf_theme", request.form.get('ctf_theme', None))
mailfrom_addr = set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None))
mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None))
max_tries = set_config("max_tries", request.form.get('max_tries', None))
db_start = Config.query.filter_by(key='start').first()
db_start.value = start
db_end = Config.query.filter_by(key='end').first()
db_end.value = end
db.session.add(db_start)
db.session.add(db_end)
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin.admin_config'))
with app.app_context():
cache.clear()
ctf_name = get_config('ctf_name')
ctf_theme = get_config('ctf_theme')
max_tries = get_config('max_tries')
mail_server = get_config('mail_server')
mail_port = get_config('mail_port')
mail_username = get_config('mail_username')
mail_password = get_config('mail_password')
mailfrom_addr = get_config('mailfrom_addr')
mg_api_key = get_config('mg_api_key')
mg_base_url = get_config('mg_base_url')
if not max_tries:
set_config('max_tries', 0)
max_tries = 0
view_after_ctf = get_config('view_after_ctf')
start = get_config('start')
end = get_config('end')
mail_tls = get_config('mail_tls')
mail_ssl = get_config('mail_ssl')
view_challenges_unregistered = get_config('view_challenges_unregistered')
view_scoreboard_if_authed = get_config('view_scoreboard_if_authed')
prevent_registration = get_config('prevent_registration')
prevent_name_change = get_config('prevent_name_change')
verify_emails = get_config('verify_emails')
db.session.commit()
db.session.close()
themes = get_themes()
themes.remove(ctf_theme)
return render_template('admin/config.html',
ctf_name=ctf_name,
ctf_theme_config=ctf_theme,
start=start,
end=end,
max_tries=max_tries,
mail_server=mail_server,
mail_port=mail_port,
mail_username=mail_username,
mail_password=mail_password,
mail_tls=mail_tls,
mail_ssl=mail_ssl,
view_challenges_unregistered=view_challenges_unregistered,
view_scoreboard_if_authed=view_scoreboard_if_authed,
prevent_registration=prevent_registration,
mailfrom_addr=mailfrom_addr,
mg_base_url=mg_base_url,
mg_api_key=mg_api_key,
prevent_name_change=prevent_name_change,
verify_emails=verify_emails,
view_after_ctf=view_after_ctf,
themes=themes)
@admin.route('/admin/css', methods=['GET', 'POST'])
@admins_only
def admin_css():
if request.method == 'POST':
css = request.form['css']
css = set_config('css', css)
with app.app_context():
cache.clear()
return '1'
return '0'
@admin.route('/admin/pages', defaults={'route': None}, methods=['GET', 'POST'])
@admin.route('/admin/pages/<route>', methods=['GET', 'POST'])
@admins_only
def admin_pages(route):
if request.method == 'GET' and request.args.get('mode') == 'create':
return render_template('admin/editor.html')
if route and request.method == 'GET':
page = Pages.query.filter_by(route=route).first()
return render_template('admin/editor.html', page=page)
if route and request.method == 'POST':
page = Pages.query.filter_by(route=route).first()
errors = []
html = request.form['html']
route = request.form['route']
if not route:
errors.append('Missing URL route')
if errors:
page = Pages(html, '')
return render_template('/admin/editor.html', page=page)
if page:
page.route = route
page.html = html
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_pages'))
page = Pages(route, html)
db.session.add(page)
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_pages'))
pages = Pages.query.all()
return render_template('admin/pages.html', routes=pages, css=get_config('css'))
@admin.route('/admin/page/<pageroute>/delete', methods=['POST'])
@admins_only
def delete_page(pageroute):
page = Pages.query.filter_by(route=pageroute).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/containers', methods=['GET'])
@admins_only
def list_container():
containers = Containers.query.all()
for c in containers:
c.status = container_status(c.name)
c.ports = ', '.join(container_ports(c.name, verbose=True))
return render_template('admin/containers.html', containers=containers)
@admin.route('/admin/containers/<int:container_id>/stop', methods=['POST'])
@admins_only
def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_stop(container.name):
return '1'
else:
return '0'
@admin.route('/admin/containers/<int:container_id>/start', methods=['POST'])
@admins_only
def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_status(container.name) == 'missing':
if run_image(container.name):
return '1'
else:
return '0'
else:
if container_start(container.name):
return '1'
else:
return '0'
@admin.route('/admin/containers/<int:container_id>/delete', methods=['POST'])
@admins_only
def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if delete_image(container.name):
db.session.delete(container)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/containers/new', methods=['POST'])
@admins_only
def new_container():
name = request.form.get('name')
if not set(name) <= set('abcdefghijklmnopqrstuvwxyz0123456789-_'):
return redirect(url_for('admin.list_container'))
buildfile = request.form.get('buildfile')
files = request.files.getlist('files[]')
create_image(name=name, buildfile=buildfile, files=files)
run_image(name)
return redirect(url_for('admin.list_container'))
@admin.route('/admin/chals', methods=['POST', 'GET'])
@admins_only
def admin_chals():
if request.method == 'POST':
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category', 'hidden').order_by(Challenges.value).all()
teams_with_points = db.session.query(Solves.teamid).join(Teams).filter(
Teams.banned == False).group_by(Solves.teamid).count()
json_data = {'game': []}
for x in chals:
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(
Solves.chalid == x[1], Teams.banned == False).count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
json_data['game'].append({
'id': x.id,
'name': x.name,
'value': x.value,
'description': x.description,
'category': x.category,
'hidden': x.hidden,
'percentage_solved': percentage
})
db.session.close()
return jsonify(json_data)
else:
return render_template('admin/chals.html')
@admin.route('/admin/keys/<int:chalid>', methods=['POST', 'GET'])
@admins_only
def admin_keys(chalid):
chal = Challenges.query.filter_by(id=chalid).first_or_404()
if request.method == 'GET':
json_data = {'keys': []}
flags = json.loads(chal.flags)
for i, x in enumerate(flags):
json_data['keys'].append({'id': i, 'key': x['flag'], 'type': x['type']})
return jsonify(json_data)
elif request.method == 'POST':
newkeys = request.form.getlist('keys[]')
newvals = request.form.getlist('vals[]')
flags = []
for flag, val in zip(newkeys, newvals):
flag_dict = {'flag': flag, 'type': int(val)}
flags.append(flag_dict)
json_data = json.dumps(flags)
chal.flags = json_data
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_tags(chalid):
if request.method == 'GET':
tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []}
for x in tags:
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
return jsonify(json_data)
elif request.method == 'POST':
newtags = request.form.getlist('tags[]')
for x in newtags:
tag = Tags(chalid, x)
db.session.add(tag)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
@admins_only
def admin_delete_tags(tagid):
if request.method == 'POST':
tag = Tags.query.filter_by(id=tagid).first_or_404()
db.session.delete(tag)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_files(chalid):
if request.method == 'GET':
files = Files.query.filter_by(chal=chalid).all()
json_data = {'files': []}
for x in files:
json_data['files'].append({'id': x.id, 'file': x.location})
return jsonify(json_data)
if request.method == 'POST':
if request.form['method'] == "delete":
f = Files.query.filter_by(id=request.form['file']).first_or_404()
if os.path.exists(os.path.join(app.root_path, 'uploads', f.location)): # Some kind of os.path.isfile issue on Windows...
os.unlink(os.path.join(app.root_path, 'uploads', f.location))
db.session.delete(f)
db.session.commit()
db.session.close()
return '1'
elif request.form['method'] == "upload":
files = request.files.getlist('files[]')
for f in files:
upload_file(file=f, chalid=chalid)
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_chals'))
@admin.route('/admin/teams', defaults={'page': '1'})
@admin.route('/admin/teams/<int:page>')
@admins_only
def admin_teams(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
count = db.session.query(db.func.count(Teams.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
@admin.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
@admins_only
def admin_team(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
if request.method == 'GET':
solves = Solves.query.filter_by(teamid=teamid).all()
solve_ids = [s.chalid for s in solves]
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
last_seen = db.func.max(Tracking.date).label('last_seen')
addrs = db.session.query(Tracking.ip, last_seen) \
.filter_by(team=teamid) \
.group_by(Tracking.ip) \
.order_by(last_seen.desc()).all()
wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all()
awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all()
score = user.score()
place = user.place()
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
place=place, wrong_keys=wrong_keys, awards=awards)
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)
password = request.form.get('password', None)
email = request.form.get('email', None)
website = request.form.get('website', None)
affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None)
errors = []
name_used = Teams.query.filter(Teams.name == name).first()
if name_used and int(name_used.id) != int(teamid):
errors.append('That name is taken')
email_used = Teams.query.filter(Teams.email == email).first()
if email_used and int(email_used.id) != int(teamid):
errors.append('That email is taken')
if errors:
db.session.close()
return jsonify({'data': errors})
else:
user.name = name
user.email = email
if password:
user.password = bcrypt_sha256.encrypt(password)
user.website = website
user.affiliation = affiliation
user.country = country
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
@admin.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only
def email_user(teamid):
message = request.form.get('msg', None)
team = Teams.query.filter(Teams.id == teamid).first()
if message and team:
if sendmail(team.email, message):
return '1'
return '0'
@admin.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@admins_only
def ban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = True
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<int:teamid>/unban', methods=['POST'])
@admins_only
def unban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = False
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<int:teamid>/delete', methods=['POST'])
@admins_only
def delete_team(teamid):
try:
WrongKeys.query.filter_by(teamid=teamid).delete()
Solves.query.filter_by(teamid=teamid).delete()
Tracking.query.filter_by(team=teamid).delete()
Teams.query.filter_by(id=teamid).delete()
db.session.commit()
db.session.close()
except DatabaseError:
return '0'
else:
return '1'
@admin.route('/admin/graphs/<graph_type>')
@admins_only
def admin_graph(graph_type):
if graph_type == 'categories':
categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all()
json_data = {'categories': []}
for category, count in categories:
json_data['categories'].append({'category': category, 'count': count})
return jsonify(json_data)
elif graph_type == "solves":
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
json_data = {}
for chal, count, name in solves:
json_data[name] = count
return jsonify(json_data)
@admin.route('/admin/scoreboard')
@admins_only
def admin_scoreboard():
standings = get_standings(admin=True)
return render_template('admin/scoreboard.html', teams=standings)
@admin.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
@admins_only
def admin_awards(teamid):
awards = Awards.query.filter_by(teamid=teamid).all()
awards_list = []
for award in awards:
awards_list.append({
'id': award.id,
'name': award.name,
'description': award.description,
'date': award.date,
'value': award.value,
'category': award.category,
'icon': award.icon
})
json_data = {'awards': awards_list}
return jsonify(json_data)
@admin.route('/admin/awards/add', methods=['POST'])
@admins_only
def create_award():
try:
teamid = request.form['teamid']
name = request.form.get('name', 'Award')
value = request.form.get('value', 0)
award = Awards(teamid, name, value)
award.description = request.form.get('description')
award.category = request.form.get('category')
db.session.add(award)
db.session.commit()
db.session.close()
return '1'
except Exception as e:
print(e)
return '0'
@admin.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
@admins_only
def delete_award(award_id):
award = Awards.query.filter_by(id=award_id).first_or_404()
db.session.delete(award)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/scores')
@admins_only
def admin_scores():
score = db.func.sum(Challenges.value).label('score')
quickest = db.func.max(Solves.date).label('quickest')
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
json_data = {'teams': []}
for i, x in enumerate(teams):
json_data['teams'].append({'place': i + 1, 'id': x.teamid, 'name': x.name, 'score': int(x.score)})
return jsonify(json_data)
@admin.route('/admin/solves/<teamid>', methods=['GET'])
@admins_only
def admin_solves(teamid="all"):
if teamid == "all":
solves = Solves.query.all()
else:
solves = Solves.query.filter_by(teamid=teamid).all()
awards = Awards.query.filter_by(teamid=teamid).all()
db.session.close()
json_data = {'solves': []}
for x in solves:
json_data['solves'].append({
'id': x.id,
'chal': x.chal.name,
'chalid': x.chalid,
'team': x.teamid,
'value': x.chal.value,
'category': x.chal.category,
'time': unix_time(x.date)
})
for award in awards:
json_data['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category,
'time': unix_time(award.date)
})
json_data['solves'].sort(key=lambda k: k['time'])
return jsonify(json_data)
@admin.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
@admins_only
def create_solve(teamid, chalid):
solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN')
db.session.add(solve)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_solve(keyid):
solve = Solves.query.filter_by(id=keyid).first_or_404()
db.session.delete(solve)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_wrong_key(keyid):
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
db.session.delete(wrong_key)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/statistics', methods=['GET'])
@admins_only
def admin_stats():
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
solve_data = {}
for chal, count, name in solves:
solve_data[name] = count
most_solved = None
least_solved = None
if len(solve_data):
most_solved = max(solve_data, key=solve_data.get)
least_solved = min(solve_data, key=solve_data.get)
db.session.expunge_all()
db.session.commit()
db.session.close()
return render_template('admin/statistics.html', team_count=teams_registered,
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
solve_data=solve_data,
most_solved=most_solved,
least_solved=least_solved)
@admin.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/wrong_keys/<int:page>', methods=['GET'])
@admins_only
def admin_wrong_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(WrongKeys.date.desc()) \
.slice(page_start, page_end) \
.all()
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0)
return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
@admin.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/correct_keys/<int:page>', methods=['GET'])
@admins_only
def admin_correct_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(Solves.date.desc()) \
.slice(page_start, page_end) \
.all()
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0)
return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)
@admin.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
@admin.route('/admin/fails/<int:teamid>', methods=['GET'])
@admins_only
def admin_fails(teamid):
if teamid == "all":
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
else:
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
@admin.route('/admin/chal/new', methods=['POST'])
@admins_only
def admin_create_chal():
files = request.files.getlist('files[]')
# TODO: Expand to support multiple flags
flags = [{'flag': request.form['key'], 'type':int(request.form['key_type[0]'])}]
# Create challenge
chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'], flags)
if 'hidden' in request.form:
chal.hidden = True
else:
chal.hidden = False
db.session.add(chal)
db.session.commit()
for f in files:
upload_file(file=f, chalid=chal.id)
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_chals'))
@admin.route('/admin/chal/delete', methods=['POST'])
@admins_only
def admin_delete_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
WrongKeys.query.filter_by(chalid=challenge.id).delete()
Solves.query.filter_by(chalid=challenge.id).delete()
Keys.query.filter_by(chal=challenge.id).delete()
files = Files.query.filter_by(chal=challenge.id).all()
Files.query.filter_by(chal=challenge.id).delete()
for file in files:
folder = os.path.dirname(os.path.join(os.path.normpath(app.root_path), 'uploads', file.location))
rmdir(folder)
Tags.query.filter_by(chal=challenge.id).delete()
Challenges.query.filter_by(id=challenge.id).delete()
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/chal/update', methods=['POST'])
@admins_only
def admin_update_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
challenge.name = request.form['name']
challenge.description = request.form['desc']
challenge.value = request.form['value']
challenge.category = request.form['category']
challenge.hidden = 'hidden' in request.form
db.session.add(challenge)
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_chals'))

163
CTFd/admin/__init__.py Normal file
View File

@ -0,0 +1,163 @@
import hashlib
import json
import os
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.admin.statistics import admin_statistics
from CTFd.admin.challenges import admin_challenges
from CTFd.admin.scoreboard import admin_scoreboard
from CTFd.admin.pages import admin_pages
from CTFd.admin.containers import admin_containers
from CTFd.admin.keys import admin_keys
from CTFd.admin.teams import admin_teams
admin = Blueprint('admin', __name__)
@admin.route('/admin', methods=['GET'])
def admin_view():
if is_admin():
return redirect(url_for('admin_statistics.admin_graphs'))
return redirect(url_for('auth.login'))
@admin.route('/admin/config', methods=['GET', 'POST'])
@admins_only
def admin_config():
if request.method == "POST":
start = None
end = None
if request.form.get('start'):
start = int(request.form['start'])
if request.form.get('end'):
end = int(request.form['end'])
try:
view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None))
view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None))
prevent_registration = bool(request.form.get('prevent_registration', None))
prevent_name_change = bool(request.form.get('prevent_name_change', None))
view_after_ctf = bool(request.form.get('view_after_ctf', None))
verify_emails = bool(request.form.get('verify_emails', None))
mail_tls = bool(request.form.get('mail_tls', None))
mail_ssl = bool(request.form.get('mail_ssl', None))
except (ValueError, TypeError):
view_challenges_unregistered = None
view_scoreboard_if_authed = None
prevent_registration = None
prevent_name_change = None
view_after_ctf = None
verify_emails = None
mail_tls = None
mail_ssl = None
finally:
view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered)
view_scoreboard_if_authed = set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
prevent_registration = set_config('prevent_registration', prevent_registration)
prevent_name_change = set_config('prevent_name_change', prevent_name_change)
view_after_ctf = set_config('view_after_ctf', view_after_ctf)
verify_emails = set_config('verify_emails', verify_emails)
mail_tls = set_config('mail_tls', mail_tls)
mail_ssl = set_config('mail_ssl', mail_ssl)
mail_server = set_config("mail_server", request.form.get('mail_server', None))
mail_port = set_config("mail_port", request.form.get('mail_port', None))
mail_username = set_config("mail_username", request.form.get('mail_username', None))
mail_password = set_config("mail_password", request.form.get('mail_password', None))
ctf_name = set_config("ctf_name", request.form.get('ctf_name', None))
ctf_theme = set_config("ctf_theme", request.form.get('ctf_theme', None))
mailfrom_addr = set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None))
mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None))
max_tries = set_config("max_tries", request.form.get('max_tries', None))
db_start = Config.query.filter_by(key='start').first()
db_start.value = start
db_end = Config.query.filter_by(key='end').first()
db_end.value = end
db.session.add(db_start)
db.session.add(db_end)
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin.admin_config'))
with app.app_context():
cache.clear()
ctf_name = get_config('ctf_name')
ctf_theme = get_config('ctf_theme')
max_tries = get_config('max_tries')
mail_server = get_config('mail_server')
mail_port = get_config('mail_port')
mail_username = get_config('mail_username')
mail_password = get_config('mail_password')
mailfrom_addr = get_config('mailfrom_addr')
mg_api_key = get_config('mg_api_key')
mg_base_url = get_config('mg_base_url')
if not max_tries:
set_config('max_tries', 0)
max_tries = 0
view_after_ctf = get_config('view_after_ctf')
start = get_config('start')
end = get_config('end')
mail_tls = get_config('mail_tls')
mail_ssl = get_config('mail_ssl')
view_challenges_unregistered = get_config('view_challenges_unregistered')
view_scoreboard_if_authed = get_config('view_scoreboard_if_authed')
prevent_registration = get_config('prevent_registration')
prevent_name_change = get_config('prevent_name_change')
verify_emails = get_config('verify_emails')
db.session.commit()
db.session.close()
themes = get_themes()
themes.remove(ctf_theme)
return render_template('admin/config.html',
ctf_name=ctf_name,
ctf_theme_config=ctf_theme,
start=start,
end=end,
max_tries=max_tries,
mail_server=mail_server,
mail_port=mail_port,
mail_username=mail_username,
mail_password=mail_password,
mail_tls=mail_tls,
mail_ssl=mail_ssl,
view_challenges_unregistered=view_challenges_unregistered,
view_scoreboard_if_authed=view_scoreboard_if_authed,
prevent_registration=prevent_registration,
mailfrom_addr=mailfrom_addr,
mg_base_url=mg_base_url,
mg_api_key=mg_api_key,
prevent_name_change=prevent_name_change,
verify_emails=verify_emails,
view_after_ctf=view_after_ctf,
themes=themes)

198
CTFd/admin/challenges.py Normal file
View File

@ -0,0 +1,198 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
import os
admin_challenges = Blueprint('admin_challenges', __name__)
@admin_challenges.route('/admin/chal_types', methods=['GET'])
@admins_only
def admin_chal_types():
data = {}
for class_id in CHALLENGE_CLASSES:
data[class_id] = CHALLENGE_CLASSES.get(class_id).name
return jsonify(data)
@admin_challenges.route('/admin/chals', methods=['POST', 'GET'])
@admins_only
def admin_chals():
if request.method == 'POST':
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category', 'hidden').order_by(Challenges.value).all()
teams_with_points = db.session.query(Solves.teamid).join(Teams).filter(
Teams.banned == False).group_by(Solves.teamid).count()
json_data = {'game': []}
for x in chals:
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(
Solves.chalid == x[1], Teams.banned == False).count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
json_data['game'].append({
'id': x.id,
'name': x.name,
'value': x.value,
'description': x.description,
'category': x.category,
'hidden': x.hidden,
'percentage_solved': percentage
})
db.session.close()
return jsonify(json_data)
else:
return render_template('admin/chals.html')
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_tags(chalid):
if request.method == 'GET':
tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []}
for x in tags:
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
return jsonify(json_data)
elif request.method == 'POST':
newtags = request.form.getlist('tags[]')
for x in newtags:
tag = Tags(chalid, x)
db.session.add(tag)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
@admins_only
def admin_delete_tags(tagid):
if request.method == 'POST':
tag = Tags.query.filter_by(id=tagid).first_or_404()
db.session.delete(tag)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_files(chalid):
if request.method == 'GET':
files = Files.query.filter_by(chal=chalid).all()
json_data = {'files': []}
for x in files:
json_data['files'].append({'id': x.id, 'file': x.location})
return jsonify(json_data)
if request.method == 'POST':
if request.form['method'] == "delete":
f = Files.query.filter_by(id=request.form['file']).first_or_404()
if os.path.exists(os.path.join(app.root_path, 'uploads', f.location)): # Some kind of os.path.isfile issue on Windows...
os.unlink(os.path.join(app.root_path, 'uploads', f.location))
db.session.delete(f)
db.session.commit()
db.session.close()
return '1'
elif request.form['method'] == "upload":
files = request.files.getlist('files[]')
for f in files:
upload_file(file=f, chalid=chalid)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/chal/<int:chalid>/<prop>', methods=['GET'])
@admins_only
def admin_get_values(chalid, prop):
challenge = Challenges.query.filter_by(id=chalid).first_or_404()
if prop == 'keys':
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
json_data = {'keys': []}
for x in chal_keys:
json_data['keys'].append({'id': x.id, 'key': x.flag, 'type': x.key_type, 'type_name': get_key_class(x.key_type).name})
return jsonify(json_data)
elif prop == 'tags':
tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []}
for x in tags:
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
return jsonify(json_data)
@admin_challenges.route('/admin/chal/new', methods=['GET', 'POST'])
@admins_only
def admin_create_chal():
if request.method == 'POST':
files = request.files.getlist('files[]')
# Create challenge
chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'], int(request.form['chaltype']))
if 'hidden' in request.form:
chal.hidden = True
else:
chal.hidden = False
db.session.add(chal)
db.session.flush()
flag = Keys(chal.id, request.form['key'], int(request.form['key_type[0]']))
if request.form.get('keydata'):
flag.data = request.form.get('keydata')
db.session.add(flag)
db.session.commit()
for f in files:
upload_file(file=f, chalid=chal.id)
db.session.commit()
db.session.close()
return redirect(url_for('admin_challenges.admin_chals'))
else:
return render_template('admin/chals/create.html')
@admin_challenges.route('/admin/chal/delete', methods=['POST'])
@admins_only
def admin_delete_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
WrongKeys.query.filter_by(chalid=challenge.id).delete()
Solves.query.filter_by(chalid=challenge.id).delete()
Keys.query.filter_by(chal=challenge.id).delete()
files = Files.query.filter_by(chal=challenge.id).all()
Files.query.filter_by(chal=challenge.id).delete()
for file in files:
folder = os.path.dirname(os.path.join(os.path.normpath(app.root_path), 'uploads', file.location))
rmdir(folder)
Tags.query.filter_by(chal=challenge.id).delete()
Challenges.query.filter_by(id=challenge.id).delete()
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/chal/update', methods=['POST'])
@admins_only
def admin_update_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
challenge.name = request.form['name']
challenge.description = request.form['desc']
challenge.value = request.form['value']
challenge.category = request.form['category']
challenge.hidden = 'hidden' in request.form
db.session.add(challenge)
db.session.commit()
db.session.close()
return redirect(url_for('admin_challenges.admin_chals'))

66
CTFd/admin/containers.py Normal file
View File

@ -0,0 +1,66 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
admin_containers = Blueprint('admin_containers', __name__)
@admin_containers.route('/admin/containers', methods=['GET'])
@admins_only
def list_container():
containers = Containers.query.all()
for c in containers:
c.status = container_status(c.name)
c.ports = ', '.join(container_ports(c.name, verbose=True))
return render_template('admin/containers.html', containers=containers)
@admin_containers.route('/admin/containers/<int:container_id>/stop', methods=['POST'])
@admins_only
def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_stop(container.name):
return '1'
else:
return '0'
@admin_containers.route('/admin/containers/<int:container_id>/start', methods=['POST'])
@admins_only
def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_status(container.name) == 'missing':
if run_image(container.name):
return '1'
else:
return '0'
else:
if container_start(container.name):
return '1'
else:
return '0'
@admin_containers.route('/admin/containers/<int:container_id>/delete', methods=['POST'])
@admins_only
def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if delete_image(container.name):
db.session.delete(container)
db.session.commit()
db.session.close()
return '1'
@admin_containers.route('/admin/containers/new', methods=['POST'])
@admins_only
def new_container():
name = request.form.get('name')
if not set(name) <= set('abcdefghijklmnopqrstuvwxyz0123456789-_'):
return redirect(url_for('admin_containers.list_container'))
buildfile = request.form.get('buildfile')
files = request.files.getlist('files[]')
create_image(name=name, buildfile=buildfile, files=files)
run_image(name)
return redirect(url_for('admin_containers.list_container'))

67
CTFd/admin/keys.py Normal file
View File

@ -0,0 +1,67 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
admin_keys = Blueprint('admin_keys', __name__)
@admin_keys.route('/admin/key_types', methods=['GET'])
@admins_only
def admin_key_types():
data = {}
for class_id in KEY_CLASSES:
data[class_id] = KEY_CLASSES.get(class_id).name
return jsonify(data)
@admin_keys.route('/admin/keys', defaults={'keyid': None}, methods=['POST', 'GET'])
@admin_keys.route('/admin/keys/<int:keyid>', methods=['POST', 'GET'])
@admins_only
def admin_keys_view(keyid):
print repr(keyid)
if request.method == 'GET':
if keyid:
saved_key = Keys.query.filter_by(id=keyid).first_or_404()
json_data = {
'id': saved_key.id,
'key': saved_key.flag,
'data': saved_key.data,
'chal': saved_key.chal,
'type': saved_key.key_type,
'type_name': get_key_class(saved_key.key_type).name
}
return jsonify(json_data)
elif request.method == 'POST':
chal = request.form.get('chal')
flag = request.form.get('key')
data = request.form.get('keydata')
key_type = int(request.form.get('key_type'))
if not keyid:
k = Keys(chal, flag, key_type)
k.data = data
db.session.add(k)
else:
k = Keys.query.filter_by(id=keyid).first()
k.chal = chal
k.flag = flag
k.data = data
k.key_type = key_type
db.session.commit()
db.session.close()
return '1'
@admin_keys.route('/admin/keys/<int:keyid>/delete', methods=['POST'])
@admins_only
def admin_delete_keys(keyid):
if request.method == 'POST':
key = Keys.query.filter_by(id=keyid).first_or_404()
db.session.delete(key)
db.session.commit()
db.session.close()
return '1'

62
CTFd/admin/pages.py Normal file
View File

@ -0,0 +1,62 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
admin_pages = Blueprint('admin_pages', __name__)
@admin_pages.route('/admin/css', methods=['GET', 'POST'])
@admins_only
def admin_css():
if request.method == 'POST':
css = request.form['css']
css = set_config('css', css)
with app.app_context():
cache.clear()
return '1'
return '0'
@admin_pages.route('/admin/pages', defaults={'route': None}, methods=['GET', 'POST'])
@admin_pages.route('/admin/pages/<route>', methods=['GET', 'POST'])
@admins_only
def admin_pages_view(route):
if request.method == 'GET' and request.args.get('mode') == 'create':
return render_template('admin/editor.html')
if route and request.method == 'GET':
page = Pages.query.filter_by(route=route).first()
return render_template('admin/editor.html', page=page)
if route and request.method == 'POST':
page = Pages.query.filter_by(route=route).first()
errors = []
html = request.form['html']
route = request.form['route']
if not route:
errors.append('Missing URL route')
if errors:
page = Pages(html, '')
return render_template('/admin/editor.html', page=page)
if page:
page.route = route
page.html = html
db.session.commit()
db.session.close()
return redirect(url_for('admin_pages.admin_pages_view'))
page = Pages(route, html)
db.session.add(page)
db.session.commit()
db.session.close()
return redirect(url_for('admin_pages.admin_pages_view'))
pages = Pages.query.all()
return render_template('admin/pages.html', routes=pages, css=get_config('css'))
@admin_pages.route('/admin/page/<pageroute>/delete', methods=['POST'])
@admins_only
def delete_page(pageroute):
page = Pages.query.filter_by(route=pageroute).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()
return '1'

27
CTFd/admin/scoreboard.py Normal file
View File

@ -0,0 +1,27 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
admin_scoreboard = Blueprint('admin_scoreboard', __name__)
@admin_scoreboard.route('/admin/scoreboard')
@admins_only
def admin_scoreboard_view():
standings = get_standings(admin=True)
return render_template('admin/scoreboard.html', teams=standings)
@admin_scoreboard.route('/admin/scores')
@admins_only
def admin_scores():
score = db.func.sum(Challenges.value).label('score')
quickest = db.func.max(Solves.date).label('quickest')
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
json_data = {'teams': []}
for i, x in enumerate(teams):
json_data['teams'].append({'place': i + 1, 'id': x.teamid, 'name': x.name, 'score': int(x.score)})
return jsonify(json_data)

112
CTFd/admin/statistics.py Normal file
View File

@ -0,0 +1,112 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
admin_statistics = Blueprint('admin_statistics', __name__)
@admin_statistics.route('/admin/graphs')
@admins_only
def admin_graphs():
return render_template('admin/graphs.html')
@admin_statistics.route('/admin/graphs/<graph_type>')
@admins_only
def admin_graph(graph_type):
if graph_type == 'categories':
categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all()
json_data = {'categories': []}
for category, count in categories:
json_data['categories'].append({'category': category, 'count': count})
return jsonify(json_data)
elif graph_type == "solves":
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
json_data = {}
for chal, count, name in solves:
json_data[name] = count
return jsonify(json_data)
@admin_statistics.route('/admin/statistics', methods=['GET'])
@admins_only
def admin_stats():
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
solve_data = {}
for chal, count, name in solves:
solve_data[name] = count
most_solved = None
least_solved = None
if len(solve_data):
most_solved = max(solve_data, key=solve_data.get)
least_solved = min(solve_data, key=solve_data.get)
db.session.expunge_all()
db.session.commit()
db.session.close()
return render_template('admin/statistics.html', team_count=teams_registered,
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
solve_data=solve_data,
most_solved=most_solved,
least_solved=least_solved)
@admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
@admin_statistics.route('/admin/wrong_keys/<int:page>', methods=['GET'])
@admins_only
def admin_wrong_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(WrongKeys.date.desc()) \
.slice(page_start, page_end) \
.all()
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0)
return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
@admin_statistics.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
@admin_statistics.route('/admin/correct_keys/<int:page>', methods=['GET'])
@admins_only
def admin_correct_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(Solves.date.desc()) \
.slice(page_start, page_end) \
.all()
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0)
return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)

270
CTFd/admin/teams.py Normal file
View File

@ -0,0 +1,270 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
admin_teams = Blueprint('admin_teams', __name__)
@admin_teams.route('/admin/teams', defaults={'page': '1'})
@admin_teams.route('/admin/teams/<int:page>')
@admins_only
def admin_teams_view(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
count = db.session.query(db.func.count(Teams.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
@admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
@admins_only
def admin_team(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
if request.method == 'GET':
solves = Solves.query.filter_by(teamid=teamid).all()
solve_ids = [s.chalid for s in solves]
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
last_seen = db.func.max(Tracking.date).label('last_seen')
addrs = db.session.query(Tracking.ip, last_seen) \
.filter_by(team=teamid) \
.group_by(Tracking.ip) \
.order_by(last_seen.desc()).all()
wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all()
awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all()
score = user.score()
place = user.place()
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
place=place, wrong_keys=wrong_keys, awards=awards)
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)
password = request.form.get('password', None)
email = request.form.get('email', None)
website = request.form.get('website', None)
affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None)
errors = []
name_used = Teams.query.filter(Teams.name == name).first()
if name_used and int(name_used.id) != int(teamid):
errors.append('That name is taken')
email_used = Teams.query.filter(Teams.email == email).first()
if email_used and int(email_used.id) != int(teamid):
errors.append('That email is taken')
if errors:
db.session.close()
return jsonify({'data': errors})
else:
user.name = name
user.email = email
if password:
user.password = bcrypt_sha256.encrypt(password)
user.website = website
user.affiliation = affiliation
user.country = country
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
@admin_teams.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only
def email_user(teamid):
message = request.form.get('msg', None)
team = Teams.query.filter(Teams.id == teamid).first()
if message and team:
if sendmail(team.email, message):
return '1'
return '0'
@admin_teams.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@admins_only
def ban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = True
db.session.commit()
db.session.close()
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
@admin_teams.route('/admin/team/<int:teamid>/unban', methods=['POST'])
@admins_only
def unban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = False
db.session.commit()
db.session.close()
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
@admin_teams.route('/admin/team/<int:teamid>/delete', methods=['POST'])
@admins_only
def delete_team(teamid):
try:
WrongKeys.query.filter_by(teamid=teamid).delete()
Solves.query.filter_by(teamid=teamid).delete()
Tracking.query.filter_by(team=teamid).delete()
Teams.query.filter_by(id=teamid).delete()
db.session.commit()
db.session.close()
except DatabaseError:
return '0'
else:
return '1'
@admin_teams.route('/admin/solves/<teamid>', methods=['GET'])
@admins_only
def admin_solves(teamid="all"):
if teamid == "all":
solves = Solves.query.all()
else:
solves = Solves.query.filter_by(teamid=teamid).all()
awards = Awards.query.filter_by(teamid=teamid).all()
db.session.close()
json_data = {'solves': []}
for x in solves:
json_data['solves'].append({
'id': x.id,
'chal': x.chal.name,
'chalid': x.chalid,
'team': x.teamid,
'value': x.chal.value,
'category': x.chal.category,
'time': unix_time(x.date)
})
for award in awards:
json_data['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category or "Award",
'time': unix_time(award.date)
})
json_data['solves'].sort(key=lambda k: k['time'])
return jsonify(json_data)
@admin_teams.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
@admin_teams.route('/admin/fails/<int:teamid>', methods=['GET'])
@admins_only
def admin_fails(teamid):
if teamid == "all":
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
else:
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
@admin_teams.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
@admins_only
def create_solve(teamid, chalid):
solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN')
db.session.add(solve)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_solve(keyid):
solve = Solves.query.filter_by(id=keyid).first_or_404()
db.session.delete(solve)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_wrong_key(keyid):
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
db.session.delete(wrong_key)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
@admins_only
def delete_award(award_id):
award = Awards.query.filter_by(id=award_id).first_or_404()
db.session.delete(award)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
@admins_only
def admin_awards(teamid):
awards = Awards.query.filter_by(teamid=teamid).all()
awards_list = []
for award in awards:
awards_list.append({
'id': award.id,
'name': award.name,
'description': award.description,
'date': award.date,
'value': award.value,
'category': award.category,
'icon': award.icon
})
json_data = {'awards': awards_list}
return jsonify(json_data)
@admin_teams.route('/admin/awards/add', methods=['POST'])
@admins_only
def create_award():
try:
teamid = request.form['teamid']
name = request.form.get('name', 'Award')
value = request.form.get('value', 0)
award = Awards(teamid, name, value)
award.description = request.form.get('description')
award.category = request.form.get('category')
db.session.add(award)
db.session.commit()
db.session.close()
return '1'
except Exception as e:
print(e)
return '0'

View File

@ -30,9 +30,9 @@ def confirm_user(data=None):
team = Teams.query.filter_by(email=email).first_or_404()
team.verified = True
db.session.commit()
db.session.close()
logger = logging.getLogger('regs')
logger.warn("[{0}] {1} confirmed {2}".format(time.strftime("%m/%d/%Y %X"), team.name.encode('utf-8'), team.email.encode('utf-8')))
db.session.close()
if authed():
return redirect(url_for('challenges.challenges_view'))
return redirect(url_for('auth.login'))

View File

@ -7,7 +7,9 @@ from flask import render_template, request, redirect, jsonify, url_for, session,
from sqlalchemy.sql import or_
from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, user_can_view_challenges, is_admin, get_config, get_ip, is_verified, ctf_started, ctf_ended, ctf_name
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Tags, Teams, Awards
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards
from CTFd.plugins.keys import get_key_class
from CTFd.plugins.challenges import get_chal_class
challenges = Blueprint('challenges', __name__)
@ -49,13 +51,22 @@ def chals():
else:
return redirect(url_for('views.static_html'))
if user_can_view_challenges() and (ctf_started() or is_admin()):
chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
json = {'game': []}
for x in chals:
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x[1]).all()]
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()]
files = [str(f.location) for f in Files.query.filter_by(chal=x.id).all()]
json['game'].append({'id': x[1], 'name': x[2], 'value': x[3], 'description': x[4], 'category': x[5], 'files': files, 'tags': tags})
chal_type = get_chal_class(x.type)
json['game'].append({
'id': x.id,
'type': chal_type.name,
'name': x.name,
'value': x.value,
'description': x.description,
'category': x.category,
'files': files,
'tags': tags
})
db.session.close()
return jsonify(json)
@ -87,7 +98,10 @@ def solves(teamid=None):
if is_admin():
solves = Solves.query.filter_by(teamid=session['id']).all()
elif user_can_view_challenges():
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.teamid == session['id'], Teams.banned == False).all()
if authed():
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.teamid == session['id'], Teams.banned == False).all()
else:
return jsonify({'solves': []})
else:
return redirect(url_for('auth.login', next='solves'))
else:
@ -111,7 +125,7 @@ def solves(teamid=None):
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category,
'category': award.category or "Award",
'time': unix_time(award.date)
})
json['solves'].sort(key=lambda k: k['time'])
@ -179,8 +193,8 @@ def chal(chalid):
# Challange not solved yet
if not solves:
chal = Challenges.query.filter_by(id=chalid).first_or_404()
key = unicode(request.form['key'].strip().lower())
keys = json.loads(chal.flags)
provided_key = unicode(request.form['key'].strip())
saved_keys = Keys.query.filter_by(chal=chal.id).all()
# Hit max attempts
max_tries = int(get_config("max_tries"))
@ -190,32 +204,18 @@ def chal(chalid):
'message': "You have 0 tries remaining"
})
for x in keys:
if x['type'] == 0: # static key
print(x['flag'], key.strip().lower())
if x['flag'] and x['flag'].strip().lower() == key.strip().lower():
if ctftime():
solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key)
db.session.add(solve)
db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
# return '1' # key was correct
return jsonify({'status': '1', 'message': 'Correct'})
elif x['type'] == 1: # regex
res = re.match(x['flag'], key, re.IGNORECASE)
if res and res.group() == key:
if ctftime():
solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key)
db.session.add(solve)
db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
# return '1' # key was correct
return jsonify({'status': '1', 'message': 'Correct'})
chal_class = get_chal_class(chal.type)
if chal_class.solve(chal, provided_key):
if ctftime():
solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=provided_key)
db.session.add(solve)
db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
return jsonify({'status': '1', 'message': 'Correct'})
if ctftime():
wrong = WrongKeys(session['id'], chalid, request.form['key'])
wrong = WrongKeys(teamid=session['id'], chalid=chalid, flag=provided_key)
db.session.add(wrong)
db.session.commit()
db.session.close()
@ -236,4 +236,7 @@ def chal(chalid):
# return '2' # challenge was already solved
return jsonify({'status': '2', 'message': 'You already solved this'})
else:
return '-1'
return jsonify({
'status': '-1',
'message': "You must be logged in to solve a challenge"
})

View File

@ -60,15 +60,16 @@ class Challenges(db.Model):
description = db.Column(db.Text)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
flags = db.Column(db.Text)
type = db.Column(db.Integer)
hidden = db.Column(db.Boolean)
def __init__(self, name, description, value, category, flags):
def __init__(self, name, description, value, category, type=0):
self.name = name
self.description = description
self.value = value
self.category = category
self.flags = json.dumps(flags)
self.type = type
# self.flags = json.dumps(flags)
def __repr__(self):
return '<chal %r>' % self.name
@ -124,6 +125,7 @@ class Keys(db.Model):
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
key_type = db.Column(db.Integer)
flag = db.Column(db.Text)
data = db.Column(db.Text)
def __init__(self, chal, flag, key_type):
self.chal = chal
@ -131,7 +133,7 @@ class Keys(db.Model):
self.key_type = key_type
def __repr__(self):
return self.flag
return "<Flag {0} for challenge {1}>".format(self.flag, self.chal)
class Teams(db.Model):

View File

@ -5,9 +5,11 @@ import os
def init_plugins(app):
modules = glob.glob(os.path.dirname(__file__) + "/*")
blacklist = {'keys', 'challenges'}
for module in modules:
if os.path.isdir(module):
module = '.' + os.path.basename(module)
module_name = os.path.basename(module)
if os.path.isdir(module) and module_name not in blacklist:
module = '.' + module_name
module = importlib.import_module(module, package='CTFd.plugins')
module.load(app)
print(" * Loaded module, %s" % module)

View File

@ -0,0 +1,29 @@
from CTFd.plugins.keys import get_key_class
from CTFd.models import db, Keys
class BaseChallenge(object):
id = None
name = None
class CTFdStandardChallenge(BaseChallenge):
id = 0
name = "standard"
@staticmethod
def solve(chal, provided_key):
chal_keys = Keys.query.filter_by(chal=chal.id).all()
for chal_key in chal_keys:
if get_key_class(chal_key.key_type).compare(chal_key.flag, provided_key):
return True
return False
CHALLENGE_CLASSES = {
0 : CTFdStandardChallenge
}
def get_chal_class(class_id):
cls = CHALLENGE_CLASSES.get(class_id)
if cls is None:
raise KeyError
return cls

View File

@ -0,0 +1,48 @@
import re
import string
import hmac
class BaseKey(object):
id = None
name = None
@staticmethod
def compare(self, saved, provided):
return True
class CTFdStaticKey(BaseKey):
id = 0
name = "static"
@staticmethod
def compare(saved, provided):
if len(saved) != len(provided):
return False
result = 0
for x, y in zip(saved, provided):
result |= ord(x) ^ ord(y)
return result == 0
class CTFdRegexKey(BaseKey):
id = 1
name = "regex"
@staticmethod
def compare(saved, provided):
res = re.match(saved, provided, re.IGNORECASE)
return res and res.group() == provided
KEY_CLASSES = {
0 : CTFdStaticKey,
1 : CTFdRegexKey
}
def get_key_class(class_id):
cls = KEY_CLASSES.get(class_id)
if cls is None:
raise KeyError
return cls

View File

@ -21,6 +21,22 @@ String.prototype.hashCode = function() {
return hash;
};
function load_edit_key_modal(key_id, key_type_name) {
$.get(script_root + '/static/admin/js/templates/keys/'+key_type_name+'/edit-'+key_type_name+'-modal.hbs', function(template_data){
$.get(script_root + '/admin/keys/' + key_id, function(key_data){
$('#edit-keys').empty();
var template = Handlebars.compile(template_data);
$('#edit-keys').append(template(key_data));
$('#key-id').val(key_id);
$('#submit-keys').click(function (e) {
e.preventDefault();
updatekey()
});
$('#edit-keys').modal();
});
});
}
function loadchal(id, update) {
// $('#chal *').show()
// $('#chal > h1').hide()
@ -54,23 +70,31 @@ function submitkey(chal, key) {
})
}
function create_key(chal, key, key_type) {
$.post(script_root + "/admin/keys", {
chal: chal,
key: key,
key_type: key_type,
nonce: $('#nonce').val()
}, function (data) {
if (data == "1"){
loadkeys(chal);
$("#create-keys").modal('toggle');
}
});
}
function loadkeys(chal){
$.get(script_root + '/admin/keys/' + chal, function(data){
$.get(script_root + '/admin/chal/' + chal + '/keys', function(data){
$('#keys-chal').val(chal);
keys = $.parseJSON(JSON.stringify(data));
keys = keys['keys'];
$('#current-keys').empty();
for(x=0; x<keys.length; x++){
var elem = $('<div class="col-md-4">');
elem.append($("<div class='form-group'>").append($("<input class='current-key form-control' type='text'>").val(keys[x].key)));
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+x+']" value="0">Static</div>');
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+x+']" value="1">Regex</div>');
elem.append('<a href="#" onclick="$(this).parent().remove()" class="btn btn-danger key-remove-button">Remove</a>');
$('#current-keys').append(elem);
$('#current-keys input[name="key_type['+x+']"][value="'+keys[x].type+'"]').prop('checked',true);
}
$.get(script_root + "/static/admin/js/templates/admin-keys-table.hbs", function(data){
var template = Handlebars.compile(data);
var wrapper = {keys: keys};
$('#current-keys').append(template(wrapper));
});
});
}
@ -89,6 +113,34 @@ function updatekeys(){
$('#update-keys').modal('hide');
}
function deletekey(key_id){
$.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){
if (data == "1") {
$('tr[name={0}]'.format(key_id)).remove();
}
});
}
function updatekey(){
var key_id = $('#key-id').val();
var chal = $('#keys-chal').val();
var key_data = $('#key-data').val();
var key_type = $('#key-type').val();
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") {
loadkeys(chal);
$('#edit-keys').modal('toggle');
}
})
}
function loadtags(chal){
$('#tags-chal').val(chal)
$('#current-tags').empty()
@ -125,6 +177,25 @@ function updatetags(){
loadchal(chal)
}
function updatefiles(){
chal = $('#files-chal').val();
var form = $('#update-files form')[0];
var formData = new FormData(form);
$.ajax({
url: script_root + '/admin/files/'+chal,
data: formData,
type: 'POST',
cache: false,
contentType: false,
processData: false,
success: function(data){
form.reset();
loadfiles(chal);
$('#update-files').modal('hide');
}
});
}
function loadfiles(chal){
$('#update-files form').attr('action', script_root+'/admin/files/'+chal)
$.get(script_root + '/admin/files/' + chal, function(data){
@ -182,11 +253,11 @@ function loadchals(){
loadfiles(this.value);
});
$('.create-challenge').click(function (e) {
$('#new-chal-category').val($($(this).siblings()[0]).text().trim());
$('#new-chal-title').text($($(this).siblings()[0]).text().trim());
$('#new-challenge').modal();
});
// $('.create-challenge').click(function (e) {
// $('#new-chal-category').val($($(this).siblings()[0]).text().trim());
// $('#new-chal-title').text($($(this).siblings()[0]).text().trim());
// $('#new-challenge').modal();
// });
});
}
@ -205,6 +276,11 @@ $('#submit-tags').click(function (e) {
updatetags()
});
$('#submit-files').click(function (e) {
e.preventDefault();
updatefiles()
});
$('#delete-chal form').submit(function(e){
e.preventDefault();
$.post(script_root + '/admin/chal/delete', $(this).serialize(), function(data){
@ -247,22 +323,41 @@ $('#new-desc-edit').on('shown.bs.tab', function (event) {
});
// Open New Challenge modal when New Challenge button is clicked
$('.create-challenge').click(function (e) {
$('#create-challenge').modal();
});
// $('.create-challenge').click(function (e) {
// $('#create-challenge').modal();
// });
$('#create-key').click(function(e){
var amt = $('#current-keys input[type=text]').length
$.get(script_root + '/admin/key_types', function(data){
$("#create-keys-select").empty();
var option = "<option> -- </option>";
$("#create-keys-select").append(option);
for (var key in data){
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
$("#create-keys-select").append(option);
}
$("#create-keys").modal();
});
});
var elem = $('<div class="col-md-4">');
$('#create-keys-select').change(function(){
var key_type_name = $(this).find("option:selected").text();
elem.append($("<div class='form-group'>").append($("<input class='current-key form-control' type='text'>")));
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="0" checked>Static</div>');
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="1">Regex</div>');
elem.append('<a href="#" onclick="$(this).parent().remove()" class="btn btn-danger key-remove-button">Remove</a>');
$.get(script_root + '/static/admin/js/templates/keys/'+key_type_name +'/'+key_type_name+'.hbs', function(template_data){
var template = Handlebars.compile(template_data);
$("#create-keys-entry-div").html(template());
$("#create-keys-button-div").show();
});
});
$('#current-keys').append(elem);
$('#create-keys-submit').click(function (e) {
e.preventDefault();
var chalid = $('#create-keys').find('.chal-id').val();
var key_data = $('#create-keys').find('input[name=key]').val();
var key_type = $('#create-keys-select').val();
create_key(chalid, key_data, key_type);
});
$(function(){

View File

@ -104,7 +104,7 @@ function category_breakdown_graph(){
var data = [{
values: counts,
labels: categories,
labels: keys,
hole: .4,
type: 'pie'
}];

View File

@ -0,0 +1,22 @@
<table id="keysboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>Type</b></td>
<td class="text-center"><b>Key</b></td>
<td class="text-center"><b>Settings</b></td>
</tr>
</thead>
<tbody>
{{#each keys}}
<tr name="{{this.id}}">
<td class="key-type">{{this.type_name}}</td>
<td class="key-value">{{this.key}}</td>
<td class="text-center"><span>
<i role="button" class="fa fa-pencil-square-o" onclick="javascript:load_edit_key_modal({{this.id}}, '{{this.type_name}}')"></i>
<i role="button" class="fa fa-times" onclick="javascript:deletekey({{this.id}})"></i>
</span>
</td>
</tr>
{{/each}}
</tbody>
</table>

View File

@ -0,0 +1,84 @@
<div class="col-md-6 col-md-offset-3">
<form method="POST" action="{{ request.script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li role="presentation" class="active"><a href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
<li role="presentation"><a href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:</label>
<textarea id="new-desc-editor" class="form-control" name="desc" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview">
</div>
</div>
<div class="form-group">
<label for="value">Value</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-8">
<label for="key">Key</label>
<input type="text" class="form-control" name="key" placeholder="Enter key">
</div>
<div class="form-vertical">
<div class="col-md-2">
<div class="radio">
<label>
<input type="radio" name="key_type[0]" value="0" checked>
Static
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="key_type[0]" value="1">
Regex
</label>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-9">
<div class="checkbox">
<label>
<input name="hidden" type="checkbox">
Hide challenge on creation
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-9">
<label>Upload challenge files</label>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<input type="file" name="files[]" multiple="multiple">
</div>
</div>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<input type="hidden" value="0" name="chaltype" id="chaltype">
<div style="text-align:center">
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
</div>
</form>
</div>

View File

@ -0,0 +1,11 @@
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview'){
$(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true}))
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview'){
$(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true}))
}
});

View File

@ -0,0 +1,19 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Regex Key</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/key/{{id}}" style="text-align:center">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
<input type="hidden" id="key-type" value="1">
<input type="hidden" id="key-id">
<hr>
<div class="row">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,2 @@
<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">

View File

@ -0,0 +1,19 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Static Key</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/key/{{id}}" style="text-align:center">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data">
<input type="hidden" id="key-type" value="0">
<input type="hidden" id="key-id">
<hr>
<div class="row">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,2 @@
<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">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,11 @@
var challenges;
var templates = {};
Handlebars.registerHelper('splitSlash', function(filename) {
var filename = filename.split("/");
filename = filename[filename.length - 1];
return filename;
});
function loadchal(id) {
var obj = $.grep(challenges['game'], function (e) {
@ -17,34 +24,54 @@ function loadchalbyname(chalname) {
}
function updateChalWindow(obj) {
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
var chal = $('#chal-window');
chal.find('.chal-name').text(obj.name);
chal.find('.chal-desc').html(marked(obj.description, {'gfm':true, 'breaks':true}));
chal.find('.chal-files').empty();
for (var i = 0; i < obj.files.length; i++) {
var filename = obj.files[i].split('/');
filename = filename[filename.length - 1];
$('#chal-window').find('.chal-files').append("<div class='col-md-3 file-button-wrapper'><a class='file-button' href='" + script_root + '/files/' + obj.files[i] + "'><label class='challenge-wrapper file-wrapper hide-text'>" + filename + "</label></a></div>")
}
$.get(script_root + '/static/original/js/templates/challenges/'+obj.type+'/'+obj.type+'-challenge-modal.hbs', function(template_data){
$('#chal-window').empty();
templates[obj.type] = template_data;
var template_data = templates[obj.type];
var template = Handlebars.compile(template_data);
var solves = obj.solves == 1 ? " Solve" : " Solves";
solves = obj.solves + solves;
var tags = chal.find('.chal-tags');
tags.empty();
var tag = "<span class='label label-primary chal-tag'>{0}</span>";
for (var i = 0; i < obj.tags.length; i++){
var data = tag.format(obj.tags[i]);
tags.append($(data));
}
var nonce = $('#nonce').val();
var wrapper = {
id: obj.id,
name: obj.name,
value: obj.value,
tags: obj.tags,
desc: marked(obj.description, {'gfm':true, 'breaks':true}),
solves: solves,
files: obj.files,
};
chal.find('.chal-value').text(obj.value);
chal.find('.chal-category').text(obj.category);
chal.find('#chal-id').val(obj.id);
var solves = obj.solves == 1 ? " Solve" : " Solves";
chal.find('.chal-solves').text(obj.solves + solves);
$('#answer').val("");
$('#chal-window').append(template(wrapper));
$.getScript(script_root + '/static/original/js/templates/challenges/'+obj.type+'/'+obj.type+'-challenge-script.js',
function() {
// Handle Solves tab
$('.chal-solves').click(function (e) {
getsolves($('#chal-id').val())
});
$('.nav-tabs a').click(function (e) {
e.preventDefault();
$(this).tab('show')
});
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
// Handle modal toggling
$('#chal-window').on('hide.bs.modal', function (event) {
$("#answer-input").removeClass("wrong");
$("#answer-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
$("#too-fast").slideUp();
});
// $('pre code').each(function(i, block) {
// hljs.highlightBlock(block);
// });
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
$('#chal-window').modal();
});
});
}
@ -71,7 +98,7 @@ function submitkey(chal, key, nonce) {
result_message.text(result.message);
if (result.status == -1){
window.location="/login"
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash
return
}
else if (result.status == 0){ // Incorrect key
@ -164,7 +191,7 @@ function getsolves(id){
});
}
function loadchals() {
function loadchals(refresh) {
$.get(script_root + "/chals", function (data) {
var categories = [];
challenges = $.parseJSON(JSON.stringify(data));
@ -215,7 +242,9 @@ function loadchals() {
}
};
updatesolves(load_location_hash);
if (!refresh){
updatesolves(load_location_hash);
}
marksolves();
$('.challenge-button').click(function (e) {
@ -264,7 +293,7 @@ function colorhash (x) {
}
function update(){
loadchals();
loadchals(true);
}
$(function() {

View File

@ -114,7 +114,7 @@ function category_breakdown_graph() {
var data = [{
values: counts,
labels: categories,
labels: keys,
hole: .4,
type: 'pie'
}];

View File

@ -0,0 +1,68 @@
<div class="modal-dialog" role="document">
<div class="modal-content content">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#challenge" aria-controls="challenge" role="tab">Challenge</a></li>
<li role="presentation"><a href="#solves" aria-controls="solves" class="chal-solves" role="tab">{{solves}}</a></li>
</ul>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="challenge">
<h3 class='chal-name'>{{ name }}</h3>
<h4 class="chal-value">{{ value }}</h4>
<div class="chal-tags">
{{#each tags }}
<span class='label label-primary chal-tag'>{{this}}</span>
{{/each}}
</div>
<span class="chal-desc">{{{ desc }}}</span>
<div class="chal-files file-row row">
{{#each files}}
<div class='col-md-3 file-button-wrapper'>
<a class='file-button' href='/files/{{this}}'>
<label class='challenge-wrapper file-wrapper hide-text'>
{{splitSlash this}}
</label>
</a>
</div>
{{/each}}
</div>
<div class="row submit-row">
<div class="col-md-9" style="padding-right:0px;padding-left:10px;">
<span class="input">
<input class="input-field" type="text" name="answer" id="answer-input" placeholder="Key" />
</span>
<input id="chal-id" type="hidden" value="{{id}}">
</div>
<div class="col-md-3 key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-theme btn-outlined pull-right" style="height:46.375px">Submit</button>
</div>
</div>
<div class="row notification-row">
<div id="result-notification" class="alert alert-dismissable" role="alert">
<strong id="result-message"></strong>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<table class="table table-striped">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
$('#submit-key').unbind('click');
$('#submit-key').click(function (e) {
e.preventDefault();
submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val())
});
$("#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' );
}
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,7 @@
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/static/admin/css/style.css">
<script src="{{ request.script_root }}/static/admin/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/static/admin/js/vendor/moment-timezone-with-data.min.js"></script>
<script src="{{ request.script_root }}/static/admin/js/vendor/handlebars.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>

View File

@ -184,6 +184,34 @@
</div>
</div>
<div id="create-keys" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<input type="hidden" class="chal-id" name="chal-id">
<div class="modal-header text-center">
<h3>Create Key</h3>
</div>
<div class="modal-body">
<div class="create-keys-select-div">
<label for="create-keys-select" class="control-label">Choose Key Type</label>
<select class="form-control" id="create-keys-select">
</select>
</div>
<br>
<div id="create-keys-entry-div">
</div>
<br>
<div style="text-align:center;display:none;" id="create-keys-button-div">
<button id="create-keys-submit" class="btn btn-theme btn-outlined">Create Key</button>
</div>
</div>
</div>
</div>
</div>
<div id="edit-keys" class="modal fade" tabindex="-1">
</div>
<div id="update-keys" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
@ -224,7 +252,7 @@
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" type="submit">Update</button>
<button class="btn btn-theme btn-outlined" id="submit-files">Update</button>
</div>
</form>
</div>
@ -291,7 +319,7 @@
<div style="text-align:center">
<br>
<h1 class="text-center">Challenges</h1>
<button class="btn btn-theme btn-outlined create-challenge">New Challenge</button>
<a class="btn btn-theme btn-outlined create-challenge" href="{{ request.script_root }}/admin/chal/new" role="button">New Challenge</a>
<div>
<table id='challenges'>
</table>

View File

@ -0,0 +1,72 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary { background-color: #337ab7; }
.btn-danger { background-color: #d9534f; }
.col-md-4 { margin-bottom: 15px; }
.key-remove-button { margin-top: 10px; }
.delete-tag { color: white; margin-left: 3px; cursor: pointer; }
</style>
{% endblock %}
{% block content %}
<div id="create-challenge" class="container main-container">
<h1 class="text-center">Create Challenge</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="create-chals-select-div">
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
<select class="form-control" id="create-chals-select">
</select>
</div>
</div>
</div>
<br>
<div class="row">
<div id="create-chal-entry-div">
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/static/admin/js/utils.js"></script>
<script src="{{ request.script_root }}/static/admin/js/vendor/codemirror.min.js"></script>
<script>
function load_chal_template(chal_type_name){
$.get(script_root + '/static/admin/js/templates/challenges/'+ chal_type_name +'/' + chal_type_name + '-challenge-create.hbs', function(template_data){
var template = Handlebars.compile(template_data);
$("#create-chal-entry-div").html(template({'nonce':nonce}));
$.getScript(script_root + '/static/admin/js/templates/challenges/'+chal_type_name+'/'+chal_type_name+'-challenge-create.js', function(){
console.log('loaded');
});
});
}
nonce = "{{ nonce }}";
$.get(script_root + '/admin/chal_types', function(data){
console.log(data);
$("#create-chals-select").empty();
var chal_type_amt = Object.keys(data).length;
if (chal_type_amt > 1){
var option = "<option> -- </option>";
$("#create-chals-select").append(option);
for (var key in data){
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
$("#create-chals-select").append(option);
}
} else if (chal_type_amt == 1) {
var key = Object.keys(data)[0];
$("#create-chals-select").parent().parent().parent().empty();
load_chal_template(data[key]);
}
});
$('#create-chals-select').change(function(){
var chal_type_name = $(this).find("option:selected").text();
load_chal_template(chal_type_name);
});
</script>
{% endblock %}

View File

@ -124,15 +124,15 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<tbody>
{% for team in teams %}
<tr name="{{ team.id }}">
<td class="team-id">{{ team.id }}</td>
<td class="team-name"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td>
<td class="team-email">{{ team.email | truncate(32) }}</td>
<td class="team-website">{% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %}
</td>
<td class="team-affiliation"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
<td class="team-affiliation" value="{{ team.affiliation if team.affiliation is not none }}"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
</td>
<td class="team-country"><span>{% if team.country %}{{ team.country }}{% endif %}</span>
<td class="team-country" value="{{ team.country if team.country is not none }}"><span>{% if team.country %}{{ team.country }}{% endif %}</span>
</td>
<td class="team-admin">
<div class="center-block checkbox text-center">
@ -263,12 +263,12 @@ $('#delete-user').click(function(e){
$('.fa-pencil-square-o').click(function(){
var elem = $(this).parent().parent().parent();
var id = elem.find('.team-id').text().trim();
var name = elem.find('.team-name').text().trim();
var email = elem.find('.team-email').text().trim();
var website = elem.find('.team-website').text().trim();
var affiliation = elem.find('.team-affiliation').text().trim();
var country = elem.find('.team-country').text().trim();
var id = elem.find('.team-id').attr('value') || '';
var name = elem.find('.team-name').attr('value') || '';
var email = elem.find('.team-email').attr('value') || '';
var website = elem.find('.team-website > a').attr('href') || '';
var affiliation = elem.find('.team-affiliation').attr('value') || '';
var country = elem.find('.team-country').attr('value') || '';
load_update_modal(id, name, email, website, affiliation, country);
});

View File

@ -14,6 +14,7 @@
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/static/user.css">
{% block stylesheets %}{% endblock %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/static/original/js/vendor/handlebars.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>

View File

@ -101,62 +101,9 @@
</div>
</div>
<div class="modal fade" id="chal-window" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content content">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#challenge" aria-controls="challenge" role="tab">Challenge</a></li>
<li role="presentation"><a href="#solves" aria-controls="solves" class="chal-solves" role="tab"></a></li>
</ul>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="challenge">
<h3 class='chal-name'></h3>
<h4 class="chal-value"></h4>
<div class="chal-tags"></div>
<p class="chal-desc"></p>
<div class="chal-files file-row row">
</div>
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="row submit-row">
<div class="col-md-9" style="padding-right:0px;padding-left:10px;">
<span class="input">
<input class="input-field" type="text" name="answer" id="answer-input" placeholder="Key" />
</span>
<input type="hidden" id="nonce" name="nonce" value={{ nonce }}>
<input id="chal-id" type="hidden">
</div>
<div class="col-md-3 key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-theme btn-outlined pull-right" style="height:46.375px">Submit</button>
</div>
</div>
<div class="row notification-row">
<div id="result-notification" class="alert alert-dismissable" role="alert">
<strong id="result-message"></strong>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<table class="table table-striped">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="chal-window" tabindex="-1" role="dialog">
</div>
{% endif %}
{% endblock %}

View File

@ -1,77 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ ctf_name() }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ request.script_root }}/static/{{ ctf_theme() }}/img/favicon.ico"
type="image/x-icon">
<link rel="icon" href="{{ request.script_root }}/static/{{ ctf_theme() }}/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ request.script_root }}/static/{{ ctf_theme() }}/css/vendor/bootstrap.min.css">
<link rel="stylesheet"
href="{{ request.script_root }}/static/{{ ctf_theme() }}/css/vendor/font-awesome/css/font-awesome.min.css"/>
<link href='{{ request.script_root }}/static/{{ ctf_theme() }}/css/vendor/lato.css' rel='stylesheet'
type='text/css'>
<link href='{{ request.script_root }}/static/{{ ctf_theme() }}/css/vendor/raleway.css' rel='stylesheet'
type='text/css'>
<link rel="stylesheet" href="{{ request.script_root }}/static/{{ ctf_theme() }}/css/style.css">
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/static/user.css">
{% block stylesheets %}{% endblock %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/vendor/moment.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>
</head>
<body>
<div class="body-container">
<div class="navbar navbar-inverse home">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse" type="button">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ request.script_root }}/" class="navbar-brand">{{ ctf_name() }}</a>
</div>
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0px">
<ul class="nav navbar-nav">
{% for page in pages() %}
<li><a href="{{ request.script_root }}/{{ page.route }}">{{ page.route|title }}</a></li>
{% endfor %}
<li><a href="{{ request.script_root }}/teams">Teams</a></li>
<li><a href="{{ request.script_root }}/scoreboard">Scoreboard</a></li>
<li><a href="{{ request.script_root }}/challenges">Challenges</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if username is defined %}
{% if admin %}
<li><a href="{{ request.script_root }}/admin">Admin</a></li>
{% endif %}
<li><a href="{{ request.script_root }}/team/{{ id }}">Team</a></li>
<li><a href="{{ request.script_root }}/profile">Profile</a></li>
<li><a href="{{ request.script_root }}/logout">Logout</a></li>
{% else %}
{% if can_register() %}
<li><a href="{{ request.script_root }}/register">Register</a></li>
<li><a style="padding-left:0px;padding-right:0px;">|</a></li>
{% endif %}
<li><a href="{{ request.script_root }}/login">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% extends "base.html" %}
{% block content %}
<div class="container main-container">
{{ content | safe }}
</div>
</div>
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/vendor/marked.min.js"></script>
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/vendor/bootstrap.min.js"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

View File

@ -12,5 +12,5 @@ WORKDIR /opt/CTFd
RUN pip install -r requirements.txt
RUN pip install pymysql
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "-w", "4", "CTFd:create_app()"]
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "-w", "4", "CTFd:create_app()", "--access-logfile", "/opt/CTFd/CTFd/logs/access.log", "--error-logfile", "/opt/CTFd/CTFd/logs/error.log"]
EXPOSE 8000

View File

@ -0,0 +1,100 @@
"""Adds challenge types and uses keys table
Revision ID: 87733981ca0e
Revises: cb3cfcc47e2f
Create Date: 2017-02-04 14:50:16.999303
"""
from CTFd.models import db, Challenges, Keys
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text, table, column
from sqlalchemy.orm import sessionmaker
import json
# revision identifiers, used by Alembic.
revision = '87733981ca0e'
down_revision = 'cb3cfcc47e2f'
branch_labels = None
depends_on = None
keys_table = table('keys',
column('id', db.Integer),
column('chal', db.Integer),
column('key_type', db.Integer),
column('flag', db.Text)
)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
## Copy over flags data to Keys table
print "Getting bind..."
conn = op.get_bind()
print "Executing: SELECT id, flags from challenges"
res = conn.execute(text("SELECT id, flags from challenges"))
results = res.fetchall()
print "There are {} results".format(len(results))
new_keys = []
print "Processing existing flags"
for r in results:
if r[1]: ## Check if flags are NULL
data = json.loads(r[1])
for old_keys in data:
new_keys.append({'chal':r[0], 'flag':old_keys.get('flag'), 'key_type':old_keys.get('type')})
if new_keys:
## Base CTFd databases actually already insert into Keys but the database does not make use of them
## This prevents duplicate entries of keys
print "Executing: TRUNCATE keys"
conn.execute(text("TRUNCATE `keys`"))
print "Bulk inserting the keys"
op.bulk_insert(keys_table, new_keys)
## Add type column to challenges
print "Adding type column to challenges"
op.add_column('challenges', sa.Column('type', sa.Integer(), nullable=True, default=0))
## Set all NULLs to 0
print "Setting all NULLs to 0"
conn.execute("UPDATE challenges set type=0 WHERE type IS NULL")
## Drop flags from challenges
print "Dropping flags column from challenges"
op.drop_column('challenges', 'flags')
print "Finished"
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
print "Getting bind..."
conn = op.get_bind()
print "Adding flags column back to challenges table"
op.add_column('challenges', sa.Column('flags', sa.TEXT(), nullable=True))
print "Dropping type column from challenges table"
op.drop_column('challenges', 'type')
print "Executing: SELECT id, flags from challenges"
res = conn.execute("SELECT id, flags from challenges")
results = res.fetchall()
print "There are {} results".format(len(results))
for chal_id in results:
new_keys = Keys.query.filter_by(chal=chal_id[0]).all()
old_flags = []
for new_key in new_keys:
flag_dict = {'flag': new_key.flag, 'type': new_key.key_type}
old_flags.append(flag_dict)
old_flags =json.dumps(old_flags)
print "Updating challenge {} to insert {}".format(chal_id[0], flag_dict)
conn.execute(text('UPDATE challenges SET flags=:flags WHERE id=:id'), id=chal_id[0], flags=old_flags)
print "Finished"
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""Add data column to keys table
Revision ID: a4e30c94c360
Revises: 87733981ca0e
Create Date: 2017-02-13 21:43:46.929248
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a4e30c94c360'
down_revision = '87733981ca0e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('keys', sa.Column('data', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('keys', 'data')
# ### end Alembic commands ###

View File

@ -1,148 +0,0 @@
"""empty message
Revision ID: cb3cfcc47e2f
Revises:
Create Date: 2017-01-17 15:39:42.804290
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3cfcc47e2f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('challenges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('flags', sa.Text(), nullable=True),
sa.Column('hidden', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.Text(), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('containers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('pages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('route', sa.String(length=80), nullable=True),
sa.Column('html', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('route')
)
op.create_table('teams',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('email', sa.String(length=128), nullable=True),
sa.Column('password', sa.String(length=128), nullable=True),
sa.Column('website', sa.String(length=128), nullable=True),
sa.Column('affiliation', sa.String(length=128), nullable=True),
sa.Column('country', sa.String(length=32), nullable=True),
sa.Column('bracket', sa.String(length=32), nullable=True),
sa.Column('banned', sa.Boolean(), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('joined', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('name')
)
op.create_table('awards',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('icon', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('key_type', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('solves',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('ip', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('chalid', 'teamid')
)
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('tag', sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tracking',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ip', sa.BigInteger(), nullable=True),
sa.Column('team', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('wrong_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('wrong_keys')
op.drop_table('tracking')
op.drop_table('tags')
op.drop_table('solves')
op.drop_table('keys')
op.drop_table('files')
op.drop_table('awards')
op.drop_table('teams')
op.drop_table('pages')
op.drop_table('containers')
op.drop_table('config')
op.drop_table('challenges')
# ### end Alembic commands ###

View File

@ -0,0 +1,174 @@
"""Base 1.0.0 CTFd database
Revision ID: cb3cfcc47e2f
Revises:
Create Date: 2017-01-17 15:39:42.804290
"""
from CTFd import create_app
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3cfcc47e2f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
app = create_app()
engine = sa.create_engine(app.config.get('SQLALCHEMY_DATABASE_URI'))
# ### commands auto generated by Alembic - please adjust! ###
if not engine.dialect.has_table(engine, 'challenges'):
op.create_table('challenges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('flags', sa.Text(), nullable=True),
sa.Column('hidden', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'config'):
op.create_table('config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.Text(), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'containers'):
op.create_table('containers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'pages'):
op.create_table('pages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('route', sa.String(length=80), nullable=True),
sa.Column('html', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('route')
)
if not engine.dialect.has_table(engine, 'teams'):
op.create_table('teams',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('email', sa.String(length=128), nullable=True),
sa.Column('password', sa.String(length=128), nullable=True),
sa.Column('website', sa.String(length=128), nullable=True),
sa.Column('affiliation', sa.String(length=128), nullable=True),
sa.Column('country', sa.String(length=32), nullable=True),
sa.Column('bracket', sa.String(length=32), nullable=True),
sa.Column('banned', sa.Boolean(), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('joined', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('name')
)
if not engine.dialect.has_table(engine, 'awards'):
op.create_table('awards',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('icon', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'files'):
op.create_table('files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'keys'):
op.create_table('keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('key_type', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'solves'):
op.create_table('solves',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('ip', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('chalid', 'teamid')
)
if not engine.dialect.has_table(engine, 'tags'):
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('tag', sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'tracking'):
op.create_table('tracking',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ip', sa.BigInteger(), nullable=True),
sa.Column('team', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'wrong_keys'):
op.create_table('wrong_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('wrong_keys')
op.drop_table('tracking')
op.drop_table('tags')
op.drop_table('solves')
op.drop_table('keys')
op.drop_table('files')
op.drop_table('awards')
op.drop_table('teams')
op.drop_table('pages')
op.drop_table('containers')
op.drop_table('config')
op.drop_table('challenges')
# ### end Alembic commands ###

View File

@ -220,8 +220,7 @@ if __name__ == '__main__':
print("GENERATING CHALLENGES")
for x in range(CHAL_AMOUNT):
word = gen_word()
flags = [{'flag': word, 'type': 0}]
db.session.add(Challenges(word, gen_sentence(), gen_value(), gen_category(), flags))
db.session.add(Challenges(word, gen_sentence(), gen_value(), gen_category()))
db.session.commit()
db.session.add(Keys(x + 1, word, 0))
db.session.commit()