Merging changes from various forks

Until v1 is released all changes are fair game.

Caching support
Fixes for decoding confirmation and reset_password email tokens
Starting work on #154 specifying why challenges are not open
Adding a required parameter to HTML to sort of fix #153
Adding a column to specify when a team registered
Check static key by default in new key
Decreasing capability of pages functionality to address security
concerns
Fixing confirmations restrictions by modifying can__view_challenges()
This commit is contained in:
Kevin Chung
2016-09-24 17:56:07 -04:00
parent 92ebd88025
commit 50043b42c5
15 changed files with 286 additions and 100 deletions

View File

@@ -4,9 +4,11 @@ from logging.handlers import RotatingFileHandler
from flask_session import Session from flask_session import Session
from sqlalchemy_utils import database_exists, create_database from sqlalchemy_utils import database_exists, create_database
from jinja2 import FileSystemLoader, TemplateNotFound from jinja2 import FileSystemLoader, TemplateNotFound
from utils import get_config, set_config from utils import get_config, set_config, cache
import os import os
import sqlalchemy import sqlalchemy
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import OperationalError
class ThemeLoader(FileSystemLoader): class ThemeLoader(FileSystemLoader):
@@ -24,15 +26,26 @@ def create_app(config='CTFd.config'):
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
## sqlite database creation is relative to the script which causes issues with serve.py url = make_url(app.config['SQLALCHEMY_DATABASE_URI'])
if not database_exists(app.config['SQLALCHEMY_DATABASE_URI']) and not app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite'): if url.drivername == 'postgres':
create_database(app.config['SQLALCHEMY_DATABASE_URI']) url.drivername = 'postgresql'
db.init_app(app) db.init_app(app)
db.create_all()
try:
if not database_exists(url):
create_database(url)
db.create_all()
except OperationalError:
db.create_all()
else:
db.create_all()
app.db = db app.db = db
cache.init_app(app)
app.cache = cache
if not get_config('ctf_theme'): if not get_config('ctf_theme'):
set_config('ctf_theme', 'original') set_config('ctf_theme', 'original')

View File

@@ -1,7 +1,7 @@
from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, \ from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes container_stop, container_start, get_themes, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings from CTFd.scoreboard import get_standings
from itsdangerous import TimedSerializer, BadTimeSignature from itsdangerous import TimedSerializer, BadTimeSignature
@@ -105,8 +105,12 @@ def admin_config():
db.session.commit() db.session.commit()
db.session.close() db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin.admin_config')) return redirect(url_for('admin.admin_config'))
with app.app_context():
cache.clear()
ctf_name = get_config('ctf_name') ctf_name = get_config('ctf_name')
ctf_theme = get_config('ctf_theme') ctf_theme = get_config('ctf_theme')
max_tries = get_config('max_tries') max_tries = get_config('max_tries')
@@ -758,7 +762,7 @@ def admin_wrong_key(page='1'):
wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,\ wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,\
Challenges.name.label('chal_name'), Teams.name.label('team_name')).\ Challenges.name.label('chal_name'), Teams.name.label('team_name')).\
join(Challenges).join(Teams).order_by('team_name ASC').slice(page_start, page_end).all() join(Challenges).join(Teams).order_by(WrongKeys.date.desc()).slice(page_start, page_end).all()
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0] wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0) pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0)
@@ -776,7 +780,7 @@ def admin_correct_key(page='1'):
solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag, \ solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag, \
Challenges.name.label('chal_name'), Teams.name.label('team_name')).\ Challenges.name.label('chal_name'), Teams.name.label('team_name')).\
join(Challenges).join(Teams).order_by('team_name ASC').slice(page_start, page_end).all() join(Challenges).join(Teams).order_by(Solves.date.desc()).slice(page_start, page_end).all()
solve_count = db.session.query(db.func.count(Solves.id)).first()[0] solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0) pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0)

View File

@@ -10,6 +10,7 @@ import logging
import time import time
import re import re
import os import os
import urllib
auth = Blueprint('auth', __name__) auth = Blueprint('auth', __name__)
@@ -22,20 +23,28 @@ def confirm_user(data=None):
if data and request.method == "GET": ## User is confirming email account if data and request.method == "GET": ## User is confirming email account
try: try:
s = Signer(app.config['SECRET_KEY']) s = Signer(app.config['SECRET_KEY'])
email = s.unsign(data.decode('base64')) email = s.unsign(urllib.unquote(data.decode('base64')))
except BadSignature: except BadSignature:
return render_template('confirm.html', errors=['Your confirmation link seems wrong']) return render_template('confirm.html', errors=['Your confirmation link seems wrong'])
except:
return render_template('reset_password.html', errors=['Your link appears broken, please try again.'])
team = Teams.query.filter_by(email=email).first() team = Teams.query.filter_by(email=email).first()
team.verified = True team.verified = True
db.session.commit() db.session.commit()
db.session.close() db.session.close()
logger = logging.getLogger('regs')
logger.warn("[{0}] {1} confirmed {2}".format(time.strftime("%m/%d/%Y %X"), team.name.encode('utf-8'), team.email.encode('utf-8')))
if authed(): if authed():
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.challenges_view'))
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
if not data and request.method == "GET": ## User has been directed to the confirm page because his account is not verified if not data and request.method == "GET": ## User has been directed to the confirm page because his account is not verified
if not authed():
return redirect(url_for('auth.login'))
team = Teams.query.filter_by(id=session['id']).first() team = Teams.query.filter_by(id=session['id']).first()
if team.verified: if team.verified:
return redirect(url_for('views.profile')) return redirect(url_for('views.profile'))
else:
verify_email(team.email)
return render_template('confirm.html', team=team) return render_template('confirm.html', team=team)
@@ -48,7 +57,7 @@ def reset_password(data=None):
if data is not None and request.method == "POST": if data is not None and request.method == "POST":
try: try:
s = TimedSerializer(app.config['SECRET_KEY']) s = TimedSerializer(app.config['SECRET_KEY'])
name = s.loads(data.decode('base64'), max_age=1800) name = s.loads(urllib.unquote(data.decode('base64')), max_age=1800)
except BadTimeSignature: except BadTimeSignature:
return render_template('reset_password.html', errors=['Your link has expired']) return render_template('reset_password.html', errors=['Your link has expired'])
team = Teams.query.filter_by(name=name).first() team = Teams.query.filter_by(name=name).first()
@@ -92,7 +101,7 @@ def register():
emails = Teams.query.add_columns('email', 'id').filter_by(email=email).first() emails = Teams.query.add_columns('email', 'id').filter_by(email=email).first()
pass_short = len(password) == 0 pass_short = len(password) == 0
pass_long = len(password) > 128 pass_long = len(password) > 128
valid_email = re.match("[^@]+@[^@]+\.[^@]+", request.form['email']) valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", request.form['email'])
if not valid_email: if not valid_email:
errors.append("That email doesn't look right") errors.append("That email doesn't look right")
@@ -121,10 +130,15 @@ def register():
session['admin'] = team.admin session['admin'] = team.admin
session['nonce'] = sha512(os.urandom(10)) session['nonce'] = sha512(os.urandom(10))
if can_send_mail() and get_config('verify_emails'): if can_send_mail() and get_config('verify_emails'): ## Confirming users is enabled and we can send email.
verify_email(team.email) db.session.close()
else: logger = logging.getLogger('regs')
if can_send_mail(): logger.warn("[{0}] {1} registered (UNCONFIRMED) with {2}".format(time.strftime("%m/%d/%Y %X"),
request.form['name'].encode('utf-8'),
request.form['email'].encode('utf-8')))
return redirect(url_for('auth.confirm_user'))
else: ## Don't care about confirming users
if can_send_mail(): ## We want to notify the user that they have registered.
sendmail(request.form['email'], "You've successfully registered for {}".format(get_config('ctf_name'))) sendmail(request.form['email'], "You've successfully registered for {}".format(get_config('ctf_name')))
db.session.close() db.session.close()
@@ -142,25 +156,30 @@ def login():
errors = [] errors = []
name = request.form['name'] name = request.form['name']
team = Teams.query.filter_by(name=name).first() team = Teams.query.filter_by(name=name).first()
if team and bcrypt_sha256.verify(request.form['password'], team.password): if team:
try: if team and bcrypt_sha256.verify(request.form['password'], team.password):
session.regenerate() # NO SESSION FIXATION FOR YOU try:
except: session.regenerate() # NO SESSION FIXATION FOR YOU
pass # TODO: Some session objects don't implement regenerate :( except:
session['username'] = team.name pass # TODO: Some session objects don't implement regenerate :(
session['id'] = team.id session['username'] = team.name
session['admin'] = team.admin session['id'] = team.id
session['nonce'] = sha512(os.urandom(10)) session['admin'] = team.admin
db.session.close() session['nonce'] = sha512(os.urandom(10))
db.session.close()
logger = logging.getLogger('logins') logger = logging.getLogger('logins')
logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'))) logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8')))
if request.args.get('next') and is_safe_url(request.args.get('next')): if request.args.get('next') and is_safe_url(request.args.get('next')):
return redirect(request.args.get('next')) return redirect(request.args.get('next'))
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.challenges_view'))
else: else: # This user exists but the password is wrong
errors.append("That account doesn't seem to exist") errors.append("That account doesn't seem to exist")
db.session.close()
return render_template('login.html', errors=errors)
else: # This user just doesn't exist
errors.append("Your username or password is incorrect")
db.session.close() db.session.close()
return render_template('login.html', errors=errors) return render_template('login.html', errors=errors)
else: else:

View File

@@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint
from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, can_view_challenges, is_admin, get_config, get_ip, is_verified from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, user_can_view_challenges, is_admin, get_config, get_ip, is_verified, ctf_started, ctf_ended, ctf_name
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards
from sqlalchemy.sql import and_, or_, not_ from sqlalchemy.sql import and_, or_, not_
@@ -15,16 +15,26 @@ challenges = Blueprint('challenges', __name__)
@challenges.route('/challenges', methods=['GET']) @challenges.route('/challenges', methods=['GET'])
def challenges_view(): def challenges_view():
if not is_admin(): errors = []
start = get_config('start') or 0
end = get_config('end') or 0
if not is_admin(): # User is not an admin
if not ctftime(): if not ctftime():
if view_after_ctf(): # It is not CTF time
if view_after_ctf(): # But we are allowed to view after the CTF ends
pass pass
else: else: # We are NOT allowed to view after the CTF ends
errors.append('{} has ended'.format(ctf_name()))
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
return redirect(url_for('views.static_html')) return redirect(url_for('views.static_html'))
if get_config('verify_emails') and not is_verified(): if get_config('verify_emails') and not is_verified(): # User is not confirmed
return redirect(url_for('auth.confirm_user')) return redirect(url_for('auth.confirm_user'))
if can_view_challenges(): if user_can_view_challenges(): # Do we allow unauthenticated users?
return render_template('chals.html', ctftime=ctftime()) if get_config('start') and not ctf_started():
errors.append('{} has not started yet'.format(ctf_name()))
if (get_config('end') and ctf_ended()) and not view_after_ctf():
errors.append('{} has ended'.format(ctf_name()))
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
else: else:
return redirect(url_for('auth.login', next='challenges')) return redirect(url_for('auth.login', next='challenges'))
@@ -37,7 +47,7 @@ def chals():
pass pass
else: else:
return redirect(url_for('views.static_html')) return redirect(url_for('views.static_html'))
if can_view_challenges(): if user_can_view_challenges():
chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all() chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
json = {'game':[]} json = {'game':[]}
@@ -55,21 +65,24 @@ def chals():
@challenges.route('/chals/solves') @challenges.route('/chals/solves')
def chals_per_solves(): def chals_per_solves():
if can_view_challenges(): if not user_can_view_challenges():
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves')).join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).group_by(Solves.chalid).subquery() return redirect(url_for('auth.login', next=request.path))
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves, Challenges.name) \ solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves')).join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).group_by(Solves.chalid).subquery()
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all() solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves, Challenges.name) \
json = {} .join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
for chal, count, name in solves: json = {}
json[name] = count for chal, count, name in solves:
db.session.close() json[name] = count
return jsonify(json) db.session.close()
return redirect(url_for('auth.login', next='chals/solves')) return jsonify(json)
@challenges.route('/solves') @challenges.route('/solves')
@challenges.route('/solves/<teamid>') @challenges.route('/solves/<teamid>')
def solves(teamid=None): def solves(teamid=None):
if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
solves = None solves = None
awards = None awards = None
if teamid is None: if teamid is None:
@@ -109,6 +122,8 @@ def solves(teamid=None):
@challenges.route('/maxattempts') @challenges.route('/maxattempts')
def attempts(): def attempts():
if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
chals = Challenges.query.add_columns('id').all() chals = Challenges.query.add_columns('id').all()
json = {'maxattempts':[]} json = {'maxattempts':[]}
for chal, chalid in chals: for chal, chalid in chals:
@@ -120,6 +135,8 @@ def attempts():
@challenges.route('/fails/<teamid>', methods=['GET']) @challenges.route('/fails/<teamid>', methods=['GET'])
def fails(teamid): def fails(teamid):
if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
fails = WrongKeys.query.filter_by(teamid=teamid).count() fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close() db.session.close()
@@ -129,6 +146,8 @@ def fails(teamid):
@challenges.route('/chal/<chalid>/solves', methods=['GET']) @challenges.route('/chal/<chalid>/solves', methods=['GET'])
def who_solved(chalid): def who_solved(chalid):
if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc()) solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
json = {'teams':[]} json = {'teams':[]}
for solve in solves: for solve in solves:
@@ -138,9 +157,11 @@ def who_solved(chalid):
@challenges.route('/chal/<chalid>', methods=['POST']) @challenges.route('/chal/<chalid>', methods=['POST'])
def chal(chalid): def chal(chalid):
if not ctftime(): if ctf_ended() and not view_after_ctf():
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.challenges_view'))
if authed(): if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
if authed() and is_verified() and (ctf_started() or view_after_ctf()):
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
logger = logging.getLogger('keys') logger = logging.getLogger('keys')
data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), get_kpm(session['id'])) data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), get_kpm(session['id']))
@@ -148,10 +169,11 @@ def chal(chalid):
# Anti-bruteforce / submitting keys too quickly # Anti-bruteforce / submitting keys too quickly
if get_kpm(session['id']) > 10: if get_kpm(session['id']) > 10:
wrong = WrongKeys(session['id'], chalid, request.form['key']) if ctftime():
db.session.add(wrong) wrong = WrongKeys(session['id'], chalid, request.form['key'])
db.session.commit() db.session.add(wrong)
db.session.close() db.session.commit()
db.session.close()
logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data)) logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
# return "3" # Submitting too fast # return "3" # Submitting too fast
return jsonify({'status': '3', 'message': "You're submitting keys too fast. Slow down."}) return jsonify({'status': '3', 'message': "You're submitting keys too fast. Slow down."})
@@ -176,28 +198,31 @@ def chal(chalid):
if x['type'] == 0: #static key if x['type'] == 0: #static key
print(x['flag'], key.strip().lower()) print(x['flag'], key.strip().lower())
if x['flag'] and x['flag'].strip().lower() == key.strip().lower(): if x['flag'] and x['flag'].strip().lower() == key.strip().lower():
solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) if ctftime():
db.session.add(solve) solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key)
db.session.commit() db.session.add(solve)
db.session.close() db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
# return "1" # key was correct # return "1" # key was correct
return jsonify({'status':'1', 'message':'Correct'}) return jsonify({'status':'1', 'message':'Correct'})
elif x['type'] == 1: #regex elif x['type'] == 1: #regex
res = re.match(str(x['flag']), key, re.IGNORECASE) res = re.match(str(x['flag']), key, re.IGNORECASE)
if res and res.group() == key: if res and res.group() == key:
solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) if ctftime():
db.session.add(solve) solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key)
db.session.commit() db.session.add(solve)
db.session.close() db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
# return "1" # key was correct # return "1" # key was correct
return jsonify({'status': '1', 'message': 'Correct'}) return jsonify({'status': '1', 'message': 'Correct'})
wrong = WrongKeys(session['id'], chalid, request.form['key']) if ctftime():
db.session.add(wrong) wrong = WrongKeys(session['id'], chalid, request.form['key'])
db.session.commit() db.session.add(wrong)
db.session.close() db.session.commit()
db.session.close()
logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data)) logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data))
# return '0' # key was wrong # return '0' # key was wrong
if max_tries: if max_tries:

View File

@@ -11,7 +11,7 @@ with open('.ctfd_secret_key', 'a+') as secret:
##### SERVER SETTINGS ##### ##### SERVER SETTINGS #####
SECRET_KEY = key SECRET_KEY = key
SQLALCHEMY_DATABASE_URI = 'sqlite:///ctfd.db' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///ctfd.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
SESSION_TYPE = "filesystem" SESSION_TYPE = "filesystem"
SESSION_FILE_DIR = "/tmp/flask_session" SESSION_FILE_DIR = "/tmp/flask_session"
@@ -31,3 +31,7 @@ TRUSTED_PROXIES = [
'^172\.(1[6-9]|2[0-9]|3[0-1])\.', '^172\.(1[6-9]|2[0-9]|3[0-1])\.',
'^192\.168\.' '^192\.168\.'
] ]
CACHE_TYPE = "simple"
if CACHE_TYPE == 'redis':
CACHE_REDIS_URL = os.environ.get('REDIS_URL')

View File

@@ -147,6 +147,7 @@ class Teams(db.Model):
banned = db.Column(db.Boolean, default=False) banned = db.Column(db.Boolean, default=False)
verified = db.Column(db.Boolean, default=False) verified = db.Column(db.Boolean, default=False)
admin = db.Column(db.Boolean, default=False) admin = db.Column(db.Boolean, default=False)
joined = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, name, email, password): def __init__(self, name, email, password):
self.name = name self.name = name

View File

@@ -258,7 +258,7 @@ $('#create-key').click(function(e){
var elem = $('<div class="col-md-4">'); var elem = $('<div class="col-md-4">');
elem.append($("<div class='form-group'>").append($("<input class='current-key form-control' type='text'>"))); elem.append($("<div class='form-group'>").append($("<input class='current-key form-control' type='text'>")));
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="0">Static</div>'); elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="0" checked>Static</div>');
elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="1">Regex</div>'); elem.append('<div class="radio-inline"><input type="radio" name="key_type['+amt+']" value="1">Regex</div>');
elem.append('<a href="#" onclick="$(this).parent().remove()" class="btn btn-danger key-remove-button">Remove</a>'); elem.append('<a href="#" onclick="$(this).parent().remove()" class="btn btn-danger key-remove-button">Remove</a>');

View File

@@ -58,7 +58,7 @@
<div class="form-group"> <div class="form-group">
<label for="value">Value</label> <label for="value">Value</label>
<input type="number" class="form-control" name="value" placeholder="Enter value"> <input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div> </div>
<div class="form-group" style="height:75px"> <div class="form-group" style="height:75px">
<div class="col-md-9" style="padding-left:0px"> <div class="col-md-9" style="padding-left:0px">
@@ -90,7 +90,7 @@
</div> </div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce"> <input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div style="text-align:center"> <div style="text-align:center">
<button class="btn btn-theme btn-outlined create-challenge" type="submit">Create</button> <button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
</div> </div>
</form> </form>
</div> </div>
@@ -134,7 +134,7 @@
<div class="form-group"> <div class="form-group">
<label for="value">Value</label> <label for="value">Value</label>
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value"> <input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
</div> </div>
<input class="chal-id" type='hidden' name='id' placeholder='ID'> <input class="chal-id" type='hidden' name='id' placeholder='ID'>
@@ -153,7 +153,7 @@
</div> </div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce"> <input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div style="text-align:center"> <div style="text-align:center">
<button class="btn btn-theme btn-outlined create-challenge" type="submit">Update</button> <button class="btn btn-theme btn-outlined update-challenge-submit" type="submit">Update</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -67,11 +67,11 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<input type="hidden" name="id"> <input type="hidden" name="id">
<div class="form-group"> <div class="form-group">
<label for="name">Team Name</label> <label for="name">Team Name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="Enter new team name"> <input type="text" class="form-control" name="name" id="name" placeholder="Enter new team name" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">Email</label>
<input type="email" class="form-control" name="email" id="email" placeholder="Enter new email"> <input type="email" class="form-control" name="email" id="email" placeholder="Enter new email" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>

View File

@@ -80,6 +80,17 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if errors %}
<div class="container main-container">
<div id='errors' class="row">
{% for error in errors %}
<h1>{{ error }}</h1>
{% endfor %}
</div>
</div>
{% else %}
<div class="jumbotron home"> <div class="jumbotron home">
<div class="container"> <div class="container">
<h1>Challenges</h1> <h1>Challenges</h1>
@@ -147,10 +158,11 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/utils.js"></script> <script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/utils.js"></script>
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/chalboard.js"></script> {% if not errors %}<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/chalboard.js"></script>{% endif %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/style.js"></script> <script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/style.js"></script>
{% endblock %} {% endblock %}

View File

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

View File

@@ -43,6 +43,10 @@
<div class="alert alert-info alert-dismissable submit-row" role="alert"> <div class="alert alert-info alert-dismissable submit-row" role="alert">
Your email address isn't confirmed! Your email address isn't confirmed!
Please check your email to confirm your email address. Please check your email to confirm your email address.
<br>
<br>
To have the confirmation email reset please <a href="{{ request.script_root }}/confirm">click
here.</a>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button> aria-hidden="true">×</span></button>
</div> </div>
@@ -73,7 +77,7 @@
<span class="input"> <span class="input">
<input class="input-field" type="password" name="confirm" id="confirm-input" /> <input class="input-field" type="password" name="confirm" id="confirm-input" />
<label class="input-label" for="confirm-input"> <label class="input-label" for="confirm-input">
<span class="label-content" data-content="Password">Password</span> <span class="label-content" data-content="Password">Current Password</span>
</label> </label>
</span> </span>
</div> </div>

View File

@@ -1,9 +1,11 @@
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Containers, ip2long, long2ip from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Containers, ip2long, long2ip
from six.moves.urllib.parse import urlparse, urljoin from six.moves.urllib.parse import urlparse, urljoin
import six
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from functools import wraps from functools import wraps
from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort
from flask_cache import Cache
from itsdangerous import Signer, BadSignature from itsdangerous import Signer, BadSignature
from socket import inet_aton, inet_ntoa, socket from socket import inet_aton, inet_ntoa, socket
from struct import unpack, pack, error from struct import unpack, pack, error
@@ -24,8 +26,11 @@ import smtplib
import email import email
import tempfile import tempfile
import subprocess import subprocess
import urllib
import json import json
cache = Cache()
def init_logs(app): def init_logs(app):
logger_keys = logging.getLogger('keys') logger_keys = logging.getLogger('keys')
@@ -132,11 +137,13 @@ def init_utils(app):
abort(403) abort(403)
@cache.memoize()
def ctf_name(): def ctf_name():
name = get_config('ctf_name') name = get_config('ctf_name')
return name if name else 'CTFd' return name if name else 'CTFd'
@cache.memoize()
def ctf_theme(): def ctf_theme():
theme = get_config('ctf_theme') theme = get_config('ctf_theme')
return theme if theme else '' return theme if theme else ''
@@ -152,13 +159,17 @@ def authed():
def is_verified(): def is_verified():
team = Teams.query.filter_by(id=session.get('id')).first() if get_config('verify_emails'):
if team: team = Teams.query.filter_by(id=session.get('id')).first()
return team.verified if team:
return team.verified
else:
return False
else: else:
return False return True
@cache.memoize()
def is_setup(): def is_setup():
setup = Config.query.filter_by(key='setup').first() setup = Config.query.filter_by(key='setup').first()
if setup: if setup:
@@ -174,12 +185,9 @@ def is_admin():
return False return False
@cache.memoize()
def can_register(): def can_register():
config = Config.query.filter_by(key='prevent_registration').first() return not bool(get_config('prevent_registration'))
if config:
return config.value != '1'
else:
return True
def admins_only(f): def admins_only(f):
@@ -192,11 +200,9 @@ def admins_only(f):
return decorated_function return decorated_function
@cache.memoize()
def view_after_ctf(): def view_after_ctf():
if get_config('view_after_ctf') == '1' and time.time() > int(get_config("end")): return bool(get_config('view_after_ctf'))
return True
else:
return False
def ctftime(): def ctftime():
@@ -234,10 +240,19 @@ def ctftime():
return False return False
def can_view_challenges(): def ctf_started():
config = Config.query.filter_by(key="view_challenges_unregistered").first() return time.time() > int(get_config("start") or 0)
def ctf_ended():
return time.time() > int(get_config("end") or 0)
def user_can_view_challenges():
config = bool(get_config('view_challenges_unregistered'))
verify_emails = bool(get_config('verify_emails'))
if config: if config:
return authed() or config.value == '1' return authed() or config
else: else:
return authed() return authed()
@@ -284,14 +299,20 @@ def get_themes():
if os.path.isdir(os.path.join(dir, name))] if os.path.isdir(os.path.join(dir, name))]
@cache.memoize()
def get_config(key): def get_config(key):
config = Config.query.filter_by(key=key).first() config = Config.query.filter_by(key=key).first()
if config and config.value: if config and config.value:
value = config.value value = config.value
if value and value.isdigit(): if value and value.isdigit():
return int(value) return int(value)
else: elif value and isinstance(value, six.string_types):
return value if value.lower() == 'true':
return True
elif value.lower() == 'false':
return False
else:
return value
else: else:
set_config(key, None) set_config(key, None)
return None return None
@@ -308,10 +329,12 @@ def set_config(key, value):
return config return config
@cache.memoize()
def can_send_mail(): def can_send_mail():
return mailgun() or mailserver() return mailgun() or mailserver()
@cache.memoize()
def mailgun(): def mailgun():
if app.config.get('MAILGUN_API_KEY') and app.config.get('MAILGUN_BASE_URL'): if app.config.get('MAILGUN_API_KEY') and app.config.get('MAILGUN_BASE_URL'):
return True return True
@@ -319,6 +342,8 @@ def mailgun():
return True return True
return False return False
@cache.memoize()
def mailserver(): def mailserver():
if (get_config('mail_server') and get_config('mail_port')): if (get_config('mail_server') and get_config('mail_port')):
return True return True
@@ -384,7 +409,7 @@ def verify_email(addr):
token = s.sign(addr) token = s.sign(addr)
text = """Please click the following link to confirm your email address for {}: {}""".format( text = """Please click the following link to confirm your email address for {}: {}""".format(
get_config('ctf_name'), get_config('ctf_name'),
url_for('auth.confirm_user', _external=True) + '/' + token.encode('base64') url_for('auth.confirm_user', _external=True) + '/' + urllib.quote_plus(token.encode('base64'))
) )
sendmail(addr, text) sendmail(addr, text)
@@ -407,6 +432,7 @@ def sha512(string):
return hashlib.sha512(string).hexdigest() return hashlib.sha512(string).hexdigest()
@cache.memoize()
def can_create_container(): def can_create_container():
try: try:
output = subprocess.check_output(['docker', 'version']) output = subprocess.check_output(['docker', 'version'])

View File

@@ -1,5 +1,5 @@
from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint, Response from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint, Response
from CTFd.utils import authed, ip2long, long2ip, is_setup, validate_url, get_config, set_config, sha512, get_ip from CTFd.utils import authed, ip2long, long2ip, is_setup, validate_url, get_config, set_config, sha512, get_ip, cache
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
@@ -90,6 +90,8 @@ def setup():
db.session.commit() db.session.commit()
db.session.close() db.session.close()
app.setup = False app.setup = False
with app.app_context():
cache.clear()
return redirect(url_for('views.static_html')) return redirect(url_for('views.static_html'))
return render_template('setup.html', nonce=session.get('nonce')) return render_template('setup.html', nonce=session.get('nonce'))
return redirect(url_for('views.static_html')) return redirect(url_for('views.static_html'))
@@ -110,7 +112,7 @@ def static_html(template):
except TemplateNotFound: except TemplateNotFound:
page = Pages.query.filter_by(route=template).first() page = Pages.query.filter_by(route=template).first()
if page: if page:
return render_template_string('{% extends "base.html" %}{% block content %}' + page.html + '{% endblock %}') return render_template('page.html', content=page.html)
else: else:
abort(404) abort(404)

View File

@@ -1,6 +1,7 @@
Flask Flask
Flask-SQLAlchemy Flask-SQLAlchemy
Flask-Session Flask-Session
Flask-Caching
SQLAlchemy SQLAlchemy
sqlalchemy-utils sqlalchemy-utils
passlib passlib