├── 2 ├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── config.py ├── ctferror.py ├── ctftool ├── database.py ├── docs └── concepts.md ├── modules ├── admin.py └── api.py ├── regenerate_score.py ├── requirements.txt ├── static ├── api.js ├── sorttable.js └── tjctf.css ├── templates ├── admin │ ├── base.html │ ├── dashboard.html │ ├── login.html │ ├── team.html │ ├── ticket_detail.html │ └── tickets.html ├── base.html ├── challenge_solves.html ├── challenges.html ├── chat.html ├── dashboard.html ├── login.html ├── open_ticket.html ├── register.html ├── scoreboard.html ├── ticket_detail.html └── tickets.html ├── utils ├── __init__.py ├── admin.py ├── cache.py ├── captcha.py ├── decorators.py ├── email.py ├── flag.py ├── misc.py ├── notification.py └── scoreboard.py └── yeshello.py /2: -------------------------------------------------------------------------------- 1 | from app import app, url_for 2 | app.config["SERVER_NAME"] = "server" 3 | 4 | with app.app_context(): 5 | import urllib 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.unquote("{:50s} {:20s} {}".format(rule.endpoint, methods, url)) 16 | output.append(line) 17 | 18 | for line in sorted(output): 19 | print(line) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.swp 3 | dev.db 4 | dump.rdb 5 | /problems 6 | __pycache__ 7 | /secrets 8 | venv 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015-2016, Fox Wilson 2 | 3 | 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. 4 | 5 | DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flagbase 2 | This is the platform for TJCTF. It's (what I think is) the best of the MITRE, 3 | CTFd, and picoCTF platforms rolled up into a Flask application. 4 | 5 | I'm bad at naming things. 6 | 7 | ## Installation 8 | You're going to want to edit `config.py`. The variable names should be fairly 9 | self-explanatory. 10 | 11 | You're going to want to create a file called `secrets`. It will look like this: 12 | 13 | ```yml 14 | mailgun_url: https://api.mailgun.net/v3/tjctf.org 15 | mailgun_key: key-asdflkjasdhflkjsdahflkhsdaklfjhasd 16 | recaptcha_key: asdlkfjhasdlkjfhlsdakjfh 17 | recaptcha_secret: sdakjfhsdalkfjhsdalkfjh 18 | key: this can be anything you want, it is your flask secret_key 19 | ``` 20 | 21 | You should edit line 2 of database.py, unless you want to use SQLite. This uses 22 | Peewee, so you can use any Peewee-supported database. 23 | 24 | You can create some problem YAML files that look like this: 25 | 26 | ```yml 27 | name: Problem Name 28 | author: ME! 29 | category: Binary 30 | description: binary binary binary binary. i love binary 31 | points: 250 32 | flags: "flag{whatever}" 33 | ``` 34 | 35 | Then add them with `./ctftool add-challenge problem.yml` and it'll get put in the 36 | database. 37 | 38 | Run `python app.py` and you have a server running. You probably want to deploy 39 | it with `gunicorn` or similar, long-term. 40 | 41 | ## ctftool 42 | 43 | You can do some really fancy stuff with `ctftool`. If you have a directory structure 44 | like this: 45 | 46 | - ctf-platform 47 | - ctf-problems 48 | - problem1 49 | - problem.yml 50 | - static.yml 51 | - problem2 52 | - problem.yml 53 | - static.yml 54 | - problem3 55 | - problem.yml 56 | - problem4 57 | 58 | You can run `./ctftool scan ../ctf-problems/` and get a fully populated database 59 | with information from all the problem.yml files, and automatically generated 60 | static file names, and automatic substitutions for static file links in 61 | problem.yml. More documentation on this to come soon. 62 | 63 | ## Contributing 64 | 65 | Flagbase is under really heavy development right now. That means 66 | 67 | - **Absolutely do** submit issues: bugs and feature requests are awesome. 68 | 69 | - **Don't** submit a pull request for: 70 | - a major feature addition 71 | - database model changes 72 | 73 | - **Do** submit pull requests for: 74 | - documentation addition/edits 75 | - minor bugfixes 76 | - small changes to existing features 77 | 78 | If you're touching `database.py` or `config.py`, you're probably doing it wrong. 79 | 80 | If you decide to ignore my guidelines, **write detailed documentation** on what your 81 | pull request consists of, what problems it fixes, how it works, and what issues 82 | it could bring up. 83 | 84 | Of course, you are more than welcome to fork the repository. 85 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, session, redirect, url_for, request, g, flash, jsonify 2 | app = Flask(__name__) 3 | 4 | from database import Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification, db 5 | from datetime import datetime 6 | from peewee import fn 7 | 8 | from utils import decorators, flag, cache, misc, captcha, email 9 | import utils.scoreboard 10 | 11 | import config 12 | import utils 13 | import redis 14 | import requests 15 | import socket 16 | 17 | app.secret_key = config.secret.key 18 | 19 | import logging 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | @app.before_request 23 | def make_info_available(): 24 | if "team_id" in session: 25 | g.team = Team.get(Team.id == session["team_id"]) 26 | g.team_restricts = g.team.restricts.split(",") 27 | 28 | @app.context_processor 29 | def scoreboard_variables(): 30 | var = dict(config=config) 31 | if "team_id" in session: 32 | var["logged_in"] = True 33 | var["team"] = g.team 34 | var["notifications"] = Notification.select().where(Notification.team == g.team) 35 | else: 36 | var["logged_in"] = False 37 | var["notifications"] = [] 38 | 39 | return var 40 | 41 | # Blueprints 42 | from modules import api, admin 43 | app.register_blueprint(api.api) 44 | app.register_blueprint(admin.admin) 45 | 46 | # Publically accessible things 47 | 48 | @app.route('/') 49 | def root(): 50 | return redirect(url_for('scoreboard')) 51 | 52 | @app.route('/chat/') 53 | def chat(): 54 | return render_template("chat.html") 55 | 56 | @app.route('/scoreboard/') 57 | def scoreboard(): 58 | data = cache.get_complex("scoreboard") 59 | graphdata = cache.get_complex("graph") 60 | if data is None or graphdata is None: 61 | if config.immediate_scoreboard: 62 | data = utils.scoreboard.calculate_scores() 63 | graphdata = utils.scoreboard.calculate_graph(data) 64 | utils.scoreboard.set_complex("scoreboard", data, 120) 65 | utils.scoreboard.set_complex("graph", graphdata, 120) 66 | else: 67 | return "No scoreboard data available. Please contact an organizer." 68 | 69 | return render_template("scoreboard.html", data=data, graphdata=graphdata) 70 | 71 | @app.route('/login/', methods=["GET", "POST"]) 72 | def login(): 73 | if request.method == "GET": 74 | return render_template("login.html") 75 | elif request.method == "POST": 76 | team_key = request.form["team_key"] 77 | 78 | try: 79 | team = Team.get(Team.key == team_key) 80 | TeamAccess.create(team=team, ip=misc.get_ip(), time=datetime.now()) 81 | session["team_id"] = team.id 82 | flash("Login successful.") 83 | return redirect(url_for('dashboard')) 84 | except Team.DoesNotExist: 85 | flash("Couldn't find your team. Check your team key.", "error") 86 | return render_template("login.html") 87 | 88 | @app.route('/register/', methods=["GET", "POST"]) 89 | def register(): 90 | if not config.registration: 91 | if "admin" in session and session["admin"]: 92 | pass 93 | else: 94 | return "Registration is currently disabled. Email ctf@tjhsst.edu to create an account." 95 | 96 | if request.method == "GET": 97 | return render_template("register.html") 98 | elif request.method == "POST": 99 | error, message = captcha.verify_captcha() 100 | if error: 101 | flash(message) 102 | return render_template("register.html") 103 | 104 | team_name = request.form["team_name"].strip() 105 | team_email = request.form["team_email"].strip() 106 | team_elig = "team_eligibility" in request.form 107 | affiliation = request.form["affiliation"].strip() 108 | 109 | if len(team_name) > 50 or not team_name: 110 | flash("You must have a team name!") 111 | return render_template("register.html") 112 | 113 | if not (team_email and "." in team_email and "@" in team_email): 114 | flash("You must have a valid team email!") 115 | return render_template("register.html") 116 | 117 | if not affiliation or len(affiliation) > 100: 118 | affiliation = "No affiliation" 119 | 120 | if not email.is_valid_email(team_email): 121 | flash("You're lying") 122 | return render_template("register.html") 123 | 124 | team_key = misc.generate_team_key() 125 | confirmation_key = misc.generate_confirmation_key() 126 | 127 | team = Team.create(name=team_name, email=team_email, eligible=team_elig, affiliation=affiliation, key=team_key, 128 | email_confirmation_key=confirmation_key) 129 | TeamAccess.create(team=team, ip=misc.get_ip(), time=datetime.now()) 130 | 131 | email.send_confirmation_email(team_email, confirmation_key, team_key) 132 | 133 | session["team_id"] = team.id 134 | flash("Team created.") 135 | return redirect(url_for('dashboard')) 136 | 137 | @app.route('/logout/') 138 | def logout(): 139 | session.pop("team_id") 140 | flash("You've successfully logged out.") 141 | return redirect(url_for('root')) 142 | 143 | # Things that require a team 144 | 145 | @app.route('/confirm_email/', methods=["POST"]) 146 | @decorators.login_required 147 | def confirm_email(): 148 | if request.form["confirmation_key"] == g.team.email_confirmation_key: 149 | flash("Email confirmed!") 150 | g.team.email_confirmed = True 151 | g.team.save() 152 | else: 153 | flash("Incorrect confirmation key.") 154 | return redirect(url_for('dashboard')) 155 | 156 | @app.route('/team/', methods=["GET", "POST"]) 157 | @decorators.login_required 158 | def dashboard(): 159 | if request.method == "GET": 160 | team_solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).where(ChallengeSolve.team == g.team) 161 | team_adjustments = ScoreAdjustment.select().where(ScoreAdjustment.team == g.team) 162 | team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments]) 163 | first_login = False 164 | if g.team.first_login: 165 | first_login = True 166 | g.team.first_login = False 167 | g.team.save() 168 | return render_template("dashboard.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score, first_login=first_login) 169 | 170 | elif request.method == "POST": 171 | if g.redis.get("ul{}".format(session["team_id"])): 172 | flash("You're changing your information too fast!") 173 | return redirect(url_for('dashboard')) 174 | 175 | team_name = request.form["team_name"].strip() 176 | team_email = request.form["team_email"].strip() 177 | affiliation = request.form["affiliation"].strip() 178 | team_elig = "team_eligibility" in request.form 179 | 180 | if len(team_name) > 50 or not team_name: 181 | flash("You must have a team name!") 182 | return redirect(url_for('dashboard')) 183 | 184 | if not (team_email and "." in team_email and "@" in team_email): 185 | flash("You must have a valid team email!") 186 | return redirect(url_for('dashboard')) 187 | 188 | if not affiliation or len(affiliation) > 100: 189 | affiliation = "No affiliation" 190 | 191 | email_changed = (team_email != g.team.email) 192 | 193 | g.team.name = team_name 194 | g.team.email = team_email 195 | g.team.affiliation = affiliation 196 | if not g.team.eligibility_locked: 197 | g.team.eligible = team_elig 198 | 199 | g.redis.set("ul{}".format(session["team_id"]), str(datetime.now()), 120) 200 | 201 | if email_changed: 202 | if not email.is_valid_email(team_email): 203 | flash("You're lying") 204 | return redirect(url_for('dashboard')) 205 | 206 | g.team.email_confirmation_key = misc.generate_confirmation_key() 207 | g.team.email_confirmed = False 208 | 209 | email.send_confirmation_email(team_email, g.team.email_confirmation_key, g.team.key) 210 | flash("Changes saved. Please check your email for a new confirmation key.") 211 | else: 212 | flash("Changes saved.") 213 | g.team.save() 214 | 215 | 216 | return redirect(url_for('dashboard')) 217 | 218 | @app.route('/teamconfirm/', methods=["POST"]) 219 | def teamconfirm(): 220 | if utils.misc.get_ip() in config.confirm_ip: 221 | team_name = request.form["team_name"].strip() 222 | team_key = request.form["team_key"].strip() 223 | try: 224 | team = Team.get(Team.name == team_name) 225 | except Team.DoesNotExist: 226 | return "invalid", 403 227 | if team.key == team_key: 228 | return "ok", 200 229 | else: 230 | return "invalid", 403 231 | else: 232 | return "unauthorized", 401 233 | 234 | @app.route('/challenges/') 235 | @decorators.must_be_allowed_to("view challenges") 236 | @decorators.competition_running_required 237 | @decorators.confirmed_email_required 238 | def challenges(): 239 | chals = Challenge.select().order_by(Challenge.points, Challenge.name) 240 | solved = Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team) 241 | solves = {i: int(g.redis.hget("solves", i).decode()) for i in [k.id for k in chals]} 242 | categories = sorted(list({chal.category for chal in chals})) 243 | return render_template("challenges.html", challenges=chals, solved=solved, categories=categories, solves=solves) 244 | 245 | @app.route('/challenges//solves/') 246 | @decorators.must_be_allowed_to("view challenge solves") 247 | @decorators.must_be_allowed_to("view challenges") 248 | @decorators.competition_running_required 249 | @decorators.confirmed_email_required 250 | def challenge_show_solves(challenge): 251 | chal = Challenge.get(Challenge.id == challenge) 252 | solves = ChallengeSolve.select(ChallengeSolve, Team).join(Team).order_by(ChallengeSolve.time).where(ChallengeSolve.challenge == chal) 253 | return render_template("challenge_solves.html", challenge=chal, solves=solves) 254 | 255 | @app.route('/submit//', methods=["POST"]) 256 | @decorators.must_be_allowed_to("solve challenges") 257 | @decorators.must_be_allowed_to("view challenges") 258 | @decorators.competition_running_required 259 | @decorators.confirmed_email_required 260 | def submit(challenge): 261 | chal = Challenge.get(Challenge.id == challenge) 262 | flagval = request.form["flag"] 263 | 264 | code, message = flag.submit_flag(g.team, chal, flagval) 265 | flash(message) 266 | return redirect(url_for('challenges')) 267 | 268 | # Trouble tickets 269 | 270 | @app.route('/tickets/') 271 | @decorators.must_be_allowed_to("view tickets") 272 | @decorators.login_required 273 | def team_tickets(): 274 | return render_template("tickets.html", tickets=list(g.team.tickets)) 275 | 276 | @app.route('/tickets/new/', methods=["GET", "POST"]) 277 | @decorators.must_be_allowed_to("submit tickets") 278 | @decorators.must_be_allowed_to("view tickets") 279 | @decorators.login_required 280 | def open_ticket(): 281 | if request.method == "GET": 282 | return render_template("open_ticket.html") 283 | elif request.method == "POST": 284 | if g.redis.get("ticketl{}".format(session["team_id"])): 285 | return "You're doing that too fast." 286 | g.redis.set("ticketl{}".format(g.team.id), "1", 30) 287 | summary = request.form["summary"] 288 | description = request.form["description"] 289 | opened_at = datetime.now() 290 | ticket = TroubleTicket.create(team=g.team, summary=summary, description=description, opened_at=opened_at) 291 | flash("Ticket #{} opened.".format(ticket.id)) 292 | return redirect(url_for("team_ticket_detail", ticket=ticket.id)) 293 | 294 | @app.route('/tickets//') 295 | @decorators.must_be_allowed_to("view tickets") 296 | @decorators.login_required 297 | def team_ticket_detail(ticket): 298 | try: 299 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 300 | except TroubleTicket.DoesNotExist: 301 | flash("Couldn't find ticket #{}.".format(ticket)) 302 | return redirect(url_for("team_tickets")) 303 | 304 | if ticket.team != g.team: 305 | flash("That's not your ticket.") 306 | return redirect(url_for("team_tickets")) 307 | 308 | comments = TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time) 309 | return render_template("ticket_detail.html", ticket=ticket, comments=comments) 310 | 311 | @app.route('/tickets//comment/', methods=["POST"]) 312 | @decorators.must_be_allowed_to("comment on tickets") 313 | @decorators.must_be_allowed_to("view tickets") 314 | def team_ticket_comment(ticket): 315 | if g.redis.get("ticketl{}".format(session["team_id"])): 316 | return "You're doing that too fast." 317 | g.redis.set("ticketl{}".format(g.team.id), "1", 30) 318 | try: 319 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 320 | except TroubleTicket.DoesNotExist: 321 | flash("Couldn't find ticket #{}.".format(ticket)) 322 | return redirect(url_for("team_tickets")) 323 | 324 | if ticket.team != g.team: 325 | flash("That's not your ticket.") 326 | return redirect(url_for("team_tickets")) 327 | 328 | if request.form["comment"]: 329 | TicketComment.create(ticket=ticket, comment_by=g.team.name, comment=request.form["comment"], time=datetime.now()) 330 | flash("Comment added.") 331 | 332 | if ticket.active and "resolved" in request.form: 333 | ticket.active = False 334 | ticket.save() 335 | flash("Ticket closed.") 336 | 337 | elif not ticket.active and "resolved" not in request.form: 338 | ticket.active = True 339 | ticket.save() 340 | flash("Ticket re-opened.") 341 | 342 | return redirect(url_for("team_ticket_detail", ticket=ticket.id)) 343 | 344 | # Debug 345 | @app.route('/debug/') 346 | def debug_app(): 347 | return jsonify(hostname=socket.gethostname()) 348 | 349 | # Manage Peewee database sessions and Redis 350 | 351 | @app.before_request 352 | def before_request(): 353 | db.connect() 354 | g.redis = redis.StrictRedis() 355 | 356 | @app.teardown_request 357 | def teardown_request(exc): 358 | db.close() 359 | g.redis.connection_pool.disconnect() 360 | 361 | # CSRF things 362 | 363 | @app.before_request 364 | def csrf_protect(): 365 | csrf_exempt = ['/teamconfirm/'] 366 | 367 | if request.method == "POST": 368 | token = session.get('_csrf_token', None) 369 | if (not token or token != request.form["_csrf_token"]) and not request.path in csrf_exempt: 370 | return "Invalid CSRF token!" 371 | 372 | def generate_csrf_token(): 373 | if '_csrf_token' not in session: 374 | session['_csrf_token'] = misc.generate_random_string(64) 375 | return session['_csrf_token'] 376 | 377 | app.jinja_env.globals['csrf_token'] = generate_csrf_token 378 | 379 | if __name__ == '__main__': 380 | app.run(debug=True, port=8001) 381 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | ctf_name = "TJCTF" 5 | #IRC Channel 6 | ctf_chat_channel = "#tjctf" 7 | ctf_home_url = "http://tjctf.org" 8 | eligibility = "In order to be eligible for prizes, all members of your team must be in high school, and you must not have more than four team members." 9 | tagline = "a cybersecurity competition created by TJHSST students" 10 | 11 | cdn = True 12 | apisubmit = True 13 | registration = True 14 | 15 | proxied_ip_header = "X-Forwarded-For" 16 | 17 | flag_rl = 5 18 | teams_on_graph = 10 19 | 20 | mail_from = "tjctf@sandbox1431.mailgun.org" 21 | 22 | immediate_scoreboard = False 23 | 24 | # IPs that are allowed to confirm teams by posting to /teamconfirm/ 25 | # Useful for verifying resumes and use with resume server. 26 | confirm_ip = [] 27 | 28 | static_prefix = "http://127.0.0.1/tjctf-static/" 29 | static_dir = "{}/static/".format(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 30 | custom_stylesheet = "tjctf.css" 31 | 32 | competition_begin = datetime(1970, 1, 1, 0, 0) 33 | competition_end = datetime(2018, 1, 1, 0, 0) 34 | 35 | # Are you using a resume server? 36 | resumes = True 37 | # If yes, where's it hosted? Otherwise, just put None. 38 | resume_server = "https://resumes.tjctf.org" 39 | 40 | disallowed_domain = "tjctf.org" 41 | 42 | 43 | def competition_is_running(): 44 | return competition_begin < datetime.now() < competition_end 45 | 46 | # Don't touch these. Instead, copy secrets.example to secrets and edit that. 47 | import yaml 48 | from collections import namedtuple 49 | with open("secrets") as f: 50 | _secret = yaml.load(f) 51 | secret = namedtuple('SecretsDict', _secret.keys())(**_secret) 52 | -------------------------------------------------------------------------------- /ctferror.py: -------------------------------------------------------------------------------- 1 | SUCCESS = (0, "Success!") 2 | 3 | FLAG_SUBMISSION_TOO_FAST = (1001, "You're submitting flags too fast!") 4 | FLAG_SUBMITTED_ALREADY = (1002, "You've already solved that problem!") 5 | FLAG_INCORRECT = (1003, "Incorrect flag.") 6 | FLAG_CANNOT_SUBMIT_WHILE_DISABLED = (1004, "You cannot submit a flag for a disabled problem.") 7 | 8 | CAPTCHA_NOT_COMPLETED = (2001, "Please complete the CAPTCHA.") 9 | CAPTCHA_INVALID = (2002, "Invalid CAPTCHA response.") 10 | 11 | NOTIFICATION_NOT_YOURS = (3001, "You cannot dismiss notifications that do not belong to you.") 12 | -------------------------------------------------------------------------------- /ctftool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from 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 yaml 16 | 17 | tables = [Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser] 18 | 19 | operation = sys.argv[1] 20 | if operation == "create-tables": 21 | [i.create_table() for i in tables] 22 | print("Tables created") 23 | 24 | elif operation == "drop-tables": 25 | if input("Are you sure? Type yes to continue: ") == "yes": 26 | [i.drop_table() for i in tables] 27 | print("Done") 28 | else: 29 | print("Okay, nothing happened.") 30 | 31 | elif operation == "add-challenge": 32 | challengefile = sys.argv[2] 33 | with open(challengefile) as f: 34 | chal = Challenge.create(**yaml.load(f)) 35 | print("Challenge added with id {}".format(chal.id)) 36 | 37 | elif operation == "gen-challenge": 38 | n = int(sys.argv[2]) 39 | for i in range(n): 40 | name = str(random.randint(0, 999999999)) 41 | 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") 42 | print("Challenge added with id {}".format(chal.id)) 43 | 44 | elif operation == "gen-team": 45 | n = int(sys.argv[2]) 46 | chals = list(Challenge.select()) 47 | ctz = datetime.now() 48 | diff = timedelta(minutes=5) 49 | for i in range(n): 50 | name = "Team {}".format(i + 1) 51 | t = Team.create(name=name, email="none@none.com", affiliation="Autogenerated", eligible=True, key="", email_confirmation_key="autogen", email_confirmed=True) 52 | t.key = "autogen{}".format(t.id) 53 | t.save() 54 | print("Team added with id {}".format(t.id)) 55 | 56 | elif operation == "add-admin": 57 | username = input("Username: ") 58 | password = getpass.getpass().encode() 59 | pwhash = utils.admin.create_password(password) 60 | r = random.SystemRandom() 61 | secret = "".join([r.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for i in range(16)]) 62 | AdminUser.create(username=username, password=pwhash, secret=secret) 63 | print("AdminUser created; Enter the following key into your favorite TOTP application (Google Authenticator Recommended): {}".format(secret)) 64 | 65 | elif operation == "scan": 66 | path = sys.argv[2] 67 | dirs = [j for j in [os.path.join(path, i) for i in os.listdir(path)] if os.path.isdir(j)] 68 | print(dirs) 69 | n = 0 70 | 71 | for d in dirs: 72 | staticpaths = {} 73 | if os.path.exists(os.path.join(d, "static.yml")): 74 | with open(os.path.join(d, "static.yml")) as f: 75 | statics = yaml.load(f) 76 | for static in statics: 77 | h = hashlib.sha256() 78 | with open(os.path.join(d, static), "rb") as staticfile: 79 | while True: 80 | buf = staticfile.read(4096) 81 | h.update(buf) 82 | if not buf: 83 | break 84 | 85 | if "." in static: 86 | name, ext = static.split(".", maxsplit=1) 87 | fn = "{}_{}.{}".format(name, h.hexdigest(), ext) 88 | else: 89 | fn = "{}_{}".format(static, h.hexdigest()) 90 | staticpaths[static] = fn 91 | shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn)) 92 | print(fn) 93 | 94 | if os.path.exists(os.path.join(d, "problem.yml")): 95 | with open(os.path.join(d, "problem.yml")) as f: 96 | n += 1 97 | data = yaml.load(f) 98 | for i in staticpaths: 99 | print("looking for |{}|".format(i)) 100 | data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i])) 101 | 102 | query = Challenge.select().where(Challenge.name == data["name"]) 103 | if query.exists(): 104 | print("Updating " + str(data["name"]) + "...") 105 | q = Challenge.update(**data).where(Challenge.name == data["name"]) 106 | q.execute() 107 | else: 108 | Challenge.create(**data) 109 | 110 | print(n, "challenges loaded") 111 | 112 | elif operation == "recache-solves": 113 | r = redis.StrictRedis() 114 | for chal in Challenge.select(): 115 | r.hset("solves", chal.id, chal.solves.count()) 116 | print(r.hvals("solves")) 117 | 118 | elif operation == "list-challenges": 119 | for chal in Challenge.select(): 120 | print("{} {}".format(str(chal.id).rjust(3), chal.name)) 121 | 122 | elif operation == "del-challenge": 123 | id = sys.argv[2] 124 | c = Challenge.get(id=id) 125 | ChallengeFailure.delete().where(ChallengeFailure.challenge == c).execute() 126 | ChallengeSolve.delete().where(ChallengeSolve.challenge == c).execute() 127 | c.delete_instance() 128 | 129 | # vim: syntax=python:ft=python 130 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | db = SqliteDatabase("dev.db") 3 | 4 | class BaseModel(Model): 5 | class Meta: 6 | database = db 7 | 8 | class Team(BaseModel): 9 | name = CharField() 10 | email = CharField() 11 | affiliation = CharField() 12 | eligible = BooleanField() 13 | eligibility_locked = BooleanField(default=False) 14 | first_login = BooleanField(default=True) 15 | email_confirmed = BooleanField(default=False) 16 | email_confirmation_key = CharField() 17 | restricts = TextField(default="") 18 | key = CharField() 19 | 20 | def solved(self, challenge): 21 | return ChallengeSolve.select().where(ChallengeSolve.team == self, ChallengeSolve.challenge == challenge).count() 22 | 23 | @property 24 | def score(self): 25 | challenge_points = sum([i.challenge.points for i in self.solves]) 26 | adjust_points = sum([i.value for i in self.adjustments]) 27 | return challenge_points + adjust_points 28 | 29 | class TeamAccess(BaseModel): 30 | team = ForeignKeyField(Team, related_name='accesses') 31 | ip = CharField() 32 | time = DateTimeField() 33 | 34 | class Challenge(BaseModel): 35 | name = CharField() 36 | category = CharField() 37 | author = CharField() 38 | description = TextField() 39 | points = IntegerField() 40 | breakthrough_bonus = IntegerField(default=0) 41 | enabled = BooleanField(default=True) 42 | flag = TextField() 43 | 44 | class ChallengeSolve(BaseModel): 45 | team = ForeignKeyField(Team, related_name='solves') 46 | challenge = ForeignKeyField(Challenge, related_name='solves') 47 | time = DateTimeField() 48 | 49 | class Meta: 50 | primary_key = CompositeKey('team', 'challenge') 51 | 52 | class ChallengeFailure(BaseModel): 53 | team = ForeignKeyField(Team, related_name='failures') 54 | challenge = ForeignKeyField(Challenge, related_name='failures') 55 | attempt = CharField() 56 | time = DateTimeField() 57 | 58 | class NewsItem(BaseModel): 59 | summary = CharField() 60 | description = TextField() 61 | 62 | class TroubleTicket(BaseModel): 63 | team = ForeignKeyField(Team, related_name='tickets') 64 | summary = CharField() 65 | description = TextField() 66 | active = BooleanField(default=True) 67 | opened_at = DateTimeField() 68 | 69 | class TicketComment(BaseModel): 70 | ticket = ForeignKeyField(TroubleTicket, related_name='comments') 71 | comment_by = CharField() 72 | comment = TextField() 73 | time = DateTimeField() 74 | 75 | class Notification(BaseModel): 76 | team = ForeignKeyField(Team, related_name='notifications') 77 | notification = TextField() 78 | 79 | class ScoreAdjustment(BaseModel): 80 | team = ForeignKeyField(Team, related_name='adjustments') 81 | value = IntegerField() 82 | reason = TextField() 83 | 84 | class AdminUser(BaseModel): 85 | username = CharField() 86 | password = CharField() 87 | secret = CharField() 88 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/admin.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, session, redirect, url_for, flash 2 | from database import AdminUser, Team, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification 3 | import utils 4 | import utils.admin 5 | import utils.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", "admin", url_prefix="/admin") 11 | 12 | @admin.route("/") 13 | def admin_root(): 14 | if "admin" in session: 15 | return redirect(url_for(".admin_dashboard")) 16 | else: 17 | return redirect(url_for(".admin_login")) 18 | 19 | @admin.route("/login/", methods=["GET", "POST"]) 20 | def admin_login(): 21 | if request.method == "GET": 22 | return render_template("admin/login.html") 23 | 24 | elif request.method == "POST": 25 | username = request.form["username"] 26 | password = request.form["password"] 27 | two = request.form["two"] 28 | if getattr(secret, "admin_username", False): 29 | if username == secret.admin_username and password == secret.admin_password: 30 | session["admin"] = username 31 | return redirect(url_for(".admin_dashboard")) 32 | else: 33 | try: 34 | user = AdminUser.get(AdminUser.username == username) 35 | result = utils.admin.verify_password(user, password) 36 | result = result and utils.admin.verify_otp(user, two) 37 | if result: 38 | session["admin"] = user.username 39 | return redirect(url_for(".admin_dashboard")) 40 | except AdminUser.DoesNotExist: 41 | pass 42 | 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̷͍̤̠͖̣͈.̺̩̦̻.") 43 | return render_template("admin/login.html") 44 | 45 | @admin.route("/dashboard/") 46 | @admin_required 47 | def admin_dashboard(): 48 | teams = Team.select() 49 | solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) 50 | adjustments = ScoreAdjustment.select() 51 | scoredata = utils.scoreboard.get_all_scores(teams, solves, adjustments) 52 | lastsolvedata = utils.scoreboard.get_last_solves(teams, solves) 53 | tickets = list(TroubleTicket.select().where(TroubleTicket.active == True)) 54 | return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets) 55 | 56 | @admin.route("/tickets/") 57 | @admin_required 58 | def admin_tickets(): 59 | tickets = list(TroubleTicket.select(TroubleTicket, Team).join(Team).order_by(TroubleTicket.id.desc())) 60 | return render_template("admin/tickets.html", tickets=tickets) 61 | 62 | @admin.route("/tickets//") 63 | @admin_required 64 | def admin_ticket_detail(ticket): 65 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 66 | comments = list(TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)) 67 | return render_template("admin/ticket_detail.html", ticket=ticket, comments=comments) 68 | 69 | @admin.route("/tickets//comment/", methods=["POST"]) 70 | @admin_required 71 | def admin_ticket_comment(ticket): 72 | ticket = TroubleTicket.get(TroubleTicket.id == ticket) 73 | if request.form["comment"]: 74 | TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now()) 75 | Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) 76 | flash("Comment added.") 77 | 78 | if ticket.active and "resolved" in request.form: 79 | ticket.active = False 80 | ticket.save() 81 | Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) 82 | flash("Ticket closed.") 83 | 84 | elif not ticket.active and "resolved" not in request.form: 85 | ticket.active = True 86 | ticket.save() 87 | Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) 88 | flash("Ticket reopened.") 89 | 90 | return redirect(url_for(".admin_ticket_detail", ticket=ticket.id)) 91 | 92 | @admin.route("/team//") 93 | @admin_required 94 | def admin_show_team(tid): 95 | team = Team.get(Team.id == tid) 96 | return render_template("admin/team.html", team=team) 97 | 98 | @admin.route("/team///impersonate/") 99 | @csrf_check 100 | @admin_required 101 | def admin_impersonate_team(tid): 102 | session["team_id"] = tid 103 | return redirect(url_for("scoreboard")) 104 | 105 | @admin.route("/team///toggle_eligibility/") 106 | @csrf_check 107 | @admin_required 108 | def admin_toggle_eligibility(tid): 109 | team = Team.get(Team.id == tid) 110 | team.eligible = not team.eligible 111 | team.save() 112 | flash("Eligibility set to {}".format(team.eligible)) 113 | return redirect(url_for(".admin_show_team", tid=tid)) 114 | 115 | @admin.route("/team///toggle_eligibility_lock/") 116 | @csrf_check 117 | @admin_required 118 | def admin_toggle_eligibility_lock(tid): 119 | team = Team.get(Team.id == tid) 120 | team.eligibility_locked = not team.eligibility_locked 121 | team.save() 122 | flash("Eligibility lock set to {}".format(team.eligibility_locked)) 123 | return redirect(url_for(".admin_show_team", tid=tid)) 124 | 125 | @admin.route("/team//adjust_score/", methods=["POST"]) 126 | @admin_required 127 | def admin_score_adjust(tid): 128 | value = int(request.form["value"]) 129 | reason = request.form["reason"] 130 | 131 | team = Team.get(Team.id == tid) 132 | 133 | ScoreAdjustment.create(team=team, value=value, reason=reason) 134 | flash("Score adjusted.") 135 | 136 | return redirect(url_for(".admin_show_team", tid=tid)) 137 | 138 | @admin.route("/logout/") 139 | def admin_logout(): 140 | del session["admin"] 141 | return redirect(url_for('.admin_login')) 142 | -------------------------------------------------------------------------------- /modules/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, g, request 2 | from database import Challenge, Notification, Team, Challenge, ChallengeSolve 3 | from utils import decorators, flag, scoreboard 4 | from ctferror import * 5 | from datetime import datetime 6 | import config 7 | 8 | api = Blueprint("api", "api", url_prefix="/api") 9 | @api.route("/submit/.json", methods=["POST"]) 10 | @decorators.must_be_allowed_to("solve challenges") 11 | @decorators.must_be_allowed_to("view challenges") 12 | @decorators.competition_running_required 13 | @decorators.confirmed_email_required 14 | def submit_api(challenge): 15 | chal = Challenge.get(Challenge.id == challenge) 16 | flagval = request.form["flag"] 17 | 18 | code, message = flag.submit_flag(g.team, chal, flagval) 19 | return jsonify(dict(code=code, message=message)) 20 | 21 | @api.route("/dismiss/.json", methods=["POST"]) 22 | @decorators.login_required 23 | def dismiss_notification(nid): 24 | n = Notification.get(Notification.id == nid) 25 | if g.team != n.team: 26 | code, message = NOTIFICATION_NOT_YOURS 27 | else: 28 | Notification.delete().where(Notification.id == nid).execute() 29 | code, message = SUCCESS 30 | return jsonify(dict(code=code, message=message)) 31 | 32 | @api.route("/_ctftime/") 33 | def ctftime_scoreboard_json(): 34 | if not config.immediate_scoreboard and datetime.now() < config.competition_end: 35 | return "unavailable", 503 36 | 37 | scores = scoreboard.calculate_scores() 38 | standings = [dict(team=i[2], score=i[4], outward=not i[0]) for i in scores] 39 | for index, standing in enumerate(standings): 40 | standing["pos"] = index + 1 41 | 42 | return jsonify(standings=standings) 43 | -------------------------------------------------------------------------------- /regenerate_score.py: -------------------------------------------------------------------------------- 1 | import redis 2 | r = redis.StrictRedis() 3 | import json 4 | 5 | def set_complex(key, val): 6 | r.set(key, json.dumps(val)) 7 | 8 | import utils 9 | import utils.scoreboard 10 | data = utils.scoreboard.calculate_scores() 11 | graphdata = utils.scoreboard.calculate_graph(data) 12 | set_complex("scoreboard", data) 13 | set_complex("graph", graphdata) 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | peewee 3 | flask 4 | bcrypt 5 | redis 6 | pyyaml 7 | oath 8 | -------------------------------------------------------------------------------- /static/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/sorttable.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TJCSec/ctf-platform/d58f4255ea3a8138d004d548c17ee4038498e55b/static/sorttable.js -------------------------------------------------------------------------------- /static/tjctf.css: -------------------------------------------------------------------------------- 1 | .light-blue { 2 | background-color: rgb(0, 138, 255) !important; 3 | } 4 | 5 | .light-blue-text { 6 | color: rgb(0, 138, 255) !important; 7 | } 8 | 9 | .dark-blue { 10 | background-color: rgb(49, 78, 97) !important; 11 | } 12 | 13 | .dark-blue-text { 14 | color: rgb(49, 78, 97) !important; 15 | } 16 | 17 | .brand-logo { 18 | background-image: url(https://www.tjctf.org/static/logo-thin-white-header.png?1); 19 | width: 150px; 20 | height: 50px; 21 | line-height: 0; 22 | text-indent: -9999px; 23 | position: absolute; 24 | top: 50%; 25 | margin-top: -25px; 26 | } 27 | 28 | .competition-page .navbar-fixed > nav { 29 | background-color: rgb(0, 138, 255) !important; 30 | } 31 | 32 | @media (max-width: 450px) { 33 | nav ul a { 34 | font-size: 0.9rem; 35 | padding: 0 10px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | 42 | {% endif %} 43 |
44 | {% block content %}{% endblock %} 45 |
46 | {% if config.cdn %} 47 | 48 | 49 | 50 | 51 | {% else %} 52 | 53 | 54 | 55 | 56 | {% endif %} 57 | 58 | 65 | 70 | 71 | {% block postscript %} 72 | {% endblock %} 73 | 74 | 75 | -------------------------------------------------------------------------------- /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 | Eligibility is {{ "locked" if team.eligibility_locked else "unlocked" }} (toggle). 8 |

9 |

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

10 |

Email

11 |

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

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

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

Score adjustment

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

Score calculation

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

Solved problems

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

No problems have been solved.

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

Score adjustments

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

No score adjustments have been made.

65 | {% endif %} 66 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /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 | {% endif %} 13 | 14 | 15 | 22 | 23 | 24 | {% block head %}{% endblock %} 25 | 26 | 27 | 84 |
85 | {% if session.admin %} 86 |
87 |
88 | You are an admin. 89 | Please note that team restrictions do not currently apply. 90 |
91 |
92 | {% endif %} 93 | {% for notification in notifications %} 94 |
95 |
96 | {{ notification.notification | safe }} (Click to dismiss) 97 |
98 |
99 | {% endfor %} 100 |
101 |
102 | {% block content %}{% endblock %} 103 |
104 | {% if config.cdn %} 105 | 106 | 107 | 108 | 109 | {% else %} 110 | 111 | 112 | 113 | 114 | {% endif %} 115 | 116 | 123 | 128 | {% block postscript %} 129 | {% endblock %} 130 | 131 | 132 | -------------------------------------------------------------------------------- /templates/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.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Challenges{% endblock %} 3 | {% block head %} 4 | 29 | 34 | {% endblock %} 35 | {% block content %} 36 |

You are playing on behalf of {{ team.name }}. If this is incorrect, you should 37 | logout and login with the correct team key.

38 | 39 | 45 | {% if solved.count() %}Expand all challenges.{% else %}Collapse all challenges.{% endif %} 46 |
47 | 48 |
    49 | {% for challenge in challenges %} 50 |
  • 51 |
    52 | {{ challenge.name }} 53 | 54 | check 55 | 56 | 57 | {{ challenge.author }} 58 | · 59 | {{ solves[challenge.id] }} solve(s) 60 | · 61 | {{ challenge.category }} 62 | · 63 | {{ challenge.points }} pt 64 | 65 |
    66 |
    67 |

    {{ challenge.description | safe }} 68 | {% if challenge in solved %} 69 |

    You've solved this challenge!
    70 | View solves 71 |

    72 | {% else %} 73 |

    74 | View solves 75 |

    76 |
    77 |
    78 |
    79 | 80 | 81 |
    82 | 83 |
    84 | 85 |

    86 | {% endif %} 87 |
  • 88 | {% endfor %} 89 |
90 | {% endblock %} 91 | {% block postscript %} 92 | 97 | {% if config.apisubmit %} 98 | 115 | {% endif %} 116 | {% endblock %} 117 | -------------------------------------------------------------------------------- /templates/chat.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 | 5 |
6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Team Dashboard{% endblock %} 3 | {% block head %} 4 | 31 | {% endblock %} 32 | {% block content %} 33 |

{{ team.name }}

34 |
35 |
36 |
37 |
38 |
39 | Team key: {{ team.key }} 40 |

Share this with your teammates, and keep it in a safe place. You need your team key 41 | in order to log in. If you lose it, an organizer can send it to your team email, 42 | which is shown below.

43 |
44 |
45 |
46 |
47 | {% if not team.email_confirmed %} 48 |
49 |
50 |
51 |
52 | Email unconfirmed 53 |

It looks like you haven't confirmed your email yet. Check the email you used for registration; 54 | the system should have sent you an email confirmation key. Paste it below:

55 |
56 |
57 | 58 | 59 |
60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 | {% endif %} 68 |

Team information

69 |

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

70 |

Your team email is {{ team.email }}, and you are affiliated with 71 | {{ team.affiliation }}.

72 |

Your team is currently marked {{ "eligible" if team.eligible else "ineligible" }}.

73 |
74 |
75 |

Edit information

76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 | {% if not team.eligibility_locked %} 90 |

{{ config.eligibility }}

91 |

If you do not meet these requirements, you are still welcome to play, but you 92 | will not be eligible for prizes. By checking the "Eligibility Certification" 93 | checkbox below, you are certifying that you meet the prize eligibility 94 | requirement. We may request appropriate documentation to verify your eligibility 95 | status before sending you prizes.

96 | 97 | 98 | {% endif %} 99 | 100 |

101 | 102 |
103 |
104 |
105 |

Score calculation

106 | {% if team_solves.count() %} 107 |
Solved problems
108 |
109 |
110 | 111 | 112 | 113 | 114 | 115 | {% for solve in team_solves %} 116 | 117 | 118 | 119 | 120 | 121 | {% endfor %} 122 | 123 |
NameCategoryTimeValue
{{ solve.challenge.name }}{{ solve.challenge.category }}{{ solve.time }}{{ solve.challenge.points }}
124 |
125 |
126 | {% else %} 127 |

No problems have been solved.

128 | {% endif %} 129 | {% if team_adjustments.count() %} 130 |
Score adjustments
131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | {% for adj in team_adjustments %} 139 | 140 | 141 | 142 | 143 | {% endfor %} 144 | 145 |
ReasonValue
{{ adj.reason }}{{ adj.value }}
146 |
147 |
148 | {% else %} 149 |

No score adjustments have been made.

150 | {% endif %} 151 |
152 | 153 | {% if config.resumes and team.eligible %} 154 |
155 |

Resume Submissions

156 |

Sponsors will have access to resumes submitted to our database. They may contact competitors about internship opportunities.

157 | 158 |

Feel free to submit up to 5 resume PDFs per team. These will be associated with your team by your team name and a representation of your team key, so keep it professional.

159 | 160 |
161 | 162 | 163 | 164 |
165 | 166 |
167 | {% endif %} 168 | 169 | 170 |

171 | 186 | {% endblock %} 187 | {% block postscript %} 188 | {% if first_login %} 189 | 190 | {% endif %} 191 | {% endblock %} 192 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block head %} 4 | 12 | {% endblock %} 13 | {% block content %} 14 |
15 |
16 |
17 |
18 | Login 19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /templates/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/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Register{% endblock %} 3 | {% block head %} 4 | 5 | {% endblock %} 6 | {% block content %} 7 |

Register a Team

8 |

After registering, you will be directed to your team's dashboard. This will 9 | contain a "team key", which is used to log in.

10 |

Please store your team key in a safe place, and share it with your 11 | team members.

12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |

{{ config.eligibility }}

26 |

If you do not meet these requirements, you are still welcome to play, but you 27 | will not be eligible for prizes. By checking the "Eligibility Certification" 28 | checkbox below, you are certifying that you meet the prize eligibility 29 | requirement. We may request appropriate documentation to verify your eligibility 30 | status before sending you prizes.

31 | 32 | 33 |

34 |
35 | 36 | 37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/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/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.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 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TJCSec/ctf-platform/d58f4255ea3a8138d004d548c17ee4038498e55b/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 ctferror import * 2 | from flask import request 3 | from . import misc 4 | 5 | import config 6 | import requests 7 | 8 | def verify_captcha(): 9 | if "g-recaptcha-response" not in request.form: 10 | return CAPTCHA_NOT_COMPLETED 11 | 12 | captcha_response = request.form["g-recaptcha-response"] 13 | verify_data = dict(secret=config.secret.recaptcha_secret, response=captcha_response, remoteip=misc.get_ip()) 14 | result = requests.post("https://www.google.com/recaptcha/api/siteverify", verify_data).json()["success"] 15 | if not result: 16 | return CAPTCHA_INVALID 17 | 18 | return SUCCESS 19 | -------------------------------------------------------------------------------- /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 "team_id" in session and session["team_id"]: 9 | return f(*args, **kwargs) 10 | else: 11 | flash("You need to be logged in to access that page.") 12 | return redirect(url_for('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, 'team_restricts', None) is None: 20 | return redirect(url_for('login')) 21 | if g.team_restricts and thing in g.team_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 "team_id" in session and session["team_id"]: 32 | if not g.team.email_confirmed: 33 | flash("Please confirm your email in order to access that page.") 34 | return redirect(url_for('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('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(): 46 | flash("The competition must be running in order for you to access that page.") 47 | return redirect(url_for('scoreboard')) 48 | return f(*args, **kwargs) 49 | return decorated 50 | 51 | def admin_required(f): 52 | @wraps(f) 53 | def decorated(*args, **kwargs): 54 | if "admin" in session and session["admin"]: 55 | return f(*args, **kwargs) 56 | flash("You must be an admin to access that page.") 57 | return redirect(url_for("admin.admin_login")) 58 | return decorated 59 | 60 | def csrf_check(f): 61 | @wraps(f) 62 | def decorated(*args, **kwargs): 63 | if kwargs["csrf"] != session["_csrf_token"]: 64 | abort(403) 65 | return 66 | 67 | del kwargs["csrf"] 68 | 69 | return f(*args, **kwargs) 70 | return decorated 71 | -------------------------------------------------------------------------------- /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 | def send_confirmation_email(team_email, confirmation_key, team_key): 8 | send_email(team_email, "Welcome to {}!".format(config.ctf_name), 9 | """Hello, and thanks for registering for {}! Before you can start solving problems, 10 | you must confirm your email by entering this code into the team dashboard: 11 | 12 | {} 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 | 19 | In case you lose it, your team key is: {}""".format(config.ctf_name, confirmation_key, team_key)) 20 | 21 | def is_valid_email(email): 22 | return not email.strip().lower().endswith(config.disallowed_domain) 23 | -------------------------------------------------------------------------------- /utils/flag.py: -------------------------------------------------------------------------------- 1 | from database import Challenge, ChallengeSolve, ChallengeFailure 2 | from flask import g 3 | from ctferror import * 4 | from datetime import datetime 5 | import config 6 | 7 | def submit_flag(team, challenge, flag): 8 | if g.redis.get("rl{}".format(team.id)): 9 | delta = config.competition_end - datetime.now() 10 | if delta.total_seconds() > (config.flag_rl * 6): 11 | return FLAG_SUBMISSION_TOO_FAST 12 | 13 | if team.solved(challenge): 14 | return FLAG_SUBMITTED_ALREADY 15 | elif not challenge.enabled: 16 | return FLAG_CANNOT_SUBMIT_WHILE_DISABLED 17 | elif flag.strip().lower() != challenge.flag.strip().lower(): 18 | g.redis.set("rl{}".format(team.id), str(datetime.now()), config.flag_rl) 19 | ChallengeFailure.create(team=team, challenge=challenge, attempt=flag, time=datetime.now()) 20 | return FLAG_INCORRECT 21 | else: 22 | g.redis.hincrby("solves", challenge.id, 1) 23 | if config.immediate_scoreboard: 24 | g.redis.delete("scoreboard") 25 | g.redis.delete("graph") 26 | ChallengeSolve.create(team=team, challenge=challenge, time=datetime.now()) 27 | return SUCCESS 28 | -------------------------------------------------------------------------------- /utils/misc.py: -------------------------------------------------------------------------------- 1 | import random 2 | import config 3 | import json 4 | import requests 5 | from datetime import datetime 6 | from functools import wraps 7 | from flask import request, session, redirect, url_for, flash, g 8 | from database import Team, Challenge, ChallengeSolve, ScoreAdjustment 9 | 10 | allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789" 11 | 12 | def generate_random_string(length=32, chars=allowed_chars): 13 | r = random.SystemRandom() 14 | return "".join([r.choice(chars) for i in range(length)]) 15 | 16 | def generate_team_key(): 17 | return config.ctf_name.lower() + "_" + generate_random_string(32, allowed_chars) 18 | 19 | def generate_confirmation_key(): 20 | return generate_random_string(48) 21 | 22 | def get_ip(): 23 | return request.headers.get(config.proxied_ip_header, request.remote_addr) 24 | 25 | -------------------------------------------------------------------------------- /utils/notification.py: -------------------------------------------------------------------------------- 1 | def make_link(text, target): 2 | return '{}'.format(target, text) 3 | -------------------------------------------------------------------------------- /utils/scoreboard.py: -------------------------------------------------------------------------------- 1 | from database import Team, Challenge, ChallengeSolve, ScoreAdjustment 2 | from datetime import datetime, timedelta 3 | 4 | import config 5 | 6 | def get_all_scores(teams, solves, adjustments): 7 | scores = {team.id: 0 for team in teams} 8 | for solve in solves: 9 | scores[solve.team_id] += solve.challenge.points 10 | 11 | for adjustment in adjustments: 12 | scores[adjustment.team_id] += adjustment.value 13 | 14 | return scores 15 | 16 | def get_last_solves(teams, solves): 17 | last = {team.id: datetime(1970, 1, 1) for team in teams} 18 | for solve in solves: 19 | if solve.time > last[solve.team_id]: 20 | last[solve.team_id] = solve.time 21 | return last 22 | 23 | def calculate_scores(): 24 | solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) 25 | adjustments = ScoreAdjustment.select() 26 | teams = Team.select() 27 | 28 | team_solves = {team.id: [] for team in teams} 29 | team_mapping = {team.id: team for team in teams} 30 | scores = {team.id: 0 for team in teams} 31 | for solve in solves: 32 | scores[solve.team_id] += solve.challenge.points 33 | team_solves[solve.team_id].append(solve) 34 | for adjustment in adjustments: 35 | scores[adjustment.team_id] += adjustment.value 36 | 37 | most_recent_solve = {tid: max([i.time for i in team_solves[tid]]) for tid in team_solves if team_solves[tid]} 38 | scores = {i: j for i, j in scores.items() if i in most_recent_solve} 39 | # eligible, teamid, teamname, affiliation, score 40 | 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]])))] 41 | 42 | def calculate_graph(scoredata): 43 | solves = list(ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).order_by(ChallengeSolve.time)) 44 | adjustments = list(ScoreAdjustment.select()) 45 | scoredata = [i for i in scoredata if i[0]] # Only eligible teams are on the score graph 46 | graph_data = [] 47 | for eligible, tid, name, affiliation, score in scoredata[:config.teams_on_graph]: 48 | our_solves = [i for i in solves if i.team_id == tid] 49 | team_data = [] 50 | s = sum([i.value for i in adjustments if i.team_id == tid]) 51 | for i in sorted(our_solves, key=lambda k: k.time): 52 | team_data.append((str(i.time), s)) 53 | s += i.challenge.points 54 | team_data.append((str(i.time + timedelta(microseconds=1000)), s)) 55 | team_data.append((str(datetime.now()), score)) 56 | graph_data.append((name, team_data)) 57 | return graph_data 58 | 59 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------