CTFd code push

selenium-screenshot-testing
CodeKevin 2015-01-01 00:45:25 -05:00
parent 20183dd3c9
commit 376c90189b
50 changed files with 4488 additions and 2 deletions

3
.gitignore vendored
View File

@ -52,3 +52,6 @@ docs/_build/
# PyBuilder
target/
*.db
*.log

76
CTFd/__init__.py Normal file
View File

@ -0,0 +1,76 @@
from flask import Flask, render_template, request, redirect, abort, session, jsonify, json as json_mod, url_for
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.mail import Mail, Message
from logging.handlers import RotatingFileHandler
from flask.ext.session import Session
import logging
import os
import sqlalchemy
def create_app(subdomain, username="", password=""):
app = Flask("CTFd", static_folder="../static", template_folder="../templates")
with app.app_context():
app.config.from_object('CTFd.config')
if subdomain:
app.config.update(
SQLALCHEMY_DATABASE_URI = 'mysql://'+username+':'+password+'@localhost:3306/' + subdomain + '_ctfd',
HOST = subdomain + app.config["HOST"],
SESSION_FILE_DIR = app.config['SESSION_FILE_DIR'] + "/" + subdomain,
DEBUG = True
)
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
db.init_app(app)
db.create_all()
app.db = db
# app.setup = True
mail = Mail(app)
Session(app)
from CTFd.views import init_views
init_views(app)
from CTFd.errors import init_errors
init_errors(app)
from CTFd.challenges import init_challenges
init_challenges(app)
from CTFd.scoreboard import init_scoreboard
init_scoreboard(app)
from CTFd.auth import init_auth
init_auth(app)
from CTFd.admin import init_admin
init_admin(app)
from CTFd.utils import init_utils
init_utils(app)
return app
# logger_keys = logging.getLogger('keys')
# logger_logins = logging.getLogger('logins')
# logger_regs = logging.getLogger('regs')
# logger_keys.setLevel(logging.INFO)
# logger_logins.setLevel(logging.INFO)
# logger_regs.setLevel(logging.INFO)
# try:
# parent = os.path.dirname(__file__)
# except:
# parent = os.path.dirname(os.path.realpath(sys.argv[0]))
# key_log = RotatingFileHandler(os.path.join(parent, 'logs', 'keys.log'), maxBytes=10000)
# login_log = RotatingFileHandler(os.path.join(parent, 'logs', 'logins.log'), maxBytes=10000)
# register_log = RotatingFileHandler(os.path.join(parent, 'logs', 'registers.log'), maxBytes=10000)
# logger_keys.addHandler(key_log)
# logger_logins.addHandler(login_log)
# logger_regs.addHandler(register_log)
# logger_keys.propagate = 0
# logger_logins.propagate = 0
# logger_regs.propagate = 0

397
CTFd/admin.py Normal file
View File

@ -0,0 +1,397 @@
from CTFd import render_template, request, redirect, abort, jsonify, url_for, session
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config
from itsdangerous import TimedSerializer, BadTimeSignature
from werkzeug.utils import secure_filename
from socket import inet_aton, inet_ntoa
from passlib.hash import bcrypt_sha256
from flask import current_app as app
import logging
import hashlib
import time
import re
import os
import json
def init_admin(app):
@app.route('/admin', methods=['GET', 'POST'])
def admin():
if request.method == 'POST':
username = request.form.get('name')
password = request.form.get('password')
admin = Teams.query.filter_by(name=request.form['name'], admin=True).first()
if admin and bcrypt_sha256.verify(request.form['password'], admin.password):
session.regenerate() # NO SESSION FIXATION FOR YOU
session['username'] = admin.name
session['id'] = admin.id
session['admin'] = True
session['nonce'] = sha512(os.urandom(10))
db.session.close()
return redirect('/admin/graphs')
if is_admin():
return redirect('/admin/graphs')
return render_template('admin/login.html')
@app.route('/admin/graphs')
@admins_only
def admin_graphs():
return render_template('admin/graphs.html')
@app.route('/admin/config', methods=['GET', 'POST'])
@admins_only
def admin_config():
if request.method == "POST":
start = request.form['start']
end = request.form['end']
if not start:
start = None
else:
start = int(start)
if not end:
end = None
else:
end = int(end)
print repr(start), repr(end)
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()
return redirect('/admin/config')
start = Config.query.filter_by(key="start").first().value
end = Config.query.filter_by(key="end").first().value
return render_template('admin/config.html', start=start, end=end)
@app.route('/admin/pages', defaults={'route': None}, methods=['GET', 'POST'])
@app.route('/admin/pages/<route>', methods=['GET', 'POST'])
@admins_only
def admin_pages(route):
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()
return redirect('/admin/pages')
page = Pages(route, html)
db.session.add(page)
db.session.commit()
return redirect('/admin/pages')
if not route and request.method == 'POST':
return render_template('admin/editor.html')
pages = Pages.query.all()
return render_template('admin/pages.html', routes=pages)
@app.route('/admin/chals', methods=['POST', 'GET'])
@admins_only
def admin_chals():
# if authed():
if request.method == 'POST':
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
json = {'game':[]}
for x in chals:
json['game'].append({'id':x[1], 'name':x[2], 'value':x[3], 'description':x[4], 'category':x[5]})
db.session.close()
return jsonify(json)
else:
return render_template('admin/chals.html')
@app.route('/admin/keys/<chalid>', methods=['POST', 'GET'])
@admins_only
def admin_keys(chalid):
if request.method == 'GET':
keys = Keys.query.filter_by(chal=chalid).all()
json = {'keys':[]}
for x in keys:
json['keys'].append({'id':x.id, 'key':x.flag, 'type':x.key_type})
return jsonify(json)
elif request.method == 'POST':
keys = Keys.query.filter_by(chal=chalid).all()
for x in keys:
db.session.delete(x)
newkeys = request.form.getlist('keys[]')
newvals = request.form.getlist('vals[]')
for flag, val in zip(newkeys, newvals):
key = Keys(chalid, flag, val)
db.session.add(key)
db.session.commit()
db.session.close()
return '1'
@app.route('/admin/tags/<chalid>', methods=['GET', 'POST'])
@admins_only
def admin_tags(chalid):
if request.method == 'GET':
tags = Tags.query.filter_by(chal=chalid).all()
json = {'tags':[]}
for x in tags:
json['tags'].append({'id':x.id, 'chal':x.chal, 'tag':x.tag})
return jsonify(json)
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'
@app.route('/admin/tags/<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"
@app.route('/admin/files/<chalid>', methods=['GET', 'POST'])
@admins_only
def admin_files(chalid):
if request.method == 'GET':
files = Files.query.filter_by(chal=chalid).all()
json = {'files':[]}
for x in files:
json['files'].append({'id':x.id, 'file':x.location})
return jsonify(json)
if request.method == 'POST':
if request.form['method'] == "delete":
f = Files.query.filter_by(id=request.form['file']).first_or_404()
if os.path.isfile(f.location):
os.unlink(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:
filename = secure_filename(f.filename)
if len(filename) <= 0:
continue
md5hash = hashlib.md5(os.urandom(64)).hexdigest()
# BUG NEEDS TO GO TO S3
if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)):
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash))
f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
db_f = Files(chalid, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
db.session.add(db_f)
db.session.commit()
db.session.close()
return redirect('/admin/chals')
@app.route('/admin/teams')
@admins_only
def admin_teams():
teams = Teams.query.all()
return render_template('admin/teams.html', teams=teams)
@app.route('/admin/team/<teamid>', methods=['GET', 'POST'])
@admins_only
def admin_team(teamid):
user = Teams.query.filter_by(id=teamid).first()
solves = Solves.query.filter_by(teamid=teamid).all()
addrs = Tracking.query.filter_by(team=teamid).group_by(Tracking.ip).all()
db.session.close()
if request.method == 'GET':
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs)
elif request.method == 'POST':
json = {'solves':[]}
for x in solves:
json['solves'].append({'id':x.id, 'chal':x.chalid, 'team':x.teamid})
return jsonify(json)
@app.route('/admin/team/<teamid>/ban', methods=['POST'])
@admins_only
def ban(teamid):
user = Teams.query.filter_by(id=teamid).first()
user.banned = 1;
db.session.commit()
return redirect('/scoreboard')
@app.route('/admin/team/<teamid>/unban', methods=['POST'])
@admins_only
def unban(teamid):
user = Teams.query.filter_by(id=teamid).first()
user.banned = None;
db.session.commit()
return redirect('/scoreboard')
@app.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 = {'categories':[]}
for category, count in categories:
json['categories'].append({'category':category, 'count':count})
return jsonify(json)
elif graph_type == "solves":
solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all()
json = {}
for chal, count in solves:
json[chal.chal.name] = count
return jsonify(json)
@app.route('/admin/scoreboard')
@admins_only
def admin_scoreboard():
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 == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
return render_template('admin/scoreboard.html', teams=teams)
@app.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 == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
json = {'teams':[]}
for i, x in enumerate(teams):
json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)})
return jsonify(json)
@app.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()
db.session.close()
json = {'solves':[]}
for x in solves:
json['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)})
return jsonify(json)
@app.route('/admin/statistics', methods=['GET'])
@admins_only
def admin_stats():
db.session.commit()
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
site_hits = db.session.query(db.func.count(Tracking.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]
most_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves DESC').first()
least_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves ASC').first()
db.session.close()
return render_template('admin/statistics.html', team_count=teams_registered,
hit_count=site_hits,
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
most_solved=most_solved_chal,
least_solved = least_solved_chal
)
@app.route('/admin/fails/<teamid>', methods=['GET'])
@admins_only
def admin_fails(teamid='all'):
if teamid == "all":
fails = WrongKeys.query.count()
solves = Solves.query.count()
db.session.close()
json = {'fails':str(fails), 'solves': str(solves)}
return jsonify(json)
else:
fails = WrongKeys.query.filter_by(team=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
json = {'fails':str(fails), 'solves': str(solves)}
return jsonify(json)
@app.route('/admin/chal/new', methods=['POST'])
def admin_create_chal():
files = request.files.getlist('files[]')
# Create challenge
chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'])
db.session.add(chal)
db.session.commit()
# Add keys
key = Keys(chal.id, request.form['key'], request.form['key_type[0]'])
db.session.add(key)
db.session.commit()
for f in files:
filename = secure_filename(f.filename)
if len(filename) <= 0:
continue
md5hash = hashlib.md5(filename).hexdigest()
if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)):
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash))
f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
db_f = Files(chal.id, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
db.session.add(db_f)
db.session.commit()
db.session.close()
return redirect('/admin/chals')
@app.route('/admin/chal/update', methods=['POST'])
def admin_update_chal():
challenge=Challenges.query.filter_by(id=request.form['id']).first()
challenge.name = request.form['name']
challenge.description = request.form['desc']
challenge.value = request.form['value']
challenge.category = request.form['category']
db.session.add(challenge)
db.session.commit()
db.session.close()
return redirect('/admin/chals')

129
CTFd/auth.py Normal file
View File

@ -0,0 +1,129 @@
from CTFd import render_template, request, redirect, abort, jsonify, url_for, session
from CTFd.utils import sha512, is_safe_url, authed, mailserver
from CTFd.models import db, Teams
from itsdangerous import TimedSerializer, BadTimeSignature
from passlib.hash import bcrypt_sha256
from flask import current_app as app
import logging
import time
import re
import os
def init_auth(app):
@app.context_processor
def inject_user():
if authed():
return dict(session)
return dict()
@app.route('/reset_password', methods=['POST', 'GET'])
@app.route('/reset_password/<data>', methods=['POST', 'GET'])
def reset_password(data=None):
if data is not None and request.method == "GET":
return render_template('reset_password.html', mode='set')
if data is not None and request.method == "POST":
try:
s = TimedSerializer(app.config['SECRET_KEY'])
name = s.loads(data.decode('base64'), max_age=1800)
except BadTimeSignature:
return render_template('reset_password.html', errors=['Your link has expired'])
team = Teams.query.filter_by(name=name).first()
team.password = sha512(request.form['password'].strip())
db.session.commit()
db.session.close()
return redirect('/login')
if request.method == 'POST':
email = request.form['email'].strip()
team = Teams.query.filter_by(email=email).first()
if not team:
return render_template('reset_password.html', errors=['Check your email'])
s = TimedSerializer(app.config['SECRET_KEY'])
token = s.dumps(team.name)
text = """
Did you initiate a password reset?
{0}/reset_password/{1}
""".format(app.config['HOST'], token.encode('base64'))
sendmail(email, text)
return render_template('reset_password.html', errors=['Check your email'])
return render_template('reset_password.html')
@app.route('/register', methods=['POST', 'GET'])
def register():
if request.method == 'POST':
errors = []
name_len = len(request.form['name']) == 0
names = Teams.query.add_columns('name', 'id').filter_by(name=request.form['name']).first()
emails = Teams.query.add_columns('email', 'id').filter_by(email=request.form['email']).first()
pass_len = len(request.form['password']) == 0
valid_email = re.match("[^@]+@[^@]+\.[^@]+", request.form['email'])
if not valid_email:
errors.append("That email doesn't look right")
if names:
errors.append('That team name is already taken')
if emails:
errors.append('That email has already been used')
if pass_len:
errors.append('Pick a longer password')
if name_len:
errors.append('Pick a longer team name')
if not errors:
with app.app_context():
team = Teams(request.form['name'], request.form['email'], request.form['password'])
db.session.add(team)
db.session.commit()
if mailserver():
sendmail(request.form['email'], "You've successfully registered for the CTF")
db.session.close()
if len(errors) > 0:
return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password'])
logger = logging.getLogger('regs')
logger.warn("[{0}] {1} registered with {2}".format(time.strftime("%m/%d/%Y %X"), request.form['name'], request.form['email']))
return redirect('/login')
else:
return render_template('register.html')
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
errors = []
# team = Teams.query.filter_by(name=request.form['name'], password=sha512(request.form['password'])).first()
team = Teams.query.filter_by(name=request.form['name']).first()
if team and bcrypt_sha256.verify(request.form['password'], team.password):
# session.regenerate() # NO SESSION FIXATION FOR YOU
session['username'] = team.name
session['id'] = team.id
session['admin'] = team.admin
session['nonce'] = sha512(os.urandom(10))
db.session.close()
logger = logging.getLogger('logins')
logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username']))
# if request.args.get('next') and is_safe_url(request.args.get('next')):
# return redirect(request.args.get('next'))
return redirect('/team/{0}'.format(team.id))
else:
errors.append("That account doesn't seem to exist")
db.session.close()
return render_template('login.html', errors=errors)
else:
db.session.close()
return render_template('login.html')
@app.route('/logout')
def logout():
if authed():
session.clear()
return redirect('/')

127
CTFd/challenges.py Normal file
View File

@ -0,0 +1,127 @@
from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for
from CTFd import session, logging
from CTFd.utils import ctftime, authed, unix_time, get_kpm
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys
import time
def init_challenges(app):
@app.route('/challenges', methods=['GET'])
def challenges():
if not ctftime():
return redirect('/')
if authed():
return render_template('chals.html')
else:
return redirect(url_for('login', next="challenges"))
@app.route('/chals', methods=['GET'])
def chals():
if not ctftime():
return redirect('/')
if authed():
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
json = {'game':[]}
for x in chals:
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})
db.session.close()
return jsonify(json)
else:
db.session.close()
return redirect('/login')
@app.route('/chals/solves')
def chals_per_solves():
if authed():
solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all()
json = {}
for chal, count in solves:
json[chal.chal.name] = count
return jsonify(json)
return redirect(url_for('login', next="/chals/solves"))
@app.route('/solves')
@app.route('/solves/<teamid>')
def solves(teamid=None):
if teamid is None:
if authed():
solves = Solves.query.filter_by(teamid=session['id']).all()
else:
return redirect('/login')
else:
solves = Solves.query.filter_by(teamid=teamid).all()
db.session.close()
json = {'solves':[]}
for x in solves:
json['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)})
return jsonify(json)
@app.route('/fails/<teamid>', methods=['GET'])
def fails(teamid):
fails = WrongKeys.query.filter_by(team=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
json = {'fails':str(fails), 'solves': str(solves)}
return jsonify(json)
@app.route('/chal/<chalid>/solves', methods=['GET'])
def who_solved(chalid):
solves = Solves.query.filter_by(chalid=chalid)
json = {'teams':[]}
for solve in solves:
json['teams'].append({'id':solve.team.id, 'name':solve.team.name, 'date':solve.date})
return jsonify(json)
@app.route('/chal/<chalid>', methods=['POST'])
def chal(chalid):
if not ctftime():
return redirect('/')
if authed():
logger = logging.getLogger('keys')
data = (time.strftime("%m/%d/%Y %X"), session['username'], request.form['key'], get_kpm(session['id']))
print "[{0}] {1} submitted {2} with kpm {3}".format(*data)
if get_kpm(session['id']) > 10:
wrong = WrongKeys(session['id'], chalid, request.form['key'])
db.session.add(wrong)
db.session.commit()
db.session.close()
logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
return "3" # Submitting too fast
solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first()
if not solves:
keys = Keys.query.filter_by(chal=chalid).all()
key = request.form['key'].strip().lower()
for x in keys:
if x.key_type == 0: #static key
if x.flag.strip().lower() == key:
solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr)
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
elif x.key_type == 1: #regex
res = re.match(str(x), key, re.IGNORECASE)
if res and res.group() == key:
solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr)
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
wrong = WrongKeys(session['id'], chalid, request.form['key'])
db.session.add(wrong)
db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data))
return '0' # key was wrong
else:
logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data))
return "2" # challenge was already solved
else:
return "-1"

19
CTFd/config.py Normal file
View File

@ -0,0 +1,19 @@
import os
##### SERVER SETTINGS #####
SECRET_KEY = os.urandom(64)
SQLALCHEMY_DATABASE_URI = 'sqlite:///ctfd.db'
SESSION_TYPE = "filesystem"
SESSION_FILE_DIR = "/tmp/flask_session"
SESSION_COOKIE_HTTPONLY = True
HOST = ".ctfd.io"
UPLOAD_FOLDER = 'static/uploads'
##### EMAIL #####
CTF_NAME = ''
MAIL_SERVER = ''
MAIL_PORT = 0
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = ''
MAIL_PASSWORD = ''
ADMINS = []

14
CTFd/errors.py Normal file
View File

@ -0,0 +1,14 @@
from flask import current_app as app, render_template
def init_errors(app):
@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html'), 404
@app.errorhandler(403)
def forbidden(error):
return render_template('errors/403.html'), 403
@app.errorhandler(500)
def forbidden(error):
return render_template('errors/500.html'), 500

169
CTFd/models.py Normal file
View File

@ -0,0 +1,169 @@
#from CTFd import db
from flask.ext.sqlalchemy import SQLAlchemy
from socket import inet_aton, inet_ntoa
from struct import unpack, pack
from passlib.hash import bcrypt_sha256
import datetime
import hashlib
def sha512(string):
return hashlib.sha512(string).hexdigest()
def ip2long(ip):
return unpack('!I', inet_aton(ip))[0]
def long2ip(ip_int):
return inet_ntoa(pack('!I', ip_int))
db = SQLAlchemy()
class Pages(db.Model):
id = db.Column(db.Integer, primary_key=True)
route = db.Column(db.String(80), unique=True)
html = db.Column(db.Text)
def __init__(self, route, html):
self.route = route
self.html = html
def __repr__(self):
return "<Tag {0} for challenge {1}>".format(self.tag, self.chal)
class Challenges(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
# add open category
def __init__(self, name, description, value, category):
self.name = name
self.description = description
self.value = value
self.category = category
def __repr__(self):
return '<chal %r>' % self.name
class Tags(db.Model):
id = db.Column(db.Integer, primary_key=True)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
tag = db.Column(db.String(80))
def __init__(self, chal, tag):
self.chal = chal
self.tag = tag
def __repr__(self):
return "<Tag {0} for challenge {1}>".format(self.tag, self.chal)
class Files(db.Model):
id = db.Column(db.Integer, primary_key=True)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
location = db.Column(db.Text)
def __init__(self, chal, location):
self.chal = chal
self.location = location
def __repr__(self):
return "<File {0} for challenge {1}>".format(self.location, self.chal)
class Keys(db.Model):
id = db.Column(db.Integer, primary_key=True)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
key_type = db.Column(db.Integer)
flag = db.Column(db.Text)
def __init__(self, chal, flag, key_type):
self.chal = chal
self.flag = flag
self.key_type = key_type
def __repr__(self):
return self.flag
class Teams(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128), unique=True)
password = db.Column(db.String(128))
website = db.Column(db.String(128))
affiliation = db.Column(db.String(128))
country = db.Column(db.String(32))
bracket = db.Column(db.String(32))
banned = db.Column(db.Boolean)
admin = db.Column(db.Boolean)
def __init__(self, name, email, password):
self.name = name
self.email = email
self.password = bcrypt_sha256.encrypt(password)
def __repr__(self):
return '<team %r>' % self.name
class Solves(db.Model):
__table_args__ = (db.UniqueConstraint('chalid', 'teamid'), {})
id = db.Column(db.Integer, primary_key=True)
chalid = db.Column(db.Integer, db.ForeignKey('challenges.id'))
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
ip = db.Column(db.Integer)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
team = db.relationship('Teams', foreign_keys="Solves.teamid", lazy='joined')
chal = db.relationship('Challenges', foreign_keys="Solves.chalid", lazy='joined')
# value = db.Column(db.Integer)
def __init__(self, chalid, teamid, ip):
self.ip = ip2long(ip)
self.chalid = chalid
self.teamid = teamid
# self.value = value
def __repr__(self):
return '<solves %r>' % self.chal
class WrongKeys(db.Model):
id = db.Column(db.Integer, primary_key=True)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
team = db.Column(db.Integer, db.ForeignKey('teams.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
flag = db.Column(db.Text)
def __init__(self, team, chal, flag):
self.team = team
self.chal = chal
self.flag = flag
def __repr__(self):
return '<wrong %r>' % self.flag
class Tracking(db.Model):
id = db.Column(db.Integer, primary_key=True)
ip = db.Column(db.BigInteger)
team = db.Column(db.Integer, db.ForeignKey('teams.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, ip, team):
self.ip = ip2long(ip)
self.team = team
def __repr__(self):
return '<ip %r>' % self.team
class Config(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.Text)
value = db.Column(db.Text)
def __init__(self, key, value):
self.key = key
self.value = value

50
CTFd/scoreboard.py Normal file
View File

@ -0,0 +1,50 @@
from flask import current_app as app
from CTFd import session, render_template, jsonify
from CTFd.utils import unix_time
from CTFd.models import db, Teams, Solves, Challenges
def init_scoreboard(app):
@app.route('/scoreboard')
def scoreboard():
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 == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;")
db.session.close()
return render_template('scoreboard.html', teams=teams)
@app.route('/scores')
def scores():
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;")
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 == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
json = {'teams':[]}
for i, x in enumerate(teams):
json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)})
return jsonify(json)
@app.route('/top/<count>')
def topteams(count):
try:
count = int(count)
except:
count = 10
if count > 20 or count < 0:
count = 10
json = {'scores':{}}
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC LIMIT {0};".format(count))
score = db.func.sum(Challenges.value).label('score')
teams = db.session.query(Solves.teamid, Teams.name, score, db.func.max(Solves.date).label('quickest')).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), Solves.date).limit(count)
for team in teams:
solves = Solves.query.filter_by(teamid=team.teamid).all()
json['scores'][team.name] = []
for x in solves:
json['scores'][team.name].append({'id':x.teamid, 'chal':x.chalid, 'team':x.teamid, 'value': x.chal.value, 'time':unix_time(x.date)})
return jsonify(json)

124
CTFd/utils.py Normal file
View File

@ -0,0 +1,124 @@
from CTFd import session
from CTFd.models import db, WrongKeys, Pages, Config
from urlparse import urlparse, urljoin
from functools import wraps
from flask import current_app as app, g, request, redirect, url_for
from socket import inet_aton, inet_ntoa
from struct import unpack, pack
import time
import datetime
import hashlib
import json
def init_utils(app):
app.jinja_env.filters['unix_time'] = unix_time
app.jinja_env.filters['unix_time_millis'] = unix_time_millis
app.jinja_env.filters['long2ip'] = long2ip
app.jinja_env.globals.update(pages=pages)
def pages():
pages = Pages.query.filter(Pages.route!="index").all()
return pages
def authed():
try:
if session['id']:
return True
except KeyError:
pass
return False
def is_setup():
setup = Config.query.filter_by(key='setup').first()
if setup:
return setup.value
else:
return False
def is_admin():
if authed():
return session['admin']
else:
return False
def admins_only(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if session.get('admin', None) is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def ctftime():
""" Checks whether it's CTF time or not. """
start = Config.query.filter_by(key="start").first().value
end = Config.query.filter_by(key="end").first().value
if start:
start = int(start)
if end:
end = int(end)
if start and end:
if start < time.time() and time.time() < end:
# Within the two time bounds
return True
if start < time.time() and end is None:
# CTF starts on a date but never ends
return True
if start is None and time.time() < end:
# CTF started but ends at a date
return True
if start is None and end is None:
# CTF has no time requirements
return True
return False
def unix_time(dt):
epoch = datetime.datetime.utcfromtimestamp(0)
delta = dt - epoch
return int(delta.total_seconds())
def unix_time_millis(dt):
return unix_time(dt) * 1000
def long2ip(ip_int):
return inet_ntoa(pack('!I', ip_int))
def ip2long(ip):
return unpack('!I', inet_aton(ip))[0]
def get_kpm(teamid): # keys per minute
one_min_ago = datetime.datetime.utcnow() + datetime.timedelta(minutes=-1)
return len(db.session.query(WrongKeys).filter(WrongKeys.team == teamid, WrongKeys.date >= one_min_ago).all())
def mailserver():
if app.config['MAIL_SERVER'] and app.config['MAIL_PORT'] and app.config['ADMINS']:
return True
return False
def sendmail(addr, text):
try:
msg = Message("Message from {0}".format(app.config['CTF_NAME']), sender = app.config['ADMINS'][0], recipients = [addr])
msg.body = text
mail.send(msg)
return True
except:
return False
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
def sha512(string):
return hashlib.sha512(string).hexdigest()

167
CTFd/views.py Normal file
View File

@ -0,0 +1,167 @@
from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for
from CTFd import session
from CTFd.utils import authed, ip2long, long2ip, is_setup
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config
from jinja2.exceptions import TemplateNotFound
from collections import OrderedDict
import logging
import os
import re
import sys
import json
import os
def init_views(app):
@app.before_request
def tracker():
if authed():
if not Tracking.query.filter_by(ip=ip2long(request.remote_addr)).first():
visit = Tracking(request.remote_addr, session['id'])
db.session.add(visit)
db.session.commit()
db.session.close()
@app.before_request
def csrf():
if authed() and request.method == "POST":
if session['nonce'] != request.form.get('nonce'):
abort(403)
@app.before_request
def redirect_setup():
if request.path == "/static/css/style.css":
return
if not is_setup() and request.path != "/setup":
return redirect('/setup')
@app.before_first_request
def needs_setup():
if not is_setup():
return redirect('/setup')
@app.route('/setup', methods=['GET', 'POST'])
def setup():
# with app.app_context():
# admin = Teams.query.filter_by(admin=True).first()
if not is_setup():
if request.method == 'POST':
## Admin user
name = request.form['name']
email = request.form['email']
password = request.form['password']
admin = Teams(name, email, password)
admin.admin = True
## Index page
html = request.form['html']
page = Pages('index', html)
## Start time
start = Config('start', None)
end = Config('end', None)
setup = Config('setup', True)
db.session.add(admin)
db.session.add(page)
db.session.add(start)
db.session.add(end)
db.session.add(setup)
db.session.commit()
app.setup = False
return redirect('/')
return render_template('setup.html')
return redirect('/')
# Static HTML files
@app.route("/", defaults={'template': 'index'})
@app.route("/<template>")
def static_html(template):
try:
return render_template('%s.html' % template)
except TemplateNotFound:
page = Pages.query.filter_by(route=template).first()
if page:
return render_template_string('{% extends "base.html" %}{% block content %}' + page.html + '{% endblock %}')
else:
abort(404)
@app.route('/teams')
def teams():
teams = Teams.query.all()
return render_template('teams.html', teams=teams)
@app.route('/team/<teamid>', methods=['GET', 'POST'])
def team(teamid):
user = Teams.query.filter_by(id=teamid).first()
solves = Solves.query.filter_by(teamid=teamid).all()
db.session.close()
if request.method == 'GET':
return render_template('team.html', solves=solves, team=user)
elif request.method == 'POST':
json = {'solves':[]}
for x in solves:
json['solves'].append({'id':x.id, 'chal':x.chalid, 'team':x.teamid})
return jsonify(json)
@app.route('/profile', methods=['POST', 'GET'])
def profile():
if authed():
if request.method == "POST":
errors = []
name = request.form.get('name')
email = request.form.get('email')
if 'password' in request.form.keys() and not len(request.form['password']) == 0:
password = request.form.get('password')
website = request.form.get('website')
affiliation = request.form.get('affiliation')
country = request.form.get('country')
names = Teams.query.filter_by(name=name).first()
emails = Teams.query.filter_by(email=email).first()
valid_email = re.match("[^@]+@[^@]+\.[^@]+", email)
name_len = len(request.form['name']) == 0
if not bcrypt_sha256.verify(request.form.get('confirm').strip(), names.password):
errors.append("Your old password doesn't match what we have.")
if not valid_email:
errors.append("That email doesn't look right")
if names and name!=session['username']:
errors.append('That team name is already taken')
if emails and emails.id != session['id']:
errors.append('That email has already been used')
if name_len:
errors.append('Pick a longer team name')
if len(errors) > 0:
return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation, country=country, errors=errors)
else:
team = Teams.query.filter_by(id=session['id']).first()
team.name = name
team.email = email
team.password = bcrypt_sha256.encrypt(password)
team.website = website
team.affiliation = affiliation
team.country = country
db.session.commit()
db.session.close()
return redirect('/profile')
else:
user = Teams.query.filter_by(id=session['id']).first()
name = user.name
email = user.email
website = user.website
affiliation = user.affiliation
country = user.country
return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation, country=country)
else:
return redirect('/login')

View File

@ -1,4 +1,10 @@
CTFd
![](https://github.com/isislab/CTFd/blob/master/static/img/logo.png)
====
CTFs as you need them
CTFd is a CTF in a can. Easily modifiable and has everything you need to run a jeopardy style CTF.
Install:
1. `./prepare.sh` to install dependencies using apt.
2. Modify [CTFd/config.py](https://github.com/isislab/CTFd/blob/master/CTFd/config.py) to your liking.
3. Use `python serve.py` in a terminal to drop into debug mode.
4. [Here](http://flask.pocoo.org/docs/0.10/deploying/) are some Flask deployment options

942
populate.py Normal file
View File

@ -0,0 +1,942 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
from CTFd import create_app
from random import randint
import datetime
import random
import hashlib
import os
import sys
app = create_app(sys.argv[1], sys.argv[2], sys.argv[3])
USER_AMOUNT = 50
CHAL_AMOUNT = 20
categories = [
'Exploitation',
'Reversing',
'Web',
'Forensics',
'Scripting',
'Cryptography',
'Networking',
]
lorems = [
'Lorem',
'ipsum',
'dolor',
'sit',
'amet,',
'consectetur',
'adipiscing',
'elit.',
'Proin',
'fringilla',
'elit',
'velit,',
'sed',
'scelerisque',
'tellus',
'dapibus',
'vel.',
'Aenean',
'at',
'urna',
'porta,',
'fringilla',
'erat',
'eget,',
'lobortis',
'quam.',
'Praesent',
'luctus,',
'quam',
'at',
'consequat',
'luctus,',
'mauris',
'sem',
'pretium',
'metus,',
'eu',
'viverra',
'dui',
'leo',
'in',
'tortor.',
'Cras',
'iaculis',
'enim',
'erat,',
'sed',
'gravida',
'velit',
'consectetur',
'a.',
'Duis',
'eget',
'fermentum',
'elit.',
'Vivamus',
'laoreet',
'elementum',
'massa,',
'ut',
'sodales',
'mi',
'gravida',
'at.',
'Vivamus',
'dignissim',
'in',
'eros',
'non',
'iaculis.',
'Vivamus',
'nec',
'sem',
'fringilla,',
'semper',
'lectus',
'in,',
'malesuada',
'tellus.',
'Vestibulum',
'mattis',
'commodo',
'enim',
'sit',
'amet',
'scelerisque.',
'Proin',
'at',
'condimentum',
'nisi,',
'nec',
'fringilla',
'ante.',
'Vestibulum',
'sit',
'amet',
'neque',
'sit',
'amet',
'elit',
'placerat',
'interdum',
'egestas',
'ac',
'malesuada',
'quis',
'arcu',
'ac',
'blandit.',
'Vivamus',
'in',
'massa',
'a',
'purus',
'bibendum',
'sagittis.',
'Nunc',
'venenatis',
'lacus',
'sed',
'nulla',
'dapibus,',
'consequat',
'laoreet',
'nisi',
'faucibus.',
'Nam',
'consequat',
'viverra',
'nibh',
'a',
'cursus.',
'Phasellus',
'tristique',
'justo',
'vitae',
'rutrum',
'pharetra.',
'Sed',
'sed',
'porttitor',
'lacus.',
'Nam',
'ornare',
'sit',
'amet',
'nisi',
'imperdiet',
'vulputate.',
'Maecenas',
'hendrerit',
'ullamcorper',
'elit,',
'sed',
'pellentesque',
'lacus',
'bibendum',
'sit',
'amet.',
'Aliquam',
'consectetur',
'odio',
'quis',
'tellus',
'ornare,',
'id',
'malesuada',
'dui',
'rhoncus.',
'Quisque',
'fringilla',
'pellentesque',
'nulla',
'id',
'congue.',
'Nulla',
'ultricies',
'dolor',
'tristique',
'facilisis',
'at',
'accumsan',
'nisi.',
'Praesent',
'commodo,',
'mauris',
'sit',
'amet',
'placerat',
'condimentum,',
'nibh',
'leo',
'pulvinar',
'justo,',
'vel',
'dignissim',
'mi',
'dolor',
'et',
'est.',
'Nulla',
'facilisi.',
'Sed',
'nunc',
'est,',
'lobortis',
'id',
'diam',
'nec,',
'vulputate',
'varius',
'orci.',
'Maecenas',
'iaculis',
'vehicula',
'eros',
'eu',
'congue.',
'Nam',
'tempor',
'commodo',
'lobortis.',
'Donec',
'eget',
'posuere',
'dolor,',
'ut',
'rhoncus',
'tortor.',
'Donec',
'et',
'quam',
'quis',
'urna',
'rhoncus',
'fermentum',
'et',
'ut',
'tellus.',
'Aliquam',
'erat',
'volutpat.',
'Morbi',
'porttitor',
'ante',
'nec',
'porta',
'mollis.',
'Ut',
'sodales',
'pellentesque',
'rutrum.',
'Nullam',
'elit',
'eros,',
'sollicitudin',
'ac',
'rutrum',
'sit',
'amet,',
'eleifend',
'vel',
'nulla.',
'Morbi',
'quis',
'lacinia',
'nisi.',
'Integer',
'at',
'neque',
'vel',
'velit',
'tincidunt',
'elementum',
'lobortis',
'sit',
'amet',
'tellus.',
'Nunc',
'volutpat',
'diam',
'ac',
'diam',
'lacinia,',
'id',
'molestie',
'quam',
'eu',
'ultricies',
'ligula.',
'Duis',
'iaculis',
'massa',
'massa,',
'eget',
'venenatis',
'dolor',
'fermentum',
'laoreet.',
'Nam',
'posuere,',
'erat',
'quis',
'tempor',
'consequat,',
'purus',
'erat',
'hendrerit',
'arcu,',
'nec',
'aliquam',
'ligula',
'augue',
'vitae',
'felis.',
'Vestibulum',
'tincidunt',
'ipsum',
'vel',
'pharetra',
'lacinia.',
'Quisque',
'dignissim,',
'arcu',
'non',
'feugiat',
'semper,',
'felis',
'est',
'commodo',
'lorem,',
'malesuada',
'elementum',
'nibh',
'lectus',
'porttitor',
'nisi.',
'Duis',
'non',
'lacinia',
'nisl.',
'Etiam',
'ante',
'nisl,',
'mattis',
'eget',
'convallis',
'vel,',
'ullamcorper',
'ac',
'nisl.',
'Duis',
'eu',
'massa',
'at',
'urna',
'laoreet',
'convallis.',
'Donec',
'tincidunt',
'sapien',
'sit',
'amet',
'varius',
'eu',
'dignissim',
'tortor,',
'elementum',
'gravida',
'eros.',
'Cras',
'viverra',
'accumsan',
'erat,',
'et',
'euismod',
'dui',
'placerat',
'ac.',
'Ut',
'tortor',
'arcu,',
'euismod',
'vitae',
'aliquam',
'in,',
'interdum',
'vitae',
'magna.',
'Vestibulum',
'leo',
'ante,',
'posuere',
'eget',
'est',
'non,',
'adipiscing',
'ultrices',
'erat.',
'Donec',
'suscipit',
'felis',
'molestie,',
'ultricies',
'dui',
'a,',
'facilisis',
'magna.',
'Cum',
'sociis',
'natoque',
'penatibus',
'et',
'magnis',
'dis',
'parturient',
'montes,',
'nascetur',
'ridiculus',
'mus.',
'Nulla',
'quis',
'odio',
'sit',
'amet',
'ante',
'tristique',
'accumsan',
'ut',
'iaculis',
'neque.',
'Vivamus',
'in',
'venenatis',
'enim.',
'Nunc',
'dignissim',
'justo',
'neque,',
'sed',
'ultricies',
'justo',
'dictum',
'in.',
'Nulla',
'eget',
'nunc',
'ac',
'arcu',
'vestibulum',
'bibendum',
'vitae',
'quis',
'tellus.',
'Morbi',
'bibendum,',
'quam',
'ac',
'cursus',
'posuere,',
'purus',
'lectus',
'tempor',
'est,',
'eu',
'iaculis',
'quam',
'enim',
'a',
'nibh.',
'Etiam',
'consequat',
'libero',
'vitae',
'ullamcorper',
'tincidunt.',
]
hipsters = ['Ethnic', 'narwhal', 'pickled', 'Odd', 'Future', 'cliche', 'VHS', 'whatever', 'Etsy', 'American', 'Apparel', 'kitsch', 'wolf', 'mlkshk', 'fashion', 'axe', 'ethnic', 'banh', 'mi', 'cornhole', 'scenester', 'Echo', 'Park', 'Dreamcatcher', 'tofu', 'fap', 'selvage', 'authentic', 'cliche', 'High', 'Life', 'brunch', 'pork', 'belly', 'viral', 'XOXO', 'drinking', 'vinegar', 'bitters', 'Wayfarers', 'gastropub', 'dreamcatcher', 'chillwave', 'Shoreditch', 'kale', 'chips', 'swag', 'street', 'art', 'put', 'a', 'bird', 'on', 'it', 'Vice', 'synth', 'cliche', 'retro', 'Master', 'cleanse', 'ugh', 'Austin', 'slow-carb', 'small', 'batch', 'Hashtag', 'food', 'truck', 'deep', 'v', 'semiotics', 'chia', 'normcore', 'bicycle', 'rights', 'Austin', 'drinking', 'vinegar', 'hella', 'readymade', 'farm-to-table', 'Wes', 'Anderson', 'put', 'a', 'bird', 'on', 'it', 'freegan', 'Synth', 'lo-fi', 'food', 'truck', 'chambray', 'Shoreditch', 'cliche', 'kogiSynth', 'lo-fi', 'fap', 'single-origin', 'coffee', 'brunch', 'butcher', 'Pickled', 'Etsy', 'locavore', 'forage', 'pug', 'stumptown', 'occupy', 'PBR&B', 'actually', 'shabby', 'chic', 'church-key', 'disrupt', 'lomo', 'hoodie', 'Tumblr', 'biodiesel', 'Pinterest', 'butcher', 'Hella', 'Carles', 'pour-over', 'YOLO', 'VHS', 'literally', 'Selvage', 'narwhal', 'flexitarian', 'wayfarers', 'kitsch', 'bespoke', 'sriracha', 'Banh', 'mi', '8-bit', 'cornhole', 'viral', 'Tonx', 'keytar', 'gastropub', 'YOLO', 'hashtag', 'food', 'truck', '3', 'wolf', 'moonFingerstache', 'flexitarian', 'craft', 'beer', 'shabby', 'chic', '8-bit', 'try-hard', 'semiotics', 'Helvetica', 'keytar', 'PBR', 'four', 'loko', 'scenester', 'keytar', '3', 'wolf', 'moon', 'sriracha', 'gluten-free', 'literally', 'try-hard', 'put', 'a', 'bird', 'on', 'it', 'cornhole', 'blog', 'fanny', 'pack', 'Mumblecore', 'pickled', 'distillery', 'butcher', 'Ennui', 'tote', 'bag', 'letterpress', 'disrupt', 'keffiyeh', 'art', 'party', 'aesthetic', 'Helvetica', 'stumptown', 'Wes', 'Anderson', 'next', 'level', "McSweeney's", 'cornhole', 'Schlitz', 'skateboard', 'pop-up', 'Chillwave', 'biodiesel', 'semiotics', 'seitan', 'authentic', 'bicycle', 'rights', 'wolf', 'pork', 'belly', 'letterpress', 'locavore', 'whatever', 'fixie', 'viral', 'mustache', 'beard', 'Hashtag', 'sustainable', 'lomo', 'cardigan', 'lo-fiWilliamsburg', 'craft', 'beer', 'bitters', 'iPhone', 'gastropub', 'messenger', 'bag', 'Organic', 'post-ironic', 'fingerstache', 'ennui', 'banh', 'mi', 'Art', 'party', 'bitters', 'twee', 'bespoke', 'church-key', 'Intelligentsia', 'sriracha', 'Echo', 'Park', 'Tofu', 'locavore', 'street', 'art', 'freegan', 'farm-to-table', 'distillery', 'hoodie', 'swag', 'ugh', 'YOLO', 'VHS', 'Cred', 'hella', 'readymade', 'distillery', 'Banh', 'mi', 'Echo', 'Park', "McSweeney's,", 'mlkshk', 'photo', 'booth', 'swag', 'Odd', 'Future', 'squid', 'Tonx', 'craft', 'beer', 'High', 'Life', 'tousled', 'PBR', 'you', 'probably', "haven't", 'heard', 'of', 'them', 'locavore', 'PBR&B', 'street', 'art', 'pop-up', 'raw', 'denim']
names = [
'James',
'John',
'Robert',
'Michael',
'William',
'David',
'Richard',
'Joseph',
'Charles',
'Thomas',
'Christopher',
'Daniel',
'Matthew',
'Donald',
'Anthony',
'Paul',
'Mark',
'George',
'Steven',
'Kenneth',
'Andrew',
'Edward',
'Brian',
'Joshua',
'Kevin',
'Ronald',
'Timothy',
'Jason',
'Jeffrey',
'Gary',
'Ryan',
'Nicholas',
'Eric',
'Stephen',
'Jacob',
'Larry',
'Frank',
'Jonathan',
'Scott',
'Justin',
'Raymond',
'Brandon',
'Gregory',
'Samuel',
'Patrick',
'Benjamin',
'Jack',
'Dennis',
'Jerry',
'Alexander',
'Tyler',
'Douglas',
'Henry',
'Peter',
'Walter',
'Aaron',
'Jose',
'Adam',
'Harold',
'Zachary',
'Nathan',
'Carl',
'Kyle',
'Arthur',
'Gerald',
'Lawrence',
'Roger',
'Albert',
'Keith',
'Jeremy',
'Terry',
'Joe',
'Sean',
'Willie',
'Jesse',
'Ralph',
'Billy',
'Austin',
'Bruce',
'Christian',
'Roy',
'Bryan',
'Eugene',
'Louis',
'Harry',
'Wayne',
'Ethan',
'Jordan',
'Russell',
'Alan',
'Philip',
'Randy',
'Juan',
'Howard',
'Vincent',
'Bobby',
'Dylan',
'Johnny',
'Phillip',
'Craig',
'Mary',
'Patricia',
'Elizabeth',
'Jennifer',
'Linda',
'Barbara',
'Susan',
'Margaret',
'Jessica',
'Dorothy',
'Sarah',
'Karen',
'Nancy',
'Betty',
'Lisa',
'Sandra',
'Helen',
'Donna',
'Ashley',
'Kimberly',
'Carol',
'Michelle',
'Amanda',
'Emily',
'Melissa',
'Laura',
'Deborah',
'Stephanie',
'Rebecca',
'Sharon',
'Cynthia',
'Ruth',
'Kathleen',
'Anna',
'Shirley',
'Amy',
'Angela',
'Virginia',
'Brenda',
'Pamela',
'Catherine',
'Katherine',
'Nicole',
'Christine',
'Janet',
'Debra',
'Carolyn',
'Samantha',
'Rachel',
'Heather',
'Maria',
'Diane',
'Frances',
'Joyce',
'Julie',
'Martha',
'Joan',
'Evelyn',
'Kelly',
'Christina',
'Emma',
'Lauren',
'Alice',
'Judith',
'Marie',
'Doris',
'Ann',
'Jean',
'Victoria',
'Cheryl',
'Megan',
'Kathryn',
'Andrea',
'Jacqueline',
'Gloria',
'Teresa',
'Janice',
'Sara',
'Rose',
'Julia',
'Hannah',
'Theresa',
'Judy',
'Mildred',
'Grace',
'Beverly',
'Denise',
'Marilyn',
'Amber',
'Danielle',
'Brittany',
'Diana',
'Jane',
'Lori',
'Olivia',
'Tiffany',
'Kathy',
'Tammy',
'Crystal',
'Madison',
]
emails = [
'@gmail.com',
'@yahoo.com',
'@outlook.com',
'@hotmail.com',
'@mailinator.com',
'@poly.edu',
'@nyu.edu'
]
extensions = [
'.doc',
'.log',
'.msg',
'.rtf',
'.txt',
'.wpd',
'.wps',
'.123',
'.csv',
'.dat',
'.db ',
'.dll',
'.mdb',
'.pps',
'.ppt',
'.sql',
'.wks',
'.xls',
'.xml',
'.mng',
'.pct',
'.bmp',
'.gif',
'.jpe',
'.jpg',
'.png',
'.psd',
'.psp',
'.tif',
'.ai ',
'.drw',
'.dxf',
'.eps',
'.ps ',
'.svg',
'.3dm',
'.3dm',
'.ind',
'.pdf',
'.qxd',
'.qxp',
'.aac',
'.aif',
'.iff',
'.m3u',
'.mid',
'.mid',
'.mp3',
'.mpa',
'.ra ',
'.ram',
'.wav',
'.wma',
'.3gp',
'.asf',
'.asx',
'.avi',
'.mov',
'.mp4',
'.mpg',
'.qt ',
'.rm ',
'.swf',
'.wmv',
'.asp',
'.css',
'.htm',
'.htm',
'.js ',
'.jsp',
'.php',
'.xht',
'.fnt',
'.fon',
'.otf',
'.ttf',
'.8bi',
'.plu',
'.xll',
'.cab',
'.cpl',
'.cur',
'.dmp',
'.drv',
'.key',
'.lnk',
'.sys',
'.cfg',
'.ini',
'.reg',
'.app',
'.bat',
'.cgi',
'.com',
'.exe',
'.pif',
'.vb ',
'.ws ',
'.deb',
'.gz ',
'.pkg',
'.rar',
'.sea',
'.sit',
'.sit',
'.zip',
'.bin',
'.hqx',
'.0 E',
'.mim',
'.uue',
'.cpp',
'.jav',
'.pl ',
'.bak',
'.gho',
'.old',
'.ori',
'.tmp',
'.dmg',
'.iso',
'.toa',
'.vcd',
'.gam',
'.nes',
'.rom',
'.sav',
'.msi',
'.tor',
'.yps'
]
def gen_sentence():
return ' '.join(random.sample(lorems, 50))
def gen_name():
return random.choice(names)
def gen_email():
return random.choice(emails)
def gen_category():
return random.choice(categories)
def gen_value():
return random.choice(range(100, 500, 50))
def gen_word():
return random.choice(hipsters)
def gen_file():
return gen_word()+random.choice(extensions)
def random_date(start, end):
return start + datetime.timedelta(
seconds=randint(0, int((end - start).total_seconds())))
with app.app_context():
db = app.db
### Generating Challenges
print "GENERATING CHALLENGES"
for x in range(CHAL_AMOUNT):
word = gen_word()
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()
### Generating Files
print "GENERATING FILES"
AMT_CHALS_WITH_FILES = int(CHAL_AMOUNT * (3.0/4.0))
for x in range(AMT_CHALS_WITH_FILES):
chal = random.randint(1, CHAL_AMOUNT)
filename = gen_file()
md5hash = hashlib.md5(filename).hexdigest()
db.session.add( Files(chal, os.path.join('static/uploads', md5hash, filename)) )
db.session.commit()
### Generating Users
print "GENERATING USERS"
used = []
while len(used) < USER_AMOUNT:
name = gen_name()
if name not in used:
used.append(name)
db.session.add( Teams(name , name.lower() + gen_email(), 'password') )
db.session.commit()
### Generating Solves
print "GENERATING SOLVES"
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-2880)
for x in range(USER_AMOUNT):
used = []
for y in range(random.randint(1,CHAL_AMOUNT)):
chalid = random.randint(1,CHAL_AMOUNT)
if chalid not in used:
used.append(chalid)
solve = Solves(chalid, x+1, '127.0.0.1')
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=60))
solve.date = new_base
base_time = new_base
db.session.add(solve)
db.session.commit()
### Generating Wrong Keys
print "GENERATING WRONG KEYS"
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-3880)
for x in range(USER_AMOUNT):
used = []
for y in range(random.randint(1,CHAL_AMOUNT * 20)):
chalid = random.randint(1,CHAL_AMOUNT)
if chalid not in used:
used.append(chalid)
wrong = WrongKeys(x+1, chalid, gen_word())
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=60))
wrong.date = new_base
base_time = new_base
db.session.add(wrong)
db.session.commit()
db.session.close()

2
prepare.sh Normal file
View File

@ -0,0 +1,2 @@
sudo apt-get install build-essential python-dev python-pip
sudo pip install Flask Flask-Session Flask-SQLAlchemy Flask-Mail apscheduler passlib

3
serve.py Normal file
View File

@ -0,0 +1,3 @@
from CTFd import create_app
app = create_app('')
app.run(debug=True, host="0.0.0.0", port=4000)

View File

@ -0,0 +1,84 @@
#submit-key{
display: none;
}
#chal > h1{
text-align: center
}
#chal > form{
width: 400px;
margin: 0 auto;
}
#chal > form > h3,h4{
text-align: center;
}
#chal > form > input{
display: none;
}
table{
width: 100%;
}
/*Not sure why foundation needs these two...*/
.top-bar input{
height: auto;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
font-size: 0.75rem;
}
.top-bar .button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
.dropdown{
background-color: #333 !important;
padding: 5px;
}
.dropdown button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
#challenges button{
margin: 5px;
}
.row h1{
text-align: center;
}
.textbox{
height: 150px;
}
.chal-tag{
margin: 0 5px 0 5px;
}
#score-graph{
max-height: 400px;
}
#keys-pie-graph{
width: 400px;
max-height: 330px;
float: left;
}
#categories-pie-graph{
width: 600px;
float: left;
max-height: 330px;
}

View File

@ -0,0 +1,197 @@
function loadchal(id) {
// $('#chal *').show()
// $('#chal > h1').hide()
obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0]
$('#update-challenge .chal-name').val(obj.name)
$('#update-challenge .chal-desc').html(obj.description)
$('#update-challenge .chal-value').val(obj.value)
$('#update-challenge .chal-category').val(obj.category)
$('#update-challenge .chal-id').val(obj.id)
$('#update-challenge .chal-delete').attr({
'href': '/admin/chal/close/' + (id + 1)
})
$('#update-challenge').foundation('reveal', 'open');
}
function submitkey(chal, key) {
$.post("/admin/chal/" + chal, {
key: key,
nonce: $('#nonce').val()
}, function (data) {
alert(data)
})
}
function loadkeys(chal){
$.get('/admin/keys/' + chal, function(data){
$('#keys-chal').val(chal)
keys = $.parseJSON(JSON.stringify(data));
keys = keys['keys']
$('#current-keys').empty()
for(x=0; x<keys.length; x++){
$('#current-keys').append($("<input class='current-key' type='text'>").val(keys[x].key))
$('#current-keys').append('<input type="radio" name="key_type['+x+']" value="0">Static')
$('#current-keys').append('<input type="radio" name="key_type['+x+']" value="1">Regex')
$('#current-keys input[name="key_type['+x+']"][value="'+keys[x].type+'"]').prop('checked',true);
}
});
}
function updatekeys(){
keys = [];
vals = [];
chal = $('#keys-chal').val()
$('.current-key').each(function(){
keys.push($(this).val());
})
$('#current-keys > input[name*="key_type"]:checked').each(function(){
vals.push($(this).val());
})
$.post('/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
loadchal(chal)
}
function loadtags(chal){
$('#tags-chal').val(chal)
$('#current-tags').empty()
$('#chal-tags').empty()
$.get('/admin/tags/'+chal, function(data){
tags = $.parseJSON(JSON.stringify(data))
tags = tags['tags']
for (var i = 0; i < tags.length; i++) {
tag = "<span class='secondary label chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'>&#215;</a></span>"
$('#current-tags').append(tag)
};
$('.delete-tag').click(function(e){
deletetag(e.target.name)
$(e.target).parent().remove()
});
});
}
function deletetag(tagid){
$.post('/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
}
function updatetags(){
tags = [];
chal = $('#tags-chal').val()
$('#chal-tags > span > span').each(function(i, e){
tags.push($(e).text())
});
$.post('/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()})
loadchal(chal)
}
function loadfiles(chal){
$('#update-files > form').attr('action', '/admin/files/'+chal)
$.get('/admin/files/' + chal, function(data){
$('#files-chal').val(chal)
files = $.parseJSON(JSON.stringify(data));
files = files['files']
$('#current-files').empty()
for(x=0; x<files.length; x++){
filename = files[x].file.split('/')
filename = filename[filename.length - 1]
$('#current-files').append('<div data-alert class="alert-box info radius">'+filename+'<a href="#" onclick="deletefile('+chal+','+files[x].id+', $(this))" value="'+files[x].id+'" style="float:right;">Delete</a></div>')
}
});
}
function deletefile(chal, file, elem){
$.post('/admin/files/' + chal,{
'nonce': $('#nonce').val(),
'method': 'delete',
'file': file
}, function (data){
if (data == "1") {
elem.parent().remove()
}
});
}
function loadchals(){
$.post("/admin/chals", {
'nonce': $('#nonce').val()
}, function (data) {
categories = [];
challenges = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) {
if ($.inArray(challenges['game'][i].category, categories) == -1) {
categories.push(challenges['game'][i].category)
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-") + '"><td class="large-2"><h3>' + challenges['game'][i].category + '</h3></td></tr>'))
}
};
for (var i = categories.length - 1; i >= 0; i--) {
$('#new-challenge select').append('<option value="' + categories[i] + '">' + categories[i] + '</option>');
$('#update-challenge select').append('<option value="' + categories[i] + '">' + categories[i] + '</option>');
};
for (var i = 0; i <= challenges['game'].length - 1; i++) {
$('#' + challenges['game'][i].category.replace(/ /g,"-")).append($('<button value="' + challenges['game'][i].id + '">' + challenges['game'][i].value + '</button>'));
};
$('#challenges button').click(function (e) {
loadchal(this.value);
loadkeys(this.value);
loadtags(this.value);
loadfiles(this.value);
});
$('tr').append('<button class="create-challenge"><i class="fa fa-plus"></i></button>');
$('.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').foundation('reveal', 'open');
});
});
}
$('#submit-key').click(function (e) {
submitkey($('#chalid').val(), $('#answer').val())
});
$('#submit-keys').click(function (e) {
if (confirm('Updating keys. Are you sure?')){
updatekeys()
}
});
$('#submit-tags').click(function (e) {
updatetags()
});
$(".tag-insert").keyup(function (e) {
if (e.keyCode == 13) {
tag = $('.tag-insert').val()
tag = tag.replace(/'/g, '');
if (tag.length > 0){
tag = "<span class='secondary label chal-tag'><span>"+tag+"</span><a onclick='$(this).parent().remove()'>&#215;</a></span>"
$('#chal-tags').append(tag)
}
$('.tag-insert').val("")
}
});
$('.create-category').click(function (e) {
$('#new-category').foundation('reveal', 'open');
});
count = 1
$('#create-key').click(function (e) {
$('#current-keys').append("<input class='current-key' type='text' placeholder='Blank Key'>");
$('#current-keys').append('<input type="radio" name="key_type['+count+']" value="0">Static');
$('#current-keys').append('<input type="radio" name="key_type['+count+']" value="1">Regex');
count++
});
$(function(){
loadchals()
})

159
static/admin/js/team.js Normal file
View File

@ -0,0 +1,159 @@
function teamid (){
loc = window.location.pathname
return loc.substring(loc.lastIndexOf('/')+1, loc.length);
}
function cumulativesum (arr) {
var result = arr.concat();
for (var i = 0; i < arr.length; i++){
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
}
return result
}
function scoregraph () {
var times = []
var scores = []
var teamname = $('#team-id').text()
$.get('/admin/solves/'+teamid(), function( data ) {
solves = $.parseJSON(JSON.stringify(data));
solves = solves['solves']
if (solves.length == 0)
return
for (var i = 0; i < solves.length; i++) {
times.push(solves[i].time * 1000)
scores.push(solves[i].value)
};
scores = cumulativesum(scores)
times.unshift('x1')
times.push( Math.round(new Date().getTime()) )
scores.unshift('data1')
scores.push( scores[scores.length-1] )
var chart = c3.generate({
bindto: "#score-graph",
data: {
xs: {
"data1": 'x1',
},
columns: [
times,
scores,
],
type: "area",
labels: true,
names : {
data1: teamname
}
},
axis : {
x : {
tick: {
format: function (x) {
return moment(x).local().format('LLL');
}
},
},
y:{
label: {
text: 'Score'
}
}
}
});
});
}
function adjust_times () {
$.each($(".solve-time"), function(i, e){
$(e).text( moment(parseInt(e.innerText)).local().format('LLL') )
})
$(".solve-time").css('color', "#222")
}
function keys_percentage_graph(){
// Solves and Fails pie chart
$.get('/admin/fails/'+teamid(), function(data){
res = $.parseJSON(JSON.stringify(data));
solves = res['solves']
fails = res['fails']
total = solves+fails
if (total == 0)
return
var chart = c3.generate({
bindto: '#keys-pie-graph',
data: {
columns: [
['Solves', solves],
['Fails', fails],
],
type : 'donut'
},
color: {
pattern: ["#00D140", "#CF2600"]
},
donut: {
title: "Solves vs Fails",
}
});
});
}
function category_breakdown_graph(){
$.get('/admin/solves/'+teamid(), function(data){
solves = $.parseJSON(JSON.stringify(data));
solves = solves['solves']
if (solves.length == 0)
return
categories = []
for (var i = 0; i < solves.length; i++) {
categories.push(solves[i].category)
};
keys = categories.filter(function(elem, pos) {
return categories.indexOf(elem) == pos;
})
data = []
for (var i = 0; i < keys.length; i++) {
temp = []
count = 0
for (var x = 0; x < categories.length; x++) {
if (categories[x] == keys[i]){
count++
}
};
temp.push(keys[i])
temp.push(count)
data.push(temp)
};
var chart = c3.generate({
bindto: '#categories-pie-graph',
data: {
columns: data,
type : 'donut',
labels: true
},
donut: {
title: "Category Breakdown"
}
});
});
}
category_breakdown_graph()
keys_percentage_graph()
adjust_times()
scoregraph()

105
static/css/style.css Normal file
View File

@ -0,0 +1,105 @@
#chal > form{
width: 400px;
margin: 0 auto;
}
.reveal-modal{
text-align: center;
}
.chal-desc{
text-align: left;
}
table{
width: 100%;
}
/*Not sure why foundation needs these two...*/
.top-bar input{
height: auto;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
font-size: 0.75rem;
}
.top-bar .button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
.dropdown{
background-color: #333 !important;
padding: 5px;
}
.dropdown button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
#challenges button{
margin: 8px;
}
.row > h1{
text-align: center;
}
#challenges{
line-height: 66px;
}
#score-graph{
max-height: 400px;
}
.dropdown{
padding: 20px;
}
.dropdown input{
margin: 5px auto;
width: 95%;
}
.dropdown button{
margin: 10px auto;
width: 100%;
}
#keys-pie-graph{
width: 50%;
max-height: 330px;
float: left;
}
#categories-pie-graph{
width: 50%;
float: left;
max-height: 330px;
}
.logo{
margin: 0 auto;
width: 500px;
padding: 50px;
display: block;
}
@media only screen and (min-width: 40.063em){
.top-bar .dropdown{
display: block;
padding: 0 15px 5px;
width: 200% !important;
}
}
.top-bar input{
padding: 10px;
}

BIN
static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

244
static/js/chalboard.js Normal file
View File

@ -0,0 +1,244 @@
//http://stackoverflow.com/a/2648463 - wizardry!
String.prototype.format = String.prototype.f = function() {
var s = this,
i = arguments.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
}
return s;
};
var challenges;
function loadchal(id) {
obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0]
window.location.hash = obj.name
$('#chal-window .chal-name').text(obj.name)
$('#chal-window .chal-desc').html(marked(obj.description, {'gfm':true, 'breaks':true}))
for (var i = 0; i < obj.files.length; i++) {
filename = obj.files[i].split('/')
filename = filename[filename.length - 1]
$('#chal-window .chal-desc').append("<a href='"+obj.files[i]+"'>"+filename+"</a><br/>")
};
$('#chal-window .chal-value').text(obj.value)
$('#chal-window .chal-category').text(obj.category)
$('#chal-window #chal-id').val(obj.id)
$('#chal-window .chal-solves').text(obj.solves + " solves")
$('#answer').val("")
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
$('#chal-window').foundation('reveal', 'open');
}
function loadchalbyname(chalname) {
obj = $.grep(challenges['game'], function (e) {
return e.name == chalname;
})[0]
window.location.hash = obj.name
$('#chal-window .chal-name').text(obj.name)
$('#chal-window .chal-desc').html(marked(obj.description, {'gfm':true, 'breaks':true}))
for (var i = 0; i < obj.files.length; i++) {
filename = obj.files[i].split('/')
filename = filename[filename.length - 1]
$('#chal-window .chal-desc').append("<a href='"+obj.files[i]+"'>"+filename+"</a><br/>")
};
$('#chal-window .chal-value').text(obj.value)
$('#chal-window .chal-category').text(obj.category)
$('#chal-window #chal-id').val(obj.id)
$('#chal-window .chal-solves').text(obj.solves + " solves")
$('#answer').val("")
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
$('#chal-window').foundation('reveal', 'open');
}
$("#answer").keyup(function(event){
if(event.keyCode == 13){
$("#submit-key").click();
}
});
function submitkey(chal, key, nonce) {
$.post("/chal/" + chal, {
key: key,
nonce: nonce
}, function (data) {
if (data == -1){
window.location="/login"
}
else if (data == 0){ // Incorrect key
$('#submit-key').text('Incorrect, sorry')
$('#submit-key').css('background-color', 'red')
$('#submit-key').prop('disabled', true)
}
else if (data == 1){ // Challenge Solved
$('#submit-key').text('Correct!')
$('#submit-key').css('background-color', 'green')
$('#submit-key').prop('disabled', true)
$('#chal-window .chal-solves').text( (parseInt($('#chal-window .chal-solves').text().split(" ")[0]) + 1 + " solves") )
}
else if (data == 2){ // Challenge already solved
$('#submit-key').text('You already solved this')
$('#submit-key').prop('disabled', true)
}
else if (data == 3){ // Keys per minute too high
$('#submit-key').text("You're submitting keys too fast. Slow down.")
$('#submit-key').css('background-color', '#e18728')
$('#submit-key').prop('disabled', true)
}
marksolves()
updatesolves()
setTimeout(function(){
$('#submit-key').text('Submit')
$('#submit-key').prop('disabled', false)
$('#submit-key').css('background-color', '#007095')
}, 3000);
})
}
function marksolves() {
$.get('/solves', function (data) {
solves = $.parseJSON(JSON.stringify(data));
for (var i = solves['solves'].length - 1; i >= 0; i--) {
id = solves['solves'][i].chalid
$('#challenges button[value="' + id + '"]').addClass('secondary')
$('#challenges button[value="' + id + '"]').css('opacity', '0.3')
};
if (window.location.hash.length > 0){
loadchalbyname(window.location.hash.substring(1))
}
});
}
function updatesolves(){
$.get('/chals/solves', function (data) {
solves = $.parseJSON(JSON.stringify(data));
chals = Object.keys(solves);
for (var i = 0; i < chals.length; i++) {
obj = $.grep(challenges['game'], function (e) {
return e.name == chals[i];
})[0]
obj.solves = solves[chals[i]]
};
});
}
function getsolves(id){
$.get('/chal/'+id+'/solves', function (data) {
var teams = data['teams'];
var box = $('#chal-solves-names');
box.empty();
for (var i = 0; i < teams.length; i++) {
var id = teams[i].id;
var name = teams[i].name;
var date = moment(teams[i].date).local().format('LLL');
box.append('<tr><td><a href="/team/{0}">{1}</td><td>{2}</td></tr>'.format(id, name, date));
};
});
}
function loadchals() {
$.get("/chals", function (data) {
categories = [];
challenges = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) {
challenges['game'][i].solves = 0
if ($.inArray(challenges['game'][i].category, categories) == -1) {
categories.push(challenges['game'][i].category)
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-") + '"><td class="large-2"><h4>' + challenges['game'][i].category + '</h4></td></tr>'))
}
};
for (var i = 0; i <= challenges['game'].length - 1; i++) {
$('#' + challenges['game'][i].category.replace(/ /g,"-")).append($('<button value="' + challenges['game'][i].id + '">' + challenges['game'][i].value + '</button>'));
};
updatesolves()
marksolves()
$('#challenges button').click(function (e) {
loadchal(this.value);
});
});
}
$('#submit-key').click(function (e) {
submitkey($('#chal-id').val(), $('#answer').val(), $('#nonce').val())
});
$('.chal-solves').click(function (e) {
getsolves($('#chal-id').val())
});
// $.distint(array)
// Unique elements in array
$.extend({
distinct : function(anArray) {
var result = [];
$.each(anArray, function(i,v){
if ($.inArray(v, result) == -1) result.push(v);
});
return result;
}
});
function colorhash (x) {
color = ""
for (var i = 20; i <= 60; i+=20){
x += i
x *= i
color += x.toString(16)
};
return "#" + color.substring(0, 6)
}
$(document).on('close', '[data-reveal]', function () {
window.location.hash = ""
});
// function solves_graph() {
// $.get('/graphs/solves', function(data){
// solves = $.parseJSON(JSON.stringify(data));
// chals = []
// counts = []
// colors = []
// i = 1
// $.each(solves, function(key, value){
// chals.push(key)
// counts.push(value)
// colors.push(colorhash(i++))
// });
// });
// }
function update(){
$('#challenges').empty()
loadchals()
solves_graph()
}
$(function() {
loadchals()
// solves_graph()
});
setInterval(update, 300000);

109
static/js/scoreboard.js Normal file
View File

@ -0,0 +1,109 @@
//http://stackoverflow.com/a/2648463 - wizardry!
String.prototype.format = String.prototype.f = function() {
var s = this,
i = arguments.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
}
return s;
};
function updatescores () {
$.get('/scores', function( data ) {
teams = $.parseJSON(JSON.stringify(data));
$('#scoreboard > tbody').empty()
for (var i = 0; i < teams['teams'].length; i++) {
row = "<tr><td>{0}</td><td><a href='/team/{1}'>{2}</a></td><td>{3}</td></tr>".format(i+1, teams['teams'][i].id, teams['teams'][i].name, teams['teams'][i].score)
$('#scoreboard > tbody').append(row)
};
});
}
function cumulativesum (arr) {
var result = arr.concat();
for (var i = 0; i < arr.length; i++){
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
}
return result
}
function UTCtoDate(utc){
var d = new Date(0)
d.setUTCSeconds(utc)
return d;
}
function scoregraph () {
var times = []
var scores = []
$.get('/top/10', function( data ) {
scores = $.parseJSON(JSON.stringify(data));
scores = scores['scores']
if (Object.keys(scores).length == 0 ){
return;
}
$('#score-graph').show()
teams = Object.keys(scores)
xs_data = {}
column_data = []
for (var i = 0; i < teams.length; i++) {
times = []
team_scores = []
for (var x = 0; x < scores[teams[i]].length; x++) {
times.push(scores[teams[i]][x].time)
team_scores.push(scores[teams[i]][x].value)
};
team_scores = cumulativesum(team_scores)
times.unshift("x"+i)
times.push( Math.round(new Date().getTime()/1000) )
team_scores.unshift(teams[i])
team_scores.push( team_scores[team_scores.length-1] )
xs_data[teams[i]] = "x"+i
column_data.push(times)
column_data.push(team_scores)
};
var chart = c3.generate({
bindto: "#score-graph",
data: {
xs: xs_data,
columns: column_data,
type: "step",
labels: true
},
axis : {
x : {
tick: {
format: function (x) {
return moment(x*1000).local().format('LLL');
}
},
},
y:{
label: {
text: 'Score'
}
}
},
// zoom : {
// enabled: true
// }
});
});
}
function update(){
updatescores()
scoregraph()
}
setInterval(update, 300000); // Update scores every 5 minutes
scoregraph()

168
static/js/team.js Normal file
View File

@ -0,0 +1,168 @@
function teamid (){
loc = window.location.pathname
return loc.substring(loc.lastIndexOf('/')+1, loc.length);
}
function colorhash (x) {
color = ""
for (var i = 20; i <= 60; i+=20){
x += i
x *= i
color += x.toString(16)
};
return "#" + color.substring(0, 6)
}
function cumulativesum (arr) {
var result = arr.concat();
for (var i = 0; i < arr.length; i++){
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
}
return result
}
function scoregraph () {
var times = []
var scores = []
var teamname = $('#team-id').text()
$.get('/solves/'+teamid(), function( data ) {
solves = $.parseJSON(JSON.stringify(data));
solves = solves['solves']
console.log(solves)
if (solves.length == 0)
return
for (var i = 0; i < solves.length; i++) {
times.push(solves[i].time * 1000)
scores.push(solves[i].value)
};
scores = cumulativesum(scores)
times.unshift('x1')
// times.push( Math.round(new Date().getTime()) )
scores.unshift('data1')
// scores.push( scores[scores.length-1] )
console.log(scores)
var chart = c3.generate({
bindto: "#score-graph",
data: {
xs: {
"data1": 'x1',
},
columns: [
times,
scores,
],
type: "area-step",
colors: {
data1: colorhash(teamid()),
},
labels: true,
names : {
data1: teamname
}
},
axis : {
x : {
tick: {
format: function (x) {
return moment(x).local().format('M/D h:mm:ss');
}
},
},
y:{
label: {
text: 'Score'
}
}
},
zoom : {
enabled: true
}
});
});
}
function keys_percentage_graph(){
// Solves and Fails pie chart
$.get('/fails/'+teamid(), function(data){
res = $.parseJSON(JSON.stringify(data));
solves = res['solves']
fails = res['fails']
total = solves+fails
if (total == 0)
return
var chart = c3.generate({
bindto: '#keys-pie-graph',
data: {
columns: [
['Solves', solves],
['Fails', fails],
],
type : 'donut'
},
color: {
pattern: ["#00D140", "#CF2600"]
},
donut: {
title: "Solves vs Fails",
}
});
});
}
function category_breakdown_graph(){
$.get('/solves/'+teamid(), function(data){
solves = $.parseJSON(JSON.stringify(data));
solves = solves['solves']
if (solves.length == 0)
return
categories = []
for (var i = 0; i < solves.length; i++) {
categories.push(solves[i].category)
};
keys = categories.filter(function(elem, pos) {
return categories.indexOf(elem) == pos;
})
data = []
for (var i = 0; i < keys.length; i++) {
temp = []
count = 0
for (var x = 0; x < categories.length; x++) {
if (categories[x] == keys[i]){
count++
}
};
temp.push(keys[i])
temp.push(count)
data.push(temp)
};
var chart = c3.generate({
bindto: '#categories-pie-graph',
data: {
columns: data,
type : 'donut',
labels: true
},
donut: {
title: "Category Breakdown",
}
});
});
}
category_breakdown_graph()
keys_percentage_graph()
scoregraph()

View File

54
templates/admin/base.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/normalize.min.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/foundation.min.css" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
<link rel="stylesheet" type="text/css" href="/static/admin/css/style.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
</head>
<body>
<nav class="top-bar" data-topbar>
<ul class="title-area">
<li class="name">
<h1><a href="/">CTF</a></h1>
</li>
<li class="toggle-topbar menu-icon"><a href="#">Menu</a></li>
</ul>
<section class="top-bar-section">
<!-- Right Nav Section -->
<ul class="right">
</li>
</ul>
<!-- Left Nav Section -->
<ul class="left">
<li><a href="/admin/graphs">Graphs</a></li>
<li><a href="/admin/pages">Pages</a></li>
<li><a href="/admin/teams">Teams</a></li>
<li><a href="/admin/scoreboard">Scoreboard</a></li>
<li><a href="/admin/chals">Challenges</a></li>
<li><a href="/admin/statistics">Statistics</a></li>
<li><a href="/admin/config">Config</a></li>
</ul>
</section>
</nav>
{% block content %}
{% endblock %}
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/jquery.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/modernizr.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Converter.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation/foundation.topbar.min.js"></script>
<script>
$(document).foundation();
</script>
{% block scripts %}
{% endblock %}
</body>
</html>

131
templates/admin/chals.html Normal file
View File

@ -0,0 +1,131 @@
{% extends "admin/base.html" %}
{% block content %}
<input type="hidden" value="{{ nonce }}" id="nonce">
<div id="new-category" class="reveal-modal" data-reveal>
<form method="POST" action="/admin/chal/new" enctype="multipart/form-data">
<h3>New Category</h3>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input type='text' name='category' placeholder='Category'><br/>
<a href="#" data-reveal-id="create-challenge" class="button">Create</a>
<a class="close-reveal-modal">&#215;</a>
</div>
<div id="create-challenge" class="reveal-modal" data-reveal>
<h3>New Challenge</h3>
<input type='text' name='name' placeholder='Name'><br/>
<textarea name='desc' placeholder='Description'></textarea><br/>
<input type='text' name='value' placeholder='Value'><br/>
<input type='text' name='key' placeholder='Key'><br/>
<input type="radio" name="key_type[0]" value="0">Static
<input type="radio" name="key_type[0]" value="1">Regex
<br/>
<input type="file" name="files[]" multiple="multiple">
<button type='submit'>Create</button>
<a class="close-reveal-modal">&#215;</a>
</form>
</div>
<div id="new-challenge" class="reveal-modal" data-reveal>
<form method="POST" action="/admin/chal/new" enctype="multipart/form-data">
<h3><span id="new-chal-title"></span> challenge</h3>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input type='text' name='name' placeholder='Name'><br/>
<textarea class="textbox" name='desc' placeholder='Description'></textarea><br/>
<input type='text' name='value' placeholder='Value'><br/>
<input id="new-chal-category" type="hidden" name="category">
<input type='text' name='key' placeholder='Key'><br/>
<input type="file" name="files[]" multiple="multiple">
<br/>
<input type="radio" name="key_type[0]" value="0">Static
<input type="radio" name="key_type[0]" value="1">Regex
<br/>
<button type='submit'>Create</button>
<a class="close-reveal-modal">&#215;</a>
</form>
</div>
<div id="update-keys" class="reveal-modal" data-reveal>
<form method="POST" action="/admin/keys">
<h3>Keys</h3>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="keys-chal" name='chal' type='hidden'>
<div id="current-keys"></div>
<a href="#" id="create-key" class="secondary button">New Key</a>
<a href="#" id="submit-keys" class="button">Update</a>
<a class="close-reveal-modal">&#215;</a>
</form>
</div>
<div id="update-files" class="reveal-modal" data-reveal>
<form method="POST" action="/admin/files" enctype="multipart/form-data">
<h3>Files</h3>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="files-chal" name='chal' type='hidden'>
<div id="current-files"></div>
<input type="hidden" name="method" value="upload">
<input type="file" name="files[]" multiple="multiple">
<button type='submit'>Upload</button>
<a class="close-reveal-modal">&#215;</a>
</form>
</div>
<div id="update-tags" class="reveal-modal" data-reveal>
<h3>Tags</h3>
<input type="text" class="tag-insert" maxlength="80" placeholder="Type tag and press Enter">
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="tags-chal" name='chal' type='hidden'>
<div id="current-tags">
</div>
<br/>
<div id="chal-tags">
</div>
<br/>
<a href="#" id="submit-tags" class="button">Update</a>
<a class="close-reveal-modal">&#215;</a>
</div>
<div id="update-challenge" class="reveal-modal" data-reveal>
<form method="POST" action="/admin/chal/update">
<h3>Update Challenge</h3>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input class="chal-name" type='text' name='name' placeholder='Name'><br/>
<textarea class="chal-desc textbox" name='desc' placeholder='Description'></textarea><br/>
<input class="chal-value" type='text' name='value' placeholder='Value'><br/>
<select class="chal-category" name="category">
<option>-</option>
</select>
<input class="chal-id" type='hidden' name='id' placeholder='ID'><br/>
<a href="#" data-reveal-id="update-tags" class="secondary button">Tags</a>
<a href="#" data-reveal-id="update-files" class="secondary button">Files</a>
<a href="#" data-reveal-id="update-keys" class="secondary button">Keys</a>
<button type='submit'>Update</button>
<a class="close-reveal-modal">&#215;</a>
</form>
</div>
<div class="row">
<h1>CTF</h1>
<div>
<table id='challenges'>
</table>
<button style="width:100%;" class="create-category">New Category</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/admin/js/chalboard.js"></script>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<br>
<h1>Config</h1>
<form method="POST">
<input name='nonce' type='hidden' value="{{ nonce }}">
<strong>Start Date:</strong>
<input name='start' type='text' placeholder="Start Date (UTC timestamp)" {% if start is defined and start != None %}value="{{ start }}"{% endif %}>
<strong>End Date:</strong>
<input name='end' type='text' placeholder="End Date (UTC timestamp)" {% if end is defined and end != None %}value="{{ end }}"{% endif %}>
<button class="radius" type='submit'>Submit</button>
</form>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "admin/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.css">
<div class="row">
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form id="page-edit" method="POST">
<br>
<strong>Route: </strong><input name='nonce' type='hidden' value="{{ nonce }}">
<input class="radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
<strong>HTML: </strong><textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
<button class="radius" type='submit'>Create</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.min.js"></script>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("admin-pages-editor"), {
lineNumbers: true,
lineWrapping: true,
mode: "text/html"
});
$('#page-edit').submit(function (e){
$(this).attr('action', '/admin/pages/'+$('#route').val());
});
</script>
{% endblock %}

168
templates/admin/graphs.html Normal file
View File

@ -0,0 +1,168 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<div id="solves-graph"></div>
<div id="keys-pie-graph"></div>
<div id="categories-pie-graph"></div>
</div>
{% endblock %}
{% block scripts %}
<style>
#solves-graph{
margin-left: -50px;
}
#solves-graph > svg{
overflow: visible;
padding: 10px 50px 0 50px;
margin-right: 20px;
}
text {
font-size: 0.8em;
}
#solves-graph rect {
width: 90%;
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script type="text/javascript">
// $.distint(array)
// Unique elements in array
$.extend({
distinct : function(anArray) {
var result = [];
$.each(anArray, function(i,v){
if ($.inArray(v, result) == -1) result.push(v);
});
return result;
}
});
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
Array.max = function( array ){
return Math.max.apply( Math, array );
};
function colorhash (x) {
color = ""
for (var i = 20; i <= 60; i+=20){
x += i
x *= i
color += x.toString(16)
};
return "#" + color.substring(0, 6)
}
function solves_graph() {
$.get('/admin/graphs/solves', function(data){
solves = $.parseJSON(JSON.stringify(data));
chals = []
counts = ['Challenges']
colors = []
i = 1
$.each(solves, function(key, value){
chals.push(key)
counts.push(value)
colors.push(colorhash(i++))
});
var chart = c3.generate({
bindto: '#solves-graph',
size: {
width: 1000,
height: 500
},
data: {
columns: [
counts
],
type: 'bar',
labels: true
},
axis: {
y: {
max: null,
min: 0,
show: false
},
x: {
type: 'categorized',
categories: chals,
show: false
},
rotated: true
}
});
});
}
function keys_percentage_graph(){
// Solves and Fails pie chart
$.get('/admin/fails/all', function(data){
res = $.parseJSON(JSON.stringify(data));
solves = res['solves']
fails = res['fails']
total = solves+fails
var chart = c3.generate({
bindto: '#keys-pie-graph',
data: {
columns: [
['Solves', solves],
['Fails', fails],
],
type : 'donut'
},
color: {
pattern: ["#00D140", "#CF2600"]
},
donut: {
title: "Solves vs Fails"
}
});
});
}
function category_breakdown_graph(){
$.get('/admin/graphs/categories', function(data){
res = $.parseJSON(JSON.stringify(data));
res = res['categories']
data = []
for (var i = 0; i < res.length; i++) {
temp = []
temp.push(res[i].category)
temp.push(res[i].count)
data.push(temp)
};
var chart = c3.generate({
bindto: '#categories-pie-graph',
data: {
columns: data,
type : 'donut',
labels: true
},
donut: {
title: "Category Breakdown"
}
});
});
}
function update(){
solves_graph()
keys_percentage_graph()
category_breakdown_graph()
}
$(function() {
update()
});
setInterval(update, 300000);
</script>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h1>Login</h1>
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form method="POST">
<input class="radius" type='text' name='name' placeholder='Name'><br/>
<input class="radius" type='password' name='password' placeholder='Password'><br/>
<p><a href="/reset_password">Forgot your password?</a></p>
<button class="radius" type='submit'>Login</button>
</form>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<br>
<table id="pages">
<thead>
<tr>
<td><b>Route</b></td>
</tr>
</thead>
<tbody>
{% for route in routes %}
<tr>
<td><a href="/admin/pages/{{ route.route }}">{{ route.route }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<form method="POST">
<input name='nonce' type='hidden' value="{{ nonce }}">
<button class="radius" type='submit'>New Page</button>
</form>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<p></p>
<table id="scoreboard">
<thead>
<tr>
<td><b>Place</b></td>
<td><b>Team</b></td>
<td><b>Score</b></td>
<td><b>Status</b></td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ loop.index }}</td>
<td><a href="/admin/team/{{ team.teamid }}">{{ team.name }}</a></td>
<td>{{ team.score }}</td>
<td>
{% if not team.banned %}
<form method="POST" style="margin:0;" action="/admin/team/{{ team.teamid }}/ban">
<a onclick="$(this).parent().submit()">Ban</a>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{%else %}
<form method="POST" style="margin:0;" action="/admin/team/{{ team.teamid }}/unban">
<a onclick="$(this).parent().submit()">Unban</a>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@ -0,0 +1,88 @@
#submit-key{
display: none;
}
#chal > h1{
text-align: center
}
#chal > form{
width: 400px;
margin: 0 auto;
}
#chal > form > h3,h4{
text-align: center;
}
#chal > form > input{
display: none;
}
table{
width: 100%;
}
/*Not sure why foundation needs these two...*/
.top-bar input{
height: auto;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
font-size: 0.75rem;
}
.top-bar .button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
.dropdown{
background-color: #333 !important;
padding: 5px;
}
.dropdown button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
#challenges button{
margin: 5px;
}
.row h1{
text-align: center;
}
.textbox{
height: 150px;
}
.chal-tag{
margin: 0 5px 0 5px;
}
#score-graph{
max-height: 400px;
}
#keys-pie-graph{
width: 400px;
max-height: 330px;
float: left;
}
#categories-pie-graph{
width: 600px;
float: left;
max-height: 330px;
}
#admin-pages-editor{
width: 100%;
}

View File

@ -0,0 +1,25 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<h1>Statistics</h1>
<h3><b>{{ team_count }}</b> teams registered</h3>
<h3><b>{{ hit_count}}</b> hits</h3>
<h3><b>{{ wrong_count }}</b> wrong keys submitted</h3>
<h3><b>{{ solve_count }}</b> right keys submitted</h3>
<h3><b>{{ challenge_count }}</b> challenges</h3>
{% if most_solved %}
<h3>Most solved: <b>{{ most_solved[0].chal.name }}</b> with {{ most_solved[1] }}</b> solves</h3>
{% endif %}
{% if least_solved %}
<h3>Least solved: <b>{{ least_solved[0].chal.name }}</b> with {{ least_solved[1] }}</b> solves</h3>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

53
templates/admin/team.html Normal file
View File

@ -0,0 +1,53 @@
{% extends "admin/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<h1 id="team-id">{{ team.name }}</h1>
<div id="keys-pie-graph"></div>
<div id="categories-pie-graph"></div>
<table>
<thead>
<tr>
<td><b>Challenge</b></td>
<td><b>Category</b></td>
<td><b>Value</b></td>
<td><b>Time</b></td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr><td>{{ solve.chal.name }}</td><td>{{ solve.chal.category }}</td><td>{{ solve.chal.value }}</td><td class="solve-time">{{ solve.date|unix_time_millis }}</td></tr>
{% endfor %}
</tbody>
</table>
<table>
<thead>
<tr>
<td><b>IP Address</b></td>
<td><b>Last Seen</b></td>
</tr>
</thead>
<tbody>
{% for addr in addrs %}
<tr><td>{{ addr.ip|long2ip }}</td><td class="solve-time">{{ addr.date|unix_time_millis }}</td></tr>
{% endfor %}
</tbody>
</table>
<div id="score-graph"></div>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script src="/static/admin/js/team.js"></script>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "admin/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<p></p>
<table id="teamsboard">
<thead>
<tr>
<td><b>Team</b>
</td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr><td><a href="/admin/team/{{ team.id }}">{{ team.name }}</a></td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

93
templates/base.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/normalize.min.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/foundation.min.css" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/styles/railscasts.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
</head>
<body>
<nav class="top-bar" data-topbar>
<ul class="title-area">
<li class="name">
<h1><a href="/">CTF</a></h1>
</li>
<li class="toggle-topbar menu-icon"><a href="#">Menu</a></li>
</ul>
<section class="top-bar-section">
<!-- Right Nav Section -->
<ul class="right">
{% if username is defined %}
<li class="has-dropdown">
<a href="/team/{{ id }}">{{ username }} {% if admin %} (ADMIN) {% endif %}</a>
<ul class="dropdown">
{% if admin %}
<li><a href="/admin">Admin</a></li>
{% endif %}
<li><a href="/team/{{ id }}">Team</a></li>
<li><a href="/profile">Account</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</li>
{%else %}
<li class="has-form">
<li class="has-dropdown">
<a href="/register">Register</a>
<ul class="dropdown">
<form method="POST" action="/register">
<li><input type='text' name='name' placeholder='Name'></li>
<li><input type='text' name='email' placeholder='Email'></li>
<li><input type='password' name='password' placeholder='Password'></li>
<li><button type='submit'>Register</button></li>
</form>
</ul>
</li>
<li class="has-dropdown">
<a href="/login">Login</a>
<ul class="dropdown">
<form method="POST" action="/login">
<li><input type='text' name='name' placeholder='Name'></li>
<li><input type='password' name='password' placeholder='Password'></li>
<li><button type='submit'>Login</button></li>
<li><a href="/reset_password" class="text-center">Forgot?</a></li>
</form>
</ul>
</li>
{% endif %}
</li>
</ul>
<!-- Left Nav Section -->
<ul class="left">
{% for page in pages() %}
<li><a href="/{{ page.route }}">{{ page.route|title }}</a></li>
{% endfor %}
<li><a href="/teams">Teams</a></li>
<li><a href="/scoreboard">Scoreboard</a></li>
<li><a href="/challenges">Challenges</a></li>
</ul>
</section>
</nav>
{% block content %}
{% endblock %}
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/jquery.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/modernizr.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation/foundation.topbar.min.js"></script>
<script>
$(document).foundation();
</script>
{% block scripts %}
{% endblock %}
</body>
</html>

54
templates/chals.html Normal file
View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div id="chal-solves-window" class="reveal-modal" data-reveal>
<h3>Solved By</h3>
<table>
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
<a class="close-reveal-modal" data-reveal-id="chal-window">&#215;</a>
</div>
<div id="chal-window" class="reveal-modal" data-reveal>
<div class="row">
<h3 class='chal-name'></h3>
<h4 class="chal-value"></h4>
<p><i><a data-reveal-id="chal-solves-window" class="chal-solves"></a></i></p>
<p class="chal-desc"></p>
<input id="answer" type="text" placeholder="Key">
<input type="hidden" id="nonce" name="nonce" value={{ nonce }}>
<button id="submit-key">Submit</button>
<input id="chal-id" type="hidden">
</div>
<a class="close-reveal-modal">&#215;</a>
</div>
<div class="row">
<h2 class="text-center">Challenges</h2>
<div class="large-12 columns">
<table id='challenges'>
</table>
</div>
<div id="solves-graph" style="width: 90%; height: 400px; margin: 0 auto"></div>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script src="/static/js/chalboard.js"></script>
{% endblock %}

1
templates/ctf.html Normal file
View File

@ -0,0 +1 @@
CTF

14
templates/errors/403.html Normal file
View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h2 style="text-align:center;">I don't think you're allowed here</h2>
<h2 style="text-align:center;">Not so sorry about that</h2>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

14
templates/errors/404.html Normal file
View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h2 style="text-align:center;">Whoops, looks like we can't find that.</h2>
<h2 style="text-align:center;">Sorry about that</h2>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

13
templates/errors/500.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h2 style="text-align:center;">Uhh what did you just do?</h2>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

29
templates/login.html Normal file
View File

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h1>Login</h1>
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form method="POST">
<input class="radius" type='text' name='name' placeholder='Name'><br/>
<input class="radius" type='password' name='password' placeholder='Password'><br/>
<p><a href="/reset_password">Forgot your password?</a></p>
<button class="radius" type='submit'>Login</button>
</form>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

46
templates/profile.html Normal file
View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<br/>
{% for error in errors %}
<h1>{{ error }}</h1>
{% endfor %}
<form method="POST">
<span>Team Name</span>
<input class="radius" type="text" name="name" placeholder="Team Name" value="{{name}}">
<span>Email Address</span>
<input class="radius" type="text" name="email" placeholder="Email Address" value="{{email}}">
<br/>
<hr/>
<br/>
<span>Old Password</span>
<input class="radius" type="password" name="confirm" placeholder="Old Password">
<span>Password</span>
<input class="radius" type="password" name="password" placeholder="Password">
<br/>
<hr/>
<br/>
<span>Website</span>
<input class="radius" type="text" name="website" placeholder="Website" value="{% if website %}{{website}}{% endif %}">
<span>Affiliation</span>
<input class="radius" type="text" name="affiliation" placeholder="Affiliation" value="{% if affiliation %}{{affiliation}}{% endif %}">
<span>Country</span>
<input class="radius" type="text" name="country" placeholder="Country" value="{% if country %}{{country}}{% endif %}">
<input type="hidden" name="nonce" value="{{nonce}}">
<button class="radius">Update</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script src="/static/js/team.js"></script>
{% endblock %}

34
templates/register.html Normal file
View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h1>Register</h1>
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form method="POST">
<input type='text' class="radius" name='name' placeholder='Team Name' {% if name %}value="{{ name }}"{% endif %}><br/>
<input type='text' class="radius" name='email' placeholder='Email' {% if email %}value="{{ email }}"{% endif %}><br/>
<input type='password' class="radius" name='password' placeholder='Password' {% if password %}value="{{ password }}"{% endif %}><br/>
<button class="radius" type='submit'>Register</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
if (window.location.hash == "#frame"){
$('.top-bar').hide()
}
</script>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h1>Reset Password</h1>
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
{% if mode %}
<form method="POST">
<input class="radius" type='password' name='password' placeholder='Password'><br/>
<button class="radius" type='submit'>Reset Password</button>
</form>
{% else %}
<form method="POST">
<input class="radius" type='text' name='email' placeholder='Email'><br/>
<button class="radius" type='submit'>Reset Password</button>
</form>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

36
templates/scoreboard.html Normal file
View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<p></p>
<div id="score-graph"></div>
<p></p>
<table id="scoreboard">
<thead>
<tr>
<td><b>Place</b>
</td>
<td><b>Team</b>
</td>
<td><b>Score</b>
</td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr><td>{{ loop.index }}</td><td><a href="/team/{{ team.teamid }}">{{ team.name }}</a></td><td>{{ team.score }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script src="/static/js/scoreboard.js"></script>
{% endblock %}

52
templates/setup.html Normal file
View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.css">
<div class="row">
<h1>Setup</h1>
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form method="POST">
<strong>Username</strong>
<input class="radius" type='text' name='name' placeholder='Name'><br/>
<strong>Email</strong>
<input class="radius" type='text' name='email' placeholder='Email'><br/>
<strong>Password</strong>
<input class="radius" type='password' name='password' placeholder='Password'><br/>
<strong>Index Page</strong>
<textarea id="pages-editor" name="html">
<div class="row">
<img class="logo" src="/static/img/logo.png">
<h3 class="text-center">Welcome to a cool CTF framwork written by <a href="https://github.com/ColdHeat">Kevin Chung</a> of <a href="https://github.com/isislab">@isislab</a></h3>
<h4 class="text-center"><a href="/admin">Click here</a> to login and setup your CTF</h4>
</div>
</textarea><br>
<button class="radius" type='submit'>Login</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.min.js"></script>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("pages-editor"), {
lineNumbers: true,
lineWrapping: true,
mode: "text/html"
});
</script>
{% endblock %}

37
templates/team.html Normal file
View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<h1 id="team-id">{{ team.name }}</h1>
<div id="keys-pie-graph"></div>
<div id="categories-pie-graph"></div>
<table>
<thead>
<tr>
<td><b>Challenge</b></td>
<td><b>Category</b></td>
<td><b>Value</b></td>
<td><b>Time</b></td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr><td><a href="/challenges#{{ solve.chal.name }}">{{ solve.chal.name }}</a></td><td>{{ solve.chal.category }}</td><td>{{ solve.chal.value }}</td><td class="solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td></tr>
{% endfor %}
</tbody>
</table>
<div id="score-graph"></div>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
<script src="/static/js/team.js"></script>
{% endblock %}

32
templates/teams.html Normal file
View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
<div class="row">
<p></p>
<table id="teamsboard">
<thead>
<tr>
<td><b>Team</b></td>
<td><b>Website</b></td>
<td><b>Affiliation</b></td>
<td><b>Country</b></td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td><a href="/team/{{ team.id }}">{{ team.name }}</a></td>
<td><a href="{{ team.website }}">{% if team.website %}{{ team.website }}{% endif %}</a></td>
<td><span>{% if team.affiliation %}{{ team.affiliation }}{% endif %}</span></td>
<td><span>{% if team.country %}{{ team.country }}{% endif %}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}