├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── config.py ├── ctftool ├── daemon_manager.py ├── daemons ├── cache_score.py └── shell_accounts.py ├── data ├── __init__.py ├── challenge.py ├── database.py ├── notification.py ├── scoreboard.py ├── ssh.py ├── team.py ├── ticket.py └── user.py ├── docs └── concepts.md ├── exceptions.py ├── requirements.txt ├── routes ├── __init__.py ├── admin.py ├── api.py ├── challenges.py ├── scoreboard.py ├── shell.py ├── teams.py ├── tickets.py └── users.py ├── secrets.example ├── service ├── ctf-platform.service └── ctf-platform.socket ├── static ├── css │ ├── flag-icon.min.css │ └── main.css ├── img │ └── logo.png └── js │ ├── api.js │ ├── main.js │ └── sorttable.js ├── templates ├── admin │ ├── base.html │ ├── dashboard.html │ ├── login.html │ ├── team.html │ ├── ticket_detail.html │ └── tickets.html ├── base.html ├── challenges │ ├── challenge_solves.html │ └── challenges.html ├── chat.html ├── scoreboard │ └── scoreboard.html ├── shell │ └── shell.html ├── teams │ └── team.html ├── tickets │ ├── open_ticket.html │ ├── ticket_detail.html │ └── tickets.html └── users │ ├── forgot_password.html │ ├── login.html │ ├── register.html │ ├── reset_password.html │ └── user.html ├── utils ├── __init__.py ├── admin.py ├── cache.py ├── captcha.py ├── decorators.py ├── email.py ├── misc.py ├── notification.py ├── ratelimit.py └── select.py └── yeshello.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # swap 92 | [._]*.s[a-w][a-z] 93 | [._]s[a-w][a-z] 94 | # session 95 | Session.vim 96 | # temporary 97 | .netrwhist 98 | *~ 99 | # auto-generated tag files 100 | tags 101 | 102 | dev.db 103 | dump.rdb 104 | /problems 105 | /problem_static 106 | /secrets 107 | /database 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 IceCTF 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | 10 | The original Flagbase license follows: 11 | 12 | Copyright 2015-2016, Fox Wilson 13 | 14 | Usage of the works is permitted provided that this instrument is retained with the works, so that any entity that uses the works is notified of this instrument. 15 | 16 | DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ColdCore 2 | 3 | This is the platform for IceCTF. It is based off of the platform for TJCTF, [TJCSec/ctf-platform](https://github.com/TJCSec/ctf-platform), although with hefty changes. 4 | 5 | ## Installation 6 | 7 | Install dependencies with `pip3 install -r requirements.txt`. 8 | 9 | Edit `config.py` to your liking. The variable names should be fairly 10 | self-explanatory. 11 | 12 | You're going to want to create a file called `secrets`. It will look like this: 13 | 14 | ```yml 15 | mailgun_url: https://api.mailgun.net/v3/tjctf.org 16 | mailgun_key: key-asdflkjasdhflkjsdahflkhsdaklfjhasd 17 | recaptcha_key: asdlkfjhasdlkjfhlsdakjfh 18 | recaptcha_secret: sdakjfhsdalkfjhsdalkfjh 19 | key: this can be anything you want, it is your flask secret_key 20 | shell_host: shell 21 | shell_username: shellaccounts 22 | shell_privkey: path to private key used to log in 23 | ``` 24 | 25 | For production, create a file called `database` as well, with the settings 26 | ``` 27 | host: localhost 28 | database: ctf 29 | user: user 30 | password: password 31 | ``` 32 | 33 | Then initiate the database with `./ctftool database create-tables`. 34 | 35 | To run the server with production settings, set the environment variable `PRODUCTION=1` before running the server. 36 | 37 | You can create some problem YAML files that look like this: 38 | 39 | ```yml 40 | name: Problem Name 41 | alias: problem alias 42 | author: ME! 43 | category: Binary 44 | description: binary binary binary binary. i love binary 45 | points: 250 46 | flag: "flag{whatever}" 47 | ``` 48 | 49 | Then add them with `./ctftool challenges add problem.yml` and it'll get put in the 50 | database. 51 | 52 | Run `python3 app.py` and you have a server running. You probably want to deploy 53 | it with `gunicorn` or similar, long-term. 54 | 55 | ## ctftool 56 | 57 | You can run `./ctftool challenges scan ../ctf-problems/` and get a fully populated database 58 | with information from all the problem.yml files, and automatically generated 59 | static file names, and automatic substitutions for static file links in 60 | problem.yml. More documentation on this to come soon. 61 | 62 | ## Contributing 63 | 64 | There are some missing features in the platform, and if you would like to contribute, feel free to send pull requests! 65 | 66 | If you find any issues or would like to send a feature request feel free to create an issue. 67 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, session, redirect, url_for, request, g, flash, jsonify 2 | 3 | import redis 4 | import socket 5 | import logging 6 | 7 | import config 8 | 9 | from utils import misc, select 10 | 11 | from data.database import db 12 | import data 13 | 14 | # Blueprints 15 | from routes import api, admin, teams, users, challenges, tickets, scoreboard, shell 16 | 17 | if config.production: 18 | logging.basicConfig(level=logging.INFO) 19 | else: 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | app = Flask(__name__) 23 | app.secret_key = config.secret.key 24 | 25 | 26 | app.register_blueprint(api.api) 27 | app.register_blueprint(admin.admin) 28 | app.register_blueprint(teams.teams) 29 | app.register_blueprint(users.users) 30 | app.register_blueprint(challenges.challenges) 31 | app.register_blueprint(tickets.tickets) 32 | app.register_blueprint(scoreboard.scoreboard) 33 | app.register_blueprint(shell.shell) 34 | 35 | 36 | @app.before_request 37 | def make_info_available(): 38 | if "user_id" in session: 39 | g.logged_in = True 40 | current_user = data.user.get_user(id=session["user_id"]) 41 | if current_user is not None: 42 | g.user = current_user 43 | g.user_restricts = g.user.restricts.split(",") 44 | g.team = g.user.team 45 | g.team_restricts = g.team.restricts.split(",") 46 | else: 47 | g.logged_in = False 48 | session.pop("user_id") 49 | return render_template("login.html") 50 | else: 51 | g.logged_in = False 52 | 53 | 54 | @app.context_processor 55 | def scoreboard_variables(): 56 | var = dict(config=config, select=select) 57 | if "user_id" in session: 58 | var["logged_in"] = True 59 | var["user"] = g.user 60 | var["team"] = g.team 61 | var["notifications"] = data.notification.get_notifications(team=g.team) 62 | else: 63 | var["logged_in"] = False 64 | var["notifications"] = [] 65 | 66 | return var 67 | 68 | 69 | @app.route('/') 70 | def root(): 71 | if g.logged_in: 72 | return redirect(url_for('teams.dashboard')) 73 | return redirect(url_for('users.register')) 74 | 75 | 76 | @app.route('/chat/') 77 | def chat(): 78 | return render_template("chat.html") 79 | 80 | 81 | # Debug 82 | @app.route('/debug/') 83 | def debug_app(): 84 | return jsonify(hostname=socket.gethostname()) 85 | 86 | 87 | # Manage Peewee database sessions and Redis 88 | @app.before_request 89 | def before_request(): 90 | db.connect() 91 | g.redis = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) 92 | g.connected = True 93 | 94 | 95 | @app.teardown_request 96 | def teardown_request(exc): 97 | if getattr(g, 'connected', False): 98 | db.close() 99 | g.redis.connection_pool.disconnect() 100 | 101 | 102 | # CSRF things 103 | @app.before_request 104 | def csrf_protect(): 105 | csrf_exempt = ['/teamconfirm/'] 106 | 107 | if request.method == "POST": 108 | token = session.get('_csrf_token', None) 109 | if (not token or token != request.form["_csrf_token"]) and request.path not in csrf_exempt: 110 | return "Invalid CSRF token!" 111 | 112 | 113 | def generate_csrf_token(): 114 | if '_csrf_token' not in session: 115 | session['_csrf_token'] = misc.generate_random_string(64) 116 | return session['_csrf_token'] 117 | 118 | 119 | app.jinja_env.globals['csrf_token'] = generate_csrf_token 120 | 121 | if __name__ == '__main__': 122 | app.run(debug=True, port=8001) 123 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | production = os.getenv("PRODUCTION", None) is not None 5 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | ctf_name = "IceCTF" 8 | eligibility = "In order to be eligible for prizes, all members of your team must be Icelandic residents, and you must not have more than three team members." 9 | tagline = "The Icelandic Hacking Competition" 10 | # IRC Channel 11 | ctf_chat_channel = "#IceCTF" 12 | ctf_home_url = "https://icec.tf" 13 | 14 | # Serve javascript libraries from CDN 15 | cdn = True 16 | # Allow users to submit via an api? 17 | apisubmit = True 18 | # Allow registration? 19 | registration = True 20 | # If running behind proxy (nginx), which header contains the real IP 21 | proxied_ip_header = "X-Forwarded-For" 22 | # How many teams to show on the scoreboard graph 23 | teams_on_graph = 10 24 | 25 | # Which email to send out notifications from 26 | mail_from = "notice@icec.tf" 27 | 28 | # Wether to render the scoreboard on request or cache 29 | immediate_scoreboard = False 30 | 31 | # Banned email domains 32 | disallowed_domain = "icec.tf" 33 | 34 | # Where the static stuff is stored 35 | static_prefix = "/problem-static/" 36 | static_dir = "{}/problem_static/".format(os.path.dirname(os.path.abspath(__file__))) 37 | custom_stylesheet = "css/main.css" 38 | 39 | # Shell accounts? 40 | 41 | enable_shell = True 42 | 43 | shell_port = 22 44 | shell_host = "shell.icec.tf" 45 | 46 | shell_user_prefixes = ["ctf-"] 47 | shell_password_length = 8 48 | shell_free_acounts = 10 49 | shell_max_accounts = 99999 50 | 51 | shell_user_creation = "sudo useradd -m {username} -p {password} -g ctf -b /home_users" 52 | 53 | 54 | # when the competition begins 55 | competition_begin = datetime(1970, 1, 1, 0, 0) 56 | competition_end = datetime(2018, 1, 1, 0, 0) 57 | 58 | if production: 59 | competition_begin = datetime(2016, 8, 12, hour=16, minute=0, second=0) 60 | competition_end = datetime(2016, 8, 26, hour=16, minute=0, second=0) 61 | 62 | 63 | def competition_is_running(): 64 | return competition_begin < datetime.now() < competition_end 65 | 66 | 67 | def competition_has_started(): 68 | return competition_begin < datetime.now() 69 | 70 | # Don't touch these. Instead, copy secrets.example to secrets and edit that. 71 | import yaml 72 | from collections import namedtuple 73 | with open("secrets") as f: 74 | _secret = yaml.load(f) 75 | secret = namedtuple('SecretsDict', _secret.keys())(**_secret) 76 | 77 | _redis = { 78 | 'host': 'localhost', 79 | 'port': 6379, 80 | 'db': 0 81 | } 82 | 83 | if production: 84 | with open("database") as f: 85 | _database = yaml.load(f) 86 | database = namedtuple('DatabaseDict', _database.keys())(**_database) 87 | _redis['db'] = 1 88 | 89 | redis = namedtuple('RedisDict', _redis.keys())(**_redis) 90 | -------------------------------------------------------------------------------- /ctftool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from data.database import * 3 | from datetime import datetime, timedelta 4 | import config 5 | import getpass 6 | import hashlib 7 | import os 8 | import os.path 9 | import shutil 10 | import sys 11 | import redis 12 | import random 13 | import utils 14 | import utils.admin 15 | import utils.misc 16 | import yaml 17 | import argparse 18 | import logging 19 | 20 | tables = [Team, User, UserAccess, Stage, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser, SshAccount] 21 | 22 | def create_tables(args): 23 | check = True 24 | if args.fail: 25 | check = False 26 | [i.create_table(check) for i in tables] 27 | print("Tables created") 28 | 29 | def drop_tables(args): 30 | if input("Are you sure? Type yes to continue: ") == "yes": 31 | [i.drop_table() for i in tables] 32 | print("Done") 33 | else: 34 | print("Okay, nothing happened.") 35 | 36 | 37 | 38 | 39 | def gen_team(args): 40 | n = args.team_count 41 | chals = list(Challenge.select()) 42 | ctz = datetime.now() 43 | diff = timedelta(minutes=5) 44 | for i in range(n): 45 | name = "Team {} {}".format(i + 1, utils.misc.generate_random_string(length=5)) 46 | team_key = utils.misc.generate_team_key() 47 | t = Team.create(name=name, affiliation="Autogenerated", key=team_key) 48 | username = "username{}".format(i+1) 49 | user = User.create(username=username, email="nope@nope.nope", 50 | background="university", country="ISL", 51 | tshirt_size="XL", gender="M", 52 | email_confirmation_key="autogen", email_confirmed=True, 53 | team=t) 54 | t.save() 55 | password = "password{}".format(user.id) 56 | user.set_password(password) 57 | user.save() 58 | print("Team added with id {}".format(t.id)) 59 | print("User added with username {} and password {}".format(username, password)) 60 | 61 | def add_admin(args): 62 | username = input("Username: ") 63 | password = getpass.getpass().encode() 64 | pwhash = utils.admin.create_password(password) 65 | r = random.SystemRandom() 66 | secret = "".join([r.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for i in range(16)]) 67 | AdminUser.create(username=username, password=pwhash, secret=secret) 68 | print("AdminUser created; Enter the following key into your favorite TOTP application (Google Authenticator Recommended): {}".format(secret)) 69 | def scan_challenges_problem(d, files): 70 | staticpaths = {} 71 | if "static.yml" in files: 72 | with open(os.path.join(d, "static.yml")) as f: 73 | statics = yaml.load(f) 74 | for static in statics: 75 | h = hashlib.sha256() 76 | with open(os.path.join(d, static), "rb") as staticfile: 77 | while True: 78 | buf = staticfile.read(4096) 79 | h.update(buf) 80 | if not buf: 81 | break 82 | 83 | if "." in static: 84 | name, ext = static.split(".", maxsplit=1) 85 | fn = "{}_{}.{}".format(name, h.hexdigest(), ext) 86 | else: 87 | fn = "{}_{}".format(static, h.hexdigest()) 88 | staticpaths[static] = fn 89 | shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn)) 90 | print(fn) 91 | 92 | with open(os.path.join(d, "problem.yml")) as f: 93 | data = yaml.load(f) 94 | print("Inserting problem in directory %s" % (d)) 95 | for i in staticpaths: 96 | print("looking for |{}|".format(i)) 97 | data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i])) 98 | 99 | data["stage"] = Stage.get(Stage.alias == data["stage"]) 100 | query = Challenge.select().where(Challenge.alias == data["alias"]) 101 | if query.exists(): 102 | print("Updating " + str(data["name"]) + "...") 103 | q = Challenge.update(**data).where(Challenge.alias == data["alias"]) 104 | q.execute() 105 | else: 106 | Challenge.create(**data) 107 | 108 | 109 | def scan_challenges_stage(d, files): 110 | 111 | with open(os.path.join(d, "stage.yml")) as f: 112 | data = yaml.load(f) 113 | query = Stage.select().where(Stage.alias == data["alias"]) 114 | if query.exists(): 115 | print("Updating %s..." % (data["alias"])) 116 | q = Stage.update(**data).where(Stage.alias == data["alias"]) 117 | q.execute() 118 | else: 119 | Stage.create(**data) 120 | 121 | 122 | def scan_challenges(args): 123 | path = args.path 124 | n = 0 125 | 126 | os.makedirs(config.static_dir, exist_ok=True) 127 | for root, dirs, files in os.walk(path): 128 | if "stage.yml" in files: 129 | scan_challenges_stage(root, files) 130 | if "problem.yml" in files: 131 | n += 1 132 | scan_challenges_problem(root, files) 133 | 134 | print(n, "challenges loaded") 135 | 136 | 137 | def add_challenge(args): 138 | challengefile = args.file 139 | with open(challengefile) as f: 140 | chal = Challenge.create(**yaml.load(f)) 141 | print("Challenge added with id {}".format(chal.id)) 142 | 143 | def gen_challenges(args): 144 | n = args.challenge_count 145 | for i in range(n): 146 | name = str(random.randint(0, 999999999)) 147 | chal = Challenge.create(name="Challenge {}".format(name), category="Generated", description="Lorem ipsum, dolor sit amet. The flag is {}".format(name), points=random.randint(50, 400), flag=name, author="autogen") 148 | print("Challenge added with id {}".format(chal.id)) 149 | 150 | 151 | 152 | def list_challenges(args): 153 | for chal in Challenge.select(): 154 | print("{} {}".format(str(chal.id).rjust(3), chal.name)) 155 | 156 | 157 | def del_challenge(args): 158 | id = args.id 159 | c = Challenge.get(id=id) 160 | ChallengeFailure.delete().where(ChallengeFailure.challenge == c).execute() 161 | ChallengeSolve.delete().where(ChallengeSolve.challenge == c).execute() 162 | c.delete_instance() 163 | 164 | def clear_challenges(args): 165 | ChallengeFailure.delete().execute() 166 | ChallengeSolve.delete().execute() 167 | Challenge.delete().execute() 168 | 169 | 170 | def recache_solves(args): 171 | r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) 172 | for chal in Challenge.select(): 173 | r.hset("solves", chal.id, chal.solves.count()) 174 | print(r.hvals("solves")) 175 | 176 | def main(): 177 | parser = argparse.ArgumentParser(description="{} problem manager".format(config.ctf_name)) 178 | debug_level = parser.add_mutually_exclusive_group() 179 | debug_level.add_argument('-v', '--verbose', help="Print intermediate results", action="store_true") 180 | debug_level.add_argument('-s', '--silent', help="Print out very little", action="store_true") 181 | subparser = parser.add_subparsers(help='Select one of the following actions') 182 | 183 | # Problems 184 | parser_problems = subparser.add_parser('challenges', help='Deal with challenges') 185 | subparser_problems = parser_problems.add_subparsers(help='Select one of the following actions') 186 | 187 | parser_problems_scan = subparser_problems.add_parser('scan', help='Scan a path for problems') 188 | parser_problems_scan.add_argument("path", help="Directory for the path") 189 | parser_problems_scan.set_defaults(func=scan_challenges) 190 | 191 | parser_problems_generate = subparser_problems.add_parser('generate', help='Generate some dummy problems') 192 | parser_problems_generate.add_argument("challenge_count", type=int, help="number of problems to generate") 193 | parser_problems_generate.set_defaults(func=gen_challenges) 194 | 195 | parser_problems_add = subparser_problems.add_parser('add', help='add a problem') 196 | parser_problems_add.add_argument("file", help="file to use") 197 | parser_problems_add.set_defaults(func=add_challenge) 198 | 199 | parser_problems_list = subparser_problems.add_parser('list', help='List problems in the database') 200 | parser_problems_list.set_defaults(func=list_challenges) 201 | 202 | parser_problems_del = subparser_problems.add_parser('del', help='Delete a problem') 203 | parser_problems_del.add_argument('id', help="ID of problem to remove") 204 | parser_problems_del.set_defaults(func=del_challenge) 205 | 206 | parser_problems_clear = subparser_problems.add_parser('clear', help='Delete a problem') 207 | parser_problems_clear.set_defaults(func=clear_challenges) 208 | 209 | 210 | # Database 211 | parser_database = subparser.add_parser("database", help="Deal with database") 212 | subparser_database = parser_database.add_subparsers(help="Select one of the following actions") 213 | 214 | parser_database_create = subparser_database.add_parser("create-tables", help="create tables") 215 | parser_database_create.add_argument("-f", "--fail", help="Do not check if tables exist", action="store_true") 216 | parser_database_create.set_defaults(func=create_tables) 217 | 218 | parser_database_clear = subparser_database.add_parser("drop-tables", help="drop tables") 219 | parser_database_clear.set_defaults(func=drop_tables) 220 | 221 | parser_database_recache_solves = subparser_database.add_parser("recache-solves", help="recache solves") 222 | parser_database_recache_solves.set_defaults(func=recache_solves) 223 | 224 | # Users 225 | parser_user = subparser.add_parser("users", help="Deal with users") 226 | subparser_users = parser_user.add_subparsers(help="Select one of the following actions") 227 | 228 | parser_users_generate = subparser_users.add_parser('generate', help='Generate some dummy teams') 229 | parser_users_generate.add_argument("team_count", type=int, help="number of teams to generate") 230 | parser_users_generate.set_defaults(func=gen_team) 231 | 232 | parser_users_admin = subparser_users.add_parser('add-admin', help='Create a new administrator') 233 | parser_users_admin.set_defaults(func=add_admin) 234 | 235 | args = parser.parse_args() 236 | if args.silent: 237 | logging.basicConfig(level=logging.CRITICAL, stream=sys.stdout) 238 | elif args.verbose: 239 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 240 | else: 241 | logging.basicConfig(level=logging.WARNING, stream=sys.stdout) 242 | 243 | if 'func' in args: 244 | args.func(args) 245 | else: 246 | parser.print_help() 247 | 248 | main() 249 | 250 | # vim: syntax=python:ft=python 251 | -------------------------------------------------------------------------------- /daemon_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # from picoctf platform 3 | 4 | import glob, imp, argparse, time 5 | from os.path import splitext, basename 6 | import config 7 | 8 | def load_modules(directory): 9 | files = glob.glob("{}/*.py".format(directory)) 10 | return [imp.load_source(splitext(basename(module))[0], module) for module in files] 11 | 12 | def run_modules(modules, interval): 13 | while True: 14 | start_time = time.time() 15 | for module in modules: 16 | module.run() 17 | time.sleep(max(interval - (time.time() - start_time), 0)) 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser(description="{} daemon manager".format(config.ctf_name)) 21 | parser.add_argument("-l", action="store_true", dest="show_list", help="List all daemons") 22 | parser.add_argument("-a", "--all", dest="run_all", action="store_true", help="Run all daemons") 23 | parser.add_argument("-i", "--interval", action="store", type=int, help="The interval in which to run the daemons", default=60) 24 | parser.add_argument("-d", "--daemon-directory", action="store", help="The directory which contains the daemons", default="daemons") 25 | parser.add_argument("modules", nargs="*", help="The daemon modules to run") 26 | 27 | args = parser.parse_args() 28 | 29 | modules = load_modules(args.daemon_directory) 30 | 31 | if args.show_list: 32 | for module in modules: 33 | print(module.__name__) 34 | 35 | elif args.run_all: 36 | run_modules(modules, args.interval) 37 | 38 | else: 39 | if len(args.modules) == 0: 40 | parser.print_help() 41 | exit(1) 42 | 43 | selected_modules = [m for m in modules if m.__name__ in args.modules] 44 | if len(selected_modules) == 0: 45 | parser.print_help() 46 | exit(1) 47 | run_modules(selected_modules, args.interval) 48 | 49 | main() 50 | -------------------------------------------------------------------------------- /daemons/cache_score.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from data import scoreboard 3 | import json 4 | import config 5 | from data.database import db 6 | 7 | 8 | def run(): 9 | r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) 10 | db.connect() 11 | 12 | def set_complex(key, val): 13 | r.set(key, json.dumps(val)) 14 | data = scoreboard.calculate_scores() 15 | graphdata = scoreboard.calculate_graph(data) 16 | set_complex("scoreboard", data) 17 | set_complex("graph", graphdata) 18 | db.close() 19 | -------------------------------------------------------------------------------- /daemons/shell_accounts.py: -------------------------------------------------------------------------------- 1 | import spur 2 | import random 3 | import config 4 | from data.database import db 5 | from data import ssh 6 | import utils.misc 7 | 8 | 9 | def run(): 10 | db.connect() 11 | 12 | try: 13 | shell = spur.SshShell( 14 | hostname=config.shell_host, 15 | username=config.secret.shell_username, 16 | private_key_file=config.secret.shell_privkey, 17 | port=config.shell_port, 18 | missing_host_key=spur.ssh.MissingHostKey.accept 19 | ) 20 | 21 | result = shell.run(["sudo", "useradd", "--help"]) 22 | 23 | if result.return_code != 0: 24 | raise ValueError("Unable to sudo useradd.") 25 | 26 | account_count = ssh.count_accounts() 27 | if account_count >= config.shell_max_accounts: 28 | raise ValueError("Max SSH accounts already created!") 29 | 30 | free_account_count = ssh.count_unassigned() 31 | 32 | new_accounts = max(config.shell_free_acounts - free_account_count, 0) 33 | 34 | print("Checking that all teams have been assigned accounts...") 35 | 36 | print("{}/{} shell accounts allocated adding {}...".format(free_account_count, account_count, new_accounts)) 37 | teams = ssh.get_teams_without_ssh() 38 | 39 | if len(teams) > 0: 40 | print("{} teams without accounts present! Adding these as well.".format(len(teams))) 41 | new_accounts += len(teams) 42 | 43 | accounts = [] 44 | while new_accounts > 0: 45 | username = random.choice(config.shell_user_prefixes) + \ 46 | str(random.randint(0, config.shell_max_accounts)) 47 | 48 | plaintext_password = utils.misc.generate_random_string(config.shell_password_length, chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") 49 | 50 | hashed_password = shell.run(["bash", "-c", "echo '{}' | openssl passwd -1 -stdin".format(plaintext_password)]) 51 | hashed_password = hashed_password.output.decode("utf-8").strip() 52 | 53 | shell_cmd = config.shell_user_creation.format(username=username, password=hashed_password) 54 | 55 | result = shell.run(shell_cmd.split(), allow_error=True) 56 | 57 | if result.return_code == 9: 58 | print("Collision! Retrying.") 59 | continue 60 | elif result.return_code != 0: 61 | raise RuntimeError(result.stderr) 62 | 63 | print("\t{}:{}".format(username, plaintext_password)) 64 | 65 | account = { 66 | "username": username, 67 | "password": plaintext_password, 68 | "hostname": config.shell_host, 69 | "port": config.shell_port 70 | } 71 | 72 | accounts.append(account) 73 | 74 | new_accounts -= 1 75 | 76 | if len(accounts) > 0: 77 | ssh.create_accounts(accounts) 78 | print("Successfully imported accounts.") 79 | 80 | for team in teams: 81 | ssh.assign_shell_account(team) 82 | 83 | except spur.ssh.ConnectionError: 84 | raise RuntimeError("Could not connect to shell server.") 85 | db.close() 86 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCTF/ColdCore/20a27a874f1480edd9e2ed7e04f8b5aef3b3b3e5/data/__init__.py -------------------------------------------------------------------------------- /data/challenge.py: -------------------------------------------------------------------------------- 1 | from data.database import Stage, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, Team 2 | from datetime import datetime 3 | from exceptions import ValidationError 4 | from flask import g 5 | import config 6 | 7 | 8 | def get_stages(): 9 | return list(Stage.select().order_by(Stage.name)) 10 | 11 | 12 | def get_stage_challenges(stage_id): 13 | print(stage_id) 14 | return list(Challenge.select(Challenge.alias).where(Challenge.stage == stage_id)) 15 | 16 | 17 | def get_categories(): 18 | return [q.category for q in Challenge.select(Challenge.category).distinct().order_by(Challenge.category)] 19 | 20 | 21 | def get_challenges(): 22 | challenges = Challenge.select().where(Challenge.enabled == True).order_by(Challenge.stage, Challenge.points, Challenge.name) 23 | d = dict() 24 | for chall in challenges: 25 | if chall.stage_id in d: 26 | d[chall.stage_id].append(chall) 27 | else: 28 | d[chall.stage_id] = [chall] 29 | return d 30 | 31 | 32 | def get_solve_counts(): 33 | # TODO: optimize 34 | d = dict() 35 | for k in Challenge.select(Challenge.id): 36 | d[k.id] = get_solve_count(k.id) 37 | return d 38 | 39 | 40 | def get_solve_count(chall_id): 41 | s = g.redis.hget("solves", chall_id) 42 | if s is not None: 43 | return int(s.decode()) 44 | else: 45 | return -1 46 | 47 | 48 | def get_challenge(id=None, alias=None): 49 | try: 50 | if id is not None: 51 | return Challenge.get(Challenge.id == id, Challenge.enabled == True) 52 | elif alias is not None: 53 | return Challenge.get(Challenge.alias == alias, Challenge.enabled == True) 54 | else: 55 | raise ValueError("Invalid argument") 56 | except Challenge.DoesNotExist: 57 | raise ValidationError("Challenge does not exist!") 58 | 59 | 60 | def get_solved(team): 61 | return Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team) 62 | 63 | 64 | def get_solves(team): 65 | return ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).where(ChallengeSolve.team == g.team) 66 | 67 | 68 | def get_adjustments(team): 69 | return ScoreAdjustment.select().where(ScoreAdjustment.team == team) 70 | 71 | 72 | def get_challenge_solves(chall): 73 | return ChallengeSolve.select(ChallengeSolve, Team).join(Team).order_by(ChallengeSolve.time).where(ChallengeSolve.challenge == chall) 74 | 75 | 76 | def submit_flag(chall, user, team, flag): 77 | if team.solved(chall): 78 | raise ValidationError("Your team has already solved this problem!") 79 | elif not chall.enabled: 80 | raise ValidationError("This challenge is disabled.") 81 | elif flag.strip().lower() != chall.flag.strip().lower(): 82 | ChallengeFailure.create(user=user, team=team, challenge=chall, attempt=flag, time=datetime.now()) 83 | raise ValidationError("Incorrect flag") 84 | else: 85 | ChallengeSolve.create(user=user, team=team, challenge=chall, time=datetime.now()) 86 | g.redis.hincrby("solves", chall.id, 1) 87 | if config.immediate_scoreboard: 88 | g.redis.delete("scoreboard") 89 | g.redis.delete("graph") 90 | return "Correct!" 91 | -------------------------------------------------------------------------------- /data/database.py: -------------------------------------------------------------------------------- 1 | from peewee import PostgresqlDatabase, SqliteDatabase 2 | from peewee import Model, CharField, TextField, IntegerField, BooleanField, DateTimeField, ForeignKeyField, CompositeKey 3 | 4 | import bcrypt 5 | import config 6 | 7 | if config.production: 8 | db = PostgresqlDatabase(config.database.database, user=config.database.user, password=config.database.password) 9 | else: 10 | db = SqliteDatabase("dev.db") 11 | 12 | 13 | class BaseModel(Model): 14 | class Meta: 15 | database = db 16 | 17 | 18 | class Team(BaseModel): 19 | name = CharField(unique=True) 20 | affiliation = CharField(null=True) 21 | restricts = TextField(default="") 22 | key = CharField(unique=True, index=True) 23 | eligibility = BooleanField(null=True) 24 | 25 | def solved(self, challenge): 26 | return ChallengeSolve.select().where(ChallengeSolve.team == self, ChallengeSolve.challenge == challenge).count() 27 | 28 | def eligible(self): 29 | if self.eligibility is not None: 30 | return self.eligibility 31 | return all([member.eligible() for member in self.members]) and self.members.count() <= 3 32 | 33 | @property 34 | def score(self): 35 | challenge_points = sum([i.challenge.points for i in self.solves]) 36 | adjust_points = sum([i.value for i in self.adjustments]) 37 | return challenge_points + adjust_points 38 | 39 | 40 | class User(BaseModel): 41 | username = CharField(unique=True, index=True) 42 | email = CharField(index=True) 43 | email_confirmed = BooleanField(default=False) 44 | email_confirmation_key = CharField() 45 | password = CharField(null=True) 46 | background = CharField() 47 | country = CharField() 48 | tshirt_size = CharField(null=True) 49 | gender = CharField(null=True) 50 | first_login = BooleanField(default=True) 51 | restricts = TextField(default="") 52 | team = ForeignKeyField(Team, related_name="members") 53 | banned = BooleanField(default=False) 54 | password_reset_token = CharField(null=True) 55 | password_reset_expired = DateTimeField(null=True) 56 | 57 | def set_password(self, pw): 58 | self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt()) 59 | return 60 | 61 | def check_password(self, pw): 62 | return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8")) 63 | 64 | def eligible(self): 65 | return self.country == "ISL" and not self.banned 66 | 67 | 68 | class UserAccess(BaseModel): 69 | user = ForeignKeyField(User, related_name='accesses') 70 | ip = CharField() 71 | time = DateTimeField() 72 | 73 | 74 | class Stage(BaseModel): 75 | name = CharField() 76 | alias = CharField(unique=True, index=True) 77 | description = CharField(null=True) 78 | 79 | 80 | class Challenge(BaseModel): 81 | name = CharField() 82 | alias = CharField(unique=True, index=True) 83 | category = CharField() 84 | author = CharField() 85 | description = TextField() 86 | points = IntegerField() 87 | breakthrough_bonus = IntegerField(default=0) 88 | enabled = BooleanField(default=True) 89 | flag = TextField() 90 | stage = ForeignKeyField(Stage, related_name='challenges') 91 | 92 | 93 | class ChallengeSolve(BaseModel): 94 | user = ForeignKeyField(User, related_name='solves') 95 | team = ForeignKeyField(Team, related_name='solves') 96 | challenge = ForeignKeyField(Challenge, related_name='solves') 97 | time = DateTimeField() 98 | 99 | class Meta: 100 | primary_key = CompositeKey('team', 'challenge') 101 | 102 | 103 | class ChallengeFailure(BaseModel): 104 | user = ForeignKeyField(User, related_name='failures') 105 | team = ForeignKeyField(Team, related_name='failures') 106 | challenge = ForeignKeyField(Challenge, related_name='failures') 107 | attempt = CharField() 108 | time = DateTimeField() 109 | 110 | 111 | class NewsItem(BaseModel): 112 | summary = CharField() 113 | description = TextField() 114 | 115 | 116 | class TroubleTicket(BaseModel): 117 | team = ForeignKeyField(Team, related_name='tickets') 118 | summary = CharField() 119 | description = TextField() 120 | active = BooleanField(default=True) 121 | opened_at = DateTimeField() 122 | 123 | 124 | class TicketComment(BaseModel): 125 | ticket = ForeignKeyField(TroubleTicket, related_name='comments') 126 | comment_by = CharField() 127 | comment = TextField() 128 | time = DateTimeField() 129 | 130 | 131 | class Notification(BaseModel): 132 | team = ForeignKeyField(Team, related_name='notifications') 133 | notification = TextField() 134 | 135 | 136 | class ScoreAdjustment(BaseModel): 137 | team = ForeignKeyField(Team, related_name='adjustments') 138 | value = IntegerField() 139 | reason = TextField() 140 | 141 | 142 | class AdminUser(BaseModel): 143 | username = CharField() 144 | password = CharField() 145 | secret = CharField() 146 | 147 | 148 | class SshAccount(BaseModel): 149 | team = ForeignKeyField(Team, null=True, related_name='ssh_account') 150 | username = CharField() 151 | password = CharField() 152 | hostname = CharField() 153 | port = IntegerField() 154 | -------------------------------------------------------------------------------- /data/notification.py: -------------------------------------------------------------------------------- 1 | from data.database import Notification 2 | from exceptions import ValidationError 3 | 4 | def get_notifications(team): 5 | return Notification.select().where(Notification.team == team) 6 | 7 | def get_notification(team, id): 8 | try: 9 | return Notification.get(Notification.id == id and Notification.team == team) 10 | except Notification.DoesNotExist: 11 | raise ValidationError("Notification does not exist!") 12 | 13 | def delete_notification(notification): 14 | notification.delete_instance() 15 | -------------------------------------------------------------------------------- /data/scoreboard.py: -------------------------------------------------------------------------------- 1 | from .database import Team, Challenge, ChallengeSolve, ScoreAdjustment 2 | from datetime import datetime, timedelta 3 | 4 | import config 5 | 6 | 7 | def get_all_scores(teams, solves, adjustments): 8 | scores = {team.id: 0 for team in teams} 9 | for solve in solves: 10 | scores[solve.team_id] += solve.challenge.points 11 | 12 | for adjustment in adjustments: 13 | scores[adjustment.team_id] += adjustment.value 14 | 15 | return scores 16 | 17 | 18 | def get_last_solves(teams, solves): 19 | last = {team.id: datetime(1970, 1, 1) for team in teams} 20 | for solve in solves: 21 | if solve.time > last[solve.team_id]: 22 | last[solve.team_id] = solve.time 23 | return last 24 | 25 | 26 | def calculate_scores(): 27 | solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) 28 | adjustments = ScoreAdjustment.select() 29 | teams = Team.select() 30 | 31 | team_solves = {team.id: [] for team in teams} 32 | team_mapping = {team.id: team for team in teams} 33 | scores = {team.id: 0 for team in teams} 34 | for solve in solves: 35 | scores[solve.team_id] += solve.challenge.points 36 | team_solves[solve.team_id].append(solve) 37 | for adjustment in adjustments: 38 | scores[adjustment.team_id] += adjustment.value 39 | 40 | most_recent_solve = {tid: max([i.time for i in team_solves[tid]]) for tid in team_solves if team_solves[tid]} 41 | scores = {i: j for i, j in scores.items() if i in most_recent_solve} 42 | # eligible, teamid, teamname, affiliation, score 43 | return [(team_mapping[i[0]].eligible(), i[0], team_mapping[i[0]].name, team_mapping[i[0]].affiliation, i[1]) for idx, i in enumerate(sorted(scores.items(), key=lambda k: (-k[1], most_recent_solve[k[0]])))] 44 | 45 | 46 | def calculate_graph(scoredata): 47 | solves = list(ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).order_by(ChallengeSolve.time)) 48 | adjustments = list(ScoreAdjustment.select()) 49 | scoredata = [i for i in scoredata if i[0]] # Only eligible teams are on the score graph 50 | graph_data = [] 51 | for eligible, tid, name, affiliation, score in scoredata[:config.teams_on_graph]: 52 | our_solves = [i for i in solves if i.team_id == tid] 53 | team_data = [] 54 | s = sum([i.value for i in adjustments if i.team_id == tid]) 55 | for i in sorted(our_solves, key=lambda k: k.time): 56 | team_data.append((str(i.time), s)) 57 | s += i.challenge.points 58 | team_data.append((str(i.time + timedelta(microseconds=1000)), s)) 59 | team_data.append((str(datetime.now()), score)) 60 | graph_data.append((name, team_data)) 61 | return graph_data 62 | -------------------------------------------------------------------------------- /data/ssh.py: -------------------------------------------------------------------------------- 1 | from .database import SshAccount, Team 2 | from peewee import fn 3 | 4 | def count_accounts(): 5 | return SshAccount.select().count() 6 | 7 | def count_unassigned(): 8 | return SshAccount.select().where(SshAccount.team == None).count() 9 | 10 | def get_teams_without_ssh(): 11 | accounts = SshAccount.select(SshAccount.team).where(SshAccount.team.is_null(False)) 12 | return list(Team.select().where(Team.id.not_in(accounts))) 13 | 14 | def create_accounts(accounts): 15 | for account in accounts: 16 | SshAccount.create(username=account["username"], password=account["password"], 17 | hostname=account["hostname"], port=account["port"], team=None) 18 | 19 | def assign_shell_account(team): 20 | acct = SshAccount.select().order_by(fn.Random()).get() 21 | acct.team = team 22 | acct.save() 23 | 24 | def get_team_account(team): 25 | try: 26 | return team.ssh_account.get() 27 | except SshAccount.DoesNotExist: 28 | return None 29 | -------------------------------------------------------------------------------- /data/team.py: -------------------------------------------------------------------------------- 1 | from exceptions import ValidationError 2 | from .database import Team 3 | from utils import misc 4 | 5 | 6 | def get_team(id=None, name=None, key=None): 7 | try: 8 | if name: 9 | return Team.get(Team.name == name) 10 | elif id: 11 | return Team.get(Team.id == id) 12 | elif key: 13 | return Team.get(Team.key == key) 14 | else: 15 | raise ValueError("Invalid call") 16 | except Team.DoesNotExist: 17 | return None 18 | 19 | 20 | def validate(name, affiliation): 21 | if name is not None: 22 | if not name or len(name) > 50: 23 | raise ValidationError("A team name is required.") 24 | if get_team(name=name): 25 | raise ValidationError("A team with that name already exists.") 26 | 27 | 28 | def create_team(name, affiliation): 29 | if not affiliation: 30 | affiliation = "No affiliation" 31 | validate(name, affiliation) 32 | 33 | team_key = misc.generate_team_key() 34 | team = Team.create(name=name, affiliation=affiliation, key=team_key) 35 | 36 | return team 37 | 38 | 39 | def update_team(current_team, name, affiliation): 40 | if not affiliation: 41 | affiliation = "No affiliation" 42 | if current_team.name == name: 43 | name = None 44 | validate(name, affiliation) 45 | if name: 46 | current_team.name = name 47 | current_team.affiliation = affiliation 48 | current_team.save() 49 | -------------------------------------------------------------------------------- /data/ticket.py: -------------------------------------------------------------------------------- 1 | from data.database import TroubleTicket, TicketComment 2 | from datetime import datetime 3 | from exceptions import ValidationError 4 | 5 | 6 | def get_tickets(team): 7 | return team.tickets 8 | 9 | def get_ticket(team, id): 10 | try: 11 | return TroubleTicket.get(TroubleTicket.id == id, TroubleTicket.team == team) 12 | except TroubleTicket.DoesNotExist: 13 | raise ValidationError("Ticket not found!") 14 | 15 | def get_comments(ticket): 16 | return TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time) 17 | 18 | def create_ticket(team, summary, description): 19 | return TroubleTicket.create(team=team, summary=summary, description=description, opened_at=datetime.now()) 20 | 21 | def create_comment(ticket, user, comment): 22 | TicketComment.create(ticket=ticket, comment_by=user.username, comment=comment, time=datetime.now()) 23 | 24 | def open_ticket(ticket): 25 | ticket.active = True 26 | ticket.save() 27 | 28 | def close_ticket(ticket): 29 | ticket.active = False 30 | ticket.save() 31 | -------------------------------------------------------------------------------- /data/user.py: -------------------------------------------------------------------------------- 1 | from exceptions import ValidationError 2 | from .database import User, UserAccess 3 | from datetime import datetime, timedelta 4 | import utils 5 | import utils.email 6 | import utils.misc 7 | 8 | 9 | def get_user(username=None, id=None): 10 | try: 11 | if username: 12 | return User.get(User.username == username) 13 | elif id: 14 | return User.get(User.id == id) 15 | else: 16 | raise ValueError("Invalid call") 17 | except User.DoesNotExist: 18 | return None 19 | 20 | 21 | def login(username, password): 22 | user = get_user(username=username) 23 | if not user: 24 | return False, None 25 | 26 | if(user.check_password(password)): 27 | UserAccess.create(user=user, ip=utils.misc.get_ip(), time=datetime.now()) 28 | return True, user.id 29 | return False, None 30 | 31 | 32 | def validate(username, email, password, background, country, tshirt_size=None, gender=None): 33 | if not email or "." not in email or "@" not in email: 34 | raise ValidationError("You must have a valid email!") 35 | 36 | if not utils.email.is_valid_email(email): 37 | raise ValidationError("You're lying") 38 | 39 | if background not in utils.select.BackgroundKeys: 40 | raise ValidationError("Invalid Background") 41 | 42 | if country not in utils.select.CountryKeys: 43 | raise ValidationError("Invalid Background") 44 | 45 | if tshirt_size and (tshirt_size not in utils.select.TShirts): 46 | raise ValidationError("Invalid T-shirt size") 47 | 48 | if gender and (gender not in ["M", "F"]): 49 | raise ValidationError("Invalid gender") 50 | 51 | if password is not None: 52 | if len(password) < 6: 53 | raise ValidationError("Password is too short.") 54 | if username is not None: 55 | if not username or len(username) > 50: 56 | raise ValidationError("Invalid username") 57 | if get_user(username=username): 58 | raise ValidationError("That username has already been taken.") 59 | 60 | 61 | def create_user(username, email, password, background, country, team, tshirt_size=None, gender=None): 62 | validate(username, email, password, background, country, tshirt_size=tshirt_size, gender=gender) 63 | 64 | assert team is not None 65 | confirmation_key = utils.misc.generate_confirmation_key() 66 | 67 | user = User.create(username=username, email=email, 68 | background=background, country=country, 69 | tshirt_size=tshirt_size, gender=gender, 70 | email_confirmation_key=confirmation_key, 71 | team=team) 72 | user.set_password(password) 73 | user.save() 74 | 75 | UserAccess.create(user=user, ip=utils.misc.get_ip(), time=datetime.now()) 76 | 77 | utils.email.send_confirmation_email(email, confirmation_key) 78 | 79 | return user 80 | 81 | 82 | def confirm_email(current_user, confirmation_key): 83 | if current_user.email_confirmed: 84 | raise ValidationError("Email already confirmed") 85 | if current_user.email_confirmation_key == confirmation_key: 86 | current_user.email_confirmed = True 87 | current_user.save() 88 | else: 89 | raise ValidationError("Invalid confirmation key!") 90 | 91 | 92 | def forgot_password(username): 93 | user = get_user(username=username) 94 | if user is None: 95 | return 96 | user.password_reset_token = utils.misc.generate_confirmation_key() 97 | user.password_reset_expired = datetime.now() + timedelta(days=1) 98 | user.save() 99 | utils.email.send_password_reset_email(user.email, user.password_reset_token) 100 | 101 | 102 | def reset_password(token, password): 103 | if len(password) < 6: 104 | raise ValidationError("Password is too short!") 105 | try: 106 | user = User.get(User.password_reset_token == token) 107 | if user.password_reset_expired < datetime.now(): 108 | raise ValidationError("Token expired") 109 | user.set_password(password) 110 | user.password_reset_token = None 111 | user.save() 112 | except User.DoesNotExist: 113 | raise ValidationError("Invalid reset token!") 114 | 115 | 116 | def update_user(current_user, username, email, password, background, country, tshirt_size=None, gender=None): 117 | if username == current_user.username: 118 | username = None 119 | if password == "": 120 | password = None 121 | validate(username, email, password, background, country, tshirt_size, gender) 122 | if username: 123 | current_user.username = username 124 | if password: 125 | current_user.set_password(password) 126 | email_changed = (current_user.email != email) # send email after saving to db 127 | if email_changed: 128 | current_user.email_confirmation_key = utils.misc.generate_confirmation_key() 129 | current_user.email_confirmed = False 130 | current_user.email = email 131 | current_user.background = background 132 | current_user.country = country 133 | current_user.tshirt_size = tshirt_size 134 | current_user.gender = gender 135 | current_user.save() 136 | 137 | if email_changed: 138 | utils.email.send_confirmation_email(email, current_user.email_confirmation_key) 139 | return "Changes saved. Check your email for a new confirmation key." 140 | else: 141 | return "Changes saved." 142 | -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | # Flagbase concepts 2 | ## Competition 3 | A Competition is a single jeopardy-style CTF event. One instance of Flagbase 4 | corresponds to one Competition. 5 | 6 | ## Challenge 7 | A Challenge is a problem in a Competition. It either has a flag, or a grading 8 | script. It has a fixed number of points. 9 | 10 | ## Team 11 | A Team is a participant in a Competition. A Team can have multiple members, or 12 | a single member. Flagbase does not keep track of team membership. 13 | 14 | ## ScoreAdjustment 15 | Teams can score points in a Competition in two ways: they can either solve 16 | Challenges, or have ScoreAdjustments set on them. ScoreAdjustments are 17 | arbitrary point adjustments set by an AdminUser. They can be either positive or 18 | negative. 19 | 20 | ## AdminUser 21 | In the administrative interface, an AdminUser is an authorized user who can 22 | perform certain maintenance actions on the Competition, Challenges, and Teams. 23 | These actions can also be performed by the ctftool script, so an AdminUser can 24 | also be considered someone with access to the database. 25 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | pass 3 | 4 | class CaptchaError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | peewee 3 | flask 4 | bcrypt>=3.1.0 5 | redis 6 | pyyaml 7 | oath 8 | pycountry 9 | psycopg2 10 | spur 11 | -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCTF/ColdCore/20a27a874f1480edd9e2ed7e04f8b5aef3b3b3e5/routes/__init__.py -------------------------------------------------------------------------------- /routes/admin.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, session, redirect, url_for, flash 2 | from data.database import AdminUser, Team, Challenge, ChallengeSolve, ScoreAdjustment, TroubleTicket, TicketComment, Notification 3 | import utils 4 | import utils.admin 5 | from data import scoreboard 6 | from utils.decorators import admin_required, csrf_check 7 | from utils.notification import make_link 8 | from datetime import datetime 9 | from config import secret 10 | admin = Blueprint("admin", __name__, url_prefix="/admin") 11 | 12 | 13 | @admin.route("/") 14 | def admin_root(): 15 | if "admin" in session: 16 | return redirect(url_for(".admin_dashboard")) 17 | else: 18 | return redirect(url_for(".admin_login")) 19 | 20 | 21 | @admin.route("/login/", methods=["GET", "POST"]) 22 | def admin_login(): 23 | if request.method == "GET": 24 | return render_template("admin/login.html") 25 | 26 | elif request.method == "POST": 27 | username = request.form["username"] 28 | password = request.form["password"] 29 | two = request.form["two"] 30 | if getattr(secret, "admin_username", False): 31 | if username == secret.admin_username and password == secret.admin_password: 32 | session["admin"] = username 33 | return redirect(url_for(".admin_dashboard")) 34 | else: 35 | try: 36 | user = AdminUser.get(AdminUser.username == username) 37 | result = utils.admin.verify_password(user, password) 38 | result = result and utils.admin.verify_otp(user, two) 39 | if result: 40 | session["admin"] = user.username 41 | return redirect(url_for(".admin_dashboard")) 42 | except AdminUser.DoesNotExist: 43 | pass 44 | flash("Y̸̤̗͍̘ͅo͙̠͈͎͎͙̟u̺ ̘̘̘̹̩̹h͔̟̟̗͠a̠͈v͍̻̮̗̬̬̣e̟̫̼̹̠͕ ̠̳͖͡ma͈̱͟d̙͍̀ͅe̵͕̙̯̟̟̞̳ ͉͚̙a̡̱̮̫̰̰ ̜̙̝̭͚t̜̙͚̗͇ͅͅe͉r҉r̸͎̝̞̙̦̹i͏̙b̶̜̟̭͕l̗̰̰̠̳̝̕e͎̥ ̸m̰̯̮̲̘̻͍̀is̜̲̮͍͔̘͕͟t̟͈̮a̙̤͎̠ķ̝̺͇̩e̷͍̤̠͖̣͈.̺̩̦̻.") 45 | return render_template("admin/login.html") 46 | 47 | 48 | @admin.route("/dashboard/") 49 | @admin_required 50 | def admin_dashboard(): 51 | teams = Team.select() 52 | solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) 53 | adjustments = ScoreAdjustment.select() 54 | scoredata = scoreboard.get_all_scores(teams, solves, adjustments) 55 | lastsolvedata = scoreboard.get_last_solves(teams, solves) 56 | tickets = list(TroubleTicket.select().where(TroubleTicket.active == True)) 57 | return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets) 58 | 59 | 60 | @admin.route("/tickets/") 61 | @admin_required 62 | def admin_tickets(): 63 | tickets = list(TroubleTicket.select(TroubleTicket, Team).join(Team).order_by(TroubleTicket.id.desc())) 64 | return render_template("admin/tickets.html", tickets=tickets) 65 | 66 | 67 | @admin.route("/tickets//") 68 | @admin_required 69 | def admin_ticket_detail(ticket): 70 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 71 | comments = list(TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)) 72 | return render_template("admin/ticket_detail.html", ticket=ticket, comments=comments) 73 | 74 | 75 | @admin.route("/tickets//comment/", methods=["POST"]) 76 | @admin_required 77 | def admin_ticket_comment(ticket): 78 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 79 | if request.form["comment"]: 80 | TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now()) 81 | Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) 82 | flash("Comment added.") 83 | 84 | if ticket.active and "resolved" in request.form: 85 | ticket.active = False 86 | ticket.save() 87 | Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) 88 | flash("Ticket closed.") 89 | 90 | elif not ticket.active and "resolved" not in request.form: 91 | ticket.active = True 92 | ticket.save() 93 | Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) 94 | flash("Ticket reopened.") 95 | 96 | return redirect(url_for(".admin_ticket_detail", ticket=ticket.id)) 97 | 98 | 99 | @admin.route("/team//") 100 | @admin_required 101 | def admin_show_team(tid): 102 | team = Team.get(Team.id == tid) 103 | return render_template("admin/team.html", team=team) 104 | 105 | 106 | @admin.route("/team///impersonate/") 107 | @csrf_check 108 | @admin_required 109 | def admin_impersonate_team(tid): 110 | session["team_id"] = tid 111 | return redirect(url_for("scoreboard")) 112 | 113 | 114 | @admin.route("/team///toggle_eligibility/") 115 | @csrf_check 116 | @admin_required 117 | def admin_toggle_eligibility(tid): 118 | team = Team.get(Team.id == tid) 119 | if team.eligibility is None: 120 | team.eligibility = False 121 | else: 122 | team.eligibility = not team.eligibility 123 | team.save() 124 | flash("Eligibility set to {}".format(team.eligible)) 125 | return redirect(url_for(".admin_show_team", tid=tid)) 126 | 127 | 128 | @admin.route("/team//adjust_score/", methods=["POST"]) 129 | @admin_required 130 | def admin_score_adjust(tid): 131 | value = int(request.form["value"]) 132 | reason = request.form["reason"] 133 | 134 | team = Team.get(Team.id == tid) 135 | 136 | ScoreAdjustment.create(team=team, value=value, reason=reason) 137 | flash("Score adjusted.") 138 | 139 | return redirect(url_for(".admin_show_team", tid=tid)) 140 | 141 | 142 | @admin.route("/logout/") 143 | def admin_logout(): 144 | del session["admin"] 145 | return redirect(url_for('.admin_login')) 146 | -------------------------------------------------------------------------------- /routes/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, g, request 2 | from data import challenge, notification, scoreboard 3 | from utils import decorators, ratelimit 4 | import exceptions 5 | from datetime import datetime 6 | import config 7 | 8 | api = Blueprint("api", __name__, url_prefix="/api") 9 | 10 | 11 | @api.route("/submit/.json", methods=["POST"]) 12 | @decorators.must_be_allowed_to("solve challenges") 13 | @decorators.must_be_allowed_to("view challenges") 14 | @decorators.competition_running_required 15 | @decorators.confirmed_email_required 16 | @ratelimit.ratelimit(limit=10, per=120, over_limit=ratelimit.on_over_api_limit) 17 | def submit_api(challenge_id): 18 | try: 19 | chall = challenge.get_challenge(alias=challenge_id) 20 | except exceptions.ValidationError as e: 21 | return jsonify(dict(code=1001, message=str(e))) 22 | flag = request.form["flag"] 23 | 24 | try: 25 | challenge.submit_flag(chall, g.user, g.team, flag) 26 | solves = challenge.get_solve_count(chall.id) 27 | return jsonify(dict(code=0, message="Success!", solves=solves)) 28 | except exceptions.ValidationError as e: 29 | return jsonify(dict(code=1001, message=str(e))) 30 | 31 | 32 | @api.route("/dismiss/.json", methods=["POST"]) 33 | @decorators.login_required 34 | def dismiss_notification(nid): 35 | try: 36 | n = notification.get_notification(g.team, nid) 37 | notification.delete_notification(n) 38 | return jsonify(dict(code=0, message="Success!")) 39 | except exceptions.ValidationError as e: 40 | return jsonify(dict(code=1001, message=str(e))) 41 | 42 | 43 | @api.route("/_ctftime/") 44 | def ctftime_scoreboard_json(): 45 | if not config.immediate_scoreboard and datetime.now() < config.competition_end: 46 | return "unavailable", 503 47 | 48 | scores = scoreboard.calculate_scores() 49 | standings = [dict(team=i[2], score=i[4], outward=not i[0]) for i in scores] 50 | for index, standing in enumerate(standings): 51 | standing["pos"] = index + 1 52 | 53 | return jsonify(standings=standings) 54 | -------------------------------------------------------------------------------- /routes/challenges.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, request, render_template, flash, redirect, url_for 2 | 3 | from utils import decorators, ratelimit 4 | from data import challenge 5 | 6 | import exceptions 7 | 8 | challenges = Blueprint("challenges", __name__, template_folder="../templates/challenges") 9 | 10 | 11 | @challenges.route('/challenges/') 12 | @decorators.must_be_allowed_to("view challenges") 13 | @decorators.competition_started_required 14 | @decorators.confirmed_email_required 15 | def index(): 16 | stages = challenge.get_stages() 17 | challs = challenge.get_challenges() 18 | solved = challenge.get_solved(g.team) 19 | solves = challenge.get_solve_counts() 20 | categories = challenge.get_categories() 21 | first_stage = {chall.alias: True for chall in challs[stages[0].id]} if stages else None 22 | return render_template("challenges.html", stages=stages, first_stage=first_stage, challenges=challs, solved=solved, categories=categories, solves=solves) 23 | 24 | 25 | @challenges.route('/challenges//solves/') 26 | @decorators.must_be_allowed_to("view challenge solves") 27 | @decorators.must_be_allowed_to("view challenges") 28 | @decorators.competition_started_required 29 | @decorators.confirmed_email_required 30 | def show_solves(challenge_id): 31 | try: 32 | chall = challenge.get_challenge(alias=challenge_id) 33 | except exceptions.ValidationError as e: 34 | flash(str(e)) 35 | return redirect(url_for(".index")) 36 | solves = challenge.get_challenge_solves(chall) 37 | return render_template("challenge_solves.html", challenge=chall, solves=solves) 38 | 39 | 40 | @challenges.route('/submit//', methods=["POST"]) 41 | @decorators.must_be_allowed_to("solve challenges") 42 | @decorators.must_be_allowed_to("view challenges") 43 | @decorators.competition_running_required 44 | @decorators.confirmed_email_required 45 | @ratelimit.ratelimit(limit=10, per=120) 46 | def submit(challenge_id): 47 | try: 48 | chall = challenge.get_challenge(challenge_id) 49 | except exceptions.ValidationError as e: 50 | flash(str(e)) 51 | return redirect(url_for(".index")) 52 | flag = request.form["flag"] 53 | 54 | try: 55 | challenge.submit_flag(chall, g.user, g.team, flag) 56 | flash("Success!") 57 | except exceptions.ValidationError as e: 58 | flash(str(e)) 59 | return redirect(url_for('.index')) 60 | -------------------------------------------------------------------------------- /routes/scoreboard.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | from utils import cache 4 | 5 | import data 6 | import config 7 | 8 | scoreboard = Blueprint("scoreboard", __name__, template_folder="../templates/scoreboard") 9 | 10 | 11 | @scoreboard.route('/scoreboard/') 12 | def index(): 13 | scoreboard_data = cache.get_complex("scoreboard") 14 | graph_data = cache.get_complex("graph") 15 | if scoreboard_data is None or graph_data is None: 16 | if config.immediate_scoreboard: 17 | scoreboard_data = scoreboard.calculate_scores() 18 | graph_data = scoreboard.calculate_graph(data) 19 | cache.set_complex("scoreboard", scoreboard_data, 120) 20 | cache.set_complex("graph", graph_data, 120) 21 | else: 22 | return "CTF hasn't started!" 23 | 24 | return render_template("scoreboard.html", data=scoreboard_data, graphdata=graph_data) 25 | -------------------------------------------------------------------------------- /routes/shell.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, render_template 2 | 3 | from data import ssh 4 | from utils import decorators 5 | 6 | 7 | shell = Blueprint("shell", __name__, template_folder="../templates/shell") 8 | 9 | 10 | @shell.route('/shell/') 11 | @decorators.must_be_allowed_to("access shell") 12 | @decorators.competition_started_required 13 | @decorators.confirmed_email_required 14 | def index(): 15 | account = ssh.get_team_account(g.team) 16 | return render_template("shell.html", account=account) 17 | -------------------------------------------------------------------------------- /routes/teams.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, request, render_template, flash, url_for, redirect 2 | 3 | from data import team, challenge 4 | 5 | from utils import decorators, ratelimit 6 | import utils 7 | import config 8 | 9 | import exceptions 10 | 11 | teams = Blueprint("teams", __name__, template_folder="../templates/teams") 12 | # Things that require a team 13 | 14 | 15 | @teams.route('/team/', methods=["GET", "POST"]) 16 | @decorators.login_required 17 | @ratelimit.ratelimit(limit=6, per=120) 18 | def dashboard(): 19 | if request.method == "GET": 20 | team_solves = challenge.get_solves(g.team) 21 | team_adjustments = challenge.get_adjustments(g.team) 22 | team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments]) 23 | return render_template("team.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score) 24 | elif request.method == "POST": 25 | 26 | team_name = request.form["team_name"].strip() 27 | affiliation = request.form["team_affiliation"].strip() 28 | 29 | try: 30 | team.update_team(g.team, team_name, affiliation) 31 | flash("Changes saved.") 32 | except exceptions.ValidationError as e: 33 | flash(str(e)) 34 | 35 | return redirect(url_for('.dashboard')) 36 | 37 | 38 | @teams.route('/teamconfirm/', methods=["POST"]) 39 | def teamconfirm(): 40 | if utils.misc.get_ip() in config.confirm_ip: 41 | team_name = request.form["team_name"].strip() 42 | team_key = request.form["team_key"].strip() 43 | t = team.get_team(name=team_name) 44 | if t is None: 45 | return "invalid", 403 46 | if t.key == team_key: 47 | return "ok", 200 48 | else: 49 | return "invalid", 403 50 | else: 51 | return "unauthorized", 401 52 | -------------------------------------------------------------------------------- /routes/tickets.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, request, render_template, redirect, url_for, flash 2 | 3 | from utils import decorators, ratelimit 4 | 5 | from data import ticket 6 | 7 | import exceptions 8 | 9 | tickets = Blueprint("tickets", __name__, template_folder="../templates/tickets") 10 | # Trouble tickets 11 | 12 | 13 | @tickets.route('/tickets/') 14 | @decorators.must_be_allowed_to("view tickets") 15 | @decorators.login_required 16 | def index(): 17 | return render_template("tickets.html", tickets=list(ticket.get_tickets(g.team))) 18 | 19 | 20 | @tickets.route('/tickets/new/', methods=["GET", "POST"]) 21 | @decorators.must_be_allowed_to("submit tickets") 22 | @decorators.must_be_allowed_to("view tickets") 23 | @decorators.login_required 24 | @ratelimit.ratelimit(limit=1, per=120) 25 | def open_ticket(): 26 | if request.method == "GET": 27 | return render_template("open_ticket.html") 28 | elif request.method == "POST": 29 | summary = request.form["summary"] 30 | description = request.form["description"] 31 | t = ticket.create_ticket(g.team, summary, description) 32 | flash("Ticket #{} opened.".format(t.id)) 33 | return redirect(url_for(".detail", ticket_id=t.id)) 34 | 35 | 36 | @tickets.route('/tickets//') 37 | @decorators.must_be_allowed_to("view tickets") 38 | @decorators.login_required 39 | def detail(ticket_id): 40 | try: 41 | t = ticket.get_ticket(g.team, ticket_id) 42 | except exceptions.ValidationError as e: 43 | flash(str(e)) 44 | return redirect(url_for(".index")) 45 | 46 | comments = ticket.get_comments(t) 47 | return render_template("ticket_detail.html", ticket=t, comments=comments) 48 | 49 | 50 | @tickets.route('/tickets//comment/', methods=["POST"]) 51 | @decorators.must_be_allowed_to("comment on tickets") 52 | @decorators.must_be_allowed_to("view tickets") 53 | @ratelimit.ratelimit(limit=1, per=120) 54 | def comment(ticket_id): 55 | try: 56 | t = ticket.get_ticket(g.team, ticket_id) 57 | except exceptions.ValidationError as e: 58 | flash(str(e)) 59 | return redirect(url_for(".index")) 60 | 61 | if request.form["comment"]: 62 | ticket.create_comment(t, g.user, request.form["comment"]) 63 | flash("Comment added.") 64 | 65 | if t.active and "resolved" in request.form: 66 | ticket.close_ticket(t) 67 | flash("Ticket closed.") 68 | 69 | elif not t.active and "resolved" not in request.form: 70 | ticket.open_ticket(t) 71 | flash("Ticket re-opened.") 72 | 73 | return redirect(url_for(".detail", ticket_id=t.id)) 74 | -------------------------------------------------------------------------------- /routes/users.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, request, render_template, url_for, redirect, session, flash 2 | 3 | from data import user, team 4 | 5 | from utils import decorators, ratelimit, captcha 6 | import config 7 | import exceptions 8 | 9 | users = Blueprint("users", __name__, template_folder="../templates/users") 10 | 11 | 12 | @users.route('/login/', methods=["GET", "POST"]) 13 | @ratelimit.ratelimit(limit=6, per=120) 14 | def login(): 15 | if request.method == "GET": 16 | return render_template("login.html") 17 | elif request.method == "POST": 18 | username = request.form["username"] 19 | password = request.form["password"] 20 | 21 | success, id = user.login(username, password) 22 | if success: 23 | session["user_id"] = id 24 | flash("Login successful.") 25 | return redirect(url_for('teams.dashboard')) 26 | else: 27 | flash("Incorrect username or password", "error") 28 | return redirect(url_for('.login')) 29 | 30 | 31 | @users.route('/register/', methods=["GET", "POST"]) 32 | @ratelimit.ratelimit(limit=6, per=120) 33 | def register(): 34 | if not config.registration: 35 | if "admin" in session and session["admin"]: 36 | pass 37 | else: 38 | return "Registration is currently disabled. Email icectf@icec.tf to create an account." 39 | 40 | if request.method == "GET": 41 | return render_template("register.html") 42 | elif request.method == "POST": 43 | try: 44 | captcha.verify_captcha() 45 | except exceptions.CaptchaError as e: 46 | flash(str(e)) 47 | return redirect(url_for(".register")) 48 | 49 | username = request.form["username"].strip() 50 | user_email = request.form["email"].strip() 51 | password = request.form["password"].strip() 52 | confirm_password = request.form["confirm_password"].strip() 53 | background = request.form["background"].strip() 54 | country = request.form["country"].strip() 55 | 56 | tshirt_size = "" 57 | gender = "" 58 | if "tshirt_size" in request.form.keys(): 59 | tshirt_size = request.form["tshirt_size"].strip() 60 | if "gender" in request.form.keys(): 61 | gender = request.form["gender"].strip() 62 | 63 | if password != confirm_password: 64 | flash("Passwords do not match!") 65 | return redirect(url_for('.register')) 66 | 67 | join_team = bool(int(request.form["join_team"].strip())) 68 | if join_team: 69 | team_key = request.form["team_key"].strip() 70 | t = team.get_team(key=team_key) 71 | if not t: 72 | flash("This team could not be found, check your team key.") 73 | return redirect(url_for('.register')) 74 | else: 75 | team_name = request.form["team_name"].strip() 76 | team_affiliation = request.form["team_affiliation"].strip() 77 | try: 78 | t = team.create_team(team_name, team_affiliation) 79 | except exceptions.ValidationError as e: 80 | flash(str(e)) 81 | return redirect(url_for('.register')) 82 | 83 | # note: this is technically a race condition, the team can exist without a user but w/e 84 | # the team keys are impossible to predict 85 | try: 86 | u = user.create_user(username, user_email, 87 | password, background, 88 | country, t, 89 | tshirt_size=tshirt_size, gender=gender) 90 | except exceptions.ValidationError as e: 91 | if not join_team: 92 | t.delete_instance() 93 | flash(str(e)) 94 | return redirect(url_for('.register')) 95 | session["user_id"] = u.id 96 | flash("Registration finished") 97 | return redirect(url_for('.dashboard')) 98 | 99 | 100 | @users.route('/logout/') 101 | def logout(): 102 | session.pop("user_id") 103 | flash("You've successfully logged out.") 104 | return redirect(url_for('.login')) 105 | 106 | 107 | @users.route('/confirm_email/', methods=["GET"]) 108 | @decorators.login_required 109 | def confirm_email(confirmation_key): 110 | try: 111 | user.confirm_email(g.user, confirmation_key) 112 | flash("Email confirmed!") 113 | except exceptions.ValidationError as e: 114 | flash(str(e)) 115 | return redirect(url_for('.dashboard')) 116 | 117 | 118 | @users.route('/forgot_password/', methods=["GET", "POST"]) 119 | @ratelimit.ratelimit(limit=6, per=120) 120 | def forgot_password(): 121 | if request.method == "GET": 122 | return render_template("forgot_password.html") 123 | elif request.method == "POST": 124 | username = request.form["username"].strip() 125 | if len(username) > 50 or not username: 126 | flash("You must have a username!") 127 | return redirect(url_for('.forgot_password')) 128 | user.forgot_password(username=username) 129 | flash("Forgot password email sent! Check your email.") 130 | return render_template("forgot_password.html") 131 | 132 | 133 | @users.route('/reset_password/', methods=["GET", "POST"]) 134 | @ratelimit.ratelimit(limit=6, per=120) 135 | def reset_password(password_reset_token): 136 | if request.method == "GET": 137 | return render_template("reset_password.html") 138 | elif request.method == "POST": 139 | password = request.form["password"].strip() 140 | confirm_password = request.form["confirm_password"].strip() 141 | 142 | if not password == confirm_password: 143 | flash("Password does not match") 144 | return render_template("reset_password.html", password_reset_token=password_reset_token) 145 | 146 | try: 147 | user.reset_password(password_reset_token, password) 148 | flash("Password successfully reset") 149 | return redirect(url_for(".login")) 150 | except exceptions.ValidationError as e: 151 | flash(str(e)) 152 | return redirect(url_for(".reset_password", password_reset_token=password_reset_token)) 153 | 154 | 155 | @users.route('/user/', methods=["GET", "POST"]) 156 | @decorators.login_required 157 | @ratelimit.ratelimit(limit=6, per=120) 158 | def dashboard(): 159 | if request.method == "GET": 160 | first_login = False 161 | if g.user.first_login: 162 | first_login = True 163 | g.user.first_login = False 164 | g.user.save() 165 | return render_template("user.html", first_login=first_login) 166 | elif request.method == "POST": 167 | username = request.form["username"].strip() 168 | email = request.form["email"].strip() 169 | password = request.form["password"].strip() 170 | confirm_password = request.form["confirm_password"].strip() 171 | background = request.form["background"].strip() 172 | country = request.form["country"].strip() 173 | 174 | tshirt_size = "" 175 | gender = "" 176 | if "tshirt_size" in request.form.keys(): 177 | tshirt_size = request.form["tshirt_size"].strip() 178 | if "gender" in request.form.keys(): 179 | gender = request.form["gender"].strip() 180 | 181 | if password != "": 182 | if password != confirm_password: 183 | flash("Password does not match confirmation") 184 | return redirect(url_for('.dashboard')) 185 | 186 | try: 187 | msg = user.update_user(g.user, username, email, password, background, country, tshirt_size, gender) 188 | flash(msg) 189 | except exceptions.ValidationError as e: 190 | flash(str(e)) 191 | return redirect(url_for('.dashboard')) 192 | -------------------------------------------------------------------------------- /secrets.example: -------------------------------------------------------------------------------- 1 | mailgun_url: https://api.mailgun.net/v3/tjctf.org 2 | mailgun_key: key-asdflkjasdhflkjsdahflkhsdaklfjhasd 3 | recaptcha_key: asdlkfjhasdlkjfhlsdakjfh 4 | recaptcha_secret: sdakjfhsdalkfjhsdalkfjh 5 | key: this can be anything you want, it is your flask secret_key 6 | shell_host: shell 7 | shell_username: shellaccounts 8 | shell_privkey: path to private key used to log in 9 | -------------------------------------------------------------------------------- /service/ctf-platform.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IceCTF platform 3 | Requires=ctf-platform.socket 4 | After=network.target 5 | 6 | [Service] 7 | PIDFile=/srv/run/ctf-platform/pid 8 | User=deploy 9 | Group=deploy 10 | WorkingDirectory=/srv/ctf-platform/ 11 | ExecStart=/usr/local/bin/gunicorn --pid /srv/run/ctf-platform/pid app:app 12 | ExecReload=/bin/kill -s HUP $MAINPID 13 | ExecStop=/bin/kill -s TERM $MAINPID 14 | PrivateTmp=true 15 | Environment=PRODUCTION=true 16 | 17 | [Install] 18 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /service/ctf-platform.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IceCTF platform socket 3 | 4 | [Socket] 5 | ListenStream=/srv/run/ctf-platform/socket 6 | ListenStream=0.0.0.0:9002 7 | 8 | [Install] 9 | WantedBy=sockets.target -------------------------------------------------------------------------------- /static/css/flag-icon.min.css: -------------------------------------------------------------------------------- 1 | .flag-icon,.flag-icon-background{background-size:contain;background-position:50%;background-repeat:no-repeat}.flag-icon{position:relative;display:inline-block;width:1.33333333em;line-height:1em}.flag-icon:before{content:"\00a0"}.flag-icon.flag-icon-squared{width:1em}.flag-icon-ad{background-image:url(../flags/4x3/ad.svg)}.flag-icon-ad.flag-icon-squared{background-image:url(../flags/1x1/ad.svg)}.flag-icon-ae{background-image:url(../flags/4x3/ae.svg)}.flag-icon-ae.flag-icon-squared{background-image:url(../flags/1x1/ae.svg)}.flag-icon-af{background-image:url(../flags/4x3/af.svg)}.flag-icon-af.flag-icon-squared{background-image:url(../flags/1x1/af.svg)}.flag-icon-ag{background-image:url(../flags/4x3/ag.svg)}.flag-icon-ag.flag-icon-squared{background-image:url(../flags/1x1/ag.svg)}.flag-icon-ai{background-image:url(../flags/4x3/ai.svg)}.flag-icon-ai.flag-icon-squared{background-image:url(../flags/1x1/ai.svg)}.flag-icon-al{background-image:url(../flags/4x3/al.svg)}.flag-icon-al.flag-icon-squared{background-image:url(../flags/1x1/al.svg)}.flag-icon-am{background-image:url(../flags/4x3/am.svg)}.flag-icon-am.flag-icon-squared{background-image:url(../flags/1x1/am.svg)}.flag-icon-ao{background-image:url(../flags/4x3/ao.svg)}.flag-icon-ao.flag-icon-squared{background-image:url(../flags/1x1/ao.svg)}.flag-icon-aq{background-image:url(../flags/4x3/aq.svg)}.flag-icon-aq.flag-icon-squared{background-image:url(../flags/1x1/aq.svg)}.flag-icon-ar{background-image:url(../flags/4x3/ar.svg)}.flag-icon-ar.flag-icon-squared{background-image:url(../flags/1x1/ar.svg)}.flag-icon-as{background-image:url(../flags/4x3/as.svg)}.flag-icon-as.flag-icon-squared{background-image:url(../flags/1x1/as.svg)}.flag-icon-at{background-image:url(../flags/4x3/at.svg)}.flag-icon-at.flag-icon-squared{background-image:url(../flags/1x1/at.svg)}.flag-icon-au{background-image:url(../flags/4x3/au.svg)}.flag-icon-au.flag-icon-squared{background-image:url(../flags/1x1/au.svg)}.flag-icon-aw{background-image:url(../flags/4x3/aw.svg)}.flag-icon-aw.flag-icon-squared{background-image:url(../flags/1x1/aw.svg)}.flag-icon-ax{background-image:url(../flags/4x3/ax.svg)}.flag-icon-ax.flag-icon-squared{background-image:url(../flags/1x1/ax.svg)}.flag-icon-az{background-image:url(../flags/4x3/az.svg)}.flag-icon-az.flag-icon-squared{background-image:url(../flags/1x1/az.svg)}.flag-icon-ba{background-image:url(../flags/4x3/ba.svg)}.flag-icon-ba.flag-icon-squared{background-image:url(../flags/1x1/ba.svg)}.flag-icon-bb{background-image:url(../flags/4x3/bb.svg)}.flag-icon-bb.flag-icon-squared{background-image:url(../flags/1x1/bb.svg)}.flag-icon-bd{background-image:url(../flags/4x3/bd.svg)}.flag-icon-bd.flag-icon-squared{background-image:url(../flags/1x1/bd.svg)}.flag-icon-be{background-image:url(../flags/4x3/be.svg)}.flag-icon-be.flag-icon-squared{background-image:url(../flags/1x1/be.svg)}.flag-icon-bf{background-image:url(../flags/4x3/bf.svg)}.flag-icon-bf.flag-icon-squared{background-image:url(../flags/1x1/bf.svg)}.flag-icon-bg{background-image:url(../flags/4x3/bg.svg)}.flag-icon-bg.flag-icon-squared{background-image:url(../flags/1x1/bg.svg)}.flag-icon-bh{background-image:url(../flags/4x3/bh.svg)}.flag-icon-bh.flag-icon-squared{background-image:url(../flags/1x1/bh.svg)}.flag-icon-bi{background-image:url(../flags/4x3/bi.svg)}.flag-icon-bi.flag-icon-squared{background-image:url(../flags/1x1/bi.svg)}.flag-icon-bj{background-image:url(../flags/4x3/bj.svg)}.flag-icon-bj.flag-icon-squared{background-image:url(../flags/1x1/bj.svg)}.flag-icon-bl{background-image:url(../flags/4x3/bl.svg)}.flag-icon-bl.flag-icon-squared{background-image:url(../flags/1x1/bl.svg)}.flag-icon-bm{background-image:url(../flags/4x3/bm.svg)}.flag-icon-bm.flag-icon-squared{background-image:url(../flags/1x1/bm.svg)}.flag-icon-bn{background-image:url(../flags/4x3/bn.svg)}.flag-icon-bn.flag-icon-squared{background-image:url(../flags/1x1/bn.svg)}.flag-icon-bo{background-image:url(../flags/4x3/bo.svg)}.flag-icon-bo.flag-icon-squared{background-image:url(../flags/1x1/bo.svg)}.flag-icon-bq{background-image:url(../flags/4x3/bq.svg)}.flag-icon-bq.flag-icon-squared{background-image:url(../flags/1x1/bq.svg)}.flag-icon-br{background-image:url(../flags/4x3/br.svg)}.flag-icon-br.flag-icon-squared{background-image:url(../flags/1x1/br.svg)}.flag-icon-bs{background-image:url(../flags/4x3/bs.svg)}.flag-icon-bs.flag-icon-squared{background-image:url(../flags/1x1/bs.svg)}.flag-icon-bt{background-image:url(../flags/4x3/bt.svg)}.flag-icon-bt.flag-icon-squared{background-image:url(../flags/1x1/bt.svg)}.flag-icon-bv{background-image:url(../flags/4x3/bv.svg)}.flag-icon-bv.flag-icon-squared{background-image:url(../flags/1x1/bv.svg)}.flag-icon-bw{background-image:url(../flags/4x3/bw.svg)}.flag-icon-bw.flag-icon-squared{background-image:url(../flags/1x1/bw.svg)}.flag-icon-by{background-image:url(../flags/4x3/by.svg)}.flag-icon-by.flag-icon-squared{background-image:url(../flags/1x1/by.svg)}.flag-icon-bz{background-image:url(../flags/4x3/bz.svg)}.flag-icon-bz.flag-icon-squared{background-image:url(../flags/1x1/bz.svg)}.flag-icon-ca{background-image:url(../flags/4x3/ca.svg)}.flag-icon-ca.flag-icon-squared{background-image:url(../flags/1x1/ca.svg)}.flag-icon-cc{background-image:url(../flags/4x3/cc.svg)}.flag-icon-cc.flag-icon-squared{background-image:url(../flags/1x1/cc.svg)}.flag-icon-cd{background-image:url(../flags/4x3/cd.svg)}.flag-icon-cd.flag-icon-squared{background-image:url(../flags/1x1/cd.svg)}.flag-icon-cf{background-image:url(../flags/4x3/cf.svg)}.flag-icon-cf.flag-icon-squared{background-image:url(../flags/1x1/cf.svg)}.flag-icon-cg{background-image:url(../flags/4x3/cg.svg)}.flag-icon-cg.flag-icon-squared{background-image:url(../flags/1x1/cg.svg)}.flag-icon-ch{background-image:url(../flags/4x3/ch.svg)}.flag-icon-ch.flag-icon-squared{background-image:url(../flags/1x1/ch.svg)}.flag-icon-ci{background-image:url(../flags/4x3/ci.svg)}.flag-icon-ci.flag-icon-squared{background-image:url(../flags/1x1/ci.svg)}.flag-icon-ck{background-image:url(../flags/4x3/ck.svg)}.flag-icon-ck.flag-icon-squared{background-image:url(../flags/1x1/ck.svg)}.flag-icon-cl{background-image:url(../flags/4x3/cl.svg)}.flag-icon-cl.flag-icon-squared{background-image:url(../flags/1x1/cl.svg)}.flag-icon-cm{background-image:url(../flags/4x3/cm.svg)}.flag-icon-cm.flag-icon-squared{background-image:url(../flags/1x1/cm.svg)}.flag-icon-cn{background-image:url(../flags/4x3/cn.svg)}.flag-icon-cn.flag-icon-squared{background-image:url(../flags/1x1/cn.svg)}.flag-icon-co{background-image:url(../flags/4x3/co.svg)}.flag-icon-co.flag-icon-squared{background-image:url(../flags/1x1/co.svg)}.flag-icon-cr{background-image:url(../flags/4x3/cr.svg)}.flag-icon-cr.flag-icon-squared{background-image:url(../flags/1x1/cr.svg)}.flag-icon-cu{background-image:url(../flags/4x3/cu.svg)}.flag-icon-cu.flag-icon-squared{background-image:url(../flags/1x1/cu.svg)}.flag-icon-cv{background-image:url(../flags/4x3/cv.svg)}.flag-icon-cv.flag-icon-squared{background-image:url(../flags/1x1/cv.svg)}.flag-icon-cw{background-image:url(../flags/4x3/cw.svg)}.flag-icon-cw.flag-icon-squared{background-image:url(../flags/1x1/cw.svg)}.flag-icon-cx{background-image:url(../flags/4x3/cx.svg)}.flag-icon-cx.flag-icon-squared{background-image:url(../flags/1x1/cx.svg)}.flag-icon-cy{background-image:url(../flags/4x3/cy.svg)}.flag-icon-cy.flag-icon-squared{background-image:url(../flags/1x1/cy.svg)}.flag-icon-cz{background-image:url(../flags/4x3/cz.svg)}.flag-icon-cz.flag-icon-squared{background-image:url(../flags/1x1/cz.svg)}.flag-icon-de{background-image:url(../flags/4x3/de.svg)}.flag-icon-de.flag-icon-squared{background-image:url(../flags/1x1/de.svg)}.flag-icon-dj{background-image:url(../flags/4x3/dj.svg)}.flag-icon-dj.flag-icon-squared{background-image:url(../flags/1x1/dj.svg)}.flag-icon-dk{background-image:url(../flags/4x3/dk.svg)}.flag-icon-dk.flag-icon-squared{background-image:url(../flags/1x1/dk.svg)}.flag-icon-dm{background-image:url(../flags/4x3/dm.svg)}.flag-icon-dm.flag-icon-squared{background-image:url(../flags/1x1/dm.svg)}.flag-icon-do{background-image:url(../flags/4x3/do.svg)}.flag-icon-do.flag-icon-squared{background-image:url(../flags/1x1/do.svg)}.flag-icon-dz{background-image:url(../flags/4x3/dz.svg)}.flag-icon-dz.flag-icon-squared{background-image:url(../flags/1x1/dz.svg)}.flag-icon-ec{background-image:url(../flags/4x3/ec.svg)}.flag-icon-ec.flag-icon-squared{background-image:url(../flags/1x1/ec.svg)}.flag-icon-ee{background-image:url(../flags/4x3/ee.svg)}.flag-icon-ee.flag-icon-squared{background-image:url(../flags/1x1/ee.svg)}.flag-icon-eg{background-image:url(../flags/4x3/eg.svg)}.flag-icon-eg.flag-icon-squared{background-image:url(../flags/1x1/eg.svg)}.flag-icon-eh{background-image:url(../flags/4x3/eh.svg)}.flag-icon-eh.flag-icon-squared{background-image:url(../flags/1x1/eh.svg)}.flag-icon-er{background-image:url(../flags/4x3/er.svg)}.flag-icon-er.flag-icon-squared{background-image:url(../flags/1x1/er.svg)}.flag-icon-es{background-image:url(../flags/4x3/es.svg)}.flag-icon-es.flag-icon-squared{background-image:url(../flags/1x1/es.svg)}.flag-icon-et{background-image:url(../flags/4x3/et.svg)}.flag-icon-et.flag-icon-squared{background-image:url(../flags/1x1/et.svg)}.flag-icon-fi{background-image:url(../flags/4x3/fi.svg)}.flag-icon-fi.flag-icon-squared{background-image:url(../flags/1x1/fi.svg)}.flag-icon-fj{background-image:url(../flags/4x3/fj.svg)}.flag-icon-fj.flag-icon-squared{background-image:url(../flags/1x1/fj.svg)}.flag-icon-fk{background-image:url(../flags/4x3/fk.svg)}.flag-icon-fk.flag-icon-squared{background-image:url(../flags/1x1/fk.svg)}.flag-icon-fm{background-image:url(../flags/4x3/fm.svg)}.flag-icon-fm.flag-icon-squared{background-image:url(../flags/1x1/fm.svg)}.flag-icon-fo{background-image:url(../flags/4x3/fo.svg)}.flag-icon-fo.flag-icon-squared{background-image:url(../flags/1x1/fo.svg)}.flag-icon-fr{background-image:url(../flags/4x3/fr.svg)}.flag-icon-fr.flag-icon-squared{background-image:url(../flags/1x1/fr.svg)}.flag-icon-ga{background-image:url(../flags/4x3/ga.svg)}.flag-icon-ga.flag-icon-squared{background-image:url(../flags/1x1/ga.svg)}.flag-icon-gb{background-image:url(../flags/4x3/gb.svg)}.flag-icon-gb.flag-icon-squared{background-image:url(../flags/1x1/gb.svg)}.flag-icon-gd{background-image:url(../flags/4x3/gd.svg)}.flag-icon-gd.flag-icon-squared{background-image:url(../flags/1x1/gd.svg)}.flag-icon-ge{background-image:url(../flags/4x3/ge.svg)}.flag-icon-ge.flag-icon-squared{background-image:url(../flags/1x1/ge.svg)}.flag-icon-gf{background-image:url(../flags/4x3/gf.svg)}.flag-icon-gf.flag-icon-squared{background-image:url(../flags/1x1/gf.svg)}.flag-icon-gg{background-image:url(../flags/4x3/gg.svg)}.flag-icon-gg.flag-icon-squared{background-image:url(../flags/1x1/gg.svg)}.flag-icon-gh{background-image:url(../flags/4x3/gh.svg)}.flag-icon-gh.flag-icon-squared{background-image:url(../flags/1x1/gh.svg)}.flag-icon-gi{background-image:url(../flags/4x3/gi.svg)}.flag-icon-gi.flag-icon-squared{background-image:url(../flags/1x1/gi.svg)}.flag-icon-gl{background-image:url(../flags/4x3/gl.svg)}.flag-icon-gl.flag-icon-squared{background-image:url(../flags/1x1/gl.svg)}.flag-icon-gm{background-image:url(../flags/4x3/gm.svg)}.flag-icon-gm.flag-icon-squared{background-image:url(../flags/1x1/gm.svg)}.flag-icon-gn{background-image:url(../flags/4x3/gn.svg)}.flag-icon-gn.flag-icon-squared{background-image:url(../flags/1x1/gn.svg)}.flag-icon-gp{background-image:url(../flags/4x3/gp.svg)}.flag-icon-gp.flag-icon-squared{background-image:url(../flags/1x1/gp.svg)}.flag-icon-gq{background-image:url(../flags/4x3/gq.svg)}.flag-icon-gq.flag-icon-squared{background-image:url(../flags/1x1/gq.svg)}.flag-icon-gr{background-image:url(../flags/4x3/gr.svg)}.flag-icon-gr.flag-icon-squared{background-image:url(../flags/1x1/gr.svg)}.flag-icon-gs{background-image:url(../flags/4x3/gs.svg)}.flag-icon-gs.flag-icon-squared{background-image:url(../flags/1x1/gs.svg)}.flag-icon-gt{background-image:url(../flags/4x3/gt.svg)}.flag-icon-gt.flag-icon-squared{background-image:url(../flags/1x1/gt.svg)}.flag-icon-gu{background-image:url(../flags/4x3/gu.svg)}.flag-icon-gu.flag-icon-squared{background-image:url(../flags/1x1/gu.svg)}.flag-icon-gw{background-image:url(../flags/4x3/gw.svg)}.flag-icon-gw.flag-icon-squared{background-image:url(../flags/1x1/gw.svg)}.flag-icon-gy{background-image:url(../flags/4x3/gy.svg)}.flag-icon-gy.flag-icon-squared{background-image:url(../flags/1x1/gy.svg)}.flag-icon-hk{background-image:url(../flags/4x3/hk.svg)}.flag-icon-hk.flag-icon-squared{background-image:url(../flags/1x1/hk.svg)}.flag-icon-hm{background-image:url(../flags/4x3/hm.svg)}.flag-icon-hm.flag-icon-squared{background-image:url(../flags/1x1/hm.svg)}.flag-icon-hn{background-image:url(../flags/4x3/hn.svg)}.flag-icon-hn.flag-icon-squared{background-image:url(../flags/1x1/hn.svg)}.flag-icon-hr{background-image:url(../flags/4x3/hr.svg)}.flag-icon-hr.flag-icon-squared{background-image:url(../flags/1x1/hr.svg)}.flag-icon-ht{background-image:url(../flags/4x3/ht.svg)}.flag-icon-ht.flag-icon-squared{background-image:url(../flags/1x1/ht.svg)}.flag-icon-hu{background-image:url(../flags/4x3/hu.svg)}.flag-icon-hu.flag-icon-squared{background-image:url(../flags/1x1/hu.svg)}.flag-icon-id{background-image:url(../flags/4x3/id.svg)}.flag-icon-id.flag-icon-squared{background-image:url(../flags/1x1/id.svg)}.flag-icon-ie{background-image:url(../flags/4x3/ie.svg)}.flag-icon-ie.flag-icon-squared{background-image:url(../flags/1x1/ie.svg)}.flag-icon-il{background-image:url(../flags/4x3/il.svg)}.flag-icon-il.flag-icon-squared{background-image:url(../flags/1x1/il.svg)}.flag-icon-im{background-image:url(../flags/4x3/im.svg)}.flag-icon-im.flag-icon-squared{background-image:url(../flags/1x1/im.svg)}.flag-icon-in{background-image:url(../flags/4x3/in.svg)}.flag-icon-in.flag-icon-squared{background-image:url(../flags/1x1/in.svg)}.flag-icon-io{background-image:url(../flags/4x3/io.svg)}.flag-icon-io.flag-icon-squared{background-image:url(../flags/1x1/io.svg)}.flag-icon-iq{background-image:url(../flags/4x3/iq.svg)}.flag-icon-iq.flag-icon-squared{background-image:url(../flags/1x1/iq.svg)}.flag-icon-ir{background-image:url(../flags/4x3/ir.svg)}.flag-icon-ir.flag-icon-squared{background-image:url(../flags/1x1/ir.svg)}.flag-icon-is{background-image:url(../flags/4x3/is.svg)}.flag-icon-is.flag-icon-squared{background-image:url(../flags/1x1/is.svg)}.flag-icon-it{background-image:url(../flags/4x3/it.svg)}.flag-icon-it.flag-icon-squared{background-image:url(../flags/1x1/it.svg)}.flag-icon-je{background-image:url(../flags/4x3/je.svg)}.flag-icon-je.flag-icon-squared{background-image:url(../flags/1x1/je.svg)}.flag-icon-jm{background-image:url(../flags/4x3/jm.svg)}.flag-icon-jm.flag-icon-squared{background-image:url(../flags/1x1/jm.svg)}.flag-icon-jo{background-image:url(../flags/4x3/jo.svg)}.flag-icon-jo.flag-icon-squared{background-image:url(../flags/1x1/jo.svg)}.flag-icon-jp{background-image:url(../flags/4x3/jp.svg)}.flag-icon-jp.flag-icon-squared{background-image:url(../flags/1x1/jp.svg)}.flag-icon-ke{background-image:url(../flags/4x3/ke.svg)}.flag-icon-ke.flag-icon-squared{background-image:url(../flags/1x1/ke.svg)}.flag-icon-kg{background-image:url(../flags/4x3/kg.svg)}.flag-icon-kg.flag-icon-squared{background-image:url(../flags/1x1/kg.svg)}.flag-icon-kh{background-image:url(../flags/4x3/kh.svg)}.flag-icon-kh.flag-icon-squared{background-image:url(../flags/1x1/kh.svg)}.flag-icon-ki{background-image:url(../flags/4x3/ki.svg)}.flag-icon-ki.flag-icon-squared{background-image:url(../flags/1x1/ki.svg)}.flag-icon-km{background-image:url(../flags/4x3/km.svg)}.flag-icon-km.flag-icon-squared{background-image:url(../flags/1x1/km.svg)}.flag-icon-kn{background-image:url(../flags/4x3/kn.svg)}.flag-icon-kn.flag-icon-squared{background-image:url(../flags/1x1/kn.svg)}.flag-icon-kp{background-image:url(../flags/4x3/kp.svg)}.flag-icon-kp.flag-icon-squared{background-image:url(../flags/1x1/kp.svg)}.flag-icon-kr{background-image:url(../flags/4x3/kr.svg)}.flag-icon-kr.flag-icon-squared{background-image:url(../flags/1x1/kr.svg)}.flag-icon-kw{background-image:url(../flags/4x3/kw.svg)}.flag-icon-kw.flag-icon-squared{background-image:url(../flags/1x1/kw.svg)}.flag-icon-ky{background-image:url(../flags/4x3/ky.svg)}.flag-icon-ky.flag-icon-squared{background-image:url(../flags/1x1/ky.svg)}.flag-icon-kz{background-image:url(../flags/4x3/kz.svg)}.flag-icon-kz.flag-icon-squared{background-image:url(../flags/1x1/kz.svg)}.flag-icon-la{background-image:url(../flags/4x3/la.svg)}.flag-icon-la.flag-icon-squared{background-image:url(../flags/1x1/la.svg)}.flag-icon-lb{background-image:url(../flags/4x3/lb.svg)}.flag-icon-lb.flag-icon-squared{background-image:url(../flags/1x1/lb.svg)}.flag-icon-lc{background-image:url(../flags/4x3/lc.svg)}.flag-icon-lc.flag-icon-squared{background-image:url(../flags/1x1/lc.svg)}.flag-icon-li{background-image:url(../flags/4x3/li.svg)}.flag-icon-li.flag-icon-squared{background-image:url(../flags/1x1/li.svg)}.flag-icon-lk{background-image:url(../flags/4x3/lk.svg)}.flag-icon-lk.flag-icon-squared{background-image:url(../flags/1x1/lk.svg)}.flag-icon-lr{background-image:url(../flags/4x3/lr.svg)}.flag-icon-lr.flag-icon-squared{background-image:url(../flags/1x1/lr.svg)}.flag-icon-ls{background-image:url(../flags/4x3/ls.svg)}.flag-icon-ls.flag-icon-squared{background-image:url(../flags/1x1/ls.svg)}.flag-icon-lt{background-image:url(../flags/4x3/lt.svg)}.flag-icon-lt.flag-icon-squared{background-image:url(../flags/1x1/lt.svg)}.flag-icon-lu{background-image:url(../flags/4x3/lu.svg)}.flag-icon-lu.flag-icon-squared{background-image:url(../flags/1x1/lu.svg)}.flag-icon-lv{background-image:url(../flags/4x3/lv.svg)}.flag-icon-lv.flag-icon-squared{background-image:url(../flags/1x1/lv.svg)}.flag-icon-ly{background-image:url(../flags/4x3/ly.svg)}.flag-icon-ly.flag-icon-squared{background-image:url(../flags/1x1/ly.svg)}.flag-icon-ma{background-image:url(../flags/4x3/ma.svg)}.flag-icon-ma.flag-icon-squared{background-image:url(../flags/1x1/ma.svg)}.flag-icon-mc{background-image:url(../flags/4x3/mc.svg)}.flag-icon-mc.flag-icon-squared{background-image:url(../flags/1x1/mc.svg)}.flag-icon-md{background-image:url(../flags/4x3/md.svg)}.flag-icon-md.flag-icon-squared{background-image:url(../flags/1x1/md.svg)}.flag-icon-me{background-image:url(../flags/4x3/me.svg)}.flag-icon-me.flag-icon-squared{background-image:url(../flags/1x1/me.svg)}.flag-icon-mf{background-image:url(../flags/4x3/mf.svg)}.flag-icon-mf.flag-icon-squared{background-image:url(../flags/1x1/mf.svg)}.flag-icon-mg{background-image:url(../flags/4x3/mg.svg)}.flag-icon-mg.flag-icon-squared{background-image:url(../flags/1x1/mg.svg)}.flag-icon-mh{background-image:url(../flags/4x3/mh.svg)}.flag-icon-mh.flag-icon-squared{background-image:url(../flags/1x1/mh.svg)}.flag-icon-mk{background-image:url(../flags/4x3/mk.svg)}.flag-icon-mk.flag-icon-squared{background-image:url(../flags/1x1/mk.svg)}.flag-icon-ml{background-image:url(../flags/4x3/ml.svg)}.flag-icon-ml.flag-icon-squared{background-image:url(../flags/1x1/ml.svg)}.flag-icon-mm{background-image:url(../flags/4x3/mm.svg)}.flag-icon-mm.flag-icon-squared{background-image:url(../flags/1x1/mm.svg)}.flag-icon-mn{background-image:url(../flags/4x3/mn.svg)}.flag-icon-mn.flag-icon-squared{background-image:url(../flags/1x1/mn.svg)}.flag-icon-mo{background-image:url(../flags/4x3/mo.svg)}.flag-icon-mo.flag-icon-squared{background-image:url(../flags/1x1/mo.svg)}.flag-icon-mp{background-image:url(../flags/4x3/mp.svg)}.flag-icon-mp.flag-icon-squared{background-image:url(../flags/1x1/mp.svg)}.flag-icon-mq{background-image:url(../flags/4x3/mq.svg)}.flag-icon-mq.flag-icon-squared{background-image:url(../flags/1x1/mq.svg)}.flag-icon-mr{background-image:url(../flags/4x3/mr.svg)}.flag-icon-mr.flag-icon-squared{background-image:url(../flags/1x1/mr.svg)}.flag-icon-ms{background-image:url(../flags/4x3/ms.svg)}.flag-icon-ms.flag-icon-squared{background-image:url(../flags/1x1/ms.svg)}.flag-icon-mt{background-image:url(../flags/4x3/mt.svg)}.flag-icon-mt.flag-icon-squared{background-image:url(../flags/1x1/mt.svg)}.flag-icon-mu{background-image:url(../flags/4x3/mu.svg)}.flag-icon-mu.flag-icon-squared{background-image:url(../flags/1x1/mu.svg)}.flag-icon-mv{background-image:url(../flags/4x3/mv.svg)}.flag-icon-mv.flag-icon-squared{background-image:url(../flags/1x1/mv.svg)}.flag-icon-mw{background-image:url(../flags/4x3/mw.svg)}.flag-icon-mw.flag-icon-squared{background-image:url(../flags/1x1/mw.svg)}.flag-icon-mx{background-image:url(../flags/4x3/mx.svg)}.flag-icon-mx.flag-icon-squared{background-image:url(../flags/1x1/mx.svg)}.flag-icon-my{background-image:url(../flags/4x3/my.svg)}.flag-icon-my.flag-icon-squared{background-image:url(../flags/1x1/my.svg)}.flag-icon-mz{background-image:url(../flags/4x3/mz.svg)}.flag-icon-mz.flag-icon-squared{background-image:url(../flags/1x1/mz.svg)}.flag-icon-na{background-image:url(../flags/4x3/na.svg)}.flag-icon-na.flag-icon-squared{background-image:url(../flags/1x1/na.svg)}.flag-icon-nc{background-image:url(../flags/4x3/nc.svg)}.flag-icon-nc.flag-icon-squared{background-image:url(../flags/1x1/nc.svg)}.flag-icon-ne{background-image:url(../flags/4x3/ne.svg)}.flag-icon-ne.flag-icon-squared{background-image:url(../flags/1x1/ne.svg)}.flag-icon-nf{background-image:url(../flags/4x3/nf.svg)}.flag-icon-nf.flag-icon-squared{background-image:url(../flags/1x1/nf.svg)}.flag-icon-ng{background-image:url(../flags/4x3/ng.svg)}.flag-icon-ng.flag-icon-squared{background-image:url(../flags/1x1/ng.svg)}.flag-icon-ni{background-image:url(../flags/4x3/ni.svg)}.flag-icon-ni.flag-icon-squared{background-image:url(../flags/1x1/ni.svg)}.flag-icon-nl{background-image:url(../flags/4x3/nl.svg)}.flag-icon-nl.flag-icon-squared{background-image:url(../flags/1x1/nl.svg)}.flag-icon-no{background-image:url(../flags/4x3/no.svg)}.flag-icon-no.flag-icon-squared{background-image:url(../flags/1x1/no.svg)}.flag-icon-np{background-image:url(../flags/4x3/np.svg)}.flag-icon-np.flag-icon-squared{background-image:url(../flags/1x1/np.svg)}.flag-icon-nr{background-image:url(../flags/4x3/nr.svg)}.flag-icon-nr.flag-icon-squared{background-image:url(../flags/1x1/nr.svg)}.flag-icon-nu{background-image:url(../flags/4x3/nu.svg)}.flag-icon-nu.flag-icon-squared{background-image:url(../flags/1x1/nu.svg)}.flag-icon-nz{background-image:url(../flags/4x3/nz.svg)}.flag-icon-nz.flag-icon-squared{background-image:url(../flags/1x1/nz.svg)}.flag-icon-om{background-image:url(../flags/4x3/om.svg)}.flag-icon-om.flag-icon-squared{background-image:url(../flags/1x1/om.svg)}.flag-icon-pa{background-image:url(../flags/4x3/pa.svg)}.flag-icon-pa.flag-icon-squared{background-image:url(../flags/1x1/pa.svg)}.flag-icon-pe{background-image:url(../flags/4x3/pe.svg)}.flag-icon-pe.flag-icon-squared{background-image:url(../flags/1x1/pe.svg)}.flag-icon-pf{background-image:url(../flags/4x3/pf.svg)}.flag-icon-pf.flag-icon-squared{background-image:url(../flags/1x1/pf.svg)}.flag-icon-pg{background-image:url(../flags/4x3/pg.svg)}.flag-icon-pg.flag-icon-squared{background-image:url(../flags/1x1/pg.svg)}.flag-icon-ph{background-image:url(../flags/4x3/ph.svg)}.flag-icon-ph.flag-icon-squared{background-image:url(../flags/1x1/ph.svg)}.flag-icon-pk{background-image:url(../flags/4x3/pk.svg)}.flag-icon-pk.flag-icon-squared{background-image:url(../flags/1x1/pk.svg)}.flag-icon-pl{background-image:url(../flags/4x3/pl.svg)}.flag-icon-pl.flag-icon-squared{background-image:url(../flags/1x1/pl.svg)}.flag-icon-pm{background-image:url(../flags/4x3/pm.svg)}.flag-icon-pm.flag-icon-squared{background-image:url(../flags/1x1/pm.svg)}.flag-icon-pn{background-image:url(../flags/4x3/pn.svg)}.flag-icon-pn.flag-icon-squared{background-image:url(../flags/1x1/pn.svg)}.flag-icon-pr{background-image:url(../flags/4x3/pr.svg)}.flag-icon-pr.flag-icon-squared{background-image:url(../flags/1x1/pr.svg)}.flag-icon-ps{background-image:url(../flags/4x3/ps.svg)}.flag-icon-ps.flag-icon-squared{background-image:url(../flags/1x1/ps.svg)}.flag-icon-pt{background-image:url(../flags/4x3/pt.svg)}.flag-icon-pt.flag-icon-squared{background-image:url(../flags/1x1/pt.svg)}.flag-icon-pw{background-image:url(../flags/4x3/pw.svg)}.flag-icon-pw.flag-icon-squared{background-image:url(../flags/1x1/pw.svg)}.flag-icon-py{background-image:url(../flags/4x3/py.svg)}.flag-icon-py.flag-icon-squared{background-image:url(../flags/1x1/py.svg)}.flag-icon-qa{background-image:url(../flags/4x3/qa.svg)}.flag-icon-qa.flag-icon-squared{background-image:url(../flags/1x1/qa.svg)}.flag-icon-re{background-image:url(../flags/4x3/re.svg)}.flag-icon-re.flag-icon-squared{background-image:url(../flags/1x1/re.svg)}.flag-icon-ro{background-image:url(../flags/4x3/ro.svg)}.flag-icon-ro.flag-icon-squared{background-image:url(../flags/1x1/ro.svg)}.flag-icon-rs{background-image:url(../flags/4x3/rs.svg)}.flag-icon-rs.flag-icon-squared{background-image:url(../flags/1x1/rs.svg)}.flag-icon-ru{background-image:url(../flags/4x3/ru.svg)}.flag-icon-ru.flag-icon-squared{background-image:url(../flags/1x1/ru.svg)}.flag-icon-rw{background-image:url(../flags/4x3/rw.svg)}.flag-icon-rw.flag-icon-squared{background-image:url(../flags/1x1/rw.svg)}.flag-icon-sa{background-image:url(../flags/4x3/sa.svg)}.flag-icon-sa.flag-icon-squared{background-image:url(../flags/1x1/sa.svg)}.flag-icon-sb{background-image:url(../flags/4x3/sb.svg)}.flag-icon-sb.flag-icon-squared{background-image:url(../flags/1x1/sb.svg)}.flag-icon-sc{background-image:url(../flags/4x3/sc.svg)}.flag-icon-sc.flag-icon-squared{background-image:url(../flags/1x1/sc.svg)}.flag-icon-sd{background-image:url(../flags/4x3/sd.svg)}.flag-icon-sd.flag-icon-squared{background-image:url(../flags/1x1/sd.svg)}.flag-icon-se{background-image:url(../flags/4x3/se.svg)}.flag-icon-se.flag-icon-squared{background-image:url(../flags/1x1/se.svg)}.flag-icon-sg{background-image:url(../flags/4x3/sg.svg)}.flag-icon-sg.flag-icon-squared{background-image:url(../flags/1x1/sg.svg)}.flag-icon-sh{background-image:url(../flags/4x3/sh.svg)}.flag-icon-sh.flag-icon-squared{background-image:url(../flags/1x1/sh.svg)}.flag-icon-si{background-image:url(../flags/4x3/si.svg)}.flag-icon-si.flag-icon-squared{background-image:url(../flags/1x1/si.svg)}.flag-icon-sj{background-image:url(../flags/4x3/sj.svg)}.flag-icon-sj.flag-icon-squared{background-image:url(../flags/1x1/sj.svg)}.flag-icon-sk{background-image:url(../flags/4x3/sk.svg)}.flag-icon-sk.flag-icon-squared{background-image:url(../flags/1x1/sk.svg)}.flag-icon-sl{background-image:url(../flags/4x3/sl.svg)}.flag-icon-sl.flag-icon-squared{background-image:url(../flags/1x1/sl.svg)}.flag-icon-sm{background-image:url(../flags/4x3/sm.svg)}.flag-icon-sm.flag-icon-squared{background-image:url(../flags/1x1/sm.svg)}.flag-icon-sn{background-image:url(../flags/4x3/sn.svg)}.flag-icon-sn.flag-icon-squared{background-image:url(../flags/1x1/sn.svg)}.flag-icon-so{background-image:url(../flags/4x3/so.svg)}.flag-icon-so.flag-icon-squared{background-image:url(../flags/1x1/so.svg)}.flag-icon-sr{background-image:url(../flags/4x3/sr.svg)}.flag-icon-sr.flag-icon-squared{background-image:url(../flags/1x1/sr.svg)}.flag-icon-ss{background-image:url(../flags/4x3/ss.svg)}.flag-icon-ss.flag-icon-squared{background-image:url(../flags/1x1/ss.svg)}.flag-icon-st{background-image:url(../flags/4x3/st.svg)}.flag-icon-st.flag-icon-squared{background-image:url(../flags/1x1/st.svg)}.flag-icon-sv{background-image:url(../flags/4x3/sv.svg)}.flag-icon-sv.flag-icon-squared{background-image:url(../flags/1x1/sv.svg)}.flag-icon-sx{background-image:url(../flags/4x3/sx.svg)}.flag-icon-sx.flag-icon-squared{background-image:url(../flags/1x1/sx.svg)}.flag-icon-sy{background-image:url(../flags/4x3/sy.svg)}.flag-icon-sy.flag-icon-squared{background-image:url(../flags/1x1/sy.svg)}.flag-icon-sz{background-image:url(../flags/4x3/sz.svg)}.flag-icon-sz.flag-icon-squared{background-image:url(../flags/1x1/sz.svg)}.flag-icon-tc{background-image:url(../flags/4x3/tc.svg)}.flag-icon-tc.flag-icon-squared{background-image:url(../flags/1x1/tc.svg)}.flag-icon-td{background-image:url(../flags/4x3/td.svg)}.flag-icon-td.flag-icon-squared{background-image:url(../flags/1x1/td.svg)}.flag-icon-tf{background-image:url(../flags/4x3/tf.svg)}.flag-icon-tf.flag-icon-squared{background-image:url(../flags/1x1/tf.svg)}.flag-icon-tg{background-image:url(../flags/4x3/tg.svg)}.flag-icon-tg.flag-icon-squared{background-image:url(../flags/1x1/tg.svg)}.flag-icon-th{background-image:url(../flags/4x3/th.svg)}.flag-icon-th.flag-icon-squared{background-image:url(../flags/1x1/th.svg)}.flag-icon-tj{background-image:url(../flags/4x3/tj.svg)}.flag-icon-tj.flag-icon-squared{background-image:url(../flags/1x1/tj.svg)}.flag-icon-tk{background-image:url(../flags/4x3/tk.svg)}.flag-icon-tk.flag-icon-squared{background-image:url(../flags/1x1/tk.svg)}.flag-icon-tl{background-image:url(../flags/4x3/tl.svg)}.flag-icon-tl.flag-icon-squared{background-image:url(../flags/1x1/tl.svg)}.flag-icon-tm{background-image:url(../flags/4x3/tm.svg)}.flag-icon-tm.flag-icon-squared{background-image:url(../flags/1x1/tm.svg)}.flag-icon-tn{background-image:url(../flags/4x3/tn.svg)}.flag-icon-tn.flag-icon-squared{background-image:url(../flags/1x1/tn.svg)}.flag-icon-to{background-image:url(../flags/4x3/to.svg)}.flag-icon-to.flag-icon-squared{background-image:url(../flags/1x1/to.svg)}.flag-icon-tr{background-image:url(../flags/4x3/tr.svg)}.flag-icon-tr.flag-icon-squared{background-image:url(../flags/1x1/tr.svg)}.flag-icon-tt{background-image:url(../flags/4x3/tt.svg)}.flag-icon-tt.flag-icon-squared{background-image:url(../flags/1x1/tt.svg)}.flag-icon-tv{background-image:url(../flags/4x3/tv.svg)}.flag-icon-tv.flag-icon-squared{background-image:url(../flags/1x1/tv.svg)}.flag-icon-tw{background-image:url(../flags/4x3/tw.svg)}.flag-icon-tw.flag-icon-squared{background-image:url(../flags/1x1/tw.svg)}.flag-icon-tz{background-image:url(../flags/4x3/tz.svg)}.flag-icon-tz.flag-icon-squared{background-image:url(../flags/1x1/tz.svg)}.flag-icon-ua{background-image:url(../flags/4x3/ua.svg)}.flag-icon-ua.flag-icon-squared{background-image:url(../flags/1x1/ua.svg)}.flag-icon-ug{background-image:url(../flags/4x3/ug.svg)}.flag-icon-ug.flag-icon-squared{background-image:url(../flags/1x1/ug.svg)}.flag-icon-um{background-image:url(../flags/4x3/um.svg)}.flag-icon-um.flag-icon-squared{background-image:url(../flags/1x1/um.svg)}.flag-icon-us{background-image:url(../flags/4x3/us.svg)}.flag-icon-us.flag-icon-squared{background-image:url(../flags/1x1/us.svg)}.flag-icon-uy{background-image:url(../flags/4x3/uy.svg)}.flag-icon-uy.flag-icon-squared{background-image:url(../flags/1x1/uy.svg)}.flag-icon-uz{background-image:url(../flags/4x3/uz.svg)}.flag-icon-uz.flag-icon-squared{background-image:url(../flags/1x1/uz.svg)}.flag-icon-va{background-image:url(../flags/4x3/va.svg)}.flag-icon-va.flag-icon-squared{background-image:url(../flags/1x1/va.svg)}.flag-icon-vc{background-image:url(../flags/4x3/vc.svg)}.flag-icon-vc.flag-icon-squared{background-image:url(../flags/1x1/vc.svg)}.flag-icon-ve{background-image:url(../flags/4x3/ve.svg)}.flag-icon-ve.flag-icon-squared{background-image:url(../flags/1x1/ve.svg)}.flag-icon-vg{background-image:url(../flags/4x3/vg.svg)}.flag-icon-vg.flag-icon-squared{background-image:url(../flags/1x1/vg.svg)}.flag-icon-vi{background-image:url(../flags/4x3/vi.svg)}.flag-icon-vi.flag-icon-squared{background-image:url(../flags/1x1/vi.svg)}.flag-icon-vn{background-image:url(../flags/4x3/vn.svg)}.flag-icon-vn.flag-icon-squared{background-image:url(../flags/1x1/vn.svg)}.flag-icon-vu{background-image:url(../flags/4x3/vu.svg)}.flag-icon-vu.flag-icon-squared{background-image:url(../flags/1x1/vu.svg)}.flag-icon-wf{background-image:url(../flags/4x3/wf.svg)}.flag-icon-wf.flag-icon-squared{background-image:url(../flags/1x1/wf.svg)}.flag-icon-ws{background-image:url(../flags/4x3/ws.svg)}.flag-icon-ws.flag-icon-squared{background-image:url(../flags/1x1/ws.svg)}.flag-icon-ye{background-image:url(../flags/4x3/ye.svg)}.flag-icon-ye.flag-icon-squared{background-image:url(../flags/1x1/ye.svg)}.flag-icon-yt{background-image:url(../flags/4x3/yt.svg)}.flag-icon-yt.flag-icon-squared{background-image:url(../flags/1x1/yt.svg)}.flag-icon-za{background-image:url(../flags/4x3/za.svg)}.flag-icon-za.flag-icon-squared{background-image:url(../flags/1x1/za.svg)}.flag-icon-zm{background-image:url(../flags/4x3/zm.svg)}.flag-icon-zm.flag-icon-squared{background-image:url(../flags/1x1/zm.svg)}.flag-icon-zw{background-image:url(../flags/4x3/zw.svg)}.flag-icon-zw.flag-icon-squared{background-image:url(../flags/1x1/zw.svg)}.flag-icon-eu{background-image:url(../flags/4x3/eu.svg)}.flag-icon-eu.flag-icon-squared{background-image:url(../flags/1x1/eu.svg)}.flag-icon-gb-eng{background-image:url(../flags/4x3/gb-eng.svg)}.flag-icon-gb-eng.flag-icon-squared{background-image:url(../flags/1x1/gb-eng.svg)}.flag-icon-gb-nir{background-image:url(../flags/4x3/gb-nir.svg)}.flag-icon-gb-nir.flag-icon-squared{background-image:url(../flags/1x1/gb-nir.svg)}.flag-icon-gb-sct{background-image:url(../flags/4x3/gb-sct.svg)}.flag-icon-gb-sct.flag-icon-squared{background-image:url(../flags/1x1/gb-sct.svg)}.flag-icon-gb-wls{background-image:url(../flags/4x3/gb-wls.svg)}.flag-icon-gb-wls.flag-icon-squared{background-image:url(../flags/1x1/gb-wls.svg)}.flag-icon-un{background-image:url(../flags/4x3/un.svg)}.flag-icon-un.flag-icon-squared{background-image:url(../flags/1x1/un.svg)} -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f2f2f2; 3 | padding-top: 74px; 4 | } 5 | 6 | .nav-fixed{ 7 | position: fixed; 8 | right: 0; 9 | left: 0; 10 | z-index: 1030; 11 | } 12 | 13 | .nav-fixed { 14 | top: 0; 15 | } 16 | 17 | .status-dot { 18 | display: inline-block; 19 | } 20 | 21 | .side-nav { 22 | background: #f2f2f2; 23 | box-shadow: none; 24 | } 25 | 26 | .register-info { 27 | padding-top: 10px !important; 28 | padding-bottom: 10px !important; 29 | } 30 | 31 | .side-nav a { 32 | cursor: pointer; 33 | height: 50px; 34 | line-height: 50px; 35 | } 36 | 37 | .status-dot { 38 | height: 28px; 39 | width: 28px; 40 | position: absolute; 41 | margin-top: 7px; 42 | } 43 | 44 | .challenge-text { 45 | margin-left: 50px; 46 | display: inline-block; 47 | } 48 | 49 | .side-nav.fixed { 50 | top: 69px; 51 | } 52 | 53 | .side-icon { 54 | position: absolute; 55 | margin-top: 13px; 56 | margin-left: -15px; 57 | } 58 | 59 | .side-text { 60 | margin-left: 26px; 61 | } 62 | 63 | .collapsible-body { 64 | background: white; 65 | } 66 | 67 | .cont { 68 | padding-left: 280px; 69 | padding-right: 40px; 70 | } 71 | 72 | .top-logo { 73 | height: 50px; 74 | margin-left: 18px; 75 | margin-top: 8px; 76 | } 77 | 78 | .check-icon { 79 | margin-right: 0 !important; 80 | margin-left: 8px !important; 81 | } 82 | 83 | .check-pad { 84 | width: 33px; 85 | display: inline-block; 86 | } 87 | 88 | .no-bot { 89 | margin-bottom: 0; 90 | } 91 | 92 | .flag-submit { 93 | width: 100%; 94 | margin-top: 20px; 95 | } 96 | 97 | .flag-form { 98 | padding-left: 14px; 99 | padding-right: 14px; 100 | } 101 | 102 | .page-title-wrapper { 103 | margin-bottom: 2rem; 104 | } 105 | 106 | .page-title 107 | { 108 | font-weight: 300; 109 | font-size: 3.2rem; 110 | display: inline-block; 111 | margin-left: -3px; 112 | padding-right: 0.5rem; 113 | padding-left: 0.5rem; 114 | } 115 | 116 | .page-title-tag 117 | { 118 | font-size: 3.2rem; 119 | display: inline-block; 120 | } 121 | 122 | .form-title 123 | { 124 | font-weight: 300; 125 | font-size: 2rem; 126 | } 127 | 128 | .panel { 129 | padding: 2rem; 130 | background: white; 131 | } 132 | 133 | .tabs .tab a { 134 | color: #2196F3 !important; 135 | } 136 | 137 | .tabs .indicator { 138 | background: #76c2ff !important; 139 | } 140 | 141 | .team-wrapper { 142 | background: #f9f9f9; 143 | margin-top: 2rem; 144 | } 145 | 146 | .register-submit { 147 | margin-top: 35px; 148 | } 149 | 150 | .login-panel { 151 | margin-top: 50px; 152 | margin-right: 25%; 153 | margin-left: 25%; 154 | } 155 | 156 | .card-header { 157 | color: white; 158 | padding: 1rem; 159 | padding-top: 1.5rem; 160 | padding-bottom: 0.5rem; 161 | font-size: 2rem; 162 | font-weight: 300; 163 | } 164 | 165 | .chat-frame { 166 | border-width: 0px; 167 | width: calc(100% + 80px); 168 | height: calc(100% - 64px); 169 | margin-top: -60px; 170 | margin-left: -40px; 171 | } 172 | 173 | code, kbd, pre, samp { 174 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 175 | } 176 | 177 | .challenge code { 178 | padding: 2px 4px; 179 | font-size: 90%; 180 | color: #c7254e; 181 | background-color: #f9f2f4; 182 | border-radius: 4px; 183 | } 184 | 185 | .shell-frame { 186 | border-width: 0px; 187 | width: 100%; 188 | height: calc(100% - 64px); 189 | } 190 | 191 | 192 | /* MEDIUM */ 193 | @media only screen and (max-width : 992px) { 194 | .cont { 195 | padding-left: 40px; 196 | } 197 | 198 | .top-logo { 199 | display: block; 200 | margin-left: auto; 201 | margin-right: auto; 202 | margin-top: 0; 203 | position: relative; 204 | top: 7px; 205 | } 206 | 207 | .login-panel { 208 | margin-right: 15%; 209 | margin-left: 15%; 210 | } 211 | } 212 | 213 | /* SMALL */ 214 | @media only screen and (max-width : 600px) { 215 | .register-submit { 216 | margin-top: 0; 217 | } 218 | 219 | .login-panel { 220 | margin-right: 10%; 221 | margin-left: 10%; 222 | } 223 | } 224 | 225 | 226 | /* V -- DELETE MAYBE -- V */ 227 | 228 | 229 | .competition-page .navbar-fixed > nav { 230 | background-color: rgb(0, 138, 255) !important; 231 | } 232 | 233 | -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCTF/ColdCore/20a27a874f1480edd9e2ed7e04f8b5aef3b3b3e5/static/img/logo.png -------------------------------------------------------------------------------- /static/js/api.js: -------------------------------------------------------------------------------- 1 | var api = (function() { 2 | var makeCall = function(endpoint, data, callback) { 3 | $.ajax({ 4 | url: "/api" + endpoint, 5 | data: data, 6 | dataType: "json", 7 | method: "POST", 8 | success: function(data) { 9 | console.log(data); 10 | callback(data); 11 | console.log("API call complete"); 12 | }, 13 | failure: function(data) { 14 | console.error("API call failed"); 15 | } 16 | }); 17 | }; 18 | 19 | return { 20 | makeCall: makeCall 21 | }; 22 | })(); 23 | -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | $('select').material_select(); 3 | }); 4 | -------------------------------------------------------------------------------- /static/js/sorttable.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCTF/ColdCore/20a27a874f1480edd9e2ed7e04f8b5aef3b3b3e5/static/js/sorttable.js -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ config.ctf_name }} Admin – {% block title %}Home{% endblock %} 4 | {% if config.cdn %} 5 | 6 | 7 | 8 | {% else %} 9 | 10 | 11 | 12 | {% endif %} 13 | 14 | 15 | {% block head %}{% endblock %} 16 | 17 | 18 | {% if "admin" in session %} 19 | 41 | {% endif %} 42 |
43 | {% block content %}{% endblock %} 44 |
45 | {% if config.cdn %} 46 | 47 | 48 | 49 | 50 | {% else %} 51 | 52 | 53 | 54 | 55 | {% endif %} 56 | 57 | 64 | 69 | 70 | {% block postscript %} 71 | {% endblock %} 72 | 73 | 74 | -------------------------------------------------------------------------------- /templates/admin/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block content %} 3 | {% if tickets %} 4 |
5 |
6 | Unresolved issues 7 |

There are unresolved trouble tickets.

8 |
9 |
10 | {% endif %} 11 | 12 | 13 | 14 | 15 | 16 | {% for team in teams %} 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 | 28 |
TeamAffiliationEligibleLast solveScore
19 | {{ team.name }} 20 | {{ team.affiliation }}{{ "Eligible" if team.eligible() else "Ineligible" }}{{ lastsolvedata[team.id] }}{{ scoredata[team.id] }}
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block content %} 4 |

Login

5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/admin/team.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block content %} 3 |

{{ team.name }}

4 | Impersonate team
5 |

6 | This team is {{ "eligible" if team.eligible() else "not eligible" }} (toggle). 7 |

8 |

This team's affiliation is {{ team.affiliation }}

9 |

Email

10 |

This team's email is {{ team.email }} ({{ "confirmed" if team.email_confirmed else "unconfirmed" }}).

11 | {% if not team.email_confirmed %} 12 |

This team's confirmation key is {{ team.email_confirmation_key }}. 13 | {% endif %} 14 |

Score adjustment

15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 |

Score calculation

28 | {% if team.solves.count() %} 29 |

Solved problems

30 | 31 | 32 | 33 | 34 | 35 | {% for solve in team.solves %} 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
NameCategoryTimeValue
{{ solve.challenge.name }}{{ solve.challenge.category }}{{ solve.time }}{{ solve.challenge.points }}
44 | {% else %} 45 |

No problems have been solved.

46 | {% endif %} 47 | {% if team.adjustments.count() %} 48 |

Score adjustments

49 | 50 | 51 | 52 | 53 | 54 | {% for adj in team.adjustments %} 55 | 56 | 57 | 58 | 59 | {% endfor %} 60 | 61 |
ReasonValue
{{ adj.reason }}{{ adj.value }}
62 | {% else %} 63 |

No score adjustments have been made.

64 | {% endif %} 65 | 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /templates/admin/ticket_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block title %}Ticket #{{ ticket.id }}{% endblock %} 3 | {% block content %} 4 |

Ticket #{{ ticket.id }}: {{ ticket.summary }}

5 |

{{ ticket.description }}

6 | {{ ticket.opened_at }} · {{ ticket.team.name }} 7 | {% for comment in comments %} 8 |

{{ comment.comment }}

9 | {{ comment.time }} · {{ comment.comment_by }} 10 | {% endfor %} 11 |
12 |
13 |
14 | 15 | 16 |
17 | 18 |

19 | 20 | 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/admin/tickets.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block title %}Trouble Tickets{% endblock %} 3 | {% block content %} 4 |

Trouble tickets

5 | {% if tickets %} 6 | The following tickets are open: 7 |
8 | {% for ticket in tickets %} 9 | {% if ticket.active %} 10 | #{{ ticket.id }} {{ ticket.summary }} ({{ ticket.team.name }}) 11 | {% endif %} 12 | {% endfor %} 13 |
14 | 15 | The following tickets are closed: 16 |
17 | {% for ticket in tickets %} 18 | {% if not ticket.active %} 19 | #{{ ticket.id }} {{ ticket.summary }} ({{ ticket.team.name }}) 20 | {% endif %} 21 | {% endfor %} 22 |
23 | {% else %} 24 | Yay, no tickets! 25 | {% endif %} 26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ config.ctf_name }} – {% block title %}Home{% endblock %} 4 | {% if config.cdn %} 5 | 6 | 7 | 8 | {% else %} 9 | 10 | 11 | 12 | 13 | {% endif %} 14 | 15 | 16 | 23 | 24 | 25 | {% block head %}{% endblock %} 26 | 27 | 28 |
29 | 49 | 107 |
108 |
109 | {% if session.admin %} 110 |
111 |
112 | You are an admin. 113 | Please note that team restrictions do not currently apply. 114 |
115 |
116 | {% endif %} 117 | {% for notification in notifications %} 118 |
119 |
120 | {{ notification.notification | safe }} (Click to dismiss) 121 |
122 |
123 | {% endfor %} 124 |
125 |
126 | {% block content %}{% endblock %} 127 |
128 | {% if config.cdn %} 129 | 130 | 131 | 132 | 133 | {% else %} 134 | 135 | 136 | 137 | 138 | {% endif %} 139 | 140 | 141 | 148 | 153 | {% block postscript %} 154 | {% endblock %} 155 | {% if config.production %} 156 | 166 | {% endif %} 167 | 168 | 169 | -------------------------------------------------------------------------------- /templates/challenges/challenge_solves.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

Solves for {{ challenge.name }}

4 | << Back to challenges 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for solve in solves %} 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 | 20 |
TeamSolve time
{{ solve.team.name }}{{ solve.time }}
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/challenges/challenges.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Challenges{% endblock %} 3 | {% block head %} 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Due to way more users than expected, the platform and some problems may not be stable. Please report all errors encountered in a ticket or on IRC, and be patient :). 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 26 |
27 |
28 | 31 |
32 | {% for stage in stages %} 33 |
34 | {% if stage.description %} 35 |

{{stage.description}}

36 | {% endif %} 37 |

{{ stage.name }}

38 | Toggle 39 |
40 |
    41 | {% for challenge in challenges[stage.id] %} 42 |
  • 43 |
    44 |
    45 |
    {{ challenge.name }}
    46 | 47 | {{ challenge.author }} 48 | · 49 | {{ solves[challenge.id] }} {% if solves[challenge.id] == 1 %}solve{% else %}solves{% endif %} 50 | · 51 | {{ challenge.category }} 52 | · 53 | {{ challenge.points }} pt 54 | 55 | {% if challenge in solved %} 56 | done 57 | {% else %} 58 |
    59 | {% endif %} 60 |
    61 |
    62 |
    63 |

    {{ challenge.description | safe }} 64 | {% if challenge in solved %} 65 |

    Your team has solved this challenge!
    66 | View solves 67 |

    68 | {% else %} 69 |

    70 | View solves 71 |

    72 |
    73 |
    74 |
    75 |
    76 | 77 | 78 |
    79 |
    80 |
    81 | 82 |
    83 | 84 |
    85 |

    86 | {% endif %} 87 |
  • 88 | {% endfor %} 89 |
90 | {% endfor %} 91 | {% endblock %} 92 | {% block postscript %} 93 | 98 | {% if config.apisubmit %} 99 | 176 | {% endif %} 177 | {% endblock %} 178 | -------------------------------------------------------------------------------- /templates/chat.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 | 5 |
6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/scoreboard/scoreboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Scoreboard{% endblock %} 3 | {% block head %} 4 | 12 | 38 | {% endblock %} 39 | {% block content %} 40 |
41 |
42 |

Score progression

43 |
44 |
45 |
46 |

Rankings

47 | Hide ineligible teams 48 | 49 | 50 | 51 | 52 | 53 | {% for eligible, teamid, team, affiliation, score in data %} 54 | 55 | {% endfor %} 56 | 57 |
RankTeamAffiliationScore
{{ rank }}{{ team }}{{ affiliation }}{{ score }}
58 | {% endblock %} 59 | {% block postscript %} 60 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /templates/shell/shell.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% if account %} 4 |
5 |
6 |

Username: {{account.username}} Password: {{account.password}}

7 | ssh {{account.username}}@{{account.hostname}} -p {{account.port}} 8 |
9 |
10 |
11 | 12 |
13 | 14 | {% else %} 15 |
16 |
17 |

You don't have an account yet! Don't worry, one will be created shortly.

18 |
19 |
20 | {% endif %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/teams/team.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Dashboard{% endblock %} 3 | {% block head %} 4 | 31 | {% endblock %} 32 | {% block content %} 33 |
34 |
_
35 |
{{ team.name }}
36 |
37 |
38 |
39 |
40 |
41 |
42 | Team key: {{ team.key }} 43 |

Share this with your teammates, and keep it in a safe place.

44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Team 54 |
55 |
56 |

Your score is currently {{ team_score }}. Go solve more challenges!

57 | {{ team.affiliation }}.

58 |

Your team is {{ "eligible" if team.eligible() else "ineligible" }} for prizes.

59 | Why? 60 |
61 |
62 |
63 |
64 |
65 |
Edit Information
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 | 77 |

78 | 79 |
80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 |
Members
88 |
89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% for member in team.members %} 100 | 101 | 102 | 103 | 104 | {% endfor %} 105 | 106 |
UsernameEligibility Status
{{ member.username }}{{ "Eligible" if member.eligible() else "Ineligibile" }}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
Score Calculation
115 |
116 | {% if team_solves.count() %} 117 |
Solved problems
118 |
119 |
120 | 121 | 122 | 123 | 124 | 125 | {% for solve in team_solves %} 126 | 127 | 128 | 129 | 130 | 131 | {% endfor %} 132 | 133 |
NameCategoryTimeValue
{{ solve.challenge.name }}{{ solve.challenge.category }}{{ solve.time }}{{ solve.challenge.points }}
134 |
135 |
136 | {% else %} 137 |

No problems have been solved.

138 | {% endif %} 139 | {% if team_adjustments.count() %} 140 |
Score adjustments
141 |
142 |
143 | 144 | 145 | 146 | 147 | 148 | {% for adj in team_adjustments %} 149 | 150 | 151 | 152 | 153 | {% endfor %} 154 | 155 |
ReasonValue
{{ adj.reason }}{{ adj.value }}
156 |
157 |
158 | {% else %} 159 |

No score adjustments have been made.

160 | {% endif %} 161 |
162 |
163 |
164 | 165 | 177 | {% endblock %} 178 | {% block postscript %} 179 | 180 | {% endblock %} 181 | -------------------------------------------------------------------------------- /templates/tickets/open_ticket.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Open Ticket{% endblock %} 3 | {% block content %} 4 |

Open a Trouble Ticket

5 |

The issue summary should be a one-sentence description of the problem you 6 | are having. Please elaborate on the issue in the description field.

7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 | 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/tickets/ticket_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Ticket #{{ ticket.id }}{% endblock %} 3 | {% block content %} 4 |

Ticket #{{ ticket.id }}: {{ ticket.summary }}

5 |

{{ ticket.description }}

6 | {{ ticket.opened_at }} · {{ g.team.name }} 7 | {% for comment in comments %} 8 |

{{ comment.comment }}

9 | {{ comment.time }} · {{ comment.comment_by }} 10 | {% endfor %} 11 |
12 |
13 |
14 | 15 | 16 |
17 | 18 |

19 | 20 | 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/tickets/tickets.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Trouble Tickets{% endblock %} 3 | {% block content %} 4 |

Trouble tickets

5 | {% if tickets %} 6 | You have the following open tickets. If you're having an issue, you can open a new ticket. 7 |
8 | {% for ticket in tickets %} 9 | {% if ticket.active %} 10 | #{{ ticket.id }} {{ ticket.summary }} 11 | {% endif %} 12 | {% endfor %} 13 |
14 | 15 | You have the following closed tickets: 16 |
17 | {% for ticket in tickets %} 18 | {% if not ticket.active %} 19 | #{{ ticket.id }} {{ ticket.summary }} 20 | {% endif %} 21 | {% endfor %} 22 |
23 | {% else %} 24 | You have no open tickets right now. You can open one if you're having an issue. 25 | {% endif %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/users/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block head %} 4 | {% endblock %} 5 | {% block content %} 6 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block head %} 4 | {% endblock %} 5 | {% block content %} 6 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Register{% endblock %} 3 | {% block head %} 4 | 5 | {% endblock %} 6 | {% block content %} 7 | 8 |
9 |
10 |

{{ config.eligibility }} If you do not meet these requirements, you are still welcome to play, but you will not be eligible for prizes.

11 |
12 |
13 | 14 |
15 |
Register
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | 44 | 45 |
46 |
47 | 50 | 51 |
52 |
53 | 67 |
68 | 69 |
70 | 74 |
75 |
76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 | {% endblock %} 106 | {% block postscript %} 107 | 136 | {% endblock %} 137 | -------------------------------------------------------------------------------- /templates/users/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block head %} 4 | {% endblock %} 5 | {% block content %} 6 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/users/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Dashboard{% endblock %} 3 | {% block head %} 4 | 31 | {% endblock %} 32 | {% block content %} 33 |
34 |
_
35 |
{{ user.username }}
36 |
37 | 38 | {% if not user.email_confirmed %} 39 |
40 |
41 |
42 |
43 | Email unconfirmed 44 |

It looks like you haven't confirmed your email yet. Check the email you used for registration; 45 | the system should have sent you a confirmation link

46 |
47 |
48 |
49 |
50 | {% endif %} 51 |
52 |
Edit Information
53 |
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 |
78 |
79 | 82 | 83 |
84 |
85 | 88 | 89 |
90 |
91 | 112 |
113 | 114 |
115 | 116 |
117 |
118 |
119 | 133 | {% endblock %} 134 | {% block postscript %} 135 | {% if first_login %} 136 | 137 | {% endif %} 138 | 153 | {% endblock %} 154 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCTF/ColdCore/20a27a874f1480edd9e2ed7e04f8b5aef3b3b3e5/utils/__init__.py -------------------------------------------------------------------------------- /utils/admin.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | import oath 3 | def create_password(pw): 4 | return bcrypt.hashpw(pw, bcrypt.gensalt()) 5 | 6 | def verify_password(user, pw): 7 | return bcrypt.hashpw(pw.encode(), user.password.encode()) == user.password.encode() 8 | 9 | def verify_otp(user, otp): 10 | return oath.from_b32key(user.secret).accept(otp) 11 | -------------------------------------------------------------------------------- /utils/cache.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import g 3 | 4 | def get_complex(key): 5 | i = g.redis.get(key) 6 | if i is None: 7 | return None 8 | return json.loads(i.decode()) 9 | 10 | def set_complex(key, val, ex): 11 | g.redis.set(key, json.dumps(val), ex) 12 | -------------------------------------------------------------------------------- /utils/captcha.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from . import misc 3 | from exceptions import CaptchaError 4 | 5 | import config 6 | import requests 7 | 8 | 9 | def verify_captcha(): 10 | if "g-recaptcha-response" not in request.form: 11 | raise CaptchaError("Captcha not completed!") 12 | 13 | captcha_response = request.form["g-recaptcha-response"] 14 | verify_data = dict(secret=config.secret.recaptcha_secret, response=captcha_response, remoteip=misc.get_ip()) 15 | result = requests.post("https://www.google.com/recaptcha/api/siteverify", verify_data).json()["success"] 16 | if not result: 17 | raise CaptchaError("Captcha Invalid!") 18 | 19 | return True 20 | -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | import config 2 | from flask import session, redirect, url_for, flash, g, abort 3 | from functools import wraps 4 | 5 | def login_required(f): 6 | @wraps(f) 7 | def decorated(*args, **kwargs): 8 | if "user_id" in session and session["user_id"]: 9 | return f(*args, **kwargs) 10 | else: 11 | flash("You need to be logged in") 12 | return redirect(url_for('users.login')) 13 | return decorated 14 | 15 | def must_be_allowed_to(thing): 16 | def _must_be_allowed_to(f): 17 | @wraps(f) 18 | def decorated(*args, **kwargs): 19 | if getattr(g, 'user_restricts', None) is None: 20 | return redirect(url_for('users.login')) 21 | if g.user_restricts and thing in g.user_restricts: 22 | return "You are restricted from performing the {} action. Contact an organizer.".format(thing) 23 | 24 | return f(*args, **kwargs) 25 | return decorated 26 | return _must_be_allowed_to 27 | 28 | def confirmed_email_required(f): 29 | @wraps(f) 30 | def decorated(*args, **kwargs): 31 | if "user_id" in session and session["user_id"]: 32 | if not g.user.email_confirmed: 33 | flash("Please confirm your email") 34 | return redirect(url_for('users.dashboard')) 35 | else: 36 | return f(*args, **kwargs) 37 | else: 38 | flash("You need to be logged in to access that page.") 39 | return redirect(url_for('users.login')) 40 | return decorated 41 | 42 | def competition_running_required(f): 43 | @wraps(f) 44 | def decorated(*args, **kwargs): 45 | if not config.competition_is_running() and not ("admin" in session and session["admin"]): 46 | flash("The competition hasn't started") 47 | return redirect(url_for('scoreboard.index')) 48 | return f(*args, **kwargs) 49 | return decorated 50 | 51 | def competition_started_required(f): 52 | @wraps(f) 53 | def decorated(*args, **kwargs): 54 | if not config.competition_has_started() and not ("admin" in session and session["admin"]): 55 | flash("The competition hasn't started") 56 | return redirect(url_for('scoreboard.index')) 57 | return f(*args, **kwargs) 58 | return decorated 59 | 60 | def admin_required(f): 61 | @wraps(f) 62 | def decorated(*args, **kwargs): 63 | if "admin" in session and session["admin"]: 64 | return f(*args, **kwargs) 65 | flash("You must be an admin to access that page.") 66 | return redirect(url_for("admin.admin_login")) 67 | return decorated 68 | 69 | def csrf_check(f): 70 | @wraps(f) 71 | def decorated(*args, **kwargs): 72 | if kwargs["csrf"] != session["_csrf_token"]: 73 | abort(403) 74 | return 75 | 76 | del kwargs["csrf"] 77 | 78 | return f(*args, **kwargs) 79 | return decorated 80 | -------------------------------------------------------------------------------- /utils/email.py: -------------------------------------------------------------------------------- 1 | import config 2 | import requests 3 | 4 | def send_email(to, subject, text): 5 | return requests.post("{}/messages".format(config.secret.mailgun_url), {"from": config.mail_from, "to": to, "subject": subject, "text": text}, auth=("api", config.secret.mailgun_key)) 6 | 7 | 8 | def send_confirmation_email(team_email, confirmation_key): 9 | send_email(team_email, "Welcome to {}!".format(config.ctf_name), 10 | """Hello, and thanks for registering for {}! Before you can start solving problems, 11 | you must confirm your email by clicking the link below: 12 | http://play.icec.tf/confirm_email/{} 13 | 14 | Once you've done that, your account will be enabled, and you will be able to access 15 | the challenges. If you have any trouble, feel free to contact an organizer! 16 | 17 | If you didn't register an account, then you can disregard this email. 18 | """.format(config.ctf_name, confirmation_key)) 19 | 20 | def is_valid_email(email): 21 | return not email.strip().lower().endswith(config.disallowed_domain) 22 | 23 | def send_password_reset_email(team_email, password_reset_token): 24 | send_email(team_email, "{} Password Reset".format(config.ctf_name), 25 | """To reset your password click the link below and enter a new password. This link will expire in 24 hours. 26 | http://play.icec.tf/reset_password/{} 27 | 28 | If you didn't request this email, then you can disregard it. 29 | """.format(password_reset_token)) 30 | -------------------------------------------------------------------------------- /utils/misc.py: -------------------------------------------------------------------------------- 1 | import random 2 | import config 3 | from flask import request 4 | 5 | allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789" 6 | 7 | 8 | def generate_random_string(length=32, chars=allowed_chars): 9 | r = random.SystemRandom() 10 | return "".join([r.choice(chars) for i in range(length)]) 11 | 12 | 13 | def generate_team_key(): 14 | return config.ctf_name.lower() + "_" + generate_random_string(32, allowed_chars) 15 | 16 | 17 | def generate_confirmation_key(): 18 | return generate_random_string(48) 19 | 20 | 21 | def get_ip(): 22 | return request.headers.get(config.proxied_ip_header, request.remote_addr) 23 | 24 | -------------------------------------------------------------------------------- /utils/notification.py: -------------------------------------------------------------------------------- 1 | def make_link(text, target): 2 | return '{}'.format(target, text) 3 | -------------------------------------------------------------------------------- /utils/ratelimit.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import update_wrapper 3 | from flask import request, g, jsonify, session, flash, redirect 4 | 5 | 6 | class RateLimit(object): 7 | expiration_window = 10 8 | 9 | def __init__(self, key_prefix, limit, per, send_x_headers): 10 | self.reset = (int(time.time()) // per) * per + per 11 | self.key = key_prefix + str(self.reset) 12 | self.limit = limit 13 | self.per = per 14 | self.send_x_headers = send_x_headers 15 | p = g.redis.pipeline() 16 | p.incr(self.key) 17 | p.expireat(self.key, self.reset + self.expiration_window) 18 | self.current = min(p.execute()[0], limit) 19 | 20 | remaining = property(lambda x: x.limit - x.current) 21 | over_limit = property(lambda x: x.current > x.limit) 22 | 23 | 24 | def get_view_rate_limit(): 25 | return getattr(g, '_view_rate_limit', None) 26 | 27 | 28 | def on_over_limit(limit): 29 | flash("You are doing that too fast!") 30 | return redirect(request.path) 31 | 32 | 33 | def on_over_api_limit(limit): 34 | return jsonify(dict(code=1000, message="You are doing that too fast!")) 35 | 36 | 37 | def scope_func(): 38 | id = str(request.remote_addr) 39 | if g.logged_in: 40 | id += "/%s" % (session["user_id"]) 41 | return id 42 | 43 | 44 | def ratelimit(limit, per=300, send_x_headers=True, 45 | methods=["POST"], 46 | over_limit=on_over_limit, 47 | scope_func=scope_func, 48 | key_func=lambda: request.endpoint): 49 | def decorator(f): 50 | def rate_limited(*args, **kwargs): 51 | if request.method in methods: 52 | key = 'rate-limit/%s/%s/' % (key_func(), scope_func()) 53 | rlimit = RateLimit(key, limit, per, send_x_headers) 54 | g._view_rate_limit = rlimit 55 | if over_limit is not None and rlimit.over_limit: 56 | return over_limit(rlimit) 57 | return f(*args, **kwargs) 58 | return update_wrapper(rate_limited, f) 59 | return decorator 60 | -------------------------------------------------------------------------------- /utils/select.py: -------------------------------------------------------------------------------- 1 | import pycountry 2 | 3 | TShirts = [ 4 | "XS", 5 | "S", 6 | "M", 7 | "L", 8 | "XL", 9 | "XXL" 10 | ] 11 | 12 | Backgrounds = [ 13 | ("elementary", "Elementary School Student"), 14 | ("high", "High School Student"), 15 | ("university", "University Student"), 16 | ("teacher", "Teacher"), 17 | ("professional", "Security Professional"), 18 | ("hobbyist", "CTF Hobbyist(non-student)"), 19 | ("other", "Other") 20 | ] 21 | 22 | BackgroundKeys = [x[0] for x in Backgrounds] 23 | 24 | Countries = [(country.alpha_3, country.name) for country in pycountry.countries] 25 | Countries = (sorted(Countries, key=lambda x: "0" if x[1] == "Iceland" else x[1])) 26 | CountryKeys = [x[0] for x in Countries] 27 | 28 | 29 | def genoption(arr, selected=None, header=None): 30 | s = "" 31 | if header is not None: 32 | s += header 33 | for val in arr: 34 | if isinstance(val, tuple): 35 | val, name = val 36 | else: 37 | name = val 38 | if selected is not None and val == selected: 39 | s += ('' % (val, name)) 40 | else: 41 | s += ('' % (val, name)) 42 | return s 43 | -------------------------------------------------------------------------------- /yeshello.py: -------------------------------------------------------------------------------- 1 | from app import app, url_for 2 | app.config["SERVER_NAME"] = "server" 3 | 4 | with app.app_context(): 5 | import urllib.parse 6 | output = [] 7 | for rule in app.url_map.iter_rules(): 8 | 9 | options = {} 10 | for arg in rule.arguments: 11 | options[arg] = "[{0}]".format(arg) 12 | 13 | methods = ','.join(rule.methods) 14 | url = url_for(rule.endpoint, **options) 15 | line = urllib.parse.unquote("{:50s} {:20s} {}".format(rule.endpoint, methods, url)) 16 | output.append(line) 17 | 18 | for line in sorted(output): 19 | print(line) 20 | --------------------------------------------------------------------------------