* Fix user and admin panel user/team graphs
* Closes #682
* Unify login and logout under specific functions
* Closes #659
* Rename Challenges.hidden to Challenges.state
* Start to clean up API and front end integration starting with profile updating
* Slightly cleaner code
* Clean API to respond with success, data, and status codes
* Simpler COUNTRIES_LIST and update profile to use COUNTRIES_LIST
* Lookup country code in users page. Update front end calls to get API data properly
* Fix some API endpoints and fix JS to process new responses
* Update config.py to support new values
* Closes #635
* Update some code to handle user types, add email domain whitelisting
* Write a logging wrapper
* Use logging wrapper for submissions
* Close #656
* Break up config.html to make it easier to maintain
* Fix logging, domain_whitelist, and config
* Improving views.py, starting to add Announcements
* Starting announcements front end
* Make it easier to see large images, clean up some more REST API differences
* Closes #668
* Update Proxyfix config to REVERSE_PROXY
* Add announcements front end
* Move creation/edit modals into seperate files. Start moving user updating into their admin profile pages.
* Update font-awesome to 5.4.1
* Switch to user-edit icon
* Update the update_check function to send up more anonymous data for statistics purposes.
* Start work on #640
* Add the user action modals and update API to fix responses
* Fix admin teams page
* Add challenge requirements
* Implement anonymous locked challenges
* Team editting from admin panel
* Switch from simple cache to filesystem cache
* Implements a Cache backed server side session (#658) and fixes Users editting endpoint
* Add our messaging for docs
* Closes #700
* Remove invalid import
* Move challenge enditting around a whole lot and probably break a bunch of things
* Show challenge names in prerequisites instead of challenge IDs
* Closes #661
* Change user templates to use url_for
* Remove extra function
* Rewrite admin panel to use url_for
* Fix events to work under subdirectories
* Start cleaning up config panel
* Fix filesystem uploader; deprecate view_challenges_unregistered, view_scoreboard_if_authed, prevent_registration, view_after_ctf; implement new visibility decorators
* Remove workshop mode, fix some glitches with the new visibility settings
* Fix ctf_logo on core theme
* Fix setup errors
* Removing default from get_config b/c of memoization issues and getting some tests working
* Relax email regex validation rule (#693)
* Update to pycodestyle and fix new lint errors
* Add a ctf_id to update_check
* Change challenge plugin layout. Rename mailgun configs to be more descriptive (Closes #702)
* Detect if people try to set routes with '/' to simplify #690
* Closes #690
* Clean up some code
* Clean up challenge submit to rate limit
* Fix js version compatability issue
* Close some TODOs
* Hide challenges if not authenticated
* Make set_config reset the cache for those config values
* Return 404 on empty challenges for /api/v1/<challenge_id>/solves
* Fix setting boolean configs
* Properly change account config settings
* Move datetimes to isoformat (Closes #703)
* Remove all .isoformat() calls because it isn't UTC aware (ends in Z). Switch to isoformat function & filter
* Make /v1/submissions endpoint work for admin submission creation
* Make oauth_id unique for Users and Teams
* Move challenge submission endpoint and implement mark solved. Fix some isoformat issues.
* Only show team's missing challenges if in team mode
* Adding support for Hints & Unlocks
* Update challenge submission url
* Fix encoding functions in Python3
* Fix hexencode in Python3
* Added functional tests for challenges API for non-admin users (#705)
* Set hint default type to be standard
* Fix some JS issues. Closes #704
* Implement session.regenerate on top of the CachingSessionInterface
* Challenge challenge attempt responses from numbers to strings
* Fix password updating for UserSchema
* Remove leftover challenge submission code
* Remove old migrations :(, resolve challenge requirements not loading correctly, move migration functions
*  Added functional tests for challenges/hints/admin API (#710)
* Fix helpers and re-add JSONLite
* Install MySQL 5.7
* Try more mysql
* Update password for mysql
* Fixing issuse in Users.get_solves
* Add new import/export code
* Switch to CTFdSerializer for Python 3
* Re-implement import exports and add a very flaky test
* Redesign submissions API response
* Get export to roundtrip in tests
* Int score b/c Decimal is not JSON serializeable
* Remove unused route methods
* Fix POST /api/v1/configs and start adding admin tests
* Add user_id and team_id to top/10
* Fix admin creating Teams
* Fix Team website validation
* Change admins_only to reply with a 403 if the request is JSON
* Organize admin tests and fix authed_only to return 403 on unauthed
* Adding check_account_visibility, check_score_visibility for /api/v1/teams/<team_id>/(solves|awards|fails)
* Fix teams/me endpoints again
* Fix users/me endpoints to return 403 if unauthed
* Fix Python 3 config API
* Add fetch and promise polyfills. (#712)
* Add exec to docker-entrypoint.sh (#713)
* Display import_ctf Exceptions via repr (#651)
- Wraps exceptions on `/admin/import` returned to users in a `repr()`, making debugging easier.
* Add error messages to the admin panel, fix schemas for users, start working on UI for imports/exports
* Make unauthed challenge submission attempt return 403 instead of 302, Fix user deletion, fix associated tests, remove TODOs
* Remove old means of creating solves
* Remove most of the content from teams.js and users.js
* Remove extra code from /challenges.js
* Fix POST'ing & PATCH'ing pages
* Make (users|teams)/fails return only count to users. Fix public score graphs to factor in awards
* Fix admin side scoregraphs. Fix Awardschemas for admins
* Add requirements to db migration
* Adding some team decorators
* Fix require_team_mode decorator
* Make verified emails decorator return 403 on JSON requests
* Redo initial revision
* Add SQLiteJSON back
* Adding ratelimit to /redirect and removing POST from /oauth
* Fix PATCH tags
* Actually fix PATCH tags
* Simplify 500.html
* Added tests for challenges, awards, files, flags, hints ... (#723)
* Added tests for challenges, awards, files, flags, hints, notifications, pages, submissions, tags
* Fix user data validation functions, Fix hidden challenges and include test
* Add a locked state to attempt
* OAuth teams get verified, use logging functions in redirect route
* Removing extra print call
* Update requirements.txt
* Fix possible AttributeError
* Start work on #716
* Closes #717
* Fix issue patching teams
* Rename .j2 to .html, implement preview for challenges if admin
* Move admin/challenge.html to admin/challenges/challenge.html
* Remove old modals
* Add Reset CTF button (#639)
* Add Reset link to config.html
* Delete Tracking
* files handler should return a 404 on files it cant find
* Denote official teams (#729), make scoregraph fill to zero
* Remove old javascript files, make some challenge elements refresh by reloading
* Fix team editting modals to work more reliably
* Fix rendering of CTF paused
* Remove hide_scores funtion and roll it into scores visibility
* Log to stdout/stderr by default (#719)
* Fix user searching
* Remove searching for users/teams by country
* Add badges to admin team and user pages, implement user banning (#643)
* Remove shell.py, clean up admin team.html, add tests for banned users, teams
* Start cleaning up dynamic_challenges to meet new challenge type plugin format
* Remove POST method from teams.public
* Add credentials: 'same-origin' to all fetch calls (#734)
* Add challenge preview, add challenge deletion, fix file deletions when deleting challenges
* Fix imports UI (#735)
* Show prerequisites before adding a blank one (#738), Refresh all challenges after a submission (#739)
* Admins can see hidden challenges
* Fix some UI elements, fix loading location hash, set version to be 2.0.0
* Clean up some challenge plugin pages
* Add default for flag type
* Fix Python3 bytes/str issues
* Add in MLC urls and support user mode for oauth
* Fix seeing user graphs when scores are hidden, clean up setup.html, add links to MLC oauth
* Add state parameter support
* Use URLSafeTimedSerializer wrapper for sending token based emails
* setting APPLICATION_ROOT from env var (#732)
* Rearrange config.py and update README
* Updating README
selenium-screenshot-testing
Kevin Chung 2018-11-19 23:16:14 -05:00 committed by GitHub
parent 41933cc367
commit c8031b38c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
378 changed files with 25728 additions and 13502 deletions

View File

@ -2,19 +2,30 @@ language: python
services: services:
- mysql - mysql
- postgresql - postgresql
addons:
apt:
sources:
- mysql-5.7-trusty
packages:
- mysql-server
- mysql-client
env: env:
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd' - TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd'
- TESTING_DATABASE_URL='sqlite://' - TESTING_DATABASE_URL='sqlite://'
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd' - TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
python: python:
- 2.7 - 2.7
- 3.6 - 3.6
before_install:
- sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
- sudo mysql_upgrade -u root -ppassword
- sudo service mysql restart
install: install:
- pip install -r development.txt - pip install -r development.txt
before_script: before_script:
- psql -c 'create database ctfd;' -U postgres - psql -c 'create database ctfd;' -U postgres
script: script:
- pep8 --ignore E501,E712 CTFd/ tests/ - pycodestyle --ignore E501,E712,E402 CTFd/ tests/
- nosetests -v -d --with-randomly - nosetests -v -d --with-randomly
after_success: after_success:
- codecov - codecov

View File

@ -3,27 +3,35 @@ import os
from distutils.version import StrictVersion from distutils.version import StrictVersion
from flask import Flask from flask import Flask
from werkzeug.contrib.fixers import ProxyFix
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
from sqlalchemy.engine.url import make_url from sqlalchemy.engine.url import make_url
from sqlalchemy_utils import database_exists, create_database from sqlalchemy_utils import database_exists, create_database
from six.moves import input from six.moves import input
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp, update_check
from CTFd import utils from CTFd import utils
from CTFd.utils.migrations import migrations, migrate, upgrade, stamp, create_database
from CTFd.utils.sessions import CachingSessionInterface
from CTFd.utils.updates import update_check
from CTFd.utils.initialization import init_request_processors, init_template_filters, init_template_globals
from CTFd.utils.events import socketio
from CTFd.plugins import init_plugins
# Hack to support Unicode in Python 2 properly # Hack to support Unicode in Python 2 properly
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
reload(sys) reload(sys)
sys.setdefaultencoding("utf-8") sys.setdefaultencoding("utf-8")
__version__ = '1.2.0' __version__ = '2.0.0'
class CTFdFlask(Flask): class CTFdFlask(Flask):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Overriden Jinja constructor setting a custom jinja_environment""" """Overriden Jinja constructor setting a custom jinja_environment"""
self.jinja_environment = SandboxedBaseEnvironment self.jinja_environment = SandboxedBaseEnvironment
self.jinja_environment.cache = None
self.session_interface = CachingSessionInterface(key_prefix='session')
Flask.__init__(self, *args, **kwargs) Flask.__init__(self, *args, **kwargs)
def create_jinja_environment(self): def create_jinja_environment(self):
@ -78,7 +86,7 @@ def confirm_upgrade():
def run_upgrade(): def run_upgrade():
migrate_upgrade() upgrade()
utils.set_config('ctf_version', __version__) utils.set_config('ctf_version', __version__)
@ -90,21 +98,9 @@ def create_app(config='CTFd.config.Config'):
theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True)
app.jinja_loader = theme_loader app.jinja_loader = theme_loader
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking
url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) url = create_database()
if url.drivername == 'postgres':
url.drivername = 'postgresql'
if url.drivername.startswith('mysql'):
url.query['charset'] = 'utf8mb4'
# Creates database if the database database does not exist
if not database_exists(url):
if url.drivername.startswith('mysql'):
create_database(url, encoding='utf8mb4')
else:
create_database(url)
# This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in
# This is mostly so we can force MySQL's charset # This is mostly so we can force MySQL's charset
@ -114,32 +110,37 @@ def create_app(config='CTFd.config.Config'):
db.init_app(app) db.init_app(app)
# Register Flask-Migrate # Register Flask-Migrate
migrate.init_app(app, db) migrations.init_app(app, db)
# Alembic sqlite support is lacking so we should just create_all anyway # Alembic sqlite support is lacking so we should just create_all anyway
if url.drivername.startswith('sqlite'): if url.drivername.startswith('sqlite'):
db.create_all() db.create_all()
stamp()
else: else:
if len(db.engine.table_names()) == 0:
# This creates tables instead of db.create_all() # This creates tables instead of db.create_all()
# Allows migrations to happen properly # Allows migrations to happen properly
migrate_upgrade() upgrade()
elif 'alembic_version' not in db.engine.table_names():
# There is no alembic_version because CTFd is from before it had migrations from CTFd.models import ma
# Stamp it to the base migration
if confirm_upgrade(): ma.init_app(app)
migrate_stamp(revision='cb3cfcc47e2f')
run_upgrade()
else:
exit()
app.db = db app.db = db
app.VERSION = __version__ app.VERSION = __version__
from CTFd.cache import cache
cache.init_app(app) cache.init_app(app)
app.cache = cache app.cache = cache
update_check(force=True) # If you have multiple workers you must have a shared cache
socketio.init_app(
app,
message_queue=app.config.get('CACHE_REDIS_URL')
)
if app.config.get('REVERSE_PROXY'):
app.wsgi_app = ProxyFix(app.wsgi_app)
version = utils.get_config('ctf_version') version = utils.get_config('ctf_version')
@ -156,31 +157,39 @@ def create_app(config='CTFd.config.Config'):
if not utils.get_config('ctf_theme'): if not utils.get_config('ctf_theme'):
utils.set_config('ctf_theme', 'core') utils.set_config('ctf_theme', 'core')
update_check(force=True)
init_request_processors(app)
init_template_filters(app)
init_template_globals(app)
# Importing here allows tests to use sensible names (e.g. api instead of api_bp)
from CTFd.views import views from CTFd.views import views
from CTFd.teams import teams
from CTFd.users import users
from CTFd.challenges import challenges from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard from CTFd.scoreboard import scoreboard
from CTFd.auth import auth from CTFd.auth import auth
from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_keys, admin_teams from CTFd.admin import admin
from CTFd.utils import init_utils, init_errors, init_logs from CTFd.api import api
from CTFd.events import events
init_utils(app) from CTFd.errors import page_not_found, forbidden, general_error, gateway_error
init_errors(app)
init_logs(app)
app.register_blueprint(views) app.register_blueprint(views)
app.register_blueprint(teams)
app.register_blueprint(users)
app.register_blueprint(challenges) app.register_blueprint(challenges)
app.register_blueprint(scoreboard) app.register_blueprint(scoreboard)
app.register_blueprint(auth) app.register_blueprint(auth)
app.register_blueprint(api)
app.register_blueprint(events)
app.register_blueprint(admin) app.register_blueprint(admin)
app.register_blueprint(admin_statistics)
app.register_blueprint(admin_challenges)
app.register_blueprint(admin_teams)
app.register_blueprint(admin_scoreboard)
app.register_blueprint(admin_keys)
app.register_blueprint(admin_pages)
from CTFd.plugins import init_plugins app.register_error_handler(404, page_not_found)
app.register_error_handler(403, forbidden)
app.register_error_handler(500, general_error)
app.register_error_handler(502, gateway_error)
init_plugins(app) init_plugins(app)

View File

@ -1,42 +1,74 @@
import hashlib from flask import (
import json current_app as app,
import os render_template,
request,
redirect,
url_for,
Blueprint,
abort,
render_template_string,
send_file
)
from CTFd.utils.decorators import admins_only
from CTFd.utils.user import is_admin
from CTFd.utils.security.auth import logout_user
from CTFd.utils.config import is_setup
from CTFd.utils import (
config as ctf_config,
validators,
uploads,
user as current_user,
get_config,
get_app_config,
set_config,
)
from CTFd.cache import cache, clear_config
from CTFd.utils.exports import (
export_ctf as export_ctf_util,
import_ctf as import_ctf_util
)
from CTFd.models import (
db,
get_class_by_tablename,
Users,
Teams,
Configs,
Submissions,
Solves,
Awards,
Unlocks,
Tracking
)
import datetime import datetime
import os
import six
import csv
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint, \
abort, render_template_string, send_file
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
from sqlalchemy.exc import IntegrityError
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, \
DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.admin.statistics import admin_statistics
from CTFd.admin.challenges import admin_challenges
from CTFd.admin.scoreboard import admin_scoreboard
from CTFd.admin.pages import admin_pages
from CTFd.admin.keys import admin_keys
from CTFd.admin.teams import admin_teams
from CTFd import utils
admin = Blueprint('admin', __name__) admin = Blueprint('admin', __name__)
@admin.route('/admin', methods=['GET']) from CTFd.admin import challenges
def admin_view(): from CTFd.admin import pages
if is_admin(): from CTFd.admin import scoreboard
return redirect(url_for('admin_statistics.admin_stats')) from CTFd.admin import statistics
from CTFd.admin import teams
from CTFd.admin import users
from CTFd.admin import submissions
from CTFd.admin import notifications
@admin.route('/admin', methods=['GET'])
def view():
if is_admin():
return redirect(url_for('admin.statistics'))
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
@admin.route('/admin/plugins/<plugin>', methods=['GET', 'POST']) @admin.route('/admin/plugins/<plugin>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_plugin_config(plugin): def plugin(plugin):
if request.method == 'GET': if request.method == 'GET':
plugins_path = os.path.join(app.root_path, 'plugins') plugins_path = os.path.join(app.root_path, 'plugins')
@ -44,210 +76,116 @@ def admin_plugin_config(plugin):
if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))] if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))]
if plugin in config_html_plugins: if plugin in config_html_plugins:
config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read() config_html = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
return render_template_string(config) return render_template_string(config_html)
abort(404) abort(404)
elif request.method == 'POST': elif request.method == 'POST':
for k, v in request.form.items(): for k, v in request.form.items():
if k == "nonce": if k == "nonce":
continue continue
utils.set_config(k, v) set_config(k, v)
with app.app_context(): with app.app_context():
cache.clear() cache.clear()
return '1' return '1'
@admin.route('/admin/import', methods=['GET', 'POST']) @admin.route('/admin/import', methods=['POST'])
@admins_only @admins_only
def admin_import_ctf(): def import_ctf():
backup = request.files['backup'] backup = request.files['backup']
segments = request.form.get('segments') segments = request.form.get('segments')
errors = [] errors = []
try: try:
if segments: import_ctf_util(backup)
import_ctf(backup, segments=segments.split(','))
else:
import_ctf(backup)
except Exception as e: except Exception as e:
print(e) print(e)
errors.append(type(e).__name__) errors.append(repr(e))
if errors: if errors:
return errors[0], 500 return errors[0], 500
else: else:
return redirect(url_for('admin.admin_config')) return redirect(url_for('admin.config'))
@admin.route('/admin/export', methods=['GET', 'POST']) @admin.route('/admin/export', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_export_ctf(): def export_ctf():
segments = request.args.get('segments') backup = export_ctf_util()
if segments: ctf_name = ctf_config.ctf_name()
backup = export_ctf(segments.split(','))
else:
backup = export_ctf()
ctf_name = utils.ctf_name()
day = datetime.datetime.now().strftime("%Y-%m-%d") day = datetime.datetime.now().strftime("%Y-%m-%d")
full_name = "{}.{}.zip".format(ctf_name, day) full_name = u"{}.{}.zip".format(ctf_name, day)
return send_file(backup, as_attachment=True, attachment_filename=full_name) return send_file(backup, as_attachment=True, attachment_filename=full_name)
@admin.route('/admin/export/csv')
@admins_only
def export_csv():
table = request.args.get('table')
# TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
model = get_class_by_tablename(table)
if model is None:
abort(404)
output = six.StringIO()
writer = csv.writer(output)
header = [column.name for column in model.__mapper__.columns]
writer.writerow(header)
responses = model.query.all()
for curr in responses:
writer.writerow([getattr(curr, column.name) for column in model.__mapper__.columns])
output.seek(0)
return send_file(
output,
as_attachment=True,
cache_timeout=-1,
attachment_filename="{name}-{table}.csv".format(name=ctf_config.ctf_name(), table=table)
)
@admin.route('/admin/config', methods=['GET', 'POST']) @admin.route('/admin/config', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_config(): def config():
if request.method == "POST": # Clear the config cache so that we don't get stale values
start = None clear_config()
end = None
freeze = None
if request.form.get('start'):
start = int(request.form['start'])
if request.form.get('end'):
end = int(request.form['end'])
if request.form.get('freeze'):
freeze = int(request.form['freeze'])
try: database_tables = sorted(db.metadata.tables.keys())
# Set checkbox config values
view_challenges_unregistered = 'view_challenges_unregistered' in request.form
view_scoreboard_if_authed = 'view_scoreboard_if_authed' in request.form
hide_scores = 'hide_scores' in request.form
prevent_registration = 'prevent_registration' in request.form
prevent_name_change = 'prevent_name_change' in request.form
view_after_ctf = 'view_after_ctf' in request.form
verify_emails = 'verify_emails' in request.form
mail_tls = 'mail_tls' in request.form
mail_ssl = 'mail_ssl' in request.form
mail_useauth = 'mail_useauth' in request.form
workshop_mode = 'workshop_mode' in request.form
paused = 'paused' in request.form
finally:
utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
utils.set_config('hide_scores', hide_scores)
utils.set_config('prevent_registration', prevent_registration)
utils.set_config('prevent_name_change', prevent_name_change)
utils.set_config('view_after_ctf', view_after_ctf)
utils.set_config('verify_emails', verify_emails)
utils.set_config('mail_tls', mail_tls)
utils.set_config('mail_ssl', mail_ssl)
utils.set_config('mail_useauth', mail_useauth)
utils.set_config('workshop_mode', workshop_mode)
utils.set_config('paused', paused)
utils.set_config("mail_server", request.form.get('mail_server', None)) configs = Configs.query.all()
utils.set_config("mail_port", request.form.get('mail_port', None)) configs = dict([(c.key, get_config(c.key)) for c in configs])
if request.form.get('mail_useauth', None) and (request.form.get('mail_u', None) or request.form.get('mail_p', None)): themes = ctf_config.get_themes()
if len(request.form.get('mail_u')) > 0: themes.remove(get_config('ctf_theme'))
utils.set_config("mail_username", request.form.get('mail_u', None))
if len(request.form.get('mail_p')) > 0:
utils.set_config("mail_password", request.form.get('mail_p', None))
elif request.form.get('mail_useauth', None) is None:
utils.set_config("mail_username", None)
utils.set_config("mail_password", None)
if request.files.get('ctf_logo_file', None):
ctf_logo = request.files['ctf_logo_file']
file_id, file_loc = utils.upload_file(ctf_logo, None)
utils.set_config("ctf_logo", file_loc)
elif request.form.get('ctf_logo') == '':
utils.set_config("ctf_logo", None)
utils.set_config("ctf_name", request.form.get('ctf_name', None))
utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
utils.set_config('css', request.form.get('css', None))
utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
utils.set_config("freeze", freeze)
db_start = Config.query.filter_by(key='start').first()
db_start.value = start
db_end = Config.query.filter_by(key='end').first()
db_end.value = end
db.session.add(db_start)
db.session.add(db_end)
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin.admin_config'))
# Clear the cache so that we don't get stale values
cache.clear()
ctf_name = utils.get_config('ctf_name')
ctf_logo = utils.get_config('ctf_logo')
ctf_theme = utils.get_config('ctf_theme')
hide_scores = utils.get_config('hide_scores')
css = utils.get_config('css')
mail_server = utils.get_config('mail_server')
mail_port = utils.get_config('mail_port')
mail_username = utils.get_config('mail_username')
mail_password = utils.get_config('mail_password')
mailfrom_addr = utils.get_config('mailfrom_addr')
mg_api_key = utils.get_config('mg_api_key')
mg_base_url = utils.get_config('mg_base_url')
view_after_ctf = utils.get_config('view_after_ctf')
start = utils.get_config('start')
end = utils.get_config('end')
freeze = utils.get_config('freeze')
mail_tls = utils.get_config('mail_tls')
mail_ssl = utils.get_config('mail_ssl')
mail_useauth = utils.get_config('mail_useauth')
view_challenges_unregistered = utils.get_config('view_challenges_unregistered')
view_scoreboard_if_authed = utils.get_config('view_scoreboard_if_authed')
prevent_registration = utils.get_config('prevent_registration')
prevent_name_change = utils.get_config('prevent_name_change')
verify_emails = utils.get_config('verify_emails')
workshop_mode = utils.get_config('workshop_mode')
paused = utils.get_config('paused')
db.session.commit()
db.session.close()
themes = utils.get_themes()
themes.remove(ctf_theme)
return render_template( return render_template(
'admin/config.html', 'admin/config.html',
ctf_name=ctf_name, database_tables=database_tables,
ctf_logo=ctf_logo,
ctf_theme_config=ctf_theme,
css=css,
start=start,
end=end,
freeze=freeze,
hide_scores=hide_scores,
mail_server=mail_server,
mail_port=mail_port,
mail_useauth=mail_useauth,
mail_username=mail_username,
mail_password=mail_password,
mail_tls=mail_tls,
mail_ssl=mail_ssl,
view_challenges_unregistered=view_challenges_unregistered,
view_scoreboard_if_authed=view_scoreboard_if_authed,
prevent_registration=prevent_registration,
mailfrom_addr=mailfrom_addr,
mg_base_url=mg_base_url,
mg_api_key=mg_api_key,
prevent_name_change=prevent_name_change,
verify_emails=verify_emails,
view_after_ctf=view_after_ctf,
themes=themes, themes=themes,
workshop_mode=workshop_mode, **configs
paused=paused
) )
@admin.route('/admin/reset', methods=['GET', 'POST'])
@admins_only
def reset():
if request.method == 'POST':
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
Tracking.query.delete()
Users.query.delete()
Teams.query.delete()
Submissions.query.delete()
Solves.query.delete()
Awards.query.delete()
Unlocks.query.delete()
set_config('setup', False)
db.session.commit()
cache.clear()
logout_user()
db.session.close()
return redirect(url_for('views.setup'))
return render_template('admin/reset.html')

View File

@ -1,302 +1,50 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import current_app as app, render_template, request, redirect, jsonify, render_template_string
from CTFd.utils import admins_only, is_admin, cache from CTFd.utils.decorators import admins_only
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError from CTFd.models import db, Teams, Solves, Awards, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs, Hints, Unlocks
from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
from CTFd.admin import admin
from CTFd import utils from CTFd.utils import config, validators, uploads
import os import os
admin_challenges = Blueprint('admin_challenges', __name__)
@admin.route('/admin/challenges')
@admin_challenges.route('/admin/chal_types', methods=['GET'])
@admins_only @admins_only
def admin_chal_types(): def challenges_listing():
data = {}
for class_id in CHALLENGE_CLASSES:
challenge_class = CHALLENGE_CLASSES.get(class_id)
data[challenge_class.id] = {
'id': challenge_class.id,
'name': challenge_class.name,
'templates': challenge_class.templates,
'scripts': challenge_class.scripts,
}
return jsonify(data)
@admin_challenges.route('/admin/chals', methods=['POST', 'GET'])
@admins_only
def admin_chals():
if request.method == 'POST':
chals = Challenges.query.order_by(Challenges.value).all()
json_data = {'game': []}
for chal in chals:
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
hints = []
for hint in Hints.query.filter_by(chal=chal.id).all():
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
type_class = CHALLENGE_CLASSES.get(chal.type)
type_name = type_class.name if type_class else None
json_data['game'].append({
'id': chal.id,
'name': chal.name,
'value': chal.value,
'description': chal.description,
'category': chal.category,
'files': files,
'tags': tags,
'hints': hints,
'hidden': chal.hidden,
'max_attempts': chal.max_attempts,
'type': chal.type,
'type_name': type_name,
'type_data': {
'id': type_class.id,
'name': type_class.name,
'templates': type_class.templates,
'scripts': type_class.scripts,
}
})
db.session.close()
return jsonify(json_data)
else:
challenges = Challenges.query.all() challenges = Challenges.query.all()
return render_template('admin/challenges.html', challenges=challenges) return render_template('admin/challenges/challenges.html', challenges=challenges)
@admin_challenges.route('/admin/chal/<int:chalid>', methods=['GET', 'POST']) @admin.route('/admin/challenges/<int:challenge_id>')
@admins_only @admins_only
def admin_chal_detail(chalid): def challenges_detail(challenge_id):
chal = Challenges.query.filter_by(id=chalid).first_or_404() challenges = dict(Challenges.query.with_entities(Challenges.id, Challenges.name).all())
chal_class = get_chal_class(chal.type) challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
solves = Solves.query.filter_by(challenge_id=challenge.id).all()
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
challenge_class = get_chal_class(challenge.type)
if request.method == 'POST': static_path = os.path.basename(challenge_class.blueprint.static_url_path)
status, message = chal_class.attempt(chal, request) update_j2 = render_template_string(
if status: challenge_class.blueprint.open_resource(
return jsonify({'status': 1, 'message': message}) os.path.join(static_path, 'update.html')
else: ).read().decode('utf-8'),
return jsonify({'status': 0, 'message': message}) # Python 3
elif request.method == 'GET': challenge=challenge
obj, data = chal_class.read(chal) )
update_script = os.path.join(challenge_class.route, 'update.js')
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()] return render_template(
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()] 'admin/challenges/challenge.html',
hints = [] update_template=update_j2,
for hint in Hints.query.filter_by(chal=chal.id).all(): update_script=update_script,
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint}) challenge=challenge,
challenges=challenges,
data['tags'] = tags solves=solves,
data['files'] = files flags=flags
data['hints'] = hints )
return jsonify(data)
@admin_challenges.route('/admin/chal/<int:chalid>/solves', methods=['GET']) @admin.route('/admin/challenges/new')
@admins_only @admins_only
def admin_chal_solves(chalid): def challenges_new():
response = {'teams': []} return render_template('admin/challenges/new.html')
if utils.hide_scores():
return jsonify(response)
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid).order_by(
Solves.date.asc())
for solve in solves:
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
return jsonify(response)
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_tags(chalid):
if request.method == 'GET':
tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []}
for x in tags:
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
return jsonify(json_data)
elif request.method == 'POST':
newtags = request.form.getlist('tags[]')
for x in newtags:
tag = Tags(chalid, x)
db.session.add(tag)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
@admins_only
def admin_delete_tags(tagid):
if request.method == 'POST':
tag = Tags.query.filter_by(id=tagid).first_or_404()
db.session.delete(tag)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/hints', defaults={'hintid': None}, methods=['POST', 'GET'])
@admin_challenges.route('/admin/hints/<int:hintid>', methods=['GET', 'POST', 'DELETE'])
@admins_only
def admin_hints(hintid):
if hintid:
hint = Hints.query.filter_by(id=hintid).first_or_404()
if request.method == 'POST':
hint.hint = request.form.get('hint')
hint.chal = int(request.form.get('chal'))
hint.cost = int(request.form.get('cost') or 0)
db.session.commit()
elif request.method == 'DELETE':
db.session.delete(hint)
db.session.commit()
db.session.close()
return ('', 204)
json_data = {
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
}
db.session.close()
return jsonify(json_data)
else:
if request.method == 'GET':
hints = Hints.query.all()
json_data = []
for hint in hints:
json_data.append({
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
})
return jsonify({'results': json_data})
elif request.method == 'POST':
hint = request.form.get('hint')
chalid = int(request.form.get('chal'))
cost = int(request.form.get('cost') or 0)
hint_type = request.form.get('type', 0)
hint = Hints(chal=chalid, hint=hint, cost=cost)
db.session.add(hint)
db.session.commit()
json_data = {
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
}
db.session.close()
return jsonify(json_data)
@admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_files(chalid):
if request.method == 'GET':
files = Files.query.filter_by(chal=chalid).all()
json_data = {'files': []}
for x in files:
json_data['files'].append({'id': x.id, 'file': x.location})
return jsonify(json_data)
if request.method == 'POST':
if request.form['method'] == "delete":
utils.delete_file(request.form['file'])
db.session.commit()
db.session.close()
return '1'
elif request.form['method'] == "upload":
files = request.files.getlist('files[]')
for f in files:
utils.upload_file(file=f, chalid=chalid)
db.session.commit()
db.session.close()
return '1'
@admin_challenges.route('/admin/chal/<int:chalid>/<prop>', methods=['GET'])
@admins_only
def admin_get_values(chalid, prop):
challenge = Challenges.query.filter_by(id=chalid).first_or_404()
if prop == 'keys':
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
json_data = {'keys': []}
for x in chal_keys:
key_class = get_key_class(x.type)
json_data['keys'].append({
'id': x.id,
'key': x.flag,
'type': x.type,
'type_name': key_class.name,
'templates': key_class.templates,
})
return jsonify(json_data)
elif prop == 'tags':
tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []}
for x in tags:
json_data['tags'].append({
'id': x.id,
'chal': x.chal,
'tag': x.tag
})
return jsonify(json_data)
elif prop == 'hints':
hints = Hints.query.filter_by(chal=chalid)
json_data = {'hints': []}
for hint in hints:
json_data['hints'].append({
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
})
return jsonify(json_data)
@admin_challenges.route('/admin/chal/new', methods=['GET', 'POST'])
@admins_only
def admin_create_chal():
if request.method == 'POST':
chal_type = request.form['chaltype']
chal_class = get_chal_class(chal_type)
chal_class.create(request)
return redirect(url_for('admin_challenges.admin_chals'))
else:
return render_template('admin/chals/create.html')
@admin_challenges.route('/admin/chal/delete', methods=['POST'])
@admins_only
def admin_delete_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
chal_class = get_chal_class(challenge.type)
chal_class.delete(challenge)
return '1'
@admin_challenges.route('/admin/chal/update', methods=['POST'])
@admins_only
def admin_update_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
chal_class = get_chal_class(challenge.type)
chal_class.update(challenge, request)
return redirect(url_for('admin_challenges.admin_chals'))

View File

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

View File

@ -0,0 +1,13 @@
from flask import render_template
from CTFd.utils.decorators import admins_only
from CTFd.utils.updates import update_check
from CTFd.utils.modes import get_model
from CTFd.models import db, Notifications
from CTFd.admin import admin
@admin.route('/admin/notifications')
@admins_only
def notifications():
notifs = Notifications.query.order_by(Notifications.id.desc()).all()
return render_template('admin/notifications.html', notifications=notifs)

View File

@ -1,132 +1,44 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache, markdown from CTFd.utils.decorators import admins_only
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.models import db, Teams, Solves, Awards, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs
from CTFd.schemas.pages import PageSchema
from CTFd import utils from CTFd.utils import config, validators, markdown, uploads
from CTFd.cache import cache
admin_pages = Blueprint('admin_pages', __name__) from CTFd.admin import admin
@admin_pages.route('/admin/pages', methods=['GET', 'POST']) @admin.route('/admin/pages')
@admins_only @admins_only
def admin_pages_view(): def pages_listing():
page_id = request.args.get('id')
page_op = request.args.get('operation')
if request.method == 'GET' and page_op == 'preview':
page = Pages.query.filter_by(id=page_id).first_or_404()
return render_template('page.html', content=markdown(page.html))
if request.method == 'GET' and page_op == 'create':
return render_template('admin/editor.html')
if page_id and request.method == 'GET':
page = Pages.query.filter_by(id=page_id).first()
return render_template('admin/editor.html', page=page)
if request.method == 'POST':
page_form_id = request.form.get('id')
title = request.form['title']
html = request.form['html']
route = request.form['route'].lstrip('/')
auth_required = 'auth_required' in request.form
if page_op == 'preview':
page = Pages(title, route, html, draft=False)
return render_template('page.html', content=markdown(page.html))
page = Pages.query.filter_by(id=page_form_id).first()
errors = []
if not route:
errors.append('Missing URL route')
if errors:
page = Pages(title, html, route)
return render_template('/admin/editor.html', page=page)
if page:
page.title = title
page.route = route
page.html = html
page.auth_required = auth_required
if page_op == 'publish':
page.draft = False
db.session.commit()
data = {
'result': 'success',
'operation': page_op,
'page': {
'id': page.id,
'route': page.route,
'title': page.title
}
}
db.session.close()
cache.clear()
return jsonify(data)
if page_op == 'publish':
page = Pages(title, route, html, draft=False, auth_required=auth_required)
elif page_op == 'save':
page = Pages(title, route, html, auth_required=auth_required)
db.session.add(page)
db.session.commit()
data = {
'result': 'success',
'operation': page_op,
'page': {
'id': page.id,
'route': page.route,
'title': page.title
}
}
db.session.close()
cache.clear()
return jsonify(data)
pages = Pages.query.all() pages = Pages.query.all()
return render_template('admin/pages.html', pages=pages) return render_template('admin/pages.html', pages=pages)
@admin_pages.route('/admin/pages/delete', methods=['POST']) @admin.route('/admin/pages/new')
@admins_only @admins_only
def delete_page(): def pages_new():
id = request.form['id'] return render_template('admin/editor.html')
page = Pages.query.filter_by(id=id).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return '1'
@admin_pages.route('/admin/media', methods=['GET', 'POST', 'DELETE']) @admin.route('/admin/pages/preview', methods=['POST'])
@admins_only @admins_only
def admin_pages_media(): def pages_preview():
if request.method == 'POST': data = request.form.to_dict()
files = request.files.getlist('files[]') schema = PageSchema()
page = schema.load(data)
return render_template('page.html', content=markdown(page.data.content))
uploaded = []
for f in files: @admin.route('/admin/pages/<int:page_id>')
data = utils.upload_file(file=f, chalid=None) @admins_only
if data: def pages_detail(page_id):
uploaded.append({'id': data[0], 'location': data[1]}) page = Pages.query.filter_by(id=page_id).first_or_404()
return jsonify({'results': uploaded}) page_op = request.args.get('operation')
elif request.method == 'DELETE':
file_ids = request.form.getlist('file_ids[]') if request.method == 'GET' and page_op == 'preview':
for file_id in file_ids: return render_template('page.html', content=markdown(page.content))
utils.delete_file(file_id)
return True if request.method == 'GET' and page_op == 'create':
else: return render_template('admin/editor.html')
files = [{'id': f.id, 'location': f.location} for f in Files.query.filter_by(chal=None).all()]
return jsonify({'results': files}) return render_template('admin/editor.html', page=page)

View File

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

View File

@ -1,79 +1,62 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import render_template
from CTFd.utils import admins_only, is_admin, cache, update_check from CTFd.utils.decorators import admins_only
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.utils.updates import update_check
from CTFd.utils.modes import get_model
from CTFd import utils from CTFd.models import db, Solves, Challenges, Fails, Tracking
from CTFd.admin import admin
admin_statistics = Blueprint('admin_statistics', __name__)
@admin_statistics.route('/admin/graphs/<graph_type>') @admin.route('/admin/statistics', methods=['GET'])
@admins_only @admins_only
def admin_graph(graph_type): def statistics():
if graph_type == 'categories':
categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all()
json_data = {'categories': []}
for category, count in categories:
json_data['categories'].append({'category': category, 'count': count})
return jsonify(json_data)
elif graph_type == "solves":
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
json_data = {}
for chal, count, name in solves:
json_data[name] = count
return jsonify(json_data)
elif graph_type == "solve-percentages":
chals = Challenges.query.add_columns('id', 'name', 'hidden', 'max_attempts').order_by(Challenges.value).all()
teams_with_points = db.session.query(Solves.teamid)\
.join(Teams)\
.filter(Teams.banned == False)\
.group_by(Solves.teamid)\
.count()
percentage_data = []
for x in chals:
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id)\
.filter(Solves.chalid == x[1], Teams.banned == False)\
.count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
percentage_data.append({
'id': x.id,
'name': x.name,
'percentage': percentage,
})
percentage_data = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
json_data = {'percentages': percentage_data}
return jsonify(json_data)
@admin_statistics.route('/admin/statistics', methods=['GET'])
@admins_only
def admin_stats():
update_check() update_check()
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
wrong_count = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count() Model = get_model()
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0] teams_registered = Model.query.count()
ip_count = db.session.query(db.func.count(Tracking.ip.distinct())).first()[0]
wrong_count = Fails.query.join(
Model,
Fails.account_id == Model.id
).filter(
Model.banned == False,
Model.hidden == False
).count()
solve_count = Solves.query.join(
Model,
Solves.account_id == Model.id
).filter(
Model.banned == False,
Model.hidden == False
).count()
challenge_count = Challenges.query.count()
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
solves_sub = db.session.query(
Solves.challenge_id,
db.func.count(Solves.challenge_id).label('solves_cnt')
).join(
Model,
Solves.account_id == Model.id
).filter(
Model.banned == False,
Model.hidden == False
).group_by(
Solves.challenge_id
).subquery()
solves = db.session.query(
solves_sub.columns.challenge_id,
solves_sub.columns.solves_cnt,
Challenges.name
).join(
Challenges,
solves_sub.columns.challenge_id == Challenges.id
).all()
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
.group_by(Solves.chalid).subquery()
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
solve_data = {} solve_data = {}
for chal, count, name in solves: for chal, count, name in solves:
solve_data[name] = count solve_data[name] = count
@ -84,8 +67,6 @@ def admin_stats():
most_solved = max(solve_data, key=solve_data.get) most_solved = max(solve_data, key=solve_data.get)
least_solved = min(solve_data, key=solve_data.get) least_solved = min(solve_data, key=solve_data.get)
db.session.expunge_all()
db.session.commit()
db.session.close() db.session.close()
return render_template( return render_template(
@ -99,49 +80,3 @@ def admin_stats():
most_solved=most_solved, most_solved=most_solved,
least_solved=least_solved least_solved=least_solved
) )
@admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
@admin_statistics.route('/admin/wrong_keys/<int:page>', methods=['GET'])
@admins_only
def admin_wrong_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(WrongKeys.date.desc()) \
.slice(page_start, page_end) \
.all()
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0)
return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
@admin_statistics.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
@admin_statistics.route('/admin/correct_keys/<int:page>', methods=['GET'])
@admins_only
def admin_correct_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag,
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
.join(Challenges) \
.join(Teams) \
.order_by(Solves.date.desc()) \
.slice(page_start, page_end) \
.all()
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0)
return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)

48
CTFd/admin/submissions.py Normal file
View File

@ -0,0 +1,48 @@
from flask import render_template, request
from CTFd.utils.decorators import admins_only
from CTFd.models import Challenges, Submissions
from CTFd.utils.modes import get_model
from CTFd.admin import admin
@admin.route('/admin/submissions', defaults={'submission_type': None})
@admin.route('/admin/submissions/<submission_type>')
@admins_only
def submissions_listing(submission_type):
filters = {}
if submission_type:
filters['type'] = submission_type
curr_page = abs(int(request.args.get('page', 1)))
results_per_page = 50
page_start = results_per_page * (curr_page - 1)
page_end = results_per_page * (curr_page - 1) + results_per_page
sub_count = Submissions.query.filter_by(**filters).count()
page_count = int(sub_count / results_per_page) + (sub_count % results_per_page > 0)
Model = get_model()
submissions = Submissions.query.add_columns(
Submissions.id,
Submissions.type,
Submissions.challenge_id,
Submissions.provided,
Submissions.account_id,
Submissions.date,
Challenges.name.label('challenge_name'),
Model.name.label('team_name')
)\
.filter_by(**filters) \
.join(Challenges)\
.join(Model)\
.order_by(Submissions.date.desc())\
.slice(page_start, page_end)\
.all()
return render_template(
'admin/submissions.html',
submissions=submissions,
page_count=page_count,
curr_page=curr_page,
type=submission_type
)

View File

@ -1,20 +1,17 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache, ratelimit from CTFd.utils.decorators import admins_only, ratelimit
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs
from passlib.hash import bcrypt_sha256 from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_ from sqlalchemy.sql import not_
from CTFd import utils from CTFd import utils
from CTFd.admin import admin
import re
admin_teams = Blueprint('admin_teams', __name__)
@admin_teams.route('/admin/teams', defaults={'page': '1'}) @admin.route('/admin/teams')
@admin_teams.route('/admin/teams/<int:page>')
@admins_only @admins_only
def admin_teams_view(page): def teams_listing():
page = request.args.get('page', 1)
q = request.args.get('q') q = request.args.get('q')
if q: if q:
field = request.args.get('field') field = request.args.get('field')
@ -32,9 +29,7 @@ def admin_teams_view(page):
teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
elif field == 'affiliation': elif field == 'affiliation':
teams = Teams.query.filter(Teams.affiliation.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() teams = Teams.query.filter(Teams.affiliation.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
elif field == 'country': return render_template('admin/teams/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field)
teams = Teams.query.filter(Teams.country.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
return render_template('admin/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field)
page = abs(int(page)) page = abs(int(page))
results_per_page = 50 results_per_page = 50
@ -44,324 +39,52 @@ def admin_teams_view(page):
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all() teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
count = db.session.query(db.func.count(Teams.id)).first()[0] count = db.session.query(db.func.count(Teams.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 0) pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page) return render_template('admin/teams/teams.html', teams=teams, pages=pages, curr_page=page)
@admin_teams.route('/admin/team/new', methods=['POST']) @admin.route('/admin/teams/new')
@admins_only @admins_only
def admin_create_team(): def teams_new():
name = request.form.get('name', None) return render_template('admin/teams/new.html')
password = request.form.get('password', None)
email = request.form.get('email', None)
website = request.form.get('website', None)
affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None)
admin_user = True if request.form.get('admin', None) == 'on' else False
verified = True if request.form.get('verified', None) == 'on' else False
hidden = True if request.form.get('hidden', None) == 'on' else False
errors = []
if not name:
errors.append('The team requires a name')
elif Teams.query.filter(Teams.name == name).first():
errors.append('That name is taken')
if utils.check_email_format(name) is True:
errors.append('Team name cannot be an email address')
if not email:
errors.append('The team requires an email')
elif Teams.query.filter(Teams.email == email).first():
errors.append('That email is taken')
if email:
valid_email = utils.check_email_format(email)
if not valid_email:
errors.append("That email address is invalid")
if not password:
errors.append('The team requires a password')
if website and (website.startswith('http://') or website.startswith('https://')) is False:
errors.append('Websites must start with http:// or https://')
if errors:
db.session.close()
return jsonify({'data': errors})
team = Teams(name, email, password)
team.website = website
team.affiliation = affiliation
team.country = country
team.admin = admin_user
team.verified = verified
team.hidden = hidden
db.session.add(team)
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
@admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST']) @admin.route('/admin/teams/<int:team_id>')
@admins_only @admins_only
def admin_team(teamid): def teams_detail(team_id):
user = Teams.query.filter_by(id=teamid).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
if request.method == 'GET': # Get members
solves = Solves.query.filter_by(teamid=teamid).all() members = team.members
solve_ids = [s.chalid for s in solves] member_ids = [member.id for member in members]
# Get Solves for all members
solves = team.get_solves(admin=True)
fails = team.get_fails(admin=True)
awards = team.get_awards(admin=True)
score = team.get_score(admin=True)
place = team.get_place(admin=True)
# Get missing Challenges for all members
# TODO: How do you mark a missing challenge for a team?
solve_ids = [s.challenge_id for s in solves]
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all() missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
# Get addresses for all members
last_seen = db.func.max(Tracking.date).label('last_seen') last_seen = db.func.max(Tracking.date).label('last_seen')
addrs = db.session.query(Tracking.ip, last_seen) \ addrs = db.session.query(Tracking.ip, last_seen) \
.filter_by(team=teamid) \ .filter(Tracking.user_id.in_(member_ids)) \
.group_by(Tracking.ip) \ .group_by(Tracking.ip) \
.order_by(last_seen.desc()).all() .order_by(last_seen.desc()).all()
wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all()
awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all()
score = user.score(admin=True)
place = user.place(admin=True)
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
place=place, wrong_keys=wrong_keys, awards=awards)
elif request.method == 'POST':
name = request.form.get('name', None)
password = request.form.get('password', None)
email = request.form.get('email', None)
website = request.form.get('website', None)
affiliation = request.form.get('affiliation', None)
country = request.form.get('country', None)
admin_user = True if request.form.get('admin', None) == 'on' else False return render_template(
verified = True if request.form.get('verified', None) == 'on' else False 'admin/teams/team.html',
hidden = True if request.form.get('hidden', None) == 'on' else False team=team,
members=members,
errors = [] score=score,
place=place,
if email: solves=solves,
valid_email = utils.check_email_format(email) fails=fails,
if not valid_email: missing=missing,
errors.append("That email address is invalid") awards=awards,
addrs=addrs,
name_used = Teams.query.filter(Teams.name == name).first() )
if name_used and int(name_used.id) != int(teamid):
errors.append('That name is taken')
if utils.check_email_format(name) is True:
errors.append('Team name cannot be an email address')
email_used = Teams.query.filter(Teams.email == email).first()
if email_used and int(email_used.id) != int(teamid):
errors.append('That email is taken')
if website and (website.startswith('http://') or website.startswith('https://')) is False:
errors.append('Websites must start with http:// or https://')
if errors:
db.session.close()
return jsonify({'data': errors})
else:
user.name = name
if email:
user.email = email
if password:
user.password = bcrypt_sha256.encrypt(password)
user.website = website
user.affiliation = affiliation
user.country = country
user.admin = admin_user
user.verified = verified
user.banned = hidden
db.session.commit()
db.session.close()
return jsonify({'data': ['success']})
@admin_teams.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only
@ratelimit(method="POST", limit=10, interval=60)
def email_user(teamid):
msg = request.form.get('msg', None)
team = Teams.query.filter(Teams.id == teamid).first_or_404()
if msg and team:
result, response = utils.sendmail(team.email, msg)
return jsonify({
'result': result,
'message': response
})
else:
return jsonify({
'result': False,
'message': "Missing information"
})
@admin_teams.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@admins_only
def ban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = True
db.session.commit()
db.session.close()
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
@admin_teams.route('/admin/team/<int:teamid>/unban', methods=['POST'])
@admins_only
def unban(teamid):
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = False
db.session.commit()
db.session.close()
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
@admin_teams.route('/admin/team/<int:teamid>/delete', methods=['POST'])
@admins_only
def delete_team(teamid):
try:
Unlocks.query.filter_by(teamid=teamid).delete()
Awards.query.filter_by(teamid=teamid).delete()
WrongKeys.query.filter_by(teamid=teamid).delete()
Solves.query.filter_by(teamid=teamid).delete()
Tracking.query.filter_by(team=teamid).delete()
Teams.query.filter_by(id=teamid).delete()
db.session.commit()
db.session.close()
except DatabaseError:
return '0'
else:
return '1'
@admin_teams.route('/admin/solves/<teamid>', methods=['GET'])
@admins_only
def admin_solves(teamid="all"):
if teamid == "all":
solves = Solves.query.all()
awards = []
else:
solves = Solves.query.filter_by(teamid=teamid).all()
awards = Awards.query.filter_by(teamid=teamid).all()
db.session.close()
json_data = {'solves': []}
for x in solves:
json_data['solves'].append({
'id': x.id,
'chal': x.chal.name,
'chalid': x.chalid,
'team': x.teamid,
'value': x.chal.value,
'category': x.chal.category,
'time': utils.unix_time(x.date)
})
for award in awards:
json_data['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category or "Award",
'time': utils.unix_time(award.date)
})
json_data['solves'].sort(key=lambda k: k['time'])
return jsonify(json_data)
@admin_teams.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
@admin_teams.route('/admin/fails/<int:teamid>', methods=['GET'])
@admins_only
def admin_fails(teamid):
if teamid == "all":
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
else:
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
json_data = {'fails': str(fails), 'solves': str(solves)}
return jsonify(json_data)
@admin_teams.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
@admins_only
def create_solve(teamid, chalid):
solve = Solves(teamid=teamid, chalid=chalid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN')
db.session.add(solve)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_solve(keyid):
solve = Solves.query.filter_by(id=keyid).first_or_404()
db.session.delete(solve)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_wrong_key(keyid):
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
db.session.delete(wrong_key)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
@admins_only
def delete_award(award_id):
award = Awards.query.filter_by(id=award_id).first_or_404()
db.session.delete(award)
db.session.commit()
db.session.close()
return '1'
@admin_teams.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
@admins_only
def admin_awards(teamid):
awards = Awards.query.filter_by(teamid=teamid).all()
awards_list = []
for award in awards:
awards_list.append({
'id': award.id,
'name': award.name,
'description': award.description,
'date': award.date,
'value': award.value,
'category': award.category,
'icon': award.icon
})
json_data = {'awards': awards_list}
return jsonify(json_data)
@admin_teams.route('/admin/awards/add', methods=['POST'])
@admins_only
def create_award():
try:
teamid = request.form['teamid']
name = request.form.get('name', 'Award')
value = request.form.get('value', 0)
award = Awards(teamid, name, value)
award.description = request.form.get('description')
award.category = request.form.get('category')
db.session.add(award)
db.session.commit()
db.session.close()
return '1'
except Exception as e:
print(e)
return '0'

101
CTFd/admin/users.py Normal file
View File

@ -0,0 +1,101 @@
from flask import render_template, request
from CTFd.utils import get_config
from CTFd.utils.decorators import admins_only, ratelimit
from CTFd.utils.modes import USERS_MODE, TEAMS_MODE
from CTFd.models import db, Users, Challenges, Tracking
from sqlalchemy.sql import not_
from CTFd import utils
from CTFd.admin import admin
@admin.route('/admin/users')
@admins_only
def users_listing():
page = request.args.get('page', 1)
q = request.args.get('q')
if q:
field = request.args.get('field')
users = []
errors = []
if field == 'id':
if q.isnumeric():
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
else:
users = []
errors.append('Your ID search term is not numeric')
elif field == 'name':
users = Users.query.filter(Users.name.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
elif field == 'email':
users = Users.query.filter(Users.email.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
elif field == 'affiliation':
users = Users.query.filter(Users.affiliation.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
return render_template('admin/users/users.html', users=users, pages=None, curr_page=None, q=q, field=field)
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
users = Users.query.order_by(Users.id.asc()).slice(page_start, page_end).all()
count = db.session.query(db.func.count(Users.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('admin/users/users.html', users=users, pages=pages, curr_page=page)
@admin.route('/admin/users/new')
@admins_only
def users_new():
return render_template('admin/users/new.html')
@admin.route('/admin/users/<int:user_id>')
@admins_only
def users_detail(user_id):
# Get user object
user = Users.query.filter_by(id=user_id).first_or_404()
# Get the user's solves
solves = user.get_solves(admin=True)
# Get challenges that the user is missing
if get_config('user_mode') == TEAMS_MODE:
if user.team:
all_solves = user.team.get_solves(admin=True)
else:
all_solves = user.get_solves(admin=True)
else:
all_solves = user.get_solves(admin=True)
solve_ids = [s.challenge_id for s in all_solves]
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
# Get IP addresses that the User has used
last_seen = db.func.max(Tracking.date).label('last_seen')
addrs = db.session.query(Tracking.ip, last_seen) \
.filter_by(user_id=user_id) \
.group_by(Tracking.ip) \
.order_by(last_seen.desc()).all()
# Get Fails
fails = user.get_fails(admin=True)
# Get Awards
awards = user.get_awards(admin=True)
# Get user properties
score = user.get_score(admin=True)
place = user.get_place(admin=True)
return render_template(
'admin/users/user.html',
solves=solves,
user=user,
addrs=addrs,
score=score,
missing=missing,
place=place,
fails=fails,
awards=awards
)

37
CTFd/api/__init__.py Normal file
View File

@ -0,0 +1,37 @@
from flask import Blueprint
from flask_restplus import Api
from CTFd.api.v1.challenges import challenges_namespace
from CTFd.api.v1.teams import teams_namespace
from CTFd.api.v1.users import users_namespace
from CTFd.api.v1.scoreboard import scoreboard_namespace
from CTFd.api.v1.statistics import statistics_namespace
from CTFd.api.v1.submissions import submissions_namespace
from CTFd.api.v1.tags import tags_namespace
from CTFd.api.v1.awards import awards_namespace
from CTFd.api.v1.hints import hints_namespace
from CTFd.api.v1.flags import flags_namespace
from CTFd.api.v1.files import files_namespace
from CTFd.api.v1.config import configs_namespace
from CTFd.api.v1.notifications import notifications_namespace
from CTFd.api.v1.pages import pages_namespace
from CTFd.api.v1.unlocks import unlocks_namespace
api = Blueprint('api', __name__, url_prefix='/api/v1')
CTFd_API_v1 = Api(api, version='v1')
CTFd_API_v1.add_namespace(challenges_namespace, '/challenges')
CTFd_API_v1.add_namespace(tags_namespace, '/tags')
CTFd_API_v1.add_namespace(awards_namespace, '/awards')
CTFd_API_v1.add_namespace(hints_namespace, '/hints')
CTFd_API_v1.add_namespace(flags_namespace, '/flags')
CTFd_API_v1.add_namespace(submissions_namespace, '/submissions')
CTFd_API_v1.add_namespace(scoreboard_namespace, '/scoreboard')
CTFd_API_v1.add_namespace(teams_namespace, '/teams')
CTFd_API_v1.add_namespace(users_namespace, '/users')
CTFd_API_v1.add_namespace(statistics_namespace, '/statistics')
CTFd_API_v1.add_namespace(files_namespace, '/files')
CTFd_API_v1.add_namespace(notifications_namespace, '/notifications')
CTFd_API_v1.add_namespace(configs_namespace, '/configs')
CTFd_API_v1.add_namespace(pages_namespace, '/pages')
CTFd_API_v1.add_namespace(unlocks_namespace, '/unlocks')

71
CTFd/api/v1/awards.py Normal file
View File

@ -0,0 +1,71 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Awards
from CTFd.schemas.awards import AwardSchema
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.dates import ctf_ended
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only
)
from sqlalchemy.sql import or_
awards_namespace = Namespace('awards', description="Endpoint to retrieve Awards")
@awards_namespace.route('')
class AwardList(Resource):
@admins_only
def post(self):
req = request.get_json()
schema = AwardSchema()
response = schema.load(req, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@awards_namespace.route('/<award_id>')
@awards_namespace.param('award_id', 'An Award ID')
class Award(Resource):
@admins_only
def get(self, award_id):
award = Awards.query.filter_by(id=award_id).first_or_404()
response = AwardSchema().dump(award)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, award_id):
award = Awards.query.filter_by(id=award_id).first_or_404()
db.session.delete(award)
db.session.commit()
db.session.close()
return {
'success': True,
}

544
CTFd/api/v1/challenges.py Normal file
View File

@ -0,0 +1,544 @@
from flask import session, request, abort
from flask_restplus import Namespace, Resource
from CTFd.models import (
db,
Challenges,
Unlocks,
HintUnlocks,
Tags,
Hints,
Flags,
Solves,
Submissions,
Fails,
ChallengeFiles as ChallengeFilesModel,
)
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
from CTFd.utils.dates import ctf_ended, isoformat
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only,
authed_only
)
from CTFd.utils.decorators.visibility import (
check_challenge_visibility,
check_score_visibility
)
from CTFd.cache import cache, clear_standings
from CTFd.utils.scores import get_standings
from CTFd.utils.config.visibility import scores_visible, accounts_visible
from CTFd.utils.user import get_current_user, is_admin, authed
from CTFd.utils.modes import get_model
from CTFd.schemas.tags import TagSchema
from CTFd.schemas.hints import HintSchema
from CTFd.schemas.flags import FlagSchema
from CTFd.utils import config
from CTFd.utils import user as current_user
from CTFd.utils.user import get_current_team
from CTFd.utils.user import get_current_user
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.dates import ctf_started, ctf_ended, ctf_paused, ctftime
from CTFd.utils.logging import log
from sqlalchemy.sql import or_, and_, any_
challenges_namespace = Namespace('challenges',
description="Endpoint to retrieve Challenges")
@challenges_namespace.route('')
class ChallengeList(Resource):
@check_challenge_visibility
@during_ctf_time_only
@require_verified_emails
def get(self):
# This can return None (unauth) if visibility is set to public
user = get_current_user()
challenges = Challenges.query.filter(
and_(Challenges.state != 'hidden', Challenges.state != 'locked')
).order_by(Challenges.value).all()
if user:
solve_ids = Solves.query\
.with_entities(Solves.challenge_id)\
.filter_by(account_id=user.account_id)\
.order_by(Solves.challenge_id.asc())\
.all()
solve_ids = set([value for value, in solve_ids])
else:
solve_ids = set()
response = []
tag_schema = TagSchema(view='user', many=True)
for challenge in challenges:
requirements = challenge.requirements
if requirements:
prereqs = set(requirements.get('prerequisites', []))
anonymize = requirements.get('anonymize')
if solve_ids >= prereqs:
pass
else:
if anonymize:
response.append({
'id': challenge.id,
'type': 'hidden',
'name': '???',
'value': 0,
'category': '???',
'tags': [],
'template': '',
'script': ''
})
# Fallthrough to continue
continue
challenge_type = get_chal_class(challenge.type)
response.append({
'id': challenge.id,
'type': challenge_type.name,
'name': challenge.name,
'value': challenge.value,
'category': challenge.category,
'tags': tag_schema.dump(challenge.tags).data,
'template': challenge_type.templates['view'],
'script': challenge_type.scripts['view'],
})
db.session.close()
return {
'success': True,
'data': response
}
@admins_only
def post(self):
data = request.form or request.get_json()
challenge_type = data['type']
challenge_class = get_chal_class(challenge_type)
challenge = challenge_class.create(request)
response = challenge_class.read(challenge)
return {
'success': True,
'data': response
}
@challenges_namespace.route('/types')
class ChallengeTypes(Resource):
@admins_only
def get(self):
response = {}
for class_id in CHALLENGE_CLASSES:
challenge_class = CHALLENGE_CLASSES.get(class_id)
response[challenge_class.id] = {
'id': challenge_class.id,
'name': challenge_class.name,
'templates': challenge_class.templates,
'scripts': challenge_class.scripts,
}
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>')
@challenges_namespace.param('challenge_id', 'A Challenge ID')
class Challenge(Resource):
@check_challenge_visibility
@during_ctf_time_only
@require_verified_emails
def get(self, challenge_id):
if is_admin():
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
else:
chal = Challenges.query.filter(
Challenges.id == challenge_id, and_(Challenges.state != 'hidden', Challenges.state != 'locked')
).first_or_404()
chal_class = get_chal_class(chal.type)
tags = [
tag['value'] for tag in TagSchema(
"user", many=True).dump(
chal.tags).data]
files = [f.location for f in chal.files]
unlocked_hints = set()
hints = []
if authed():
user = get_current_user()
unlocked_hints = set([u.target for u in HintUnlocks.query.filter_by(
type='hints', account_id=user.account_id)])
for hint in Hints.query.filter_by(challenge_id=chal.id).all():
if hint.id in unlocked_hints or ctf_ended():
hints.append({'id': hint.id, 'cost': hint.cost,
'content': hint.content})
else:
hints.append({'id': hint.id, 'cost': hint.cost})
response = chal_class.read(challenge=chal)
Model = get_model()
if scores_visible() is True and accounts_visible() is True:
solves = Solves.query\
.join(Model, Solves.account_id == Model.id)\
.filter(Solves.challenge_id == chal.id, Model.banned == False, Model.hidden == False)\
.count()
response['solves'] = solves
else:
response['solves'] = None
response['files'] = files
response['tags'] = tags
response['hints'] = hints
db.session.close()
return {
'success': True,
'data': response
}
@admins_only
def patch(self, challenge_id):
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
challenge_class = get_chal_class(challenge.type)
challenge = challenge_class.update(challenge, request)
response = challenge_class.read(challenge)
return {
'success': True,
'data': response
}
@admins_only
def delete(self, challenge_id):
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
chal_class = get_chal_class(challenge.type)
chal_class.delete(challenge)
return {
'success': True,
}
@challenges_namespace.route('/attempt')
class ChallengeAttempt(Resource):
@during_ctf_time_only
@require_verified_emails
@authed_only
def post(self):
if request.content_type != 'application/json':
request_data = request.form
else:
request_data = request.get_json()
challenge_id = request_data.get('challenge_id')
if current_user.is_admin():
preview = request.args.get('preview', False)
if preview:
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
chal_class = get_chal_class(challenge.type)
status, message = chal_class.attempt(challenge, request)
return {
'success': True,
'data': {
'status': "correct" if status else "incorrect",
'message': message
}
}
if ctf_paused():
return {
'success': True,
'data': {
'status': "paused",
'message': '{} is paused'.format(config.ctf_name())
}
}, 403
user = get_current_user()
team = get_current_team()
fails = Fails.query.filter_by(
account_id=user.account_id,
challenge_id=challenge_id
).count()
challenge = Challenges.query.filter_by(
id=challenge_id).first_or_404()
if challenge.state == 'hidden':
abort(404)
if challenge.state == 'locked':
abort(403)
requirements = challenge.requirements
if requirements:
solve_ids = Solves.query \
.with_entities(Solves.challenge_id) \
.filter_by(account_id=user.account_id) \
.order_by(Solves.challenge_id.asc()) \
.all()
prereqs = set(requirements.get('prerequisites', []))
if solve_ids >= prereqs:
pass
else:
abort(403)
chal_class = get_chal_class(challenge.type)
# Anti-bruteforce / submitting Flags too quickly
if current_user.get_wrong_submissions_per_minute(session['id']) > 10:
if ctftime():
chal_class.fail(
user=user,
team=team,
challenge=challenge,
request=request
)
log(
'submissions',
"[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]",
submission=request_data['submission'].encode('utf-8'),
kpm=current_user.get_wrong_submissions_per_minute(session['id'])
)
# Submitting too fast
return {
'success': True,
'data': {
'status': "ratelimited",
'message': "You're submitting flags too fast. Slow down."
}
}, 429
solves = Solves.query.filter_by(
account_id=user.account_id,
challenge_id=challenge_id
).first()
# Challenge not solved yet
if not solves:
# Hit max attempts
max_tries = challenge.max_attempts
if max_tries and fails >= max_tries > 0:
return {
'success': True,
'data': {
'status': "incorrect",
'message': "You have 0 tries remaining"
}
}, 403
status, message = chal_class.attempt(challenge, request)
if status: # The challenge plugin says the input is right
if ctftime() or current_user.is_admin():
chal_class.solve(
user=user,
team=team,
challenge=challenge,
request=request
)
clear_standings()
log(
'submissions',
"[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]",
submission=request_data['submission'].encode('utf-8'),
kpm=current_user.get_wrong_submissions_per_minute(
session['id'])
)
return {
'success': True,
'data': {
'status': "correct",
'message': message
}
}
else: # The challenge plugin says the input is wrong
if ctftime() or current_user.is_admin():
chal_class.fail(
user=user,
team=team,
challenge=challenge,
request=request
)
clear_standings()
log(
'submissions',
"[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]",
submission=request_data['submission'].encode('utf-8'),
kpm=current_user.get_wrong_submissions_per_minute(
session['id'])
)
if max_tries:
# Off by one since fails has changed since it was gotten
attempts_left = max_tries - fails - 1
tries_str = 'tries'
if attempts_left == 1:
tries_str = 'try'
# Add a punctuation mark if there isn't one
if message[-1] not in '!().;?[]{}':
message = message + '.'
return {
'success': True,
'data': {
'status': "incorrect",
'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str)
}
}
else:
return {
'success': True,
'data': {
'status': "incorrect",
'message': message
}
}
# Challenge already solved
else:
log(
'submissions',
"[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]",
submission=request_data['submission'].encode('utf-8'),
kpm=current_user.get_wrong_submissions_per_minute(
user.account_id
)
)
return {
'success': True,
'data': {
'status': "already_solved",
'message': 'You already solved this'
}
}
@challenges_namespace.route('/<challenge_id>/solves')
@challenges_namespace.param('id', 'A Challenge ID')
class ChallengeSolves(Resource):
@check_challenge_visibility
@check_score_visibility
@during_ctf_time_only
@require_verified_emails
def get(self, challenge_id):
response = []
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
# TODO: Need a generic challenge visibility call.
# However, it should be stated that a solve on a gated challenge is not considered private.
if challenge.state == 'hidden' and is_admin() is False:
abort(404)
Model = get_model()
solves = Solves.query.join(Model, Solves.account_id == Model.id)\
.filter(Solves.challenge_id == challenge_id, Model.banned == False, Model.hidden == False)\
.order_by(Solves.date.asc())
for solve in solves:
response.append({
'account_id': solve.account_id,
'name': solve.account.name,
'date': isoformat(solve.date)
})
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>/files')
@challenges_namespace.param('id', 'A Challenge ID')
class ChallengeFiles(Resource):
@admins_only
def get(self, challenge_id):
response = []
challenge_files = ChallengeFilesModel.query.filter_by(
challenge_id=challenge_id).all()
for f in challenge_files:
response.append({
'id': f.id,
'type': f.type,
'location': f.location
})
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>/tags')
@challenges_namespace.param('id', 'A Challenge ID')
class ChallengeTags(Resource):
@admins_only
def get(self, challenge_id):
response = []
tags = Tags.query.filter_by(challenge_id=challenge_id).all()
for t in tags:
response.append({
'id': t.id,
'challenge_id': t.challenge_id,
'value': t.value
})
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>/hints')
@challenges_namespace.param('id', 'A Challenge ID')
class ChallengeHints(Resource):
@admins_only
def get(self, challenge_id):
hints = Hints.query.filter_by(challenge_id=challenge_id).all()
schema = HintSchema(many=True)
response = schema.dump(hints)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@challenges_namespace.route('/<challenge_id>/flags')
@challenges_namespace.param('id', 'A Challenge ID')
class ChallengeFlags(Resource):
@admins_only
def get(self, challenge_id):
flags = Flags.query.filter_by(challenge_id=challenge_id).all()
schema = FlagSchema(many=True)
response = schema.dump(flags)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}

125
CTFd/api/v1/config.py Normal file
View File

@ -0,0 +1,125 @@
from flask import request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Configs
from CTFd.schemas.config import ConfigSchema
from CTFd.utils.decorators import (
admins_only
)
from CTFd.utils import get_config, set_config
from CTFd.cache import clear_config, clear_standings
configs_namespace = Namespace('configs', description="Endpoint to retrieve Configs")
@configs_namespace.route('')
class ConfigList(Resource):
@admins_only
def get(self):
configs = Configs.query.all()
schema = ConfigSchema(many=True)
response = schema.dump(configs)
if response.errors:
return {
'success': False,
'errors': response.errors,
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = ConfigSchema()
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
clear_config()
clear_standings()
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self):
req = request.get_json()
for key, value in req.items():
set_config(key=key, value=value)
clear_config()
clear_standings()
return {
'success': True
}
@configs_namespace.route('/<config_key>')
class Config(Resource):
@admins_only
def get(self, config_key):
return {
'success': True,
'data': get_config(config_key)
}
@admins_only
def patch(self, config_key):
config = Configs.query.filter_by(key=config_key).first()
data = request.get_json()
if config:
schema = ConfigSchema(instance=config, partial=True)
response = schema.load(data)
else:
schema = ConfigSchema()
data['key'] = config_key
response = schema.load(data)
db.session.add(response.data)
if response.errors:
return response.errors, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
clear_config()
clear_standings()
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, config_key):
config = Configs.query.filter_by(key=config_key).first_or_404()
db.session.delete(config)
db.session.commit()
db.session.close()
clear_config()
clear_standings()
return {
'success': True,
}

89
CTFd/api/v1/files.py Normal file
View File

@ -0,0 +1,89 @@
from flask import request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Files, ChallengeFiles, PageFiles
from CTFd.schemas.files import FileSchema
from CTFd.utils import uploads
from CTFd.utils.decorators import (
admins_only
)
files_namespace = Namespace('files', description="Endpoint to retrieve Files")
@files_namespace.route('')
class FilesList(Resource):
@admins_only
def get(self):
file_type = request.args.get('type')
files = Files.query.filter_by(type=file_type).all()
schema = FileSchema(many=True)
response = schema.dump(files)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
files = request.files.getlist('file')
# challenge_id
# page_id
objs = []
for f in files:
# uploads.upload_file(file=f, chalid=req.get('challenge'))
obj = uploads.upload_file(file=f, **request.form.to_dict())
objs.append(obj)
schema = FileSchema(many=True)
response = schema.dump(objs)
if response.errors:
return {
'success': False,
'errors': response.errorss
}, 400
return {
'success': True,
'data': response.data
}
@files_namespace.route('/<file_id>')
class FilesDetail(Resource):
@admins_only
def get(self, file_id):
f = Files.query.filter_by(id=file_id).first_or_404()
schema = FileSchema()
response = schema.dump(f)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, file_id):
f = Files.query.filter_by(id=file_id).first_or_404()
db.session.delete(f)
db.session.commit()
db.session.close()
return {
'success': True,
}

143
CTFd/api/v1/flags.py Normal file
View File

@ -0,0 +1,143 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Flags
from CTFd.schemas.flags import FlagSchema
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
from CTFd.utils.dates import ctf_ended
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only
)
from sqlalchemy.sql import or_
flags_namespace = Namespace('flags', description="Endpoint to retrieve Flags")
@flags_namespace.route('')
class FlagList(Resource):
@admins_only
def get(self):
flags = Flags.query.all()
schema = FlagSchema(many=True)
response = schema.dump(flags)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = FlagSchema()
response = schema.load(req, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@flags_namespace.route('/types', defaults={'type_name': None})
@flags_namespace.route('/types/<type_name>')
class FlagTypes(Resource):
@admins_only
def get(self, type_name):
if type_name:
flag_class = get_flag_class(type_name)
response = {
'name': flag_class.name,
'templates': flag_class.templates
}
return {
'success': True,
'data': response
}
else:
response = {}
for class_id in FLAG_CLASSES:
flag_class = FLAG_CLASSES.get(class_id)
response[class_id] = {
'name': flag_class.name,
'templates': flag_class.templates,
}
return {
'success': True,
'data': response
}
@flags_namespace.route('/<flag_id>')
class Flag(Resource):
@admins_only
def get(self, flag_id):
flag = Flags.query.filter_by(id=flag_id).first_or_404()
schema = FlagSchema()
response = schema.dump(flag)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
response.data['templates'] = get_flag_class(flag.type).templates
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, flag_id):
flag = Flags.query.filter_by(id=flag_id).first_or_404()
db.session.delete(flag)
db.session.commit()
db.session.close()
return {
'success': True
}
@admins_only
def patch(self, flag_id):
flag = Flags.query.filter_by(id=flag_id).first_or_404()
schema = FlagSchema()
req = request.get_json()
response = schema.load(req, session=db.session, instance=flag, partial=True)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}

124
CTFd/api/v1/hints.py Normal file
View File

@ -0,0 +1,124 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Hints, HintUnlocks
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.dates import ctf_ended
from CTFd.utils.user import get_current_user
from CTFd.schemas.hints import HintSchema
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only,
authed_only
)
from sqlalchemy.sql import or_
hints_namespace = Namespace('hints', description="Endpoint to retrieve Hints")
@hints_namespace.route('')
class HintList(Resource):
@admins_only
def get(self):
hints = Hints.query.all()
response = HintSchema(many=True).dump(hints)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = HintSchema('admin')
response = schema.load(req, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
return {
'success': True,
'data': response.data
}
@hints_namespace.route('/<hint_id>')
class Hint(Resource):
@during_ctf_time_only
@authed_only
def get(self, hint_id):
user = get_current_user()
hint = Hints.query.filter_by(id=hint_id).first_or_404()
view = 'unlocked'
if hint.cost:
view = 'locked'
unlocked = HintUnlocks.query.filter_by(
account_id=user.account_id,
target=hint.id
).first()
if unlocked:
view = 'unlocked'
response = HintSchema(view=view).dump(hint)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self, hint_id):
hint = Hints.query.filter_by(id=hint_id).first_or_404()
req = request.get_json()
schema = HintSchema()
response = schema.load(req, instance=hint, partial=True, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, hint_id):
hint = Hints.query.filter_by(id=hint_id).first_or_404()
db.session.delete(hint)
db.session.commit()
db.session.close()
return {
'success': True
}

View File

@ -0,0 +1,52 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Notifications
from CTFd.schemas.notifications import NotificationSchema
from CTFd.utils.events import socketio
from CTFd.utils.decorators import (
admins_only
)
notifications_namespace = Namespace('notifications', description="Endpoint to retrieve Notifications")
@notifications_namespace.route('')
class NotificantionList(Resource):
def get(self):
notifications = Notifications.query.all()
schema = NotificationSchema(many=True)
result = schema.dump(notifications)
if result.errors:
return {
'success': False,
'errors': result.errors
}, 400
return {
'success': True,
'data': result.data
}
@admins_only
def post(self):
req = request.get_json()
schema = NotificationSchema()
result = schema.load(req)
if result.errors:
return {
'success': False,
'errors': result.errors
}, 400
db.session.add(result.data)
db.session.commit()
response = schema.dump(result.data)
socketio.emit('notification', response.data, broadcast=True)
return {
'success': True,
'data': response.data
}

108
CTFd/api/v1/pages.py Normal file
View File

@ -0,0 +1,108 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Pages
from CTFd.schemas.pages import PageSchema
from CTFd.utils.events import socketio
from CTFd.utils.decorators import (
admins_only
)
pages_namespace = Namespace('pages', description="Endpoint to retrieve Pages")
@pages_namespace.route('')
class PageList(Resource):
@admins_only
def get(self):
pages = Pages.query.all()
schema = PageSchema(exclude=['content'], many=True)
response = schema.dump(pages)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = PageSchema()
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@pages_namespace.route('/<page_id>')
class PageDetail(Resource):
@admins_only
def get(self, page_id):
page = Pages.query.filter_by(id=page_id).first_or_404()
schema = PageSchema()
response = schema.dump(page)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self, page_id):
page = Pages.query.filter_by(id=page_id).first_or_404()
req = request.get_json()
schema = PageSchema(partial=True)
response = schema.load(req, instance=page, partial=True)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, page_id):
page = Pages.query.filter_by(id=page_id).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()
return {
'success': True
}

114
CTFd/api/v1/scoreboard.py Normal file
View File

@ -0,0 +1,114 @@
from flask_restplus import Namespace, Resource
from CTFd.models import Solves, Awards, Teams
from CTFd.utils.scores import get_standings
from CTFd.utils import get_config
from CTFd.utils.modes import TEAMS_MODE
from CTFd.utils.dates import unix_time_to_utc, unix_time, isoformat
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
scoreboard_namespace = Namespace('scoreboard', description="Endpoint to retrieve scores")
@scoreboard_namespace.route('')
class ScoreboardList(Resource):
@check_account_visibility
@check_score_visibility
def get(self):
standings = get_standings()
response = []
mode = get_config('user_mode')
if mode == TEAMS_MODE:
team_ids = []
for team in standings:
team_ids.append(team.account_id)
teams = Teams.query.filter(Teams.id.in_(team_ids)).all()
teams = [next(t for t in teams if t.id == id) for id in team_ids]
for i, x in enumerate(standings):
entry = {
'pos': i + 1,
'account_id': x.account_id,
'oauth_id': x.oauth_id,
'name': x.name,
'score': int(x.score)
}
if mode == TEAMS_MODE:
members = []
for member in teams[i].members:
members.append({
'id': member.id,
'oauth_id': member.oauth_id,
'name': member.name,
'score': int(member.score)
})
entry['members'] = members
response.append(
entry
)
return {
'success': True,
'data': response
}
@scoreboard_namespace.route('/top/<count>')
@scoreboard_namespace.param('count', 'How many top teams to return')
class ScoreboardDetail(Resource):
@check_account_visibility
@check_score_visibility
def get(self, count):
response = {}
standings = get_standings(count=count)
team_ids = [team.account_id for team in standings]
solves = Solves.query.filter(Solves.account_id.in_(team_ids))
awards = Awards.query.filter(Awards.account_id.in_(team_ids))
freeze = get_config('freeze')
if freeze:
solves = solves.filter(Solves.date < unix_time_to_utc(freeze))
awards = awards.filter(Awards.date < unix_time_to_utc(freeze))
solves = solves.all()
awards = awards.all()
for i, team in enumerate(team_ids):
response[i + 1] = {
'id': standings[i].account_id,
'name': standings[i].name,
'solves': []
}
for solve in solves:
if solve.account_id == team:
response[i + 1]['solves'].append({
'challenge_id': solve.challenge_id,
'account_id': solve.account_id,
'team_id': solve.team_id,
'user_id': solve.user_id,
'value': solve.challenge.value,
'date': isoformat(solve.date)
})
for award in awards:
if award.account_id == team:
response[i + 1]['solves'].append({
'challenge_id': None,
'account_id': award.account_id,
'team_id': award.team_id,
'user_id': award.user_id,
'value': award.value,
'date': isoformat(award.date)
})
response[i + 1]['solves'] = sorted(response[i + 1]['solves'], key=lambda k: k['date'])
return {
'success': True,
'data': response
}

View File

@ -0,0 +1,13 @@
from flask_restplus import Namespace, Resource
from CTFd.models import db, Challenges, Unlocks, Hints
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.dates import ctf_ended
from CTFd.utils.decorators import during_ctf_time_only, require_verified_emails
from sqlalchemy.sql import or_
statistics_namespace = Namespace('statistics', description="Endpoint to retrieve Statistics")
from CTFd.api.v1.statistics import challenges
from CTFd.api.v1.statistics import teams
from CTFd.api.v1.statistics import users
from CTFd.api.v1.statistics import submissions

View File

@ -0,0 +1,128 @@
from flask_restplus import Namespace, Resource
from CTFd.models import db, Challenges, Solves, Teams, Users
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils import config
from CTFd.utils.modes import get_model
from CTFd.utils.dates import ctf_ended
from CTFd.utils.decorators import (
admins_only,
during_ctf_time_only,
require_verified_emails
)
from CTFd.api.v1.statistics import statistics_namespace
from sqlalchemy import func
from sqlalchemy.sql import or_
@statistics_namespace.route('/challenges/<column>')
class ChallengePropertyCounts(Resource):
@admins_only
def get(self, column):
if column in Challenges.__table__.columns.keys():
prop = getattr(Challenges, column)
data = Challenges.query\
.with_entities(prop, func.count(prop))\
.group_by(prop)\
.all()
return {
'success': True,
'data': dict(data)
}
else:
response = {
'message': 'That could not be found'
}, 404
return response
@statistics_namespace.route('/challenges/solves')
class ChallengeSolveStatistics(Resource):
@admins_only
def get(self):
chals = Challenges.query \
.filter(or_(Challenges.state != 'hidden', Challenges.state != 'locked')) \
.order_by(Challenges.value) \
.all()
Model = get_model()
solves_sub = db.session.query(
Solves.challenge_id,
db.func.count(Solves.challenge_id).label('solves')
) \
.join(Model, Solves.account_id == Model.id) \
.filter(Model.banned == False, Model.hidden == False) \
.group_by(Solves.challenge_id).subquery()
solves = db.session.query(
solves_sub.columns.challenge_id,
solves_sub.columns.solves,
Challenges.name
) \
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id).all()
response = []
has_solves = []
for challenge_id, count, name in solves:
challenge = {
'id': challenge_id,
'name': name,
'solves': count,
}
response.append(challenge)
has_solves.append(challenge_id)
for c in chals:
if c.id not in has_solves:
challenge = {
'id': c.id,
'name': c.name,
'solves': 0,
}
response.append(challenge)
db.session.close()
return {
'success': True,
'data': response
}
@statistics_namespace.route('/challenges/solves/percentages')
class ChallengeSolvePercentages(Resource):
@admins_only
def get(self):
challenges = Challenges.query\
.add_columns('id', 'name', 'state', 'max_attempts')\
.order_by(Challenges.value).all()
Model = get_model()
teams_with_points = db.session.query(Solves.account_id) \
.join(Model) \
.filter(Model.banned == False, Model.hidden == False) \
.group_by(Solves.account_id) \
.count()
percentage_data = []
for challenge in challenges:
solve_count = Solves.query.join(Model, Solves.account_id == Model.id) \
.filter(Solves.challenge_id == challenge.id, Model.banned == False, Model.hidden == False) \
.count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
percentage_data.append({
'id': challenge.id,
'name': challenge.name,
'percentage': percentage,
})
response = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
return {
'success': True,
'data': response
}

View File

@ -0,0 +1,30 @@
from flask_restplus import Namespace, Resource
from CTFd.models import db, Submissions
from CTFd.utils.decorators import (
admins_only,
)
from CTFd.plugins.challenges import get_chal_class
from CTFd.api.v1.statistics import statistics_namespace
from sqlalchemy import func
@statistics_namespace.route('/submissions/<column>')
class SubmissionPropertyCounts(Resource):
@admins_only
def get(self, column):
if column in Submissions.__table__.columns.keys():
prop = getattr(Submissions, column)
data = Submissions.query \
.with_entities(prop, func.count(prop)) \
.group_by(prop) \
.all()
return {
'success': True,
'data': dict(data)
}
else:
response = {
'success': False,
'errors': 'That could not be found'
}, 404
return response

View File

@ -0,0 +1,21 @@
from flask_restplus import Namespace, Resource
from CTFd.models import db, Teams
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.decorators import (
admins_only,
)
from CTFd.api.v1.statistics import statistics_namespace
@statistics_namespace.route('/teams')
class TeamStatistics(Resource):
@admins_only
def get(self):
registered = Teams.query.count()
data = {
'registered': registered,
}
return {
'success': True,
'data': data
}

View File

@ -0,0 +1,42 @@
from flask_restplus import Namespace, Resource
from CTFd.models import db, Users
from CTFd.plugins.challenges import get_chal_class
from CTFd.api.v1.statistics import statistics_namespace
from CTFd.utils.decorators import admins_only
from sqlalchemy import func
@statistics_namespace.route('/users')
class UserStatistics(Resource):
def get(self):
registered = Users.query.count()
confirmed = Users.query.filter_by(verified=True).count()
data = {
'registered': registered,
'confirmed': confirmed
}
return {
'success': True,
'data': data
}
@statistics_namespace.route('/users/<column>')
class UserPropertyCounts(Resource):
@admins_only
def get(self, column):
if column in Users.__table__.columns.keys():
prop = getattr(Users, column)
data = Users.query \
.with_entities(prop, func.count(prop)) \
.group_by(prop) \
.all()
return {
'success': True,
'data': dict(data)
}
else:
return {
'success': False,
'message': 'That could not be found'
}, 404

View File

@ -0,0 +1,96 @@
from flask import session, jsonify, request, abort
from flask_restplus import Namespace, Resource, reqparse
from CTFd.cache import cache, clear_standings
from CTFd.utils.scores import get_standings
from CTFd.models import db, Submissions
from CTFd.schemas.submissions import SubmissionSchema
from CTFd.utils.decorators import (
admins_only,
)
submissions_namespace = Namespace('submissions', description="Endpoint to retrieve Submission")
@submissions_namespace.route('')
class SubmissionsList(Resource):
@admins_only
def get(self):
args = request.args.to_dict()
schema = SubmissionSchema(many=True)
if args:
submissions = Submissions.query.filter_by(**args).all()
else:
submissions = Submissions.query.all()
response = schema.dump(submissions)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
Model = Submissions.get_child(type=req.get('type'))
schema = SubmissionSchema(instance=Model())
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
# Delete standings cache
clear_standings()
return {
'success': True,
'data': response.data
}
@submissions_namespace.route('/<submission_id>')
@submissions_namespace.param('submission_id', 'A Submission ID')
class Submission(Resource):
@admins_only
def get(self, submission_id):
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
schema = SubmissionSchema()
response = schema.dump(submission)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, submission_id):
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
db.session.delete(submission)
db.session.commit()
db.session.close()
return {
'success': True
}

112
CTFd/api/v1/tags.py Normal file
View File

@ -0,0 +1,112 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, Tags
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils.dates import ctf_ended
from CTFd.schemas.tags import TagSchema
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only
)
tags_namespace = Namespace('tags', description="Endpoint to retrieve Tags")
@tags_namespace.route('')
class TagList(Resource):
@admins_only
def get(self):
# TODO: Filter by challenge_id
tags = Tags.query.all()
schema = TagSchema(many=True)
response = schema.dump(tags)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = TagSchema()
response = schema.load(req, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@tags_namespace.route('/<tag_id>')
@tags_namespace.param('tag_id', 'A Tag ID')
class Tag(Resource):
@admins_only
def get(self, tag_id):
tag = Tags.query.filter_by(id=tag_id).first_or_404()
response = TagSchema().dump(tag)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self, tag_id):
tag = Tags.query.filter_by(id=tag_id).first_or_404()
schema = TagSchema()
req = request.get_json()
response = schema.load(req, session=db.session, instance=tag)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, tag_id):
tag = Tags.query.filter_by(id=tag_id).first_or_404()
db.session.delete(tag)
db.session.commit()
db.session.close()
return {
'success': True
}

293
CTFd/api/v1/teams.py Normal file
View File

@ -0,0 +1,293 @@
from flask import session, request, abort
from flask_restplus import Namespace, Resource
from CTFd.models import db, Teams, Solves, Awards, Fails
from CTFd.schemas.teams import TeamSchema
from CTFd.schemas.submissions import SubmissionSchema
from CTFd.schemas.awards import AwardSchema
from CTFd.cache import cache, clear_standings
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
from CTFd.utils.config.visibility import (
accounts_visible,
scores_visible
)
from CTFd.utils.user import (
get_current_team,
is_admin,
authed
)
from CTFd.utils.decorators import (
authed_only,
admins_only,
)
import copy
teams_namespace = Namespace('teams', description="Endpoint to retrieve Teams")
@teams_namespace.route('')
class TeamList(Resource):
@check_account_visibility
def get(self):
teams = Teams.query.filter_by(banned=False)
view = copy.deepcopy(TeamSchema.views.get(
session.get('type', 'user')
))
view.remove('members')
response = TeamSchema(view=view, many=True).dump(teams)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
view = TeamSchema.views.get(session.get('type', 'self'))
schema = TeamSchema(view=view)
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
response = schema.dump(response.data)
db.session.close()
clear_standings()
return {
'success': True,
'data': response.data
}
@teams_namespace.route('/<team_id>')
@teams_namespace.param('team_id', "Team ID")
class TeamPublic(Resource):
@check_account_visibility
def get(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404()
view = TeamSchema.views.get(session.get('type', 'user'))
schema = TeamSchema(view=view)
response = schema.dump(team)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404()
data = request.get_json()
data['id'] = team_id
schema = TeamSchema(view='admin', instance=team, partial=True)
response = schema.load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
response = schema.dump(response.data)
db.session.commit()
db.session.close()
clear_standings()
return {
'success': True,
'data': response.data
}
@admins_only
def delete(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404()
for member in team.members:
member.team_id = None
db.session.delete(team)
db.session.commit()
db.session.close()
clear_standings()
return {
'success': True,
}
@teams_namespace.route('/me')
@teams_namespace.param('team_id', "Current Team")
class TeamPrivate(Resource):
@authed_only
def get(self):
team = get_current_team()
response = TeamSchema(view='self').dump(team)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@authed_only
def patch(self):
team = get_current_team()
data = request.get_json()
response = TeamSchema(view='self', instance=team, partial=True).load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = TeamSchema('self').dump(response.data)
db.session.close()
return {
'success': True,
'data': response.data
}
@teams_namespace.route('/<team_id>/solves')
@teams_namespace.param('team_id', "Team ID or 'me'")
class TeamSolves(Resource):
def get(self, team_id):
if team_id == 'me':
if not authed():
abort(403)
team = get_current_team()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
team = Teams.query.filter_by(id=team_id).first_or_404()
solves = team.get_solves(
admin=is_admin()
)
view = 'admin' if is_admin() else 'user'
schema = SubmissionSchema(view=view, many=True)
response = schema.dump(solves)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@teams_namespace.route('/<team_id>/fails')
@teams_namespace.param('team_id', "Team ID or 'me'")
class TeamFails(Resource):
def get(self, team_id):
if team_id == 'me':
if not authed():
abort(403)
team = get_current_team()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
team = Teams.query.filter_by(id=team_id).first_or_404()
fails = team.get_fails(
admin=is_admin()
)
view = 'admin' if is_admin() else 'user'
schema = SubmissionSchema(view=view, many=True)
response = schema.dump(fails)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
if is_admin():
data = response.data
else:
data = []
count = len(response.data)
return {
'success': True,
'data': data,
'meta': {
'count': count
}
}
@teams_namespace.route('/<team_id>/awards')
@teams_namespace.param('team_id', "Team ID or 'me'")
class TeamAwards(Resource):
def get(self, team_id):
if team_id == 'me':
if not authed():
abort(403)
team = get_current_team()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
team = Teams.query.filter_by(id=team_id).first_or_404()
awards = team.get_awards(
admin=is_admin()
)
schema = AwardSchema(many=True)
response = schema.dump(awards)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}

88
CTFd/api/v1/unlocks.py Normal file
View File

@ -0,0 +1,88 @@
from flask import session, request
from flask_restplus import Namespace, Resource
from CTFd.models import db, get_class_by_tablename, Unlocks, Awards
from CTFd.utils.user import get_current_user
from CTFd.schemas.unlocks import UnlockSchema
from CTFd.schemas.awards import AwardSchema
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
admins_only,
authed_only
)
from sqlalchemy.sql import or_
unlocks_namespace = Namespace('unlocks', description="Endpoint to retrieve Unlocks")
@unlocks_namespace.route('')
class UnlockList(Resource):
@admins_only
def get(self):
hints = Unlocks.query.all()
schema = UnlockSchema()
response = schema.dump(hints)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@during_ctf_time_only
@require_verified_emails
@authed_only
def post(self):
req = request.get_json()
user = get_current_user()
req['user_id'] = user.id
req['team_id'] = user.team_id
Model = get_class_by_tablename(req['type'])
target = Model.query.filter_by(id=req['target']).first_or_404()
if target.cost > user.score:
return {
'success': False,
'errors': {
'score': 'You do not have enough points to unlock this hint'
}
}, 400
schema = UnlockSchema()
response = schema.load(req, session=db.session)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
award_schema = AwardSchema()
award = {
'user_id': user.id,
'team_id': user.team_id,
'name': target.name,
'description': target.description,
'value': (-target.cost),
'category': target.category
}
award = award_schema.load(award)
db.session.add(award.data)
db.session.commit()
response = schema.dump(response.data)
return {
'success': True,
'data': response.data
}

280
CTFd/api/v1/users.py Normal file
View File

@ -0,0 +1,280 @@
from flask import request, abort
from flask_restplus import Namespace, Resource
from CTFd.models import db, Users, Solves, Awards, Fails, Tracking, Unlocks, Submissions, Notifications
from CTFd.utils.decorators import (
authed_only,
admins_only,
authed
)
from CTFd.cache import cache, clear_standings
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
from CTFd.utils.config.visibility import (
accounts_visible,
challenges_visible,
registration_visible,
scores_visible
)
from CTFd.schemas.submissions import SubmissionSchema
from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.users import UserSchema
users_namespace = Namespace('users', description="Endpoint to retrieve Users")
@users_namespace.route('')
class UserList(Resource):
@check_account_visibility
def get(self):
users = Users.query.filter_by(banned=False)
response = UserSchema(view='user', many=True).dump(users)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@admins_only
def post(self):
req = request.get_json()
schema = UserSchema('admin')
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data)
db.session.commit()
clear_standings()
response = schema.dump(response.data)
return {
'success': True,
'data': response.data
}
@users_namespace.route('/<int:user_id>')
@users_namespace.param('user_id', "User ID")
class UserPublic(Resource):
@check_account_visibility
def get(self, user_id):
user = Users.query.filter_by(id=user_id).first_or_404()
response = UserSchema('self').dump(user)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
response.data['place'] = user.place
response.data['score'] = user.score
return {
'success': True,
'data': response.data
}
@admins_only
def patch(self, user_id):
user = Users.query.filter_by(id=user_id).first_or_404()
data = request.get_json()
data['id'] = user_id
schema = UserSchema(view='admin', instance=user, partial=True)
response = schema.load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
clear_standings()
return {
'success': True,
'data': response
}
@admins_only
def delete(self, user_id):
Notifications.query.filter_by(user_id=user_id).delete()
Awards.query.filter_by(user_id=user_id).delete()
Unlocks.query.filter_by(user_id=user_id).delete()
Submissions.query.filter_by(user_id=user_id).delete()
Solves.query.filter_by(user_id=user_id).delete()
Tracking.query.filter_by(user_id=user_id).delete()
Users.query.filter_by(id=user_id).delete()
db.session.commit()
db.session.close()
clear_standings()
return {
'success': True
}
@users_namespace.route('/me')
class UserPrivate(Resource):
@authed_only
def get(self):
user = get_current_user()
response = UserSchema('self').dump(user).data
response['place'] = user.place
response['score'] = user.score
return {
'success': True,
'data': response
}
@authed_only
def patch(self):
user = get_current_user()
data = request.get_json()
schema = UserSchema(view='self', instance=user, partial=True)
response = schema.load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
db.session.commit()
response = schema.dump(response.data)
db.session.close()
clear_standings()
return {
'success': True,
'data': response.data
}
@users_namespace.route('/<user_id>/solves')
@users_namespace.param('user_id', "User ID or 'me'")
class UserSolves(Resource):
def get(self, user_id):
if user_id == 'me':
if not authed():
abort(403)
user = get_current_user()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
user = Users.query.filter_by(id=user_id).first_or_404()
solves = user.get_solves(
admin=is_admin()
)
for solve in solves:
setattr(solve, 'value', 100)
view = 'user' if not is_admin() else 'admin'
response = SubmissionSchema(view=view, many=True).dump(solves)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}
@users_namespace.route('/<user_id>/fails')
@users_namespace.param('user_id', "User ID or 'me'")
class UserFails(Resource):
def get(self, user_id):
if user_id == 'me':
if not authed():
abort(403)
user = get_current_user()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
user = Users.query.filter_by(id=user_id).first_or_404()
fails = user.get_fails(
admin=is_admin()
)
view = 'user' if not is_admin() else 'admin'
response = SubmissionSchema(view=view, many=True).dump(fails)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
if is_admin():
data = response.data
else:
data = []
count = len(response.data)
return {
'success': True,
'data': data,
'meta': {
'count': count
}
}
@users_namespace.route('/<user_id>/awards')
@users_namespace.param('user_id', "User ID or 'me'")
class UserAwards(Resource):
def get(self, user_id):
if user_id == 'me':
if not authed():
abort(403)
user = get_current_user()
else:
if accounts_visible() is False or scores_visible() is False:
abort(404)
user = Users.query.filter_by(id=user_id).first_or_404()
awards = user.get_awards(
admin=is_admin()
)
view = 'user' if not is_admin() else 'admin'
response = AwardSchema(view=view, many=True).dump(awards)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {
'success': True,
'data': response.data
}

View File

@ -1,17 +1,21 @@
import logging from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint, abort
import os
import re
import time
from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint
from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature
from passlib.hash import bcrypt_sha256 from passlib.hash import bcrypt_sha256
from CTFd.models import db, Teams from CTFd.models import db, Users, Teams
from CTFd import utils
from CTFd.utils import ratelimit from CTFd.utils import get_config, get_app_config
from CTFd.utils.decorators import ratelimit
from CTFd.utils import user as current_user
from CTFd.utils import config, validators
from CTFd.utils import email
from CTFd.utils.security.auth import login_user, logout_user
from CTFd.utils.logging import log
from CTFd.utils.decorators.visibility import check_registration_visibility
from CTFd.utils.modes import TEAMS_MODE, USERS_MODE
from CTFd.utils.security.signing import serialize, unserialize, SignatureExpired, BadSignature, BadTimeSignature
import base64 import base64
import requests
auth = Blueprint('auth', __name__) auth = Blueprint('auth', __name__)
@ -19,40 +23,34 @@ auth = Blueprint('auth', __name__)
@auth.route('/confirm', methods=['POST', 'GET']) @auth.route('/confirm', methods=['POST', 'GET'])
@auth.route('/confirm/<data>', methods=['GET']) @auth.route('/confirm/<data>', methods=['GET'])
@ratelimit(method="POST", limit=10, interval=60) @ratelimit(method="POST", limit=10, interval=60)
def confirm_user(data=None): def confirm(data=None):
if not utils.get_config('verify_emails'): if not get_config('verify_emails'):
# If the CTF doesn't care about confirming email addresses then redierct to challenges # If the CTF doesn't care about confirming email addresses then redierct to challenges
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.listing'))
logger = logging.getLogger('regs')
# User is confirming email account # User is confirming email account
if data and request.method == "GET": if data and request.method == "GET":
try: try:
s = TimedSerializer(app.config['SECRET_KEY']) user_email = unserialize(data, max_age=1800)
email = s.loads(utils.base64decode(data), max_age=1800) except (BadTimeSignature, SignatureExpired):
except BadTimeSignature:
return render_template('confirm.html', errors=['Your confirmation link has expired']) return render_template('confirm.html', errors=['Your confirmation link has expired'])
except (BadSignature, TypeError, base64.binascii.Error): except (BadSignature, TypeError, base64.binascii.Error):
return render_template('confirm.html', errors=['Your confirmation token is invalid']) return render_template('confirm.html', errors=['Your confirmation token is invalid'])
team = Teams.query.filter_by(email=email).first_or_404()
team = Users.query.filter_by(email=user_email).first_or_404()
team.verified = True team.verified = True
log('registrations', format="[{date}] {ip} - successful password reset for {name}")
db.session.commit() db.session.commit()
logger.warn("[{date}] {ip} - {username} confirmed their account".format(
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=team.name.encode('utf-8'),
email=team.email.encode('utf-8')
))
db.session.close() db.session.close()
if utils.authed(): if current_user.authed():
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.listing'))
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# User is trying to start or restart the confirmation flow # User is trying to start or restart the confirmation flow
if not utils.authed(): if not current_user.authed():
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
team = Teams.query.filter_by(id=session['id']).first_or_404() team = Users.query.filter_by(id=session['id']).first_or_404()
if data is None: if data is None:
if request.method == "POST": if request.method == "POST":
@ -60,17 +58,12 @@ def confirm_user(data=None):
if team.verified: if team.verified:
return redirect(url_for('views.profile')) return redirect(url_for('views.profile'))
else: else:
utils.verify_email(team.email) email.verify_email_address(team.email)
logger.warn("[{date}] {ip} - {username} initiated a confirmation email resend".format( log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=team.name.encode('utf-8'),
email=team.email.encode('utf-8')
))
return render_template('confirm.html', team=team, infos=['Your confirmation email has been resent!']) return render_template('confirm.html', team=team, infos=['Your confirmation email has been resent!'])
elif request.method == "GET": elif request.method == "GET":
# User has been directed to the confirm page # User has been directed to the confirm page
team = Teams.query.filter_by(id=session['id']).first_or_404() team = Users.query.filter_by(id=session['id']).first_or_404()
if team.verified: if team.verified:
# If user is already verified, redirect to their profile # If user is already verified, redirect to their profile
return redirect(url_for('views.profile')) return redirect(url_for('views.profile'))
@ -81,13 +74,10 @@ def confirm_user(data=None):
@auth.route('/reset_password/<data>', methods=['POST', 'GET']) @auth.route('/reset_password/<data>', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=60) @ratelimit(method="POST", limit=10, interval=60)
def reset_password(data=None): def reset_password(data=None):
logger = logging.getLogger('logins')
if data is not None: if data is not None:
try: try:
s = TimedSerializer(app.config['SECRET_KEY']) name = unserialize(data, max_age=1800)
name = s.loads(utils.base64decode(data), max_age=1800) except (BadTimeSignature, SignatureExpired):
except BadTimeSignature:
return render_template('reset_password.html', errors=['Your link has expired']) return render_template('reset_password.html', errors=['Your link has expired'])
except (BadSignature, TypeError, base64.binascii.Error): except (BadSignature, TypeError, base64.binascii.Error):
return render_template('reset_password.html', errors=['Your reset token is invalid']) return render_template('reset_password.html', errors=['Your reset token is invalid'])
@ -95,24 +85,20 @@ def reset_password(data=None):
if request.method == "GET": if request.method == "GET":
return render_template('reset_password.html', mode='set') return render_template('reset_password.html', mode='set')
if request.method == "POST": if request.method == "POST":
team = Teams.query.filter_by(name=name).first_or_404() team = Users.query.filter_by(name=name).first_or_404()
team.password = bcrypt_sha256.encrypt(request.form['password'].strip()) team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
db.session.commit() db.session.commit()
logger.warn("[{date}] {ip} - successful password reset for {username}".format( log('logins', format="[{date}] {ip} - successful password reset for {name}")
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=team.name.encode('utf-8')
))
db.session.close() db.session.close()
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
if request.method == 'POST': if request.method == 'POST':
email = request.form['email'].strip() email_address = request.form['email'].strip()
team = Teams.query.filter_by(email=email).first() team = Users.query.filter_by(email=email_address).first()
errors = [] errors = []
if utils.can_send_mail() is False: if config.can_send_mail() is False:
return render_template( return render_template(
'reset_password.html', 'reset_password.html',
errors=['Email could not be sent due to server misconfiguration'] errors=['Email could not be sent due to server misconfiguration']
@ -124,7 +110,7 @@ def reset_password(data=None):
errors=['If that account exists you will receive an email, please check your inbox'] errors=['If that account exists you will receive an email, please check your inbox']
) )
utils.forgot_password(email, team.name) email.forgot_password(email_address, team.name)
return render_template( return render_template(
'reset_password.html', 'reset_password.html',
@ -134,27 +120,36 @@ def reset_password(data=None):
@auth.route('/register', methods=['POST', 'GET']) @auth.route('/register', methods=['POST', 'GET'])
@check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5) @ratelimit(method="POST", limit=10, interval=5)
def register(): def register():
logger = logging.getLogger('regs')
if not utils.can_register():
return redirect(url_for('auth.login'))
if request.method == 'POST': if request.method == 'POST':
errors = [] errors = []
name = request.form['name'] name = request.form['name']
email = request.form['email'] email_address = request.form['email']
password = request.form['password'] password = request.form['password']
name_len = len(name) == 0 name_len = len(name) == 0
names = Teams.query.add_columns('name', 'id').filter_by(name=name).first() names = Users.query.add_columns('name', 'id').filter_by(name=name).first()
emails = Teams.query.add_columns('email', 'id').filter_by(email=email).first() emails = Users.query.add_columns('email', 'id').filter_by(email=email_address).first()
pass_short = len(password) == 0 pass_short = len(password) == 0
pass_long = len(password) > 128 pass_long = len(password) > 128
valid_email = utils.check_email_format(request.form['email']) valid_email = validators.validate_email(request.form['email'])
team_name_email_check = utils.check_email_format(name) team_name_email_check = validators.validate_email(name)
local_id, _, domain = email_address.partition('@')
domain_whitelist = get_config('domain_whitelist')
if not valid_email: if not valid_email:
errors.append("Please enter a valid email address") errors.append("Please enter a valid email address")
if domain_whitelist:
domain_whitelist = domain_whitelist.split(',')
if domain not in domain_whitelist:
errors.append(
"Only email addresses under {domains} may register".format(
domains=', '.join(domain_whitelist))
)
if names: if names:
errors.append('That team name is already taken') errors.append('That team name is already taken')
if team_name_email_check is True: if team_name_email_check is True:
@ -169,42 +164,41 @@ def register():
errors.append('Pick a longer team name') errors.append('Pick a longer team name')
if len(errors) > 0: if len(errors) > 0:
return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password']) return render_template(
'register.html',
errors=errors,
name=request.form['name'],
email=request.form['email'],
password=request.form['password']
)
else: else:
with app.app_context(): with app.app_context():
team = Teams(name, email.lower(), password) user = Users(
db.session.add(team) name=name.strip(),
email=email_address.lower(),
password=password.strip()
)
db.session.add(user)
db.session.commit() db.session.commit()
db.session.flush() db.session.flush()
session['username'] = team.name login_user(user)
session['id'] = team.id
session['admin'] = team.admin
session['nonce'] = utils.sha512(os.urandom(10))
if utils.can_send_mail() and utils.get_config('verify_emails'): # Confirming users is enabled and we can send email. if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email.
logger = logging.getLogger('regs') log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}")
logger.warn("[{date}] {ip} - {username} registered (UNCONFIRMED) with {email}".format( email.verify_email_address(user.email)
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=request.form['name'].encode('utf-8'),
email=request.form['email'].encode('utf-8')
))
utils.verify_email(team.email)
db.session.close() db.session.close()
return redirect(url_for('auth.confirm_user')) return redirect(url_for('auth.confirm'))
else: # Don't care about confirming users else: # Don't care about confirming users
if utils.can_send_mail(): # We want to notify the user that they have registered. if config.can_send_mail(): # We want to notify the user that they have registered.
utils.sendmail(request.form['email'], "You've successfully registered for {}".format(utils.get_config('ctf_name'))) email.sendmail(
request.form['email'],
"You've successfully registered for {}".format(get_config('ctf_name'))
)
logger.warn("[{date}] {ip} - {username} registered with {email}".format( log('registrations', "[{date}] {ip} - {name} registered with {email}")
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=request.form['name'].encode('utf-8'),
email=request.form['email'].encode('utf-8')
))
db.session.close() db.session.close()
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.listing'))
else: else:
return render_template('register.html') return render_template('register.html')
@ -212,65 +206,149 @@ def register():
@auth.route('/login', methods=['POST', 'GET']) @auth.route('/login', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=5) @ratelimit(method="POST", limit=10, interval=5)
def login(): def login():
logger = logging.getLogger('logins')
if request.method == 'POST': if request.method == 'POST':
errors = [] errors = []
name = request.form['name'] name = request.form['name']
# Check if the user submitted an email address or a team name # Check if the user submitted an email address or a team name
if utils.check_email_format(name) is True: if validators.validate_email(name) is True:
team = Teams.query.filter_by(email=name).first() user = Users.query.filter_by(email=name).first()
else: else:
team = Teams.query.filter_by(name=name).first() user = Users.query.filter_by(name=name).first()
if user:
if user and bcrypt_sha256.verify(request.form['password'], user.password):
session.regenerate()
login_user(user)
log('logins', "[{date}] {ip} - {name} logged in")
if team:
if team and bcrypt_sha256.verify(request.form['password'], team.password):
try:
session.regenerate() # NO SESSION FIXATION FOR YOU
except:
pass # TODO: Some session objects don't implement regenerate :(
session['username'] = team.name
session['id'] = team.id
session['admin'] = team.admin
session['nonce'] = utils.sha512(os.urandom(10))
db.session.close() db.session.close()
if request.args.get('next') and validators.is_safe_url(request.args.get('next')):
logger.warn("[{date}] {ip} - {username} logged in".format(
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip(),
username=session['username'].encode('utf-8')
))
if request.args.get('next') and utils.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.listing'))
else: # This user exists but the password is wrong else:
logger.warn("[{date}] {ip} - submitted invalid password for {username}".format( # This user exists but the password is wrong
date=time.strftime("%m/%d/%Y %X"), log('logins', "[{date}] {ip} - submitted invalid password for {name}")
ip=utils.get_ip(),
username=team.name.encode('utf-8')
))
errors.append("Your username or password is incorrect") 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: # This user just doesn't exist # This user just doesn't exist
logger.warn("[{date}] {ip} - submitted invalid account information".format( log('logins', "[{date}] {ip} - submitted invalid account information")
date=time.strftime("%m/%d/%Y %X"),
ip=utils.get_ip()
))
errors.append("Your username or password is incorrect") 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:
db.session.close() db.session.close()
return render_template('login.html') return render_template('login.html')
@auth.route('/oauth')
def oauth_login():
endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \
or get_config('oauth_authorization_endpoint') \
or 'https://auth.majorleaguecyber.org/oauth/authorize'
if get_config('user_mode') == 'teams':
scope = 'profile team'
else:
scope = 'profile'
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
endpoint=endpoint,
client_id=client_id,
scope=scope,
state=session['nonce']
)
return redirect(redirect_url)
@auth.route('/redirect', methods=['GET'])
@ratelimit(method="GET", limit=10, interval=60)
def oauth_redirect():
oauth_code = request.args.get('code')
state = request.args.get('state')
if session['nonce'] != state:
log('logins', "[{date}] {ip} - OAuth State validation mismatch")
abort(500)
if oauth_code:
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \
or get_config('oauth_token_endpoint') \
or 'https://auth.majorleaguecyber.org/oauth/token'
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
client_secret = get_app_config('OAUTH_CLIENT_SECRET') or get_config('oauth_client_secret')
headers = {
'content-type': 'application/x-www-form-urlencoded'
}
data = {
'code': oauth_code,
'client_id': client_id,
'client_secret': client_secret,
'grant_type': 'authorization_code'
}
token_request = requests.post(url, data=data, headers=headers)
if token_request.status_code == requests.codes.ok:
token = token_request.json()['access_token']
user_url = get_app_config('OAUTH_API_ENDPOINT') \
or get_config('oauth_api_endpoint') \
or 'http://api.majorleaguecyber.org/user'
headers = {
'Authorization': 'Bearer ' + str(token),
'Content-type': 'application/json'
}
api_data = requests.get(url=user_url, headers=headers).json()
user_id = api_data['id']
user_name = api_data['name']
user_email = api_data['email']
user = Users.query.filter_by(email=user_email).first()
if user is None:
user = Users(
name=user_name,
email=user_email,
oauth_id=user_id,
verified=True
)
db.session.add(user)
db.session.commit()
if get_config('user_mode') == TEAMS_MODE:
team_id = api_data['team']['id']
team_name = api_data['team']['name']
team = Teams.query.filter_by(oauth_id=team_id).first()
if team is None:
team = Teams(
name=team_name,
oauth_id=team_id
)
db.session.add(team)
db.session.commit()
team.members.append(user)
db.session.commit()
login_user(user)
return redirect(url_for('challenges.listing'))
else:
log('logins', "[{date}] {ip} - OAuth token retrieval failure")
abort(500)
else:
log('logins', "[{date}] {ip} - Received redirect without OAuth code")
abort(500)
@auth.route('/logout') @auth.route('/logout')
def logout(): def logout():
if utils.authed(): if current_user.authed():
session.clear() logout_user()
return redirect(url_for('views.static_html')) return redirect(url_for('views.static_html'))

14
CTFd/cache/__init__.py vendored Normal file
View File

@ -0,0 +1,14 @@
from flask_caching import Cache
cache = Cache()
def clear_config():
from CTFd.utils import get_config, get_app_config
cache.delete_memoized(get_config)
cache.delete_memoized(get_app_config)
def clear_standings():
from CTFd.utils.scores import get_standings
cache.delete_memoized(get_standings)

View File

@ -1,427 +1,40 @@
import json
import logging
import re
import time
from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort
from sqlalchemy.sql import or_ from CTFd.models import db, Challenges, Files, Solves, Fails, Flags, Tags, Teams, Awards, Hints, Unlocks
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards, Hints, Unlocks
from CTFd.plugins.keys import get_key_class
from CTFd.plugins.challenges import get_chal_class from CTFd.plugins.challenges import get_chal_class
from CTFd import utils
from CTFd.utils.decorators import ( from CTFd.utils.decorators import (
authed_only, authed_only,
admins_only, admins_only,
during_ctf_time_only, during_ctf_time_only,
require_verified_emails, require_verified_emails,
viewable_without_authentication ratelimit,
require_team
) )
from CTFd.utils import text_type from CTFd.utils.decorators.visibility import check_challenge_visibility
from CTFd.utils import config, text_type, user as current_user, get_config
from CTFd.utils.dates import ctftime, ctf_started, ctf_paused, ctf_ended, unix_time, unix_time_to_utc
challenges = Blueprint('challenges', __name__) challenges = Blueprint('challenges', __name__)
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
@during_ctf_time_only
@authed_only
def hints_view(hintid):
hint = Hints.query.filter_by(id=hintid).first_or_404()
chal = Challenges.query.filter_by(id=hint.chal).first()
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
if request.method == 'GET':
if unlock:
return jsonify({
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
})
else:
return jsonify({
'chal': hint.chal,
'cost': hint.cost
})
elif request.method == 'POST':
if unlock is None: # The user does not have an unlock.
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()) or utils.is_admin() is True:
# It's ctftime or the CTF has ended (but we allow views after)
team = Teams.query.filter_by(id=session['id']).first()
if team.score() < hint.cost:
return jsonify({'errors': 'Not enough points'})
unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
award = Awards(teamid=session['id'], name=text_type('Hint for {}'.format(chal.name)), value=(-hint.cost))
db.session.add(unlock)
db.session.add(award)
db.session.commit()
json_data = {
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
}
db.session.close()
return jsonify(json_data)
elif utils.ctf_ended(): # The CTF has ended. No views after.
abort(403)
else: # The user does have an unlock, we should give them their hint.
json_data = {
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
}
db.session.close()
return jsonify(json_data)
@challenges.route('/challenges', methods=['GET']) @challenges.route('/challenges', methods=['GET'])
@during_ctf_time_only @during_ctf_time_only
@require_verified_emails @require_verified_emails
@viewable_without_authentication() @check_challenge_visibility
def challenges_view(): @require_team
def listing():
infos = [] infos = []
errors = [] errors = []
start = utils.get_config('start') or 0 start = get_config('start') or 0
end = utils.get_config('end') or 0 end = get_config('end') or 0
if utils.ctf_paused(): if ctf_paused():
infos.append('{} is paused'.format(utils.ctf_name())) infos.append('{} is paused'.format(config.ctf_name()))
if not utils.ctftime(): if not ctftime():
# It is not CTF time if ctf_started() is False:
if utils.view_after_ctf(): # But we are allowed to view after the CTF ends errors.append('{} has not started yet'.format(config.ctf_name()))
pass if ctf_ended():
else: # We are NOT allowed to view after the CTF ends errors.append('{} has ended'.format(config.ctf_name()))
if utils.get_config('start') and not utils.ctf_started():
errors.append('{} has not started yet'.format(utils.ctf_name()))
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
errors.append('{} has ended'.format(utils.ctf_name()))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
@challenges.route('/chals', methods=['GET'])
@during_ctf_time_only
@require_verified_emails
@viewable_without_authentication(status_code=403)
def chals():
db_chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
response = {'game': []}
for chal in db_chals:
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
chal_type = get_chal_class(chal.type)
response['game'].append({
'id': chal.id,
'type': chal_type.name,
'name': chal.name,
'value': chal.value,
'category': chal.category,
'tags': tags,
'template': chal_type.templates['modal'],
'script': chal_type.scripts['modal'],
})
db.session.close()
return jsonify(response)
@challenges.route('/chals/<int:chal_id>', methods=['GET'])
@during_ctf_time_only
@require_verified_emails
@viewable_without_authentication(status_code=403)
def chal_view(chal_id):
teamid = session.get('id')
chal = Challenges.query.filter_by(id=chal_id).first_or_404()
if chal.hidden:
abort(404)
chal_class = get_chal_class(chal.type)
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
hints = []
for hint in Hints.query.filter_by(chal=chal.id).all():
if hint.id in unlocked_hints or utils.ctf_ended():
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
else:
hints.append({'id': hint.id, 'cost': hint.cost})
challenge, response = chal_class.read(challenge=chal)
response['files'] = files
response['tags'] = tags
response['hints'] = hints
db.session.close()
return jsonify(response)
@challenges.route('/chals/solves')
@viewable_without_authentication(status_code=403)
def solves_per_chal():
chals = Challenges.query\
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
.order_by(Challenges.value)\
.all()
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()
solves = db.session.query(
solves_sub.columns.chalid,
solves_sub.columns.solves,
Challenges.name
) \
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
data = {}
if utils.hide_scores():
for chal, count, name in solves:
data[chal] = -1
for c in chals:
if c.id not in data:
data[c.id] = -1
else:
for chal, count, name in solves:
data[chal] = count
for c in chals:
if c.id not in data:
data[c.id] = 0
db.session.close()
return jsonify(data)
@challenges.route('/solves')
@authed_only
def solves_private():
solves = None
awards = None
if utils.is_admin():
solves = Solves.query.filter_by(teamid=session['id']).all()
elif utils.user_can_view_challenges():
if utils.authed():
solves = Solves.query\
.join(Teams, Solves.teamid == Teams.id)\
.filter(Solves.teamid == session['id'])\
.all()
else:
return jsonify({'solves': []})
else:
return redirect(url_for('auth.login', next='solves'))
db.session.close()
response = {'solves': []}
for solve in solves:
response['solves'].append({
'chal': solve.chal.name,
'chalid': solve.chalid,
'team': solve.teamid,
'value': solve.chal.value,
'category': solve.chal.category,
'time': utils.unix_time(solve.date)
})
if awards:
for award in awards:
response['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category or "Award",
'time': utils.unix_time(award.date)
})
response['solves'].sort(key=lambda k: k['time'])
return jsonify(response)
@challenges.route('/solves/<int:teamid>')
def solves_public(teamid=None):
solves = None
awards = None
if utils.authed() and session['id'] == teamid:
solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid)
freeze = utils.get_config('freeze')
if freeze:
freeze = utils.unix_time_to_utc(freeze)
if teamid != session.get('id'):
solves = solves.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
solves = solves.all()
awards = awards.all()
elif utils.hide_scores():
# Use empty values to hide scores
solves = []
awards = []
else:
solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid)
freeze = utils.get_config('freeze')
if freeze:
freeze = utils.unix_time_to_utc(freeze)
if teamid != session.get('id'):
solves = solves.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
solves = solves.all()
awards = awards.all()
db.session.close()
response = {'solves': []}
for solve in solves:
response['solves'].append({
'chal': solve.chal.name,
'chalid': solve.chalid,
'team': solve.teamid,
'value': solve.chal.value,
'category': solve.chal.category,
'time': utils.unix_time(solve.date)
})
if awards:
for award in awards:
response['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category or "Award",
'time': utils.unix_time(award.date)
})
response['solves'].sort(key=lambda k: k['time'])
return jsonify(response)
@challenges.route('/fails')
@authed_only
def fails_private():
fails = WrongKeys.query.filter_by(teamid=session['id']).count()
solves = Solves.query.filter_by(teamid=session['id']).count()
db.session.close()
response = {
'fails': str(fails),
'solves': str(solves)
}
return jsonify(response)
@challenges.route('/fails/<int:teamid>')
def fails_public(teamid=None):
if utils.authed() and session['id'] == teamid:
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
elif utils.hide_scores():
fails = 0
solves = 0
else:
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close()
response = {
'fails': str(fails),
'solves': str(solves)
}
return jsonify(response)
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
@during_ctf_time_only
@viewable_without_authentication(status_code=403)
def who_solved(chalid):
response = {'teams': []}
if utils.hide_scores():
return jsonify(response)
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
for solve in solves:
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
return jsonify(response)
@challenges.route('/chal/<int:chalid>', methods=['POST'])
@during_ctf_time_only
@viewable_without_authentication()
def chal(chalid):
if utils.ctf_paused():
return jsonify({
'status': 3,
'message': '{} is paused'.format(utils.ctf_name())
})
if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin():
team = Teams.query.filter_by(id=session['id']).first()
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
logger = logging.getLogger('keys')
data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), utils.get_kpm(session['id']))
print("[{0}] {1} submitted {2} with kpm {3}".format(*data))
chal = Challenges.query.filter_by(id=chalid).first_or_404()
if chal.hidden:
abort(404)
chal_class = get_chal_class(chal.type)
# Anti-bruteforce / submitting keys too quickly
if utils.get_kpm(session['id']) > 10:
if utils.ctftime():
chal_class.fail(team=team, chal=chal, request=request)
logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
# return '3' # Submitting too fast
return jsonify({'status': 3, 'message': "You're submitting keys too fast. Slow down."})
solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first()
# Challange not solved yet
if not solves:
provided_key = request.form['key'].strip()
saved_keys = Keys.query.filter_by(chal=chal.id).all()
# Hit max attempts
max_tries = chal.max_attempts
if max_tries and fails >= max_tries > 0:
return jsonify({
'status': 0,
'message': "You have 0 tries remaining"
})
status, message = chal_class.attempt(chal, request)
if status: # The challenge plugin says the input is right
if utils.ctftime() or utils.is_admin():
chal_class.solve(team=team, chal=chal, request=request)
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
return jsonify({'status': 1, 'message': message})
else: # The challenge plugin says the input is wrong
if utils.ctftime() or utils.is_admin():
chal_class.fail(team=team, chal=chal, request=request)
logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data))
# return '0' # key was wrong
if max_tries:
attempts_left = max_tries - fails - 1 # Off by one since fails has changed since it was gotten
tries_str = 'tries'
if attempts_left == 1:
tries_str = 'try'
if message[-1] not in '!().;?[]\{\}': # Add a punctuation mark if there isn't one
message = message + '.'
return jsonify({'status': 0, 'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str)})
else:
return jsonify({'status': 0, 'message': message})
# Challenge already solved
else:
logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data))
# return '2' # challenge was already solved
return jsonify({'status': 2, 'message': 'You already solved this'})
else:
return jsonify({
'status': -1,
'message': "You must be logged in to solve a challenge"
})

View File

@ -27,124 +27,182 @@ if not os.environ.get('SECRET_KEY'):
class Config(object): class Config(object):
"""
CTFd Configuration Object
"""
''' '''
SECRET_KEY is the secret value used to creation sessions and sign strings. This should be set to a random string. In the === REQUIRED SETTINGS ===
interest of ease, CTFd will automatically create a secret key file for you. If you wish to add this secret key to
your instance you should hard code this value to a random static value. SECRET_KEY:
The secret value used to creation sessions and sign strings. This should be set to a random string. In the
interest of ease, CTFd will automatically create a secret key file for you. If you wish to add this secret key
to your instance you should hard code this value to a random static value.
You can also remove .ctfd_secret_key from the .gitignore file and commit this file into whatever repository You can also remove .ctfd_secret_key from the .gitignore file and commit this file into whatever repository
you are using. you are using.
http://flask.pocoo.org/docs/0.11/quickstart/#sessions http://flask.pocoo.org/docs/latest/quickstart/#sessions
'''
SECRET_KEY = os.environ.get('SECRET_KEY') or key
''' SQLALCHEMY_DATABASE_URI:
SQLALCHEMY_DATABASE_URI is the URI that specifies the username, password, hostname, port, and database of the server The URI that specifies the username, password, hostname, port, and database of the server
used to hold the CTFd database. used to hold the CTFd database.
http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys e.g. mysql+pymysql://root:<YOUR_PASSWORD_HERE>@localhost/ctfd
CACHE_TYPE:
Specifies how CTFd should cache configuration values. If CACHE_TYPE is set to 'redis', CTFd will make use
of the REDIS_URL specified in environment variables. You can also choose to hardcode the REDIS_URL here.
It is important that you specify some sort of cache as CTFd uses it to store values received from the database. If
no cache is specified, CTFd will default to a simple per-worker cache. The simple cache cannot be effectively used
with multiple workers.
REDIS_URL is the URL to connect to a Redis server.
e.g. redis://user:password@localhost:6379
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
''' '''
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = os.environ.get('SECRET_KEY') or key
DATABASE_URL = os.environ.get(
'DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))
REDIS_URL = os.environ.get('REDIS_URL')
SQLALCHEMY_DATABASE_URI = DATABASE_URL
CACHE_REDIS_URL = os.environ.get('CACHE_REDIS_URL') or REDIS_URL
if CACHE_REDIS_URL:
CACHE_TYPE = 'redis'
else:
CACHE_TYPE = 'filesystem'
CACHE_DIR = os.path.join(os.path.dirname(
__file__), os.pardir, '.data', 'filesystem_cache')
''' '''
SQLALCHEMY_TRACK_MODIFICATIONS is automatically disabled to suppress warnings and save memory. You should only enable === SECURITY ===
this if you need it.
'''
SQLALCHEMY_TRACK_MODIFICATIONS = False
''' SESSION_COOKIE_HTTPONLY:
SESSION_TYPE is a configuration value used for Flask-Session. It is currently unused in CTFd. Controls if cookies should be set with the HttpOnly flag.
http://pythonhosted.org/Flask-Session/#configuration
'''
SESSION_TYPE = "filesystem"
''' PERMANENT_SESSION_LIFETIME:
SESSION_FILE_DIR is a configuration value used for Flask-Session. It is currently unused in CTFd. The lifetime of a session. The default is 604800 seconds.
http://pythonhosted.org/Flask-Session/#configuration
'''
SESSION_FILE_DIR = "/tmp/flask_session"
''' TRUSTED_PROXIES:
SESSION_COOKIE_HTTPONLY controls if cookies should be set with the HttpOnly flag. Defines a set of regular expressions used for finding a user's IP address if the CTFd instance
'''
SESSION_COOKIE_HTTPONLY = True
'''
PERMANENT_SESSION_LIFETIME is the lifetime of a session.
'''
PERMANENT_SESSION_LIFETIME = 604800 # 7 days in seconds
'''
HOST specifies the hostname where the CTFd instance will exist. It is currently unused.
'''
HOST = ".ctfd.io"
'''
MAILFROM_ADDR is the email address that emails are sent from if not overridden in the configuration panel.
'''
MAILFROM_ADDR = "noreply@ctfd.io"
'''
LOG_FOLDER is the location where logs are written
These are the logs for CTFd key submissions, registrations, and logins
The default location is the CTFd/logs folder
'''
LOG_FOLDER = os.environ.get('LOG_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
'''
UPLOAD_FOLDER is the location where files are uploaded.
The default destination is the CTFd/uploads folder. If you need Amazon S3 files
you can use the CTFd S3 plugin: https://github.com/ColdHeat/CTFd-S3-plugin
'''
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
'''
TEMPLATES_AUTO_RELOAD specifies whether Flask should check for modifications to templates and
reload them automatically
'''
TEMPLATES_AUTO_RELOAD = True
'''
TRUSTED_PROXIES defines a set of regular expressions used for finding a user's IP address if the CTFd instance
is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove
some proxies from the list. some proxies from the list.
CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based
solely on IP addresses. solely on IP addresses unless you know what you are doing.
''' '''
SESSION_COOKIE_HTTPONLY = True
PERMANENT_SESSION_LIFETIME = 604800 # 7 days in seconds
TRUSTED_PROXIES = [ TRUSTED_PROXIES = [
'^127\.0\.0\.1$', r'^127\.0\.0\.1$',
# Remove the following proxies if you do not trust the local network # Remove the following proxies if you do not trust the local network
# For example if you are running a CTF on your laptop and the teams are all on the same network # For example if you are running a CTF on your laptop and the teams are
'^::1$', # all on the same network
'^fc00:', r'^::1$',
'^10\.', r'^fc00:',
'^172\.(1[6-9]|2[0-9]|3[0-1])\.', r'^10\.',
'^192\.168\.' r'^172\.(1[6-9]|2[0-9]|3[0-1])\.',
r'^192\.168\.'
] ]
''' '''
CACHE_TYPE specifies how CTFd should cache configuration values. If CACHE_TYPE is set to 'redis', CTFd will make use === EMAIL ===
of the REDIS_URL specified in environment variables. You can also choose to hardcode the REDIS_URL here.
It is important that you specify some sort of cache as CTFd uses it to store values received from the database. MAILFROM_ADDR:
The email address that emails are sent from if not overridden in the configuration panel.
CACHE_REDIS_URL is the URL to connect to Redis server. MAIL_SERVER:
Example: redis://user:password@localhost:6379 The mail server that emails are sent from if not overriden in the configuration panel.
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching MAIL_PORT:
The mail port that emails are sent from if not overriden in the configuration panel.
''' '''
CACHE_REDIS_URL = os.environ.get('REDIS_URL') MAILFROM_ADDR = "noreply@ctfd.io"
if CACHE_REDIS_URL: MAIL_SERVER = None
CACHE_TYPE = 'redis' MAIL_PORT = None
else: MAIL_USERNAME = None
CACHE_TYPE = 'simple' MAIL_PASSWORD = None
MAIL_TLS = False
MAIL_SSL = False
MAILGUN_API_KEY = None
MAILGUN_BASE_URL = None
''' '''
UPDATE_CHECK specifies whether or not CTFd will check whether or not there is a new version of CTFd === LOGS ===
LOG_FOLDER:
The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins.
The default location is the CTFd/logs folder.
''' '''
LOG_FOLDER = os.environ.get('LOG_FOLDER') or os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'logs')
'''
=== UPLOADS ===
UPLOAD_PROVIDER:
Specifies the service that CTFd should use to store files.
UPLOAD_FOLDER:
The location where files are uploaded. The default destination is the CTFd/uploads folder.
AWS_ACCESS_KEY_ID:
AWS access token used to authenticate to the S3 bucket.
AWS_SECRET_ACCESS_KEY:
AWS secret token used to authenticate to the S3 bucket.
AWS_S3_BUCKET:
The unique identifier for your S3 bucket.
AWS_S3_ENDPOINT_URL:
A URL pointing to a custom S3 implementation.
'''
UPLOAD_PROVIDER = os.environ.get('UPLOAD_PROVIDER') or 'filesystem'
if UPLOAD_PROVIDER == 'filesystem':
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or \
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
elif UPLOAD_PROVIDER == 's3':
AWS_ACCESS_KEY_ID = None
AWS_SECRET_ACCESS_KEY = None
AWS_S3_BUCKET = None
AWS_S3_ENDPOINT_URL = None
'''
=== OPTIONAL ===
REVERSE_PROXY:
Specifies whether CTFd is behind a reverse proxy or not. Set to True if using a reverse proxy like nginx.
TEMPLATES_AUTO_RELOAD:
Specifies whether Flask should check for modifications to templates and reload them automatically.
SQLALCHEMY_TRACK_MODIFICATIONS:
Automatically disabled to suppress warnings and save memory. You should only enable this if you need it.
UPDATE_CHECK:
Specifies whether or not CTFd will check whether or not there is a new version of CTFd
APPLICATION_ROOT:
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
Example: /ctfd
'''
REVERSE_PROXY = False
TEMPLATES_AUTO_RELOAD = True
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPDATE_CHECK = True UPDATE_CHECK = True
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT') or '/'
'''
=== OAUTH ===
MajorLeagueCyber Integration
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here
'''
OAUTH_CLIENT_ID = None
OAUTH_CLIENT_SECRET = None
class TestingConfig(Config): class TestingConfig(Config):
@ -155,5 +213,5 @@ class TestingConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://' SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
SERVER_NAME = 'localhost' SERVER_NAME = 'localhost'
UPDATE_CHECK = False UPDATE_CHECK = False
CACHE_REDIS_URL = None REDIS_URL = None
CACHE_TYPE = 'simple' CACHE_TYPE = 'simple'

21
CTFd/errors.py Normal file
View File

@ -0,0 +1,21 @@
from flask import current_app as app, render_template
# 404
def page_not_found(error):
return render_template('errors/404.html', error=error.description), 404
# 403
def forbidden(error):
return render_template('errors/403.html', error=error.description), 403
# 500
def general_error(error):
return render_template('errors/500.html'), 500
# 502
def gateway_error(error):
return render_template('errors/502.html', error=error.description), 502

3
CTFd/events/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from flask import Blueprint
events = Blueprint('events', __name__)

View File

@ -1,343 +0,0 @@
import datetime
import hashlib
import json
import netaddr
from socket import inet_pton, inet_ntop, AF_INET, AF_INET6
from struct import unpack, pack, error as struct_error
from flask_sqlalchemy import SQLAlchemy
from passlib.hash import bcrypt_sha256
from sqlalchemy.exc import DatabaseError
from sqlalchemy.sql.expression import union_all
def sha512(string):
return str(hashlib.sha512(string).hexdigest())
def ip2long(ip):
'''Converts a user's IP address into an integer/long'''
return int(netaddr.IPAddress(ip))
def long2ip(ip_int):
'''Converts a saved integer/long back into an IP address'''
return str(netaddr.IPAddress(ip_int))
db = SQLAlchemy()
class Pages(db.Model):
id = db.Column(db.Integer, primary_key=True)
auth_required = db.Column(db.Boolean)
title = db.Column(db.String(80))
route = db.Column(db.Text, unique=True)
html = db.Column(db.Text)
draft = db.Column(db.Boolean)
def __init__(self, title, route, html, draft=True, auth_required=False):
self.title = title
self.route = route
self.html = html
self.draft = draft
self.auth_required = auth_required
def __repr__(self):
return "<Pages route {0}>".format(self.route)
class Challenges(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
max_attempts = db.Column(db.Integer, default=0)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
type = db.Column(db.String(80))
hidden = db.Column(db.Boolean)
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, name, description, value, category, type='standard'):
self.name = name
self.description = description
self.value = value
self.category = category
self.type = type
def __repr__(self):
return '<chal %r>' % self.name
class Hints(db.Model):
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.Integer, default=0)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
hint = db.Column(db.Text)
cost = db.Column(db.Integer, default=0)
def __init__(self, chal, hint, cost=0, type=0):
self.chal = chal
self.hint = hint
self.cost = cost
self.type = type
def __repr__(self):
return '<hint %r>' % self.hint
class Awards(db.Model):
id = db.Column(db.Integer, primary_key=True)
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
name = db.Column(db.String(80))
description = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
icon = db.Column(db.Text)
def __init__(self, teamid, name, value):
self.teamid = teamid
self.name = name
self.value = value
def __repr__(self):
return '<award %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'))
type = db.Column(db.String(80))
flag = db.Column(db.Text)
data = db.Column(db.Text)
def __init__(self, chal, flag, type):
self.chal = chal
self.flag = flag
self.type = type
def __repr__(self):
return "<Flag {0} for challenge {1}>".format(self.flag, self.chal)
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, default=False)
verified = 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):
self.name = name
self.email = email
self.password = bcrypt_sha256.encrypt(str(password))
def __repr__(self):
return '<team %r>' % self.name
def score(self, admin=False):
score = db.func.sum(Challenges.value).label('score')
team = db.session.query(Solves.teamid, score).join(Teams).join(Challenges).filter(Teams.id == self.id)
award_score = db.func.sum(Awards.value).label('award_score')
award = db.session.query(award_score).filter_by(teamid=self.id)
if not admin:
freeze = Config.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
team = team.filter(Solves.date < freeze)
award = award.filter(Awards.date < freeze)
team = team.group_by(Solves.teamid).first()
award = award.first()
if team and award:
return int(team.score or 0) + int(award.award_score or 0)
elif team:
return int(team.score or 0)
elif award:
return int(award.award_score or 0)
else:
return 0
def place(self, admin=False):
"""
This method is generally a clone of CTFd.scoreboard.get_standings.
The point being that models.py must be self-reliant and have little
to no imports within the CTFd application as importing from the
application itself will result in a circular import.
"""
scores = db.session.query(
Solves.teamid.label('teamid'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.teamid)
awards = db.session.query(
Awards.teamid.label('teamid'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
).filter(Awards.value != 0).group_by(Awards.teamid)
if not admin:
freeze = Config.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
results = union_all(scores, awards).alias('results')
sumscores = db.session.query(
results.columns.teamid,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.teamid).subquery()
if admin:
standings_query = db.session.query(
Teams.id.label('teamid'),
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Teams.id.label('teamid'),
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
standings = standings_query.all()
# http://codegolf.stackexchange.com/a/4712
try:
i = standings.index((self.id,)) + 1
k = i % 10
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
except ValueError:
return 0
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.String(46))
flag = db.Column(db.Text)
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, teamid, chalid, ip, flag):
self.ip = ip
self.chalid = chalid
self.teamid = teamid
self.flag = flag
# self.value = value
def __repr__(self):
return '<solve {}, {}, {}, {}>'.format(self.teamid, self.chalid, self.ip, self.flag)
class WrongKeys(db.Model):
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.String(46))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
flag = db.Column(db.Text)
chal = db.relationship('Challenges', foreign_keys="WrongKeys.chalid", lazy='joined')
def __init__(self, teamid, chalid, ip, flag):
self.ip = ip
self.teamid = teamid
self.chalid = chalid
self.flag = flag
def __repr__(self):
return '<wrong {}, {}, {}, {}>'.format(self.teamid, self.chalid, self.ip, self.flag)
class Unlocks(db.Model):
id = db.Column(db.Integer, primary_key=True)
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
itemid = db.Column(db.Integer)
model = db.Column(db.String(32))
def __init__(self, model, teamid, itemid):
self.model = model
self.teamid = teamid
self.itemid = itemid
def __repr__(self):
return '<unlock %r>' % self.teamid
class Tracking(db.Model):
id = db.Column(db.Integer, primary_key=True)
ip = db.Column(db.String(46))
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 = 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

788
CTFd/models/__init__.py Normal file
View File

@ -0,0 +1,788 @@
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from passlib.hash import bcrypt_sha256
from sqlalchemy import TypeDecorator, String, func, types, CheckConstraint, and_
from sqlalchemy.sql.expression import union_all
from sqlalchemy.types import JSON, NullType
from sqlalchemy.orm import validates, column_property
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
from sqlalchemy.sql import or_, and_, any_
from CTFd.utils.crypto import hash_password
from CTFd.cache import cache
import datetime
import json
import six
db = SQLAlchemy()
ma = Marshmallow()
def get_class_by_tablename(tablename):
"""Return class reference mapped to table.
https://stackoverflow.com/a/23754464
:param tablename: String with name of table.
:return: Class reference or None.
"""
for c in db.Model._decl_class_registry.values():
if hasattr(c, '__tablename__') and c.__tablename__ == tablename:
return c
return None
class SQLiteJson(TypeDecorator):
impl = String
class Comparator(String.Comparator):
def __getitem__(self, index):
if isinstance(index, tuple):
index = "$%s" % (
"".join([
"[%s]" % elem if isinstance(elem, int)
else '."%s"' % elem for elem in index
])
)
elif isinstance(index, int):
index = "$[%s]" % index
else:
index = '$."%s"' % index
# json_extract does not appear to return JSON sub-elements
# which is weird.
return func.json_extract(self.expr, index, type_=NullType)
comparator_factory = Comparator
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
JSON = types.JSON().with_variant(SQLiteJson, 'sqlite')
class Notifications(db.Model):
__tablename__ = 'notifications'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text)
content = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
user = db.relationship('Users', foreign_keys="Notifications.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Notifications.team_id", lazy='select')
def __init__(self, *args, **kwargs):
super(Notifications, self).__init__(**kwargs)
class Pages(db.Model):
__tablename__ = 'pages'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80))
route = db.Column(db.String(128), unique=True)
content = db.Column(db.Text)
draft = db.Column(db.Boolean)
hidden = db.Column(db.Boolean)
auth_required = db.Column(db.Boolean)
# TODO: Use hidden attribute
files = db.relationship("PageFiles", backref="page")
def __init__(self, *args, **kwargs):
super(Pages, self).__init__(**kwargs)
def __repr__(self):
return "<Pages {0}>".format(self.route)
class Challenges(db.Model):
__tablename__ = 'challenges'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
max_attempts = db.Column(db.Integer, default=0)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
type = db.Column(db.String(80))
state = db.Column(db.String(80), nullable=False, default='visible')
requirements = db.Column(JSON)
files = db.relationship("ChallengeFiles", backref="challenge")
tags = db.relationship("Tags", backref="challenge")
hints = db.relationship("Hints", backref="challenge")
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Challenges, self).__init__(**kwargs)
def __repr__(self):
return '<Challenge %r>' % self.name
class Hints(db.Model):
__tablename__ = 'hints'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(80), default='standard')
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
content = db.Column(db.Text)
cost = db.Column(db.Integer, default=0)
requirements = db.Column(JSON)
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
@property
def name(self):
return "Hint {id}".format(id=self.id)
@property
def category(self):
return self.__tablename__
@property
def description(self):
return "Hint for {name}".format(name=self.challenge.name)
def __init__(self, *args, **kwargs):
super(Hints, self).__init__(**kwargs)
def __repr__(self):
return '<Hint %r>' % self.content
class Awards(db.Model):
__tablename__ = 'awards'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
name = db.Column(db.String(80))
description = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
icon = db.Column(db.Text)
requirements = db.Column(JSON)
user = db.relationship('Users', foreign_keys="Awards.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Awards.team_id", lazy='select')
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
def __init__(self, *args, **kwargs):
super(Awards, self).__init__(**kwargs)
def __repr__(self):
return '<Award %r>' % self.name
class Tags(db.Model):
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
value = db.Column(db.String(80))
def __init__(self, *args, **kwargs):
super(Tags, self).__init__(**kwargs)
class Files(db.Model):
__tablename__ = 'files'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(80), default='standard')
location = db.Column(db.Text)
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Files, self).__init__(**kwargs)
def __repr__(self):
return "<File type={type} location={location}>".format(type=self.type, location=self.location)
class ChallengeFiles(Files):
__mapper_args__ = {
'polymorphic_identity': 'challenge'
}
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
def __init__(self, *args, **kwargs):
super(ChallengeFiles, self).__init__(**kwargs)
class PageFiles(Files):
__mapper_args__ = {
'polymorphic_identity': 'page'
}
page_id = db.Column(db.Integer, db.ForeignKey('pages.id'))
def __init__(self, *args, **kwargs):
super(PageFiles, self).__init__(**kwargs)
class Flags(db.Model):
__tablename__ = 'flags'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
type = db.Column(db.String(80))
content = db.Column(db.Text)
data = db.Column(db.Text)
challenge = db.relationship('Challenges', foreign_keys="Flags.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Flags, self).__init__(**kwargs)
def __repr__(self):
return "<Flag {0} for challenge {1}>".format(self.content, self.challenge_id)
class Users(db.Model):
__tablename__ = 'users'
__table_args__ = (
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes
id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True)
# User names are not constrained to be unique to allow for official/unofficial teams.
name = db.Column(db.String(128))
password = db.Column(db.String(128))
email = db.Column(db.String(128), unique=True)
type = db.Column(db.String(80))
secret = db.Column(db.String(128))
# Supplementary attributes
website = db.Column(db.String(128))
affiliation = db.Column(db.String(128))
country = db.Column(db.String(32))
bracket = db.Column(db.String(32))
hidden = db.Column(db.Boolean, default=False)
banned = db.Column(db.Boolean, default=False)
verified = db.Column(db.Boolean, default=False)
# Relationship for Teams
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
__mapper_args__ = {
'polymorphic_identity': 'user',
'polymorphic_on': type
}
def __init__(self, **kwargs):
super(Users, self).__init__(**kwargs)
if kwargs.get('password'):
self.password = hash_password(str(kwargs['password']))
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.id
@property
def solves(self):
return self.get_solves(admin=False)
@property
def fails(self):
return self.get_fails(admin=False)
@property
def awards(self):
return self.get_awards(admin=False)
@property
def score(self):
return self.get_score(admin=False)
@property
def place(self):
return self.get_place(admin=False)
def get_solves(self, admin=False):
solves = Solves.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt)
return solves.all()
def get_fails(self, admin=False):
fails = Fails.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Solves.date < dt)
return fails.all()
def get_awards(self, admin=False):
awards = Awards.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Solves.date < dt)
return awards.all()
def get_score(self, admin=False):
score = db.func.sum(Challenges.value).label('score')
user = db.session.query(
Solves.user_id,
score
) \
.join(Users, Solves.user_id == Users.id) \
.join(Challenges, Solves.challenge_id == Challenges.id) \
.filter(Users.id == self.id)
award_score = db.func.sum(Awards.value).label('award_score')
award = db.session.query(award_score).filter_by(user_id=self.id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
user = user.filter(Solves.date < freeze)
award = award.filter(Awards.date < freeze)
user = user.group_by(Solves.user_id).first()
award = award.first()
if user and award:
return int(user.score or 0) + int(award.award_score or 0)
elif user:
return int(user.score or 0)
elif award:
return int(award.award_score or 0)
else:
return 0
def get_place(self, admin=False, numeric=False):
"""
This method is generally a clone of CTFd.scoreboard.get_standings.
The point being that models.py must be self-reliant and have little
to no imports within the CTFd application as importing from the
application itself will result in a circular import.
"""
scores = db.session.query(
Solves.user_id.label('user_id'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id)
awards = db.session.query(
Awards.user_id.label('user_id'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
).filter(Awards.value != 0).group_by(Awards.user_id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
results = union_all(scores, awards).alias('results')
sumscores = db.session.query(
results.columns.user_id,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.user_id).subquery()
if admin:
standings_query = db.session.query(
Users.id.label('user_id'),
) \
.join(sumscores, Users.id == sumscores.columns.user_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Users.id.label('user_id'),
) \
.join(sumscores, Users.id == sumscores.columns.user_id) \
.filter(Users.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
standings = standings_query.all()
# http://codegolf.stackexchange.com/a/4712
try:
i = standings.index((self.id,)) + 1
if numeric:
return i
else:
k = i % 10
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
except ValueError:
return 0
class Admins(Users):
__tablename__ = 'admins'
__mapper_args__ = {
'polymorphic_identity': 'admin'
}
class Teams(db.Model):
__tablename__ = 'teams'
__table_args__ = (
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes
id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True)
# Team names are not constrained to be unique to allow for official/unofficial teams.
name = db.Column(db.String(128))
email = db.Column(db.String(128), unique=True)
password = db.Column(db.String(128))
secret = db.Column(db.String(128))
members = db.relationship("Users", backref="team")
# Supplementary attributes
website = db.Column(db.String(128))
affiliation = db.Column(db.String(128))
country = db.Column(db.String(32))
bracket = db.Column(db.String(32))
hidden = db.Column(db.Boolean, default=False)
banned = db.Column(db.Boolean, default=False)
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, **kwargs):
super(Teams, self).__init__(**kwargs)
if kwargs.get('password'):
self.password = hash_password(str(kwargs['password']))
@property
def solves(self):
return self.get_solves(admin=False)
@property
def fails(self):
return self.get_fails(admin=False)
@property
def awards(self):
return self.get_awards(admin=False)
@property
def score(self):
return self.get_score(admin=False)
@property
def place(self):
return self.get_place(admin=False)
def get_solves(self, admin=False):
member_ids = [member.id for member in self.members]
solves = Solves.query.filter(
Solves.user_id.in_(member_ids)
).order_by(
Fails.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
fails = solves.filter(Solves.date < dt)
return solves.all()
def get_fails(self, admin=False):
member_ids = [member.id for member in self.members]
fails = Fails.query.filter(
Fails.user_id.in_(member_ids)
).order_by(
Fails.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Solves.date < dt)
return fails.all()
def get_awards(self, admin=False):
member_ids = [member.id for member in self.members]
awards = Awards.query.filter(
Awards.user_id.in_(member_ids)
).order_by(
Awards.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Solves.date < dt)
return awards.all()
def get_score(self, admin=False):
score = 0
for member in self.members:
score += member.get_score(admin=admin)
return score
def get_place(self, admin=False):
"""
This method is generally a clone of CTFd.scoreboard.get_standings.
The point being that models.py must be self-reliant and have little
to no imports within the CTFd application as importing from the
application itself will result in a circular import.
"""
scores = db.session.query(
Solves.team_id.label('team_id'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id)
awards = db.session.query(
Awards.team_id.label('team_id'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
).filter(Awards.value != 0).group_by(Awards.team_id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
results = union_all(scores, awards).alias('results')
sumscores = db.session.query(
results.columns.team_id,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.team_id).subquery()
if admin:
standings_query = db.session.query(
Teams.id.label('team_id'),
) \
.join(sumscores, Teams.id == sumscores.columns.team_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Teams.id.label('team_id'),
) \
.join(sumscores, Teams.id == sumscores.columns.team_id) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
standings = standings_query.all()
# http://codegolf.stackexchange.com/a/4712
try:
i = standings.index((self.id,)) + 1
k = i % 10
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
except ValueError:
return 0
class Submissions(db.Model):
__tablename__ = 'submissions'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
ip = db.Column(db.String(46))
provided = db.Column(db.Text)
type = db.Column(db.String(32))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
# Relationships
user = db.relationship('Users', foreign_keys="Submissions.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Submissions.team_id", lazy='select')
challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type,
}
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
@hybrid_property
def account(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team
elif user_mode == 'users':
return self.user
@staticmethod
def get_child(type):
child_classes = {
x.polymorphic_identity: x.class_
for x in Submissions.__mapper__.self_and_descendants
}
return child_classes[type]
def __repr__(self):
return '<Submission {}, {}, {}, {}>'.format(self.team_id, self.challenge_id, self.ip, self.provided)
class Solves(Submissions):
__tablename__ = 'solves'
__table_args__ = (
db.UniqueConstraint('challenge_id', 'user_id'),
db.UniqueConstraint('challenge_id', 'team_id'),
{}
)
id = db.Column(None, db.ForeignKey('submissions.id', ondelete='CASCADE'), primary_key=True)
challenge_id = column_property(db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')),
Submissions.challenge_id)
user_id = column_property(db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')), Submissions.user_id)
team_id = column_property(db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')), Submissions.team_id)
user = db.relationship('Users', foreign_keys="Solves.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Solves.team_id", lazy='select')
challenge = db.relationship('Challenges', foreign_keys="Solves.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_identity': 'correct'
}
class Fails(Submissions):
__mapper_args__ = {
'polymorphic_identity': 'incorrect'
}
class Unlocks(db.Model):
__tablename__ = 'unlocks'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
target = db.Column(db.Integer)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
type = db.Column(db.String(32))
__mapper_args__ = {
'polymorphic_on': type,
}
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
def __repr__(self):
return '<Unlock %r>' % self.teamid
class HintUnlocks(Unlocks):
__mapper_args__ = {
'polymorphic_identity': 'hints'
}
class Tracking(db.Model):
__tablename__ = 'tracking'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(32))
ip = db.Column(db.String(46))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
user = db.relationship('Users', foreign_keys="Tracking.user_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type,
}
def __init__(self, *args, **kwargs):
super(Tracking, self).__init__(**kwargs)
def __repr__(self):
return '<Tracking %r>' % self.team
class Configs(db.Model):
__tablename__ = 'config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.Text)
value = db.Column(db.Text)
def __init__(self, *args, **kwargs):
super(Configs, self).__init__(**kwargs)
@cache.memoize()
def get_config(key):
"""
This should be a direct clone of its implementation in utils. It is used to avoid a circular import.
"""
config = Configs.query.filter_by(key=key).first()
if config and config.value:
value = config.value
if value and value.isdigit():
return int(value)
elif value and isinstance(value, six.string_types):
if value.lower() == 'true':
return True
elif value.lower() == 'false':
return False
else:
return value

View File

@ -5,13 +5,13 @@ import os
from collections import namedtuple from collections import namedtuple
from flask.helpers import safe_join from flask.helpers import safe_join
from flask import current_app as app, send_file, send_from_directory, abort from flask import current_app as app, send_file, send_from_directory, abort
from CTFd.utils import ( from CTFd.utils.decorators import admins_only as admins_only_wrapper
admins_only as admins_only_wrapper, from CTFd.utils.plugins import (
override_template as utils_override_template, override_template as utils_override_template,
register_plugin_script as utils_register_plugin_script, register_script as utils_register_plugin_script,
register_plugin_stylesheet as utils_register_plugin_stylesheet, register_stylesheet as utils_register_plugin_stylesheet,
pages as db_pages
) )
from CTFd.utils.config.pages import get_pages
Menu = namedtuple('Menu', ['title', 'route']) Menu = namedtuple('Menu', ['title', 'route'])
@ -122,7 +122,7 @@ def get_user_page_menu_bar():
:return: Returns a list of Menu namedtuples. They have name, and route attributes. :return: Returns a list of Menu namedtuples. They have name, and route attributes.
""" """
return db_pages() + USER_PAGE_MENU_BAR return get_pages() + USER_PAGE_MENU_BAR
def bypass_csrf_protection(f): def bypass_csrf_protection(f):
@ -146,7 +146,7 @@ def init_plugins(app):
:param app: A CTFd application :param app: A CTFd application
:return: :return:
""" """
modules = glob.glob(os.path.dirname(__file__) + "/*") modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
blacklist = {'__pycache__'} blacklist = {'__pycache__'}
for module in modules: for module in modules:
module_name = os.path.basename(module) module_name = os.path.basename(module)

View File

@ -1,7 +1,11 @@
from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins import register_plugin_assets_directory
from CTFd.plugins.keys import get_key_class from CTFd.plugins.flags import get_flag_class
from CTFd.models import db, Solves, WrongKeys, Keys, Challenges, Files, Tags, Hints from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints
from CTFd import utils from CTFd import utils
from CTFd.utils.user import get_ip
from CTFd.utils.uploads import upload_file, delete_file
from flask import Blueprint
import six
class BaseChallenge(object): class BaseChallenge(object):
@ -14,16 +18,20 @@ class BaseChallenge(object):
class CTFdStandardChallenge(BaseChallenge): class CTFdStandardChallenge(BaseChallenge):
id = "standard" # Unique identifier used to register challenges id = "standard" # Unique identifier used to register challenges
name = "standard" # Name of a challenge type name = "standard" # Name of a challenge type
templates = { # Nunjucks templates used for each aspect of challenge editing & viewing templates = { # Templates used for each aspect of challenge editing & viewing
'create': '/plugins/challenges/assets/standard-challenge-create.njk', 'create': '/plugins/challenges/assets/create.html',
'update': '/plugins/challenges/assets/standard-challenge-update.njk', 'update': '/plugins/challenges/assets/update.html',
'modal': '/plugins/challenges/assets/standard-challenge-modal.njk', 'view': '/plugins/challenges/assets/view.html',
} }
scripts = { # Scripts that are loaded when a template is loaded scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/challenges/assets/standard-challenge-create.js', 'create': '/plugins/challenges/assets/create.js',
'update': '/plugins/challenges/assets/standard-challenge-update.js', 'update': '/plugins/challenges/assets/update.js',
'modal': '/plugins/challenges/assets/standard-challenge-modal.js', 'view': '/plugins/challenges/assets/view.js',
} }
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
route = '/plugins/challenges/assets/'
# Blueprint used to access the static_folder directory.
blueprint = Blueprint('standard', __name__, template_folder='templates', static_folder='assets')
@staticmethod @staticmethod
def create(request): def create(request):
@ -33,39 +41,14 @@ class CTFdStandardChallenge(BaseChallenge):
:param request: :param request:
:return: :return:
""" """
# Create challenge data = request.form or request.get_json()
chal = Challenges(
name=request.form['name'],
description=request.form['description'],
value=request.form['value'],
category=request.form['category'],
type=request.form['chaltype']
)
if 'hidden' in request.form: challenge = Challenges(**data)
chal.hidden = True
else:
chal.hidden = False
max_attempts = request.form.get('max_attempts') db.session.add(challenge)
if max_attempts and max_attempts.isdigit():
chal.max_attempts = int(max_attempts)
db.session.add(chal)
db.session.commit() db.session.commit()
flag = Keys(chal.id, request.form['key'], request.form['key_type[0]']) return challenge
if request.form.get('keydata'):
flag.data = request.form.get('keydata')
db.session.add(flag)
db.session.commit()
files = request.files.getlist('files[]')
for f in files:
utils.upload_file(file=f, chalid=chal.id)
db.session.commit()
@staticmethod @staticmethod
def read(challenge): def read(challenge):
@ -81,7 +64,7 @@ class CTFdStandardChallenge(BaseChallenge):
'value': challenge.value, 'value': challenge.value,
'description': challenge.description, 'description': challenge.description,
'category': challenge.category, 'category': challenge.category,
'hidden': challenge.hidden, 'state': challenge.state,
'max_attempts': challenge.max_attempts, 'max_attempts': challenge.max_attempts,
'type': challenge.type, 'type': challenge.type,
'type_data': { 'type_data': {
@ -91,7 +74,7 @@ class CTFdStandardChallenge(BaseChallenge):
'scripts': CTFdStandardChallenge.scripts, 'scripts': CTFdStandardChallenge.scripts,
} }
} }
return challenge, data return data
@staticmethod @staticmethod
def update(challenge, request): def update(challenge, request):
@ -103,14 +86,12 @@ class CTFdStandardChallenge(BaseChallenge):
:param request: :param request:
:return: :return:
""" """
challenge.name = request.form['name'] data = request.form or request.get_json()
challenge.description = request.form['description'] for attr, value in data.items():
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0 setattr(challenge, attr, value)
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
challenge.category = request.form['category']
challenge.hidden = 'hidden' in request.form
db.session.commit() db.session.commit()
db.session.close() return challenge
@staticmethod @staticmethod
def delete(challenge): def delete(challenge):
@ -120,15 +101,15 @@ class CTFdStandardChallenge(BaseChallenge):
:param challenge: :param challenge:
:return: :return:
""" """
WrongKeys.query.filter_by(chalid=challenge.id).delete() Fails.query.filter_by(challenge_id=challenge.id).delete()
Solves.query.filter_by(chalid=challenge.id).delete() Solves.query.filter_by(challenge_id=challenge.id).delete()
Keys.query.filter_by(chal=challenge.id).delete() Flags.query.filter_by(challenge_id=challenge.id).delete()
files = Files.query.filter_by(chal=challenge.id).all() files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all()
for f in files: for f in files:
utils.delete_file(f.id) delete_file(f.id)
Files.query.filter_by(chal=challenge.id).delete() ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete()
Tags.query.filter_by(chal=challenge.id).delete() Tags.query.filter_by(challenge_id=challenge.id).delete()
Hints.query.filter_by(chal=challenge.id).delete() Hints.query.filter_by(challenge_id=challenge.id).delete()
Challenges.query.filter_by(id=challenge.id).delete() Challenges.query.filter_by(id=challenge.id).delete()
db.session.commit() db.session.commit()
@ -143,15 +124,16 @@ class CTFdStandardChallenge(BaseChallenge):
:param request: The request the user submitted :param request: The request the user submitted
:return: (boolean, string) :return: (boolean, string)
""" """
provided_key = request.form['key'].strip() data = request.form or request.get_json()
chal_keys = Keys.query.filter_by(chal=chal.id).all() submission = data['submission'].strip()
chal_keys = Flags.query.filter_by(challenge_id=chal.id).all()
for chal_key in chal_keys: for chal_key in chal_keys:
if get_key_class(chal_key.type).compare(chal_key, provided_key): if get_flag_class(chal_key.type).compare(chal_key, submission):
return True, 'Correct' return True, 'Correct'
return False, 'Incorrect' return False, 'Incorrect'
@staticmethod @staticmethod
def solve(team, chal, request): def solve(user, team, challenge, request):
""" """
This method is used to insert Solves into the database in order to mark a challenge as solved. This method is used to insert Solves into the database in order to mark a challenge as solved.
@ -160,24 +142,38 @@ class CTFdStandardChallenge(BaseChallenge):
:param request: The request the user submitted :param request: The request the user submitted
:return: :return:
""" """
provided_key = request.form['key'].strip() data = request.form or request.get_json()
solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key) submission = data['submission'].strip()
solve = Solves(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(req=request),
provided=submission
)
db.session.add(solve) db.session.add(solve)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@staticmethod @staticmethod
def fail(team, chal, request): def fail(user, team, challenge, request):
""" """
This method is used to insert WrongKeys into the database in order to mark an answer incorrect. This method is used to insert Fails into the database in order to mark an answer incorrect.
:param team: The Team object from the database :param team: The Team object from the database
:param chal: The Challenge object from the database :param chal: The Challenge object from the database
:param request: The request the user submitted :param request: The request the user submitted
:return: :return:
""" """
provided_key = request.form['key'].strip() data = request.form or request.get_json()
wrong = WrongKeys(teamid=team.id, chalid=chal.id, ip=utils.get_ip(request), flag=provided_key) submission = data['submission'].strip()
wrong = Fails(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(request),
provided=submission
)
db.session.add(wrong) db.session.add(wrong)
db.session.commit() db.session.commit()
db.session.close() db.session.close()

View File

@ -0,0 +1,63 @@
<form method="POST" action="{{ script_root }}/admin/challenges/new" enctype="multipart/form-data">
<div class="form-group">
<label>
Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label>
Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label>
Value<br>
<small class="form-text text-muted">
This is how many points are rewarded for solving this challenge.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" name="type" value="standard">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>

View File

@ -1,99 +0,0 @@
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Name
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="form-group">
<label>Flag
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is the flag or solution for your challenge. You can choose whether your flag is a static string or a regular expression."></i>
</label>
<input type="text" class="form-control" name="key" placeholder="Enter flag">
</div>
<div class="form-group">
<select class="custom-select" name="key_type[0]">
<option value="static">Static</option>
<option value="regex">Regex</option>
</select>
</div>
<div class="form-check">
<div class="checkbox">
<label class="form-check-label">
<input class="form-check-input" name="hidden" type="checkbox">
Hide challenge on creation
</label>
</div>
</div>
<div class="form-check">
<div class="checkbox">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" id="solve-attempts-checkbox">
Limit amount of solve attempts
</label>
</div>
</div>
<div class="form-group">
<div id="solve-attempts-input" style="display: none;">
<label for="max_attempts">Maximum Attempts
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
</label>
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
</div>
</div>
<div class="form-group">
<label>Upload Challenge Files
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
</label>
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<input type="hidden" value="standard" name="chaltype" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>

View File

@ -1,37 +0,0 @@
window.challenge.data = undefined;
window.challenge.renderer = new markdownit({
html: true,
});
window.challenge.preRender = function(){
};
window.challenge.render = function(markdown){
return window.challenge.renderer.render(markdown);
};
window.challenge.postRender = function(){
};
window.challenge.submit = function(cb, preview){
var chal_id = $('#chal-id').val();
var answer = $('#answer-input').val();
var nonce = $('#nonce').val();
var url = "/chal/";
if (preview) {
url = "/admin/chal/";
}
$.post(script_root + url + chal_id, {
key: answer,
nonce: nonce
}, function (data) {
cb(data);
});
};

View File

@ -1,104 +0,0 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == '-1 Solves' %}
{% else %}
<li class="nav-item">
<a class="nav-link chal-solves" href="#solves">{{solves}}</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
<h3 class="chal-value text-center">{{ value }}</h3>
<div class="chal-tags text-center">
{% for tag in tags %}
<span class='badge badge-info chal-tag'>{{tag}}</span>
{% endfor %}
</div>
<span class="chal-desc">{{ description | safe }}</span>
<div class="chal-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block" href="javascript:;" onclick="javascript:loadhint({{hint.id}})">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{hint.cost}} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row chal-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='{{script_root}}/files/{{file}}'>
<i class="fas fa-download"></i>
<small>
{{ file.split('/')[1] }}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
<input id="chal-id" type="hidden" value="{{id}}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,94 +0,0 @@
<div id="update-challenge" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="chal-title text-center">{{ name }}</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/chal/update">
<input name='nonce' type='hidden' value="{{ nonce }}">
<div class="form-group">
<label for="name">Name
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name" value="{{ name }}">
</div>
<div class="form-group">
<label for="category">Category
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category" value="{{ category }}">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
Write
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
Preview
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ description }}</textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
</label>
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" value="{{ value }}" required>
</div>
<div class="checkbox">
<label>
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox" {% if max_attempts %}checked{% endif %}>
Limit challenge attempts
</label>
</div>
<div class="form-group" id="chal-attempts-group" {% if not max_attempts %}style="display:none;"{% endif %}>
<label for="value">Max Attempts</label>
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value" value="{{ max_attempts }}">
</div>
<input class="chal-id" type='hidden' name='id' placeholder='ID' value="{{ id }}">
<div class="checkbox">
<label>
<input class="chal-hidden" name="hidden" type="checkbox"
{% if hidden %}checked{% endif %}>
Hidden
</label>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div style="text-align:center">
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,63 @@
<form method="POST">
<div class="form-group">
<label>
Name<br>
<small class="form-text text-muted">Challenge Name</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label>
Category<br>
<small class="form-text text-muted">Challenge Category</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<div class="form-group">
<label>
Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
<div class="form-group">
<label for="value">
Value<br>
<small class="form-text text-muted">
This is how many points teams will receive once they solve this challenge.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>

View File

View File

@ -0,0 +1,112 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link chal-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
<h3 class="chal-value text-center">{{ value }}</h3>
<div class="chal-tags text-center">
{% for tag in tags %}
<span class='badge badge-info chal-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="chal-desc">{{ description | safe }}</span>
<div class="chal-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block" href="javascript:;"
onclick="javascript:loadhint({{ hint.id }})">
{% if hint.content %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row chal-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ script_root }}/files/{{ file }}'>
<i class="fas fa-download"></i>
<small>
{{ file.split('/')[1] }}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="answer-input"
placeholder="Flag"/>
<input id="chal-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5"
class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
window.challenge.data = undefined;
window.challenge.renderer = new markdownit({
html: true,
});
window.challenge.preRender = function () {
};
window.challenge.render = function (markdown) {
return window.challenge.renderer.render(markdown);
};
window.challenge.postRender = function () {
};
window.challenge.submit = function (cb, preview) {
var challenge_id = parseInt($('#chal-id').val());
var submission = $('#answer-input').val();
var url = "/api/v1/challenges/attempt";
if (preview) {
url += "?preview=true";
}
var params = {
'challenge_id': challenge_id,
'submission': submission
};
fetch(script_root + url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
cb(response);
});
};

View File

@ -0,0 +1,2 @@
__pycache__/
*.py[cod]

View File

@ -0,0 +1,54 @@
# Dynamic Value Challenges for CTFd
It's becoming commonplace in CTF to see challenges whose point values decrease
after each solve.
This CTFd plugin creates a dynamic challenge type which implements this
behavior. Each dynamic challenge starts with an initial point value and then
each solve will decrease the value of the challenge until a minimum point value.
By reducing the value of the challenge on each solve, all users who have previously
solved the challenge will have lowered scores. Thus an easier and more solved
challenge will naturally have a lower point value than a harder and less solved
challenge.
Within CTFd you are free to mix and match regular and dynamic challenges.
The current implementation requires the challenge to keep track of three values:
* Initial - The original point valuation
* Decay - The amount of solves before the challenge will be at the minimum
* Minimum - The lowest possible point valuation
The value decay logic is implemented with the following math:
<!--
$$a=\textrm{max points}$$
$$b=\textrm{min points}$$
$$s=\textrm{solve threshold}$$
$$f(x)=\frac{b-a}{s^{2}}x^{2}+a$$
-->
![](https://raw.githubusercontent.com/CTFd/DynamicValueChallenge/master/function.png)
or in pseudo code:
```
value = (((minimum - initial)/(decay**2)) * (solve_count**2)) + initial
value = math.ceil(value)
```
If the number generated is lower than the minimum, the minimum is chosen
instead.
A parabolic function is chosen instead of an exponential or logarithmic decay function
so that higher valued challenges have a slower drop from their initial value.
# Installation
**REQUIRES: CTFd >= v1.2.0**
1. Clone this repository to `CTFd/plugins`. It is important that the folder is
named `DynamicValueChallenge` so CTFd can serve the files in the `assets`
directory.

View File

@ -0,0 +1,239 @@
from __future__ import division # Use floating point for math calculations
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
from CTFd.plugins import register_plugin_assets_directory
from CTFd.plugins.flags import get_flag_class
from CTFd.models import db, Solves, Fails, Flags, Challenges, Files, Tags, Teams, Hints
from CTFd import utils
from CTFd.utils.migrations import upgrade
from CTFd.utils.user import get_ip
from CTFd.utils.uploads import upload_file, delete_file
from flask import Blueprint
import math
class DynamicValueChallenge(BaseChallenge):
id = "dynamic" # Unique identifier used to register challenges
name = "dynamic" # Name of a challenge type
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
'create': '/plugins/dynamic_challenges/assets/create.html',
'update': '/plugins/dynamic_challenges/assets/update.html',
'view': '/plugins/dynamic_challenges/assets/view.html',
}
scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/dynamic_challenges/assets/create.js',
'update': '/plugins/dynamic_challenges/assets/update.js',
'view': '/plugins/dynamic_challenges/assets/view.js',
}
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
route = '/plugins/dynamic_challenges/assets/'
# Blueprint used to access the static_folder directory.
blueprint = Blueprint('dynamic_challenges', __name__, template_folder='templates', static_folder='assets')
@staticmethod
def create(request):
"""
This method is used to process the challenge creation request.
:param request:
:return:
"""
data = request.form or request.get_json()
challenge = DynamicChallenge(**data)
db.session.add(challenge)
db.session.commit()
return challenge
@staticmethod
def read(challenge):
"""
This method is in used to access the data of a challenge in a format processable by the front end.
:param challenge:
:return: Challenge object, data dictionary to be returned to the user
"""
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = {
'id': challenge.id,
'name': challenge.name,
'value': challenge.value,
'initial': challenge.initial,
'decay': challenge.decay,
'minimum': challenge.minimum,
'description': challenge.description,
'category': challenge.category,
'state': challenge.state,
'max_attempts': challenge.max_attempts,
'type': challenge.type,
'type_data': {
'id': DynamicValueChallenge.id,
'name': DynamicValueChallenge.name,
'templates': DynamicValueChallenge.templates,
'scripts': DynamicValueChallenge.scripts,
}
}
return data
@staticmethod
def update(challenge, request):
"""
This method is used to update the information associated with a challenge. This should be kept strictly to the
Challenges table and any child tables.
:param challenge:
:param request:
:return:
"""
data = request.form or request.get_json()
data['initial'] = float(data.get('initial', 0))
data['minimum'] = float(data.get('minimum', 0))
data['decay'] = float(data.get('decay', 0))
for attr, value in data.items():
setattr(challenge, attr, value)
solve_count = Solves.query \
.join(Teams, Solves.team_id == Teams.id) \
.filter(Solves.challenge_id == challenge.id, Teams.banned == False) \
.count()
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) * (solve_count ** 2)) + challenge.initial
value = math.ceil(value)
if value < challenge.minimum:
value = challenge.minimum
challenge.value = value
db.session.commit()
return challenge
@staticmethod
def delete(challenge):
"""
This method is used to delete the resources used by a challenge.
:param challenge:
:return:
"""
Fails.query.filter_by(challenge_id=challenge.id).delete()
Solves.query.filter_by(challenge_id=challenge.id).delete()
Flags.query.filter_by(challenge_id=challenge.id).delete()
files = Files.query.filter_by(challenge_id=challenge.id).all()
for f in files:
delete_file(f.id)
Files.query.filter_by(challenge_id=challenge.id).delete()
Tags.query.filter_by(challenge_id=challenge.id).delete()
Hints.query.filter_by(challenge_id=challenge.id).delete()
DynamicChallenge.query.filter_by(id=challenge.id).delete()
Challenges.query.filter_by(id=challenge.id).delete()
db.session.commit()
@staticmethod
def attempt(chal, request):
"""
This method is used to check whether a given input is right or wrong. It does not make any changes and should
return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the
user's input from the request itself.
:param chal: The Challenge object from the database
:param request: The request the user submitted
:return: (boolean, string)
"""
data = request.form or request.get_json()
submission = data['submission'].strip()
chal_keys = Flags.query.filter_by(challenge_id=chal.id).all()
for chal_key in chal_keys:
if get_flag_class(chal_key.type).compare(chal_key, submission):
return True, 'Correct'
return False, 'Incorrect'
@staticmethod
def solve(user, team, challenge, request):
"""
This method is used to insert Solves into the database in order to mark a challenge as solved.
:param team: The Team object from the database
:param chal: The Challenge object from the database
:param request: The request the user submitted
:return:
"""
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = request.form or request.get_json()
submission = data['submission'].strip()
solve_count = Solves.query\
.join(Teams, Solves.team_id == Teams.id)\
.filter(Solves.challenge_id == chal.id, Teams.banned == False)\
.count()
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (
(
(chal.minimum - chal.initial) / (chal.decay**2)
) * (solve_count**2)
) + chal.initial
value = math.ceil(value)
if value < chal.minimum:
value = chal.minimum
chal.value = value
solve = Solves(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(req=request),
provided=submission
)
db.session.add(solve)
db.session.commit()
db.session.close()
@staticmethod
def fail(user, team, challenge, request):
"""
This method is used to insert Fails into the database in order to mark an answer incorrect.
:param team: The Team object from the database
:param challenge: The Challenge object from the database
:param request: The request the user submitted
:return:
"""
data = request.form or request.get_json()
submission = data['submission'].strip()
wrong = Fails(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(request),
provided=submission
)
db.session.add(wrong)
db.session.commit()
db.session.close()
class DynamicChallenge(Challenges):
__mapper_args__ = {'polymorphic_identity': 'dynamic'}
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True)
initial = db.Column(db.Integer)
minimum = db.Column(db.Integer)
decay = db.Column(db.Integer)
def __init__(self, *args, **kwargs):
super(DynamicChallenge, self).__init__(**kwargs)
self.initial = kwargs['value']
def load(app):
# upgrade()
app.db.create_all()
CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge
register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/')

View File

@ -0,0 +1,88 @@
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<div class="alert alert-secondary" role="alert">
Dynamic value challenges decrease in value as they receive solves. The more solves a dynamic challenge has,
the
lower its value is to everyone who has solved it.
</div>
</div>
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge. The description supports HTML and
Markdown.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth initially.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" value="dynamic" name="type" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>

View File

@ -0,0 +1,29 @@
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview'){
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview'){
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$("#solve-attempts-checkbox").change(function() {
if(this.checked) {
$('#solve-attempts-input').show();
} else {
$('#solve-attempts-input').hide();
$('#max_attempts').val('');
}
});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});

View File

@ -0,0 +1,112 @@
<form method="POST">
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home"
role="tab" data-toggle="tab">
Write
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
Preview
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description"
rows="10">{{ challenge.description }}</textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview"
style="height:214px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Current Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth right now.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge was worth initially.
</small>
</label>
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts"
value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>

View File

@ -0,0 +1,112 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link chal-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
<h3 class="chal-value text-center">{{ value }}</h3>
<div class="chal-tags text-center">
{% for tag in tags %}
<span class='badge badge-info chal-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="chal-desc">{{ description | safe }}</span>
<div class="chal-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block" href="javascript:;"
onclick="javascript:loadhint({{ hint.id }})">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row chal-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ script_root }}/files/{{ file }}'>
<i class="fas fa-download"></i>
<small>
{{ file.split('/')[1] }}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="answer-input"
placeholder="Flag"/>
<input id="chal-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5"
class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
window.challenge.data = undefined;
window.challenge.renderer = new markdownit({
html: true,
});
window.challenge.preRender = function () {
};
window.challenge.render = function (markdown) {
return window.challenge.renderer.render(markdown);
};
window.challenge.postRender = function () {
};
window.challenge.submit = function (cb, preview) {
var challenge_id = parseInt($('#chal-id').val());
var submission = $('#answer-input').val();
var url = "/api/v1/challenges/attempt";
if (preview) {
url += "?preview=true";
}
var params = {
'challenge_id': challenge_id,
'submission': submission
};
fetch(script_root + url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
cb(response);
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -5,8 +5,7 @@ import string
import hmac import hmac
class BaseKey(object): class BaseFlag(object):
id = None
name = None name = None
templates = {} templates = {}
@ -15,17 +14,16 @@ class BaseKey(object):
return True return True
class CTFdStaticKey(BaseKey): class CTFdStaticFlag(BaseFlag):
id = 0
name = "static" name = "static"
templates = { # Nunjucks templates used for key editing & viewing templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/keys/assets/static/create-static-modal.njk', 'create': '/plugins/flags/assets/static/create-static-modal.njk',
'update': '/plugins/keys/assets/static/edit-static-modal.njk', 'update': '/plugins/flags/assets/static/edit-static-modal.njk',
} }
@staticmethod @staticmethod
def compare(chal_key_obj, provided): def compare(chal_key_obj, provided):
saved = chal_key_obj.flag saved = chal_key_obj.content
data = chal_key_obj.data data = chal_key_obj.data
if len(saved) != len(provided): if len(saved) != len(provided):
@ -41,17 +39,16 @@ class CTFdStaticKey(BaseKey):
return result == 0 return result == 0
class CTFdRegexKey(BaseKey): class CTFdRegexFlag(BaseFlag):
id = 1
name = "regex" name = "regex"
templates = { # Nunjucks templates used for key editing & viewing templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/keys/assets/regex/create-regex-modal.njk', 'create': '/plugins/flags/assets/regex/create-regex-modal.njk',
'update': '/plugins/keys/assets/regex/edit-regex-modal.njk', 'update': '/plugins/flags/assets/regex/edit-regex-modal.njk',
} }
@staticmethod @staticmethod
def compare(chal_key_obj, provided): def compare(chal_key_obj, provided):
saved = chal_key_obj.flag saved = chal_key_obj.content
data = chal_key_obj.data data = chal_key_obj.data
if data == "case_insensitive": if data == "case_insensitive":
@ -62,18 +59,18 @@ class CTFdRegexKey(BaseKey):
return res and res.group() == provided return res and res.group() == provided
KEY_CLASSES = { FLAG_CLASSES = {
'static': CTFdStaticKey, 'static': CTFdStaticFlag,
'regex': CTFdRegexKey 'regex': CTFdRegexFlag
} }
def get_key_class(class_id): def get_flag_class(class_id):
cls = KEY_CLASSES.get(class_id) cls = FLAG_CLASSES.get(class_id)
if cls is None: if cls is None:
raise KeyError raise KeyError
return cls return cls
def load(app): def load(app):
register_plugin_assets_directory(app, base_path='/plugins/keys/assets/') register_plugin_assets_directory(app, base_path='/plugins/flags/assets/')

View File

@ -0,0 +1,14 @@
<label>
Regex Flag<br>
<small>Enter regex flag data</small>
</label>
<div class="form-group">
<input type="text" class="form-control" name="content" value="{{ content }}">
</div>
<div class="form-group">
<select name="data">
<option value="">Case Sensitive</option>
<option value="case_insensitive">Case Insensitive</option>
</select>
</div>
<input type="hidden" name="type" value="regex">

View File

@ -0,0 +1,19 @@
<label>
Regex<br>
<small>Enter regex key data</small>
</label>
<div class="form-group">
<input type="text" class="form-control" name="content" value="{{ content }}">
</div>
<div class="form-group">
<select name="data">
<option value="">Case Sensitive</option>
<option value="case_insensitive" {% if data %}selected{% endif %}>Case Insensitive</option>
</select>
</div>
<input type="hidden" name="type" value="regex">
<input type="hidden" name="id" value="{{ id }}">
<hr>
<div class="form-group">
<button class="btn btn-success float-right">Update</button>
</div>

View File

@ -0,0 +1,14 @@
<label>
Static<br>
<small>Enter static flag data</small>
</label>
<div class="form-group">
<input type="text" class="form-control" name="content" value="{{ content }}">
</div>
<div class="form-group">
<select name="data">
<option value="">Case Sensitive</option>
<option value="case_insensitive">Case Insensitive</option>
</select>
</div>
<input type="hidden" name="type" value="static">

View File

@ -0,0 +1,19 @@
<label>
Static<br>
<small>Enter static flag data</small>
</label>
<div class="form-group">
<input type="text" class="form-control" name="content" value="{{ content }}">
</div>
<div class="form-group">
<select name="data">
<option value="">Case Sensitive</option>
<option value="case_insensitive" {% if data %}selected{% endif %}>Case Insensitive</option>
</select>
</div>
<input type="hidden" name="type" value="static">
<input type="hidden" name="id" value="{{ id }}">
<hr>
<div class="form-group">
<button class="btn btn-success float-right">Update</button>
</div>

View File

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

View File

@ -1,33 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Regex Key</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
{% if data %}checked{% endif %}>
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>
<input type="hidden" id="key-type" name="key_type" value="regex">
<input type="hidden" id="key-id">
<hr>
<div class="form-group">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-success float-right">Update</button>
</div>
</form>
</div>
</div>
</div>

View File

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

View File

@ -1,34 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Static Key</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{ id }}">
<input type="text" id="key-data" class="form-control" name="key" value="{{ key }}"
placeholder="Enter static key data">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
{% if data %}checked{% endif %}>
<label class="form-check-label" for="keydata">Case Insensitive</label>
</div>
<input type="hidden" id="key-type" name="key_type" value="static">
<input type="hidden" id="key-id">
<hr>
<div class="form-group">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-success float-right">Update</button>
</div>
</form>
</div>
</div>
</div>

0
CTFd/schemas/__init__.py Normal file
View File

51
CTFd/schemas/awards.py Normal file
View File

@ -0,0 +1,51 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Awards
class AwardSchema(ma.ModelSchema):
class Meta:
model = Awards
include_fk = True
dump_only = ('id', 'date')
views = {
'admin': [
'category',
'user_id',
'name',
'description',
'value',
'team_id',
'user',
'team',
'date',
'requirements',
'id',
'icon'
],
'user': [
'category',
'user_id',
'name',
'description',
'value',
'team_id',
'user',
'team',
'date',
'id',
'icon'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(AwardSchema, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,11 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load, validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Challenges
class ChallengeSchema(ma.ModelSchema):
class Meta:
model = Challenges
include_fk = True
dump_only = ('id',)

29
CTFd/schemas/config.py Normal file
View File

@ -0,0 +1,29 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Configs
class ConfigSchema(ma.ModelSchema):
class Meta:
model = Configs
include_fk = True
dump_only = ('id',)
views = {
'admin': [
'id',
'key',
'value'
],
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(ConfigSchema, self).__init__(*args, **kwargs)

21
CTFd/schemas/files.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Files, ChallengeFiles, PageFiles
class FileSchema(ma.ModelSchema):
class Meta:
model = Files
include_fk = True
dump_only = ('id', 'type', 'location')
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(FileSchema, self).__init__(*args, **kwargs)

21
CTFd/schemas/flags.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Flags
class FlagSchema(ma.ModelSchema):
class Meta:
model = Flags
include_fk = True
dump_only = ('id',)
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(FlagSchema, self).__init__(*args, **kwargs)

45
CTFd/schemas/hints.py Normal file
View File

@ -0,0 +1,45 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Hints
class HintSchema(ma.ModelSchema):
class Meta:
model = Hints
include_fk = True
dump_only = ('id', 'type')
views = {
'locked': [
'id',
'type',
'challenge',
'cost'
],
'unlocked': [
'id',
'type',
'challenge',
'content',
'cost'
],
'admin': [
'id',
'type',
'challenge',
'content',
'cost',
'requirements'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(HintSchema, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,21 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Notifications
class NotificationSchema(ma.ModelSchema):
class Meta:
model = Notifications
include_fk = True
dump_only = ('id', 'date')
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(NotificationSchema, self).__init__(*args, **kwargs)

21
CTFd/schemas/pages.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Pages
class PageSchema(ma.ModelSchema):
class Meta:
model = Pages
include_fk = True
dump_only = ('id', )
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(PageSchema, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,46 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load, validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.schemas.challenges import ChallengeSchema
from CTFd.models import ma, Submissions
class SubmissionSchema(ma.ModelSchema):
challenge = fields.Nested(ChallengeSchema, only=['name', 'category', 'value'])
class Meta:
model = Submissions
include_fk = True
dump_only = ('id', )
views = {
'admin': [
'provided',
'ip',
'challenge_id',
'challenge',
'user',
'team',
'date',
'type',
'id'
],
'user': [
'challenge_id',
'challenge',
'user',
'team',
'date',
'type',
'id'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(SubmissionSchema, self).__init__(*args, **kwargs)

32
CTFd/schemas/tags.py Normal file
View File

@ -0,0 +1,32 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Tags
class TagSchema(ma.ModelSchema):
class Meta:
model = Tags
include_fk = True
dump_only = ('id',)
views = {
'admin': [
'id',
'challenge',
'value'
],
'user': [
'value'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(TagSchema, self).__init__(*args, **kwargs)

141
CTFd/schemas/teams.py Normal file
View File

@ -0,0 +1,141 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError, pre_load
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Teams
from CTFd.utils.validators import validate_country_code
from CTFd.utils.countries import lookup_country_code
from CTFd.utils.user import is_admin, get_current_team
class TeamSchema(ma.ModelSchema):
class Meta:
model = Teams
include_fk = True
dump_only = ('id', 'oauth_id', 'created', 'members')
load_only = ('password',)
name = field_for(
Teams,
'name',
required=True,
validate=[
validate.Length(min=1, max=128, error='Team names must not be empty')
]
)
email = field_for(
Teams,
'email',
validate=validate.Email('Emails must be a properly formatted email address')
)
website = field_for(
Teams,
'website',
validate=validate.URL(
error='Websites must be a proper URL starting with http or https',
schemes={'http', 'https'}
)
)
country = field_for(
Teams,
'country',
validate=[
validate_country_code
]
)
@pre_load
def validate_name(self, data):
name = data.get('name')
if name is None:
return
existing_team = Teams.query.filter_by(name=name).first()
# Admins should be able to patch anyone but they cannot cause a collision.
if is_admin():
team_id = int(data.get('id', 0))
if team_id:
if existing_team.id != team_id:
raise ValidationError('Team name has already been taken', field_names=['name'])
else:
# If there's no Team ID it means that the admin is creating a team with no ID.
if existing_team:
raise ValidationError('Team name has already been taken', field_names=['name'])
else:
current_team = get_current_team()
# We need to allow teams to edit themselves and allow the "conflict"
if data['name'] == current_team.name:
return data
else:
if existing_team:
raise ValidationError('Team name has already been taken', field_names=['name'])
@pre_load
def validate_email(self, data):
email = data.get('email')
if email is None:
return
obj = Teams.query.filter_by(email=email).first()
if obj:
if is_admin():
if data.get('id'):
target_user = Teams.query.filter_by(id=data['id']).first()
else:
target_user = get_current_team()
if target_user and obj.id != target_user.id:
raise ValidationError('Email address has already been used', field_names=['email'])
else:
if obj.id != get_current_team().id:
raise ValidationError('Email address has already been used', field_names=['email'])
views = {
'user': [
'website',
'name',
'country',
'affiliation',
'bracket',
'members',
'id',
'oauth_id',
],
'self': [
'website',
'name',
'email',
'country',
'affiliation',
'bracket',
'members',
'id',
'oauth_id',
'password'
],
'admin': [
'website',
'name',
'created',
'country',
'banned',
'email',
'affiliation',
'secret',
'bracket',
'members',
'hidden',
'id',
'oauth_id',
'password'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(TeamSchema, self).__init__(*args, **kwargs)

38
CTFd/schemas/unlocks.py Normal file
View File

@ -0,0 +1,38 @@
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Unlocks
class UnlockSchema(ma.ModelSchema):
class Meta:
model = Unlocks
include_fk = True
dump_only = ('id', 'date')
views = {
'admin': [
'user_id',
'target',
'team_id',
'date',
'type',
'id'
],
'user': [
'target',
'date',
'type',
'id'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(UnlockSchema, self).__init__(*args, **kwargs)

170
CTFd/schemas/users.py Normal file
View File

@ -0,0 +1,170 @@
from flask import session
from sqlalchemy.sql.expression import union_all
from marshmallow import fields, post_load
from marshmallow import validate, ValidationError, pre_load
from marshmallow.decorators import validates_schema
from marshmallow_sqlalchemy import field_for
from CTFd.models import ma, Users
from CTFd.utils.validators import unique_email, validate_country_code
from CTFd.utils.user import is_admin, get_current_user
from CTFd.utils.countries import lookup_country_code
from CTFd.utils.crypto import verify_password, hash_password
class UserSchema(ma.ModelSchema):
class Meta:
model = Users
include_fk = True
dump_only = ('id', 'oauth_id', 'created')
load_only = ('password',)
name = field_for(
Users,
'name',
required=True,
validate=[
validate.Length(min=1, max=128, error='Team names must not be empty')
]
)
email = field_for(
Users,
'email',
validate=[
validate.Email('Emails must be a properly formatted email address'),
validate.Length(min=1, max=128, error='Emails must not be empty'),
]
)
website = field_for(
Users,
'website',
validate=validate.URL(
error='Websites must be a proper URL starting with http or https',
schemes={'http', 'https'}
)
)
country = field_for(
Users,
'country',
validate=[
validate_country_code
]
)
password = field_for(
Users,
'password',
validate=[
validate.Length(min=1, error='Passwords must not be empty'),
]
)
@pre_load
def validate_name(self, data):
name = data.get('name')
if name is None:
return
existing_user = Users.query.filter_by(name=name).first()
user_id = data.get('id')
if user_id and is_admin():
if existing_user and existing_user.id != user_id:
raise ValidationError('User name has already been taken', field_names=['name'])
else:
current_user = get_current_user()
if name == current_user.name:
return data
else:
if existing_user:
raise ValidationError('User name has already been taken', field_names=['name'])
@pre_load
def validate_email(self, data):
email = data.get('email')
if email is None:
return
existing_user = Users.query.filter_by(email=email).first()
user_id = data.get('id')
if user_id and is_admin():
if existing_user and existing_user.id != user_id:
raise ValidationError('Email address has already been used', field_names=['email'])
else:
current_user = get_current_user()
if email == current_user.email:
return data
else:
if existing_user:
raise ValidationError('Email address has already been used', field_names=['email'])
@pre_load
def validate_password_confirmation(self, data):
password = data.get('password')
confirm = data.get('confirm')
target_user = get_current_user()
user_id = data.get('id')
if is_admin():
if user_id:
if password:
data['password'] = hash_password(data['password'])
return data
else:
if password and (confirm is None):
raise ValidationError('Please confirm your current password', field_names=['confirm'])
if password and confirm:
test = verify_password(plaintext=confirm, ciphertext=target_user.password)
if test is True:
data['password'] = hash_password(data['password'])
return data
else:
raise ValidationError('Your previous password is incorrect', field_names=['confirm'])
views = {
'user': [
'website',
'name',
'country',
'affiliation',
'bracket',
'id',
'oauth_id',
],
'self': [
'website',
'name',
'email',
'country',
'affiliation',
'bracket',
'id',
'oauth_id',
'password'
],
'admin': [
'website',
'name',
'created',
'country',
'banned',
'email',
'affiliation',
'secret',
'bracket',
'hidden',
'id',
'oauth_id',
'password',
'type'
]
}
def __init__(self, view=None, *args, **kwargs):
if view:
if type(view) == str:
kwargs['only'] = self.views[view]
elif type(view) == list:
kwargs['only'] = view
super(UserSchema, self).__init__(*args, **kwargs)

View File

@ -1,176 +1,20 @@
from flask import render_template, jsonify, Blueprint, redirect, url_for, request from flask import render_template, Blueprint, redirect, url_for, request
from sqlalchemy.sql.expression import union_all
from CTFd.models import db, Teams, Solves, Awards, Challenges from CTFd.utils import config
from CTFd.utils import get_config
from CTFd.utils.decorators.visibility import check_score_visibility
from CTFd import utils from CTFd.utils.scores import get_standings
scoreboard = Blueprint('scoreboard', __name__) scoreboard = Blueprint('scoreboard', __name__)
def get_standings(admin=False, count=None):
"""
Get team standings as a list of tuples containing team_id, team_name, and score e.g. [(team_id, team_name, score)].
Ties are broken by who reached a given score first based on the solve ID. Two users can have the same score but one
user will have a solve ID that is before the others. That user will be considered the tie-winner.
Challenges & Awards with a value of zero are filtered out of the calculations to avoid incorrect tie breaks.
"""
scores = db.session.query(
Solves.teamid.label('teamid'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges)\
.filter(Challenges.value != 0)\
.group_by(Solves.teamid)
awards = db.session.query(
Awards.teamid.label('teamid'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
)\
.filter(Awards.value != 0)\
.group_by(Awards.teamid)
"""
Filter out solves and awards that are before a specific time point.
"""
freeze = utils.get_config('freeze')
if not admin and freeze:
scores = scores.filter(Solves.date < utils.unix_time_to_utc(freeze))
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
"""
Combine awards and solves with a union. They should have the same amount of columns
"""
results = union_all(scores, awards).alias('results')
"""
Sum each of the results by the team id to get their score.
"""
sumscores = db.session.query(
results.columns.teamid,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.teamid)\
.subquery()
"""
Admins can see scores for all users but the public cannot see banned users.
Filters out banned users.
Properly resolves value ties by ID.
Different databases treat time precision differently so resolve by the row ID instead.
"""
if admin:
standings_query = db.session.query(
Teams.id.label('teamid'),
Teams.name.label('name'),
Teams.banned, sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Teams.id.label('teamid'),
Teams.name.label('name'),
sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
"""
Only select a certain amount of users if asked.
"""
if count is None:
standings = standings_query.all()
else:
standings = standings_query.limit(count).all()
db.session.close()
return standings
@scoreboard.route('/scoreboard') @scoreboard.route('/scoreboard')
def scoreboard_view(): @check_score_visibility
if utils.get_config('view_scoreboard_if_authed') and not utils.authed(): def listing():
return redirect(url_for('auth.login', next=request.path))
if utils.hide_scores():
return render_template('scoreboard.html', errors=['Scores are currently hidden'])
standings = get_standings() standings = get_standings()
return render_template('scoreboard.html', teams=standings, score_frozen=utils.is_scoreboard_frozen()) return render_template(
'scoreboard.html',
standings=standings,
@scoreboard.route('/scores') score_frozen=config.is_scoreboard_frozen()
def scores(): )
json = {'standings': []}
if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
return redirect(url_for('auth.login', next=request.path))
if utils.hide_scores():
return jsonify(json)
standings = get_standings()
for i, x in enumerate(standings):
json['standings'].append({'pos': i + 1, 'id': x.teamid, 'team': x.name, 'score': int(x.score)})
return jsonify(json)
@scoreboard.route('/top/<int:count>')
def topteams(count):
json = {'places': {}}
if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
return redirect(url_for('auth.login', next=request.path))
if utils.hide_scores():
return jsonify(json)
if count > 20 or count < 0:
count = 10
standings = get_standings(count=count)
team_ids = [team.teamid for team in standings]
solves = Solves.query.filter(Solves.teamid.in_(team_ids))
awards = Awards.query.filter(Awards.teamid.in_(team_ids))
freeze = utils.get_config('freeze')
if freeze:
solves = solves.filter(Solves.date < utils.unix_time_to_utc(freeze))
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
solves = solves.all()
awards = awards.all()
for i, team in enumerate(team_ids):
json['places'][i + 1] = {
'id': standings[i].teamid,
'name': standings[i].name,
'solves': []
}
for solve in solves:
if solve.teamid == team:
json['places'][i + 1]['solves'].append({
'chal': solve.chalid,
'team': solve.teamid,
'value': solve.chal.value,
'time': utils.unix_time(solve.date)
})
for award in awards:
if award.teamid == team:
json['places'][i + 1]['solves'].append({
'chal': None,
'team': award.teamid,
'value': award.value,
'time': utils.unix_time(award.date)
})
json['places'][i + 1]['solves'] = sorted(json['places'][i + 1]['solves'], key=lambda k: k['time'])
return jsonify(json)

149
CTFd/teams.py Normal file
View File

@ -0,0 +1,149 @@
from flask import current_app as app, render_template, request, redirect, abort, jsonify, url_for, session, Blueprint, \
Response, send_file
from CTFd.models import db, Users, Teams, Solves, Awards, Files, Pages, Tracking
from CTFd.utils.decorators import authed_only
from CTFd.utils.decorators.modes import require_team_mode
from CTFd.utils.modes import USERS_MODE
from CTFd.utils import config, get_config, set_config
from CTFd.utils.user import get_current_user, authed, get_ip
from CTFd.utils.dates import unix_time_to_utc
from CTFd.utils.crypto import verify_password
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
teams = Blueprint('teams', __name__)
@teams.route('/teams')
@check_account_visibility
@require_team_mode
def listing():
page = request.args.get('page', 1)
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
# TODO: Should teams confirm emails?
# if get_config('verify_emails'):
# count = Teams.query.filter_by(verified=True, banned=False).count()
# teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all()
# else:
count = Teams.query.filter_by(banned=False).count()
teams = Teams.query.filter_by(banned=False).slice(page_start, page_end).all()
pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('teams/teams.html', teams=teams, pages=pages, curr_page=page)
@teams.route('/teams/join', methods=['GET', 'POST'])
@authed_only
@require_team_mode
def join():
if request.method == 'GET':
return render_template('teams/join_team.html')
if request.method == 'POST':
teamname = request.form.get('name')
passphrase = request.form.get('password', '').strip()
team = Teams.query.filter_by(name=teamname).first()
user = get_current_user()
if team and verify_password(passphrase, team.password):
user.team_id = team.id
db.session.commit()
return redirect(url_for('challenges.listing'))
else:
errors = ['That information is incorrect']
return render_template('teams/join_team.html', errors=errors)
@teams.route('/teams/new', methods=['GET', 'POST'])
@authed_only
@require_team_mode
def new():
if request.method == 'GET':
return render_template("teams/new_team.html")
elif request.method == 'POST':
teamname = request.form.get('name')
passphrase = request.form.get('password', '').strip()
errors = []
user = get_current_user()
existing_team = Teams.query.filter_by(name=teamname).first()
if existing_team:
errors.append('That team name is already taken')
if not teamname:
errors.append('That team name is invalid')
if errors:
return render_template("teams/new_team.html", errors=errors)
team = Teams(
name=teamname,
password=passphrase
)
db.session.add(team)
db.session.commit()
user.team_id = team.id
db.session.commit()
return redirect(url_for('challenges.listing'))
@teams.route('/team')
@authed_only
@require_team_mode
def private():
user = get_current_user()
if not user.team_id:
return render_template(
'teams/team_enrollment.html',
)
team_id = user.team_id
team = Teams.query.filter_by(id=team_id).first_or_404()
solves = team.get_solves()
awards = team.get_awards()
place = team.place
score = team.score
return render_template(
'teams/team.html',
solves=solves,
awards=awards,
user=user,
team=team,
score=score,
place=place,
score_frozen=config.is_scoreboard_frozen()
)
@teams.route('/teams/<int:team_id>')
@check_account_visibility
@check_score_visibility
@require_team_mode
def public(team_id):
errors = []
team = Teams.query.filter_by(id=team_id).first_or_404()
solves = team.get_solves()
awards = team.get_awards()
place = team.place
score = team.score
if errors:
return render_template('teams/team.html', team=team, errors=errors)
return render_template(
'teams/team.html',
solves=solves,
awards=awards,
team=team,
score=score,
place=place,
score_frozen=config.is_scoreboard_frozen()
)

View File

@ -74,6 +74,13 @@ pre {
transition: background-color 0.3s, border-color 0.3s; transition: background-color 0.3s, border-color 0.3s;
} }
.input-filled-invalid {
background-color: transparent;
border-color: #d46767;
box-shadow: 0 0 0 0.2rem #d46767;
transition: background-color 0.3s, border-color 0.3s;
}
.btn-outlined.btn-theme { .btn-outlined.btn-theme {
background: none; background: none;
color: #545454; color: #545454;
@ -169,3 +176,9 @@ pre {
.cursor-help { .cursor-help {
cursor: help; cursor: help;
} }
.modal-content {
-webkit-border-radius: 0 !important;
-moz-border-radius: 0 !important;
border-radius: 0 !important;
}

View File

@ -1,10 +1,12 @@
pre { .chal-desc {
margin: 0; padding-left: 30px;
padding: 0; padding-right: 30px;
font-size: 14px;
} }
.chal-desc { .chal-desc img {
font-size: 14px; max-width: 100%;
height: auto;
} }
.modal-content { .modal-content {
@ -22,8 +24,35 @@ pre {
background-color: #5B7290 !important; background-color: #5B7290 !important;
} }
.challenge-button {
box-shadow: 3px 3px 3px grey;
}
.solved-challenge { .solved-challenge {
background-color: #37d63e !important; background-color: #37d63e !important;
opacity: 0.4; opacity: 0.4;
border: none; border: none;
} }
.corner-button-check {
margin-top: -10px;
margin-right: 25px;
position: absolute;
right: 0;
}
.key-submit .btn {
height: 51px;
}
#challenge-window .form-control {
position: relative;
display: block;
padding: 0.8em;
border-radius: 0;
background: #f0f0f0;
color: #aaa;
font-weight: 400;
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-appearance: none;
}

View File

@ -14,6 +14,6 @@ body {
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */ bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
width: 100%; width: 100%;
height: 60px; /* Set the fixed height of the footer here */ height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */ /*line-height: 60px; !* Vertically center the text there *!*/
/*background-color: #f5f5f5;*/ /*background-color: #f5f5f5;*/
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
@import url('https://use.fontawesome.com/releases/v5.0.9/css/all.css'); @import url('https://use.fontawesome.com/releases/v5.4.1/css/all.css');
@font-face { @font-face {
font-family: 'Font Awesome 5 Brands Offline'; font-family: 'Font Awesome 5 Brands Offline';

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 673 KiB

Some files were not shown because too many files have changed in this diff Show More