From f55ad318d2d877ffcdea5ba3bdc6578140a49ffd Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Tue, 28 Apr 2020 20:54:31 +0530 Subject: [PATCH] implemented caching and other fixes --- app.json | 6 + src/FlaskRTBCTF/__init__.py | 11 +- src/FlaskRTBCTF/admin/views.py | 20 ++- src/FlaskRTBCTF/config.py | 4 +- src/FlaskRTBCTF/ctf/forms.py | 39 +++++- src/FlaskRTBCTF/ctf/models.py | 14 +- src/FlaskRTBCTF/ctf/routes.py | 129 ++++++++++++------ src/FlaskRTBCTF/main/forms.py | 52 ++++--- src/FlaskRTBCTF/main/models.py | 15 +- src/FlaskRTBCTF/main/routes.py | 6 +- src/FlaskRTBCTF/static/main.js | 18 +++ src/FlaskRTBCTF/templates/forms.html | 60 ++++---- src/FlaskRTBCTF/templates/home.html | 6 +- src/FlaskRTBCTF/templates/layout.html | 14 +- .../templates/{machine.html => machines.html} | 61 ++++++--- src/FlaskRTBCTF/templates/macros.html | 33 +++++ src/FlaskRTBCTF/templates/new_machine.html | 40 ++++++ src/FlaskRTBCTF/templates/scoreboard.html | 11 +- src/FlaskRTBCTF/users/forms.py | 3 +- src/FlaskRTBCTF/users/models.py | 7 +- src/FlaskRTBCTF/utils/__init__.py | 3 +- src/FlaskRTBCTF/utils/cache.py | 13 ++ src/FlaskRTBCTF/utils/helpers.py | 28 ++-- src/create_db.dev.py | 6 +- src/create_db.py | 11 +- src/requirements.txt | 2 + 26 files changed, 426 insertions(+), 186 deletions(-) create mode 100644 src/FlaskRTBCTF/static/main.js rename src/FlaskRTBCTF/templates/{machine.html => machines.html} (58%) create mode 100644 src/FlaskRTBCTF/templates/new_machine.html create mode 100644 src/FlaskRTBCTF/utils/cache.py diff --git a/app.json b/app.json index c86771b..ba2207e 100644 --- a/app.json +++ b/app.json @@ -5,6 +5,9 @@ "addons": [ { "plan": "heroku-postgresql" + }, + { + "plan": "heroku-redis" } ], "buildpacks": [ @@ -21,6 +24,9 @@ "description": "Administrator password", "generator": "secret" }, + "ADMIN_EMAIL": { + "description": "Administrator email" + }, "MAIL_USER": { "description": "Username for mail service", "required": false diff --git a/src/FlaskRTBCTF/__init__.py b/src/FlaskRTBCTF/__init__.py index 998d750..20e3cff 100644 --- a/src/FlaskRTBCTF/__init__.py +++ b/src/FlaskRTBCTF/__init__.py @@ -3,10 +3,16 @@ from flask import Flask from FlaskRTBCTF.config import Config -from FlaskRTBCTF.admin.views import BaseModelView, UserAdminView, MachineAdminView +from FlaskRTBCTF.admin.views import ( + BaseModelView, + UserAdminView, + MachineAdminView, + NotificationAdminView, +) from FlaskRTBCTF.utils import ( db, bcrypt, + cache, login_manager, admin_manager, mail, @@ -30,6 +36,7 @@ def create_app(config_class=Config): db.init_app(app) bcrypt.init_app(app) + cache.init_app(app) login_manager.init_app(app) admin_manager.init_app(app) mail.init_app(app) @@ -39,7 +46,7 @@ def create_app(config_class=Config): # Add model views for admin control admin_manager.add_view(UserAdminView(User, db.session)) admin_manager.add_view(MachineAdminView(Machine, db.session)) - admin_manager.add_view(BaseModelView(Notification, db.session)) + admin_manager.add_view(NotificationAdminView(Notification, db.session)) admin_manager.add_view(BaseModelView(Logs, db.session)) for _bp in _blueprints: diff --git a/src/FlaskRTBCTF/admin/views.py b/src/FlaskRTBCTF/admin/views.py index 34b5b90..aeaa7cf 100644 --- a/src/FlaskRTBCTF/admin/views.py +++ b/src/FlaskRTBCTF/admin/views.py @@ -1,6 +1,6 @@ """ Admin Model Views. """ -from flask import abort, redirect, flash +from flask import abort, redirect, flash, url_for, request from flask_login import current_user from flask_admin import expose from flask_admin.form import SecureForm @@ -32,6 +32,7 @@ def _handle_view(self, name, **kwargs): class UserAdminView(BaseModelView): column_exclude_list = ("password",) + form_exclude_list = ("password",) column_searchable_list = ("username", "email") @expose("/new/") @@ -42,6 +43,17 @@ def create_view(self): class MachineAdminView(BaseModelView): column_searchable_list = ("name", "ip") - form_choices = { - "hardness": [("easy", "Easy"), ("medium", "Medium"), ("hard", "Hard")] - } + + @expose("/new/") + def create_view(self): + return redirect(url_for("ctf.new_machine")) + + @expose("/edit/") + def edit_view(self): + id = int(request.args["id"]) + return redirect(url_for("ctf.edit_machine", id=id)) + + +class NotificationAdminView(BaseModelView): + column_searchable_list = ("title",) + form_excluded_columns = ("timestamp",) diff --git a/src/FlaskRTBCTF/config.py b/src/FlaskRTBCTF/config.py index 51da949..34cdfcb 100644 --- a/src/FlaskRTBCTF/config.py +++ b/src/FlaskRTBCTF/config.py @@ -7,14 +7,14 @@ class Config: + DEBUG = False # Turn DEBUG OFF before deployment SECRET_KEY = handle_secret_key() SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///site.db" # For local use, one can simply use SQLlite with: 'sqlite:///site.db' # For deployment on Heroku use: `os.environ.get('DATABASE_URL')` # in all other cases: `os.environ.get('SQLALCHEMY_DATABASE_URI')` - FLASK_ADMIN_SWATCH = ["journal", "paper", "yeti", "cosmo"][2] SQLALCHEMY_TRACK_MODIFICATIONS = False - DEBUG = False # Turn DEBUG OFF before deployment + FLASK_ADMIN_SWATCH = ("journal", "paper", "yeti", "cosmo")[3] # TEMPLATES_AUTO_RELOAD = True MAIL_SERVER = "smtp.googlemail.com" MAIL_PORT = 587 diff --git a/src/FlaskRTBCTF/ctf/forms.py b/src/FlaskRTBCTF/ctf/forms.py index 984079a..17e2696 100644 --- a/src/FlaskRTBCTF/ctf/forms.py +++ b/src/FlaskRTBCTF/ctf/forms.py @@ -1,9 +1,42 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, HiddenField -from wtforms.validators import DataRequired, Length, ValidationError +from wtforms import StringField, SubmitField, HiddenField, RadioField +from wtforms.validators import DataRequired, Length, ValidationError, IPAddress +from wtforms.fields.html5 import IntegerField from .models import Machine +class MachineForm(FlaskForm): + name = StringField("Name", validators=[DataRequired(), Length(min=4, max=32)]) + os = RadioField( + "Operating System of machine", + validators=[DataRequired()], + choices=(("linux", "Linux"), ("windows", "Windows"), ("android", "Android")), + ) + user_hash = StringField( + "User Hash", validators=[DataRequired(), Length(min=32, max=32)] + ) + root_hash = StringField( + "Root Hash", validators=[DataRequired(), Length(min=32, max=32)] + ) + user_points = IntegerField("Points for User Hash", validators=[DataRequired()]) + root_points = IntegerField("Points for Root Hash", validators=[DataRequired()]) + ip = StringField( + "IPv4 address of machine", validators=[DataRequired(), IPAddress()] + ) + hardness = RadioField( + "Difficuly Level", + validators=[DataRequired()], + choices=( + ("easy", "Easy"), + ("medium", "Medium"), + ("hard", "Hard"), + ("insane", "Insane"), + ), + ) + + submit = SubmitField("Submit") + + class UserHashForm(FlaskForm): machine_id = HiddenField("Machine ID", validators=[DataRequired()]) user_hash = StringField( @@ -30,7 +63,7 @@ def validate_root_hash(self, root_hash): box = Machine.query.get(int(self.machine_id.data)) if not box: raise ValidationError("No machine with that ID exists") - elif box.user_hash == str(root_hash.data): + elif box.root_hash == str(root_hash.data): pass else: raise ValidationError("Incorrect Root Hash.") diff --git a/src/FlaskRTBCTF/ctf/models.py b/src/FlaskRTBCTF/ctf/models.py index 688e205..812bb85 100644 --- a/src/FlaskRTBCTF/ctf/models.py +++ b/src/FlaskRTBCTF/ctf/models.py @@ -1,4 +1,4 @@ -from FlaskRTBCTF.utils import db +from FlaskRTBCTF.utils import db, cache # Machine Table @@ -11,6 +11,12 @@ class Machine(db.Model): root_hash = db.Column(db.String(32), nullable=False) user_points = db.Column(db.Integer, default=0) root_points = db.Column(db.Integer, default=0) - os = db.Column(db.String(16), nullable=False) - ip = db.Column(db.String(45), nullable=False) - hardness = db.Column(db.String(16), nullable=False, default="Easy") + os = db.Column(db.String, nullable=False, default="linux") + ip = db.Column(db.String(64), nullable=False) + hardness = db.Column(db.String, nullable=False, default="Easy") + + @staticmethod + @cache.cached(timeout=3600, key_prefix="machines") + def get_all(): + _machines = Machine.query.all() + return _machines diff --git a/src/FlaskRTBCTF/ctf/routes.py b/src/FlaskRTBCTF/ctf/routes.py index 400b580..b901293 100644 --- a/src/FlaskRTBCTF/ctf/routes.py +++ b/src/FlaskRTBCTF/ctf/routes.py @@ -6,39 +6,28 @@ from flask import Blueprint, render_template, flash, request, redirect, url_for from flask_login import current_user, login_required -from FlaskRTBCTF import db from FlaskRTBCTF.users.models import User, Logs -from FlaskRTBCTF.utils import is_past_running_time +from FlaskRTBCTF.utils import db, cache, is_past_running_time, admin_only from .models import Machine -from .forms import UserHashForm, RootHashForm +from .forms import UserHashForm, RootHashForm, MachineForm ctf = Blueprint("ctf", __name__) -# context processor - - -@ctf.context_processor -def inject_context(): - boxes = Machine.query.all() - past_running_time = is_past_running_time() - - return dict(boxes=boxes, past_running_time=past_running_time) - - # Scoreboard @ctf.route("/scoreboard") -@login_required +@cache.cached(timeout=120, key_prefix="scoreboard") def scoreboard(): - users_score = User.query.order_by(User.points.desc()).all() - userNameScoreList = [] - for u in users_score: - userNameScoreList.append({"username": u.username, "score": u.points}) + users_scores = ( + User.query.with_entities(User.username, User.points) + .order_by(User.points.desc()) + .all() + ) - return render_template("scoreboard.html", scores=userNameScoreList) + return render_template("scoreboard.html", scores=users_scores) # Machines Info @@ -50,7 +39,11 @@ def machines(): userHashForm = UserHashForm() rootHashForm = RootHashForm() + boxes = Machine.get_all() + past_running_time = is_past_running_time() + if request.method == "GET": + log = Logs.query.get(current_user.id) # check if it is the first visit to machine page for user @@ -60,50 +53,106 @@ def machines(): db.session.commit() else: - if is_past_running_time(): + if past_running_time: flash("Sorry! CTF has ended.", "danger") return redirect(url_for("ctf.machines")) """ Todo: Get Object from UserMachine Model, dummy object given below """ - user_machine: object = { - "machine_id": 1, - "user_id": 1, - "owned_user": False, - "owned_root": False, - } - - if user_machine.owned_user: - flash("You already own User.", "success") - return redirect(url_for("ctf.machines")) - - elif user_machine.owned_root: - flash("You already own System.", "success") - return redirect(url_for("ctf.machines")) - - elif userHashForm.submit_user_hash.data and userHashForm.validate_on_submit(): + # user_machine: object = { + # "machine_id": 1, + # "user_id": 1, + # "owned_user": False, + # "owned_root": False, + # } + + # if user_machine.owned_user: + # flash("You already own User.", "success") + # return redirect(url_for("ctf.machines")) + + # elif user_machine.owned_root: + # flash("You already own System.", "success") + # return redirect(url_for("ctf.machines")) + + if userHashForm.submit_user_hash.data and userHashForm.validate_on_submit(): box = Machine.query.get(int(userHashForm.machine_id.data)) - user_machine.owned_user = True + # user_machine.owned_user = True current_user.points += box.user_points log = Logs.query.get(current_user.id) log.userSubmissionIP = request.access_route[0] log.userSubmissionTime = datetime.utcnow() log.userOwnTime = str(log.userSubmissionTime - log.machineVisitTime) db.session.commit() + cache.delete(key="scoreboard") flash("Congrats! correct user hash.", "success") elif rootHashForm.submit_root_hash.data and rootHashForm.validate_on_submit(): box = Machine.query.get(int(rootHashForm.machine_id.data)) - user_machine.owned_root = True + # user_machine.owned_root = True current_user.points += box.root_points log = Logs.query.get(current_user.id) log.rootSubmissionIP = request.access_route[0] log.rootSubmissionTime = datetime.utcnow() log.rootOwnTime = str(log.rootSubmissionTime - log.machineVisitTime) db.session.commit() + cache.delete(key="scoreboard") flash("Congrats! correct root hash.", "success") + else: + errors = userHashForm.user_hash.errors or rootHashForm.root_hash.errors + for e in errors: + flash(e, "danger") + + return redirect(url_for("ctf.machines")) + return render_template( - "machine.html", userHashForm=userHashForm, rootHashForm=rootHashForm, + "machines.html", + boxes=boxes, + past_running_time=past_running_time, + userHashForm=userHashForm, + rootHashForm=rootHashForm, ) + + +@ctf.route("/machines/new", methods=["GET", "POST"]) +@admin_only +def new_machine(): + form = MachineForm(obj=Machine.query.get(1)) + if request.method == "GET": + return render_template( + "new_machine.html", form_title="Add New Machine", form=form + ) + else: + if form.validate_on_submit(): + new_machine = Machine() + form.populate_obj(new_machine) + db.session.add(new_machine) + db.session.commit() + cache.delete(key="machines") + flash(f"{form.name.data} has been added.", "success") + return redirect(url_for("ctf.machines")) + else: + flash(form.errors, "danger") + return redirect(request.url) + + +@ctf.route("/machines/edit/", methods=["GET", "POST"]) +@admin_only +def edit_machine(id): + machine = Machine.query.get_or_404(id) + form = MachineForm(obj=machine) + if request.method == "GET": + return render_template( + "new_machine.html", form_title=f"Editing machine #{id}", form=form + ) + else: + if form.validate_on_submit(): + form.populate_obj(machine) + db.session.commit() + cache.delete(key="machines") + flash(f"{form.name.data} has been edited.", "success") + return redirect(url_for("ctf.machines")) + else: + flash(form.errors, "danger") + return redirect(request.url) diff --git a/src/FlaskRTBCTF/main/forms.py b/src/FlaskRTBCTF/main/forms.py index fb36e10..2ff4150 100644 --- a/src/FlaskRTBCTF/main/forms.py +++ b/src/FlaskRTBCTF/main/forms.py @@ -1,12 +1,11 @@ from flask import url_for, redirect, flash from flask_wtf import FlaskForm -from wtforms import StringField, DateField, TimeField, SubmitField, FieldList -from wtforms.widgets.html5 import URLInput, DateInput, TimeInput +from wtforms import StringField, SubmitField, FieldList +from wtforms.fields.html5 import DateField, TimeField, URLField from wtforms.validators import DataRequired, Length, URL from sqlalchemy.exc import SQLAlchemyError -from FlaskRTBCTF import db -from FlaskRTBCTF.utils import admin_only +from FlaskRTBCTF.utils import db, admin_only, cache from .models import Settings, Website @@ -17,29 +16,39 @@ class SettingsForm(FlaskForm): organization_name = StringField( "Organization Name", validators=[DataRequired(), Length(min=3, max=80)], ) - from_date = DateField("Start Date", format="%Y-%m-%d", widget=DateInput()) - from_time = TimeField("Start Time", widget=TimeInput()) - to_date = DateField("End Date", format="%Y-%m-%d", widget=DateInput()) - to_time = TimeField("End Time", widget=TimeInput()) + from_date = DateField("Start Date", format="%Y-%m-%d") + from_time = TimeField("Start Time") + to_date = DateField("End Date", format="%Y-%m-%d") + to_time = TimeField("End Time") - submit = SubmitField("Next") + submit = SubmitField("Save & Next") @admin_only def setup(self): if self.is_submitted(): - settings = Settings.query.get(1) + try: + settings = Settings.query.get(1) - settings.ctf_name = self.ctf_name.data - settings.organization_name = self.organization_name.data - settings.from_date = self.from_date.data - settings.from_time = self.from_time.data - settings.to_date = self.to_date.data - settings.to_time = self.to_time.data - settings.dummy = False + settings.ctf_name = self.ctf_name.data + settings.organization_name = self.organization_name.data + settings.from_date = self.from_date.data + settings.from_time = self.from_time.data + settings.to_date = self.to_date.data + settings.to_time = self.to_time.data + settings.dummy = False - db.session.commit() + db.session.commit() + + except SQLAlchemyError: + db.session.rollback() + flash("Transaction failed. Please try again.", "danger") + return redirect(url_for("main.setup"), step=2) + + finally: + cache.delete(key="past_running_time") + cache.delete(key="settings") + return redirect(url_for("main.setup", step=3)) - return redirect(url_for("main.setup", step=3)) else: return redirect(url_for("main.setup", step=2)) @@ -51,7 +60,7 @@ class WebsiteForm(FlaskForm): max_entries=3, ) urls = FieldList( - StringField("URL", validators=[DataRequired(), URL()], widget=URLInput()), + URLField("URL", validators=[DataRequired(), URL()]), min_entries=1, max_entries=3, ) @@ -63,9 +72,10 @@ def setup(self): try: Website.query.delete() for w in zip(self.names.data, self.urls.data): - obj = Website(settings_id=1, name=w[0], url=w[1]) + obj = Website(name=w[0], url=w[1]) db.session.add(obj) db.session.commit() + cache.delete(key="websites") flash( "CTF setup was successful! \ You can use admin controls for managing database tables.", diff --git a/src/FlaskRTBCTF/main/models.py b/src/FlaskRTBCTF/main/models.py index a4a2565..43409c7 100644 --- a/src/FlaskRTBCTF/main/models.py +++ b/src/FlaskRTBCTF/main/models.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.hybrid import hybrid_property -from FlaskRTBCTF.utils import db +from FlaskRTBCTF.utils import db, cache # Notifications Table @@ -41,7 +41,10 @@ class Settings(db.Model): ) to_time = db.Column(db.Time, nullable=False, default=time()) - websites = db.relationship("Website", backref="settings", lazy=True, uselist=True) + @staticmethod + @cache.cached(timeout=3600 * 3, key_prefix="settings") + def get_settings(): + return Settings.query.get(1) @hybrid_property def running_time_from(self): @@ -61,9 +64,6 @@ def __repr__(self): class Website(db.Model): __tablename__ = "website" id = db.Column(db.Integer, primary_key=True) - settings_id = db.Column( - db.Integer, db.ForeignKey("settings.id"), nullable=False, unique=False - ) url = db.Column( db.TEXT(), nullable=False, default="https://Abs0lut3Pwn4g3.github.io/" ) @@ -71,5 +71,10 @@ class Website(db.Model): db.TEXT(), nullable=False, default="Official Abs0lut3Pwn4g3 Website" ) + @staticmethod + @cache.cached(timeout=3600 * 6, key_prefix="websites") + def get_websites(): + return Website.query.all() + def __repr__(self): return f"Website('{self.name}','{self.url}')" diff --git a/src/FlaskRTBCTF/main/routes.py b/src/FlaskRTBCTF/main/routes.py index 86684a3..913affd 100644 --- a/src/FlaskRTBCTF/main/routes.py +++ b/src/FlaskRTBCTF/main/routes.py @@ -13,7 +13,7 @@ @main.before_request def needs_setup(): - settings = Settings.query.get(1) + settings = Settings.get_settings() if settings.dummy: if request.endpoint not in ("main.setup", "users.login"): flash("Please setup the CTF, before accessing any routes.", "info") @@ -25,7 +25,7 @@ def needs_setup(): @main.route("/") @main.route("/home") def home(): - settings = Settings.query.get(1) + settings = Settings.get_settings() running_time = { "from": settings.running_time_from, "to": settings.running_time_to, @@ -67,7 +67,7 @@ def setup(): step = int(request.args.get("step", 2)) - if step == 2: + if step == 2 and settings_form.validate_on_submit(): return settings_form.setup() elif step == 3: return website_form.setup() diff --git a/src/FlaskRTBCTF/static/main.js b/src/FlaskRTBCTF/static/main.js new file mode 100644 index 0000000..3e6b361 --- /dev/null +++ b/src/FlaskRTBCTF/static/main.js @@ -0,0 +1,18 @@ +$(document).ready( function() { + + // Tooltip + $('[data-toggle="tooltip"]').tooltip( + { + 'delay': { show: 50, hide: 50 } + } + ); + + // modal + $('#m-form').on('show.bs.modal', function (event) { + const button = $(event.relatedTarget); + const boxId = button.data('boxid'); // Extract info from data-* attributes + var modal = $(this); + modal.find('#machine-id-user').val(boxId); + modal.find('#machine-id-root').val(boxId); + }) +}); \ No newline at end of file diff --git a/src/FlaskRTBCTF/templates/forms.html b/src/FlaskRTBCTF/templates/forms.html index d82acf1..2dab761 100644 --- a/src/FlaskRTBCTF/templates/forms.html +++ b/src/FlaskRTBCTF/templates/forms.html @@ -124,28 +124,22 @@ -{% macro user_hash_form_macro(form, machine_id) %} +{% macro user_hash_form_macro(form) %}
{{ form.csrf_token() }} - {{ form.machine_id(value=machine_id) }} -
-
- {{ form.user_hash.label(class="form-control-label") }} - {% if form.user_hash.errors %} - {{ form.user_hash(class="form-control form-control-sm ml-4 mr-4 is-invalid", autocomplete="off") }} -
- {% for error in form.user_hash.errors %} - {{ error }} - {% endfor %} -
- {% else %} - {{ form.user_hash(class="form-control form-control-sm ml-4 mr-4", autocomplete="off") }} - {% endif %} -
-
+ {{ form.machine_id(id="machine-id-user") }}
- {{ form.submit_user_hash(class="btn btn-dark btn-hover-red") }} + {{ form.user_hash.label(class="form-control-label") }} + {{ form.user_hash( + class="ml-3 mr-3 is-invalid", + size=24, + minlength=32, + maxlength=32, + autocomplete="off" + ) + }} + {{ form.submit_user_hash(class="btn btn-sm btn-dark btn-hover-red") }}
@@ -153,28 +147,22 @@ -{% macro root_hash_form_macro(form, machine_id) %} +{% macro root_hash_form_macro(form) %}
{{ form.csrf_token() }} - {{ form.machine_id(value=machine_id) }} -
-
- {{ form.root_hash.label(class="form-control-label") }} - {% if form.root_hash.errors %} - {{ form.root_hash(class="form-control form-control-sm ml-4 mr-4 is-invalid", autocomplete="off") }} -
- {% for error in form.root_hash.errors %} - {{ error }} - {% endfor %} -
- {% else %} - {{ form.root_hash(class="form-control form-control-sm ml-4 mr-4", autocomplete="off") }} - {% endif %} -
-
+ {{ form.machine_id(id="machine-id-root") }}
- {{ form.submit_root_hash(class="btn btn-dark btn-hover-red") }} + {{ form.root_hash.label(class="form-control-label") }} + {{ form.root_hash( + class="ml-3 mr-3 is-invalid", + size=24, + minlength=32, + maxlength=32, + autocomplete="off" + ) + }} + {{ form.submit_root_hash(class="btn btn-sm btn-dark btn-hover-red") }}
diff --git a/src/FlaskRTBCTF/templates/home.html b/src/FlaskRTBCTF/templates/home.html index 477a161..6cb3356 100644 --- a/src/FlaskRTBCTF/templates/home.html +++ b/src/FlaskRTBCTF/templates/home.html @@ -8,9 +8,7 @@

Welcome to {{ settings.ctf_name }}

{% if current_user.is_authenticated %}

If you owned the box then you can submit the hashes - here - - . + here.

{% else %}

You need to login first.

@@ -27,7 +25,7 @@

Rules

{{ RunningTime['from'].strftime("%Y-%m-%d %I:%M %p") }} to {{ RunningTime['to'].strftime("%Y-%m-%d %I:%M %p") }} - (All times in UTC) + (All times in UTC)
  • Needless to say: no bruteforcing (you'll never guess, anyway)
  • Automated vulnerability scanners will get you nowhere (we know 'cause we made the box)
  • diff --git a/src/FlaskRTBCTF/templates/layout.html b/src/FlaskRTBCTF/templates/layout.html index 0a79e9a..f5e4884 100644 --- a/src/FlaskRTBCTF/templates/layout.html +++ b/src/FlaskRTBCTF/templates/layout.html @@ -43,7 +43,6 @@ {% if current_user.is_authenticated %} {% if current_user.isAdmin %} Admin Controls - Setup {% endif %} @{{ current_user.username }} Logout @@ -87,7 +86,7 @@

    {{ settings.organization_name }}

    @@ -106,15 +105,8 @@

    {{ settings.organization_name }}

    + + - diff --git a/src/FlaskRTBCTF/templates/machine.html b/src/FlaskRTBCTF/templates/machines.html similarity index 58% rename from src/FlaskRTBCTF/templates/machine.html rename to src/FlaskRTBCTF/templates/machines.html index db9097b..a910693 100644 --- a/src/FlaskRTBCTF/templates/machine.html +++ b/src/FlaskRTBCTF/templates/machines.html @@ -4,7 +4,7 @@ {% block content %} {% if current_user.isAdmin %} - Add new + Add new {% endif %} {% for box in boxes %} @@ -12,6 +12,8 @@
    + + @@ -24,18 +26,25 @@ - + data-toggle="modal" + href="#m-form" + data-boxId="{{ box['id'] }}"> + + + data-toggle="tooltip" + title="rate machine (coming soon)"> +
    -
    +
    {{ box['name'] }}

    @@ -49,7 +58,7 @@
    {{ box['ip'] }}
    OS
    -
    {{ box['os'] }}
    +
    {{ box['os'] }}
    @@ -61,23 +70,13 @@
    User: {{ box['user_points'] }}
    Root: {{ box['root_po
    Difficulty
    -
    {{ box['hardness'] }}
    +
    {{ box['hardness'] }}
    -
    - - {% if past_running_time %} - The CTF has ended. Scoreboard. - {% else %} - - {{ user_hash_form_macro(form=userHashForm, machine_id=box['id']) }} -
    - - {{ root_hash_form_macro(form=rootHashForm, machine_id=box['id']) }} - {% endif %}
    +
    @@ -86,5 +85,31 @@
    {{ box['hardness'] }}
    {% endfor %} + + + + {% endblock content %} + + diff --git a/src/FlaskRTBCTF/templates/macros.html b/src/FlaskRTBCTF/templates/macros.html index d456c49..e51b0bb 100644 --- a/src/FlaskRTBCTF/templates/macros.html +++ b/src/FlaskRTBCTF/templates/macros.html @@ -13,4 +13,37 @@ {{ field(class="form-control", autocomplete="off") }} {% endif %} +{% endmacro %} + + +{% macro form_select_field(field) %} + + {{ field.label(class="form-control-label") }} +
    + {% for subfield in field %} + + {{ subfield }} + {{ subfield.label }} + + {% endfor %} +
    + +{% endmacro %} + + + +{% macro inline_field(field, size) %} + + {{ field.label() }} + {% if field.errors %} + {{ field(size=size, maxlength=size, class="m-auto", autocomplete="off") }} +
    + {% for error in field.errors %} + {{ error }} + {% endfor %} +
    + {% else %} + {{ field(size=size, maxlength=size, class="m-auto", autocomplete="off") }} + {% endif %} + {% endmacro %} \ No newline at end of file diff --git a/src/FlaskRTBCTF/templates/new_machine.html b/src/FlaskRTBCTF/templates/new_machine.html new file mode 100644 index 0000000..de85502 --- /dev/null +++ b/src/FlaskRTBCTF/templates/new_machine.html @@ -0,0 +1,40 @@ +{% extends "layout.html" %} +{% from "macros.html" import form_field_group_macro, form_select_field, inline_field %} + +{% block content %} + + +
    +
    + {{ form.hidden_tag() }} + {{ form_title }} +
    + {{ inline_field(field=form.name, size=20) }} + {{ inline_field(field=form.ip, size=15) }} +
    +
    + {{ form_select_field(field=form.os) }} +
    +
    + {{ inline_field(field=form.user_hash, size=25) }} + {{ inline_field(field=form.user_points, size=5) }} +
    +
    + {{ inline_field(field=form.root_hash, size=25) }} + {{ inline_field(field=form.root_points, size=5) }} +
    +
    + {{ form_select_field(field=form.hardness) }} +
    +
    + {{ form.submit(class="btn btn-dark btn-hover-red") }} +
    +
    +
    +
    + + Note: Hashes should be 32 length each. + +
    + +{% endblock content %} \ No newline at end of file diff --git a/src/FlaskRTBCTF/templates/scoreboard.html b/src/FlaskRTBCTF/templates/scoreboard.html index ddae5f3..165206a 100644 --- a/src/FlaskRTBCTF/templates/scoreboard.html +++ b/src/FlaskRTBCTF/templates/scoreboard.html @@ -4,6 +4,7 @@
    {% if scores %}

    Scoreboard

    + Note: Updated every 2 minutes.
    @@ -15,17 +16,17 @@

    Scoreboard

    {% for score in scores %} - {% if loop.index%2 != 0 %} + {% if ( loop.index % 2 != 0 ) %} - - + + {% else %} - - + + {% endif %} {% endfor %} diff --git a/src/FlaskRTBCTF/users/forms.py b/src/FlaskRTBCTF/users/forms.py index 8fd9b8b..7677dd0 100644 --- a/src/FlaskRTBCTF/users/forms.py +++ b/src/FlaskRTBCTF/users/forms.py @@ -1,5 +1,6 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField, BooleanField +from wtforms.fields.html5 import EmailField from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from flask_login import current_user @@ -10,7 +11,7 @@ class RegistrationForm(FlaskForm): username = StringField( "Username", validators=[DataRequired(), Length(min=4, max=24)] ) - email = StringField( + email = EmailField( "Email", validators=[DataRequired(), Email(), Length(min=6, max=88)] ) password = PasswordField( diff --git a/src/FlaskRTBCTF/users/models.py b/src/FlaskRTBCTF/users/models.py index 7091031..58cc8c2 100644 --- a/src/FlaskRTBCTF/users/models.py +++ b/src/FlaskRTBCTF/users/models.py @@ -1,3 +1,4 @@ +from datetime import datetime from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask import current_app @@ -49,7 +50,9 @@ class Logs(db.Model): user_id = db.Column( db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True ) - accountCreationTime = db.Column(db.DateTime, nullable=False) + accountCreationTime = db.Column( + db.DateTime, nullable=False, default=datetime.utcnow() + ) visitedMachine = db.Column(db.Boolean, default=False) machineVisitTime = db.Column(db.DateTime, nullable=True) userSubmissionTime = db.Column(db.DateTime, nullable=True) @@ -60,4 +63,4 @@ class Logs(db.Model): rootSubmissionIP = db.Column(db.String, nullable=True) def __repr__(self): - return f"Logs('{self.user_id}','{self.visitedMachine}'" + return f"Logs('{self.user_id}','{self.visitedMachine}')" diff --git a/src/FlaskRTBCTF/utils/__init__.py b/src/FlaskRTBCTF/utils/__init__.py index fd66ce4..76ad020 100644 --- a/src/FlaskRTBCTF/utils/__init__.py +++ b/src/FlaskRTBCTF/utils/__init__.py @@ -1,12 +1,13 @@ from .models import db from .admin_manager import admin_manager from .bcrypt import bcrypt +from .cache import cache from .helpers import ( handle_admin_pass, + handle_admin_email, handle_secret_key, is_past_running_time, inject_app_context, - needs_setup, ) from .login_manager import login_manager, admin_only from .mail import mail, send_reset_email diff --git a/src/FlaskRTBCTF/utils/cache.py b/src/FlaskRTBCTF/utils/cache.py new file mode 100644 index 0000000..161dc7b --- /dev/null +++ b/src/FlaskRTBCTF/utils/cache.py @@ -0,0 +1,13 @@ +import os + +from flask_caching import Cache + +cache_redis_url = os.environ.get("REDIS_URL", None) + +if not cache_redis_url: + config = {"CACHE_TYPE": "simple"} +else: + config = {"CACHE_TYPE": "redis", "CACHE_REDIS_URL": cache_redis_url} + + +cache = Cache(config=config) diff --git a/src/FlaskRTBCTF/utils/helpers.py b/src/FlaskRTBCTF/utils/helpers.py index 114ef3b..2741a26 100644 --- a/src/FlaskRTBCTF/utils/helpers.py +++ b/src/FlaskRTBCTF/utils/helpers.py @@ -4,17 +4,8 @@ import secrets from datetime import datetime -from flask import request, redirect, url_for -from FlaskRTBCTF.main.models import Settings - - -def needs_setup(): - settings = Settings.query.get(1) - if settings.dummy: - if request.endpoint != "main.setup": - return redirect(url_for("main.setup")) - else: - return +from .cache import cache +from FlaskRTBCTF.main.models import Settings, Website def handle_secret_key(default="you-will-never-guess"): @@ -33,15 +24,20 @@ def handle_admin_pass(default="admin"): return passwd +def handle_admin_email(default="admin@admin.com"): + em = os.environ.get("ADMIN_EMAIL", default) + return em + + def inject_app_context(): - settings = Settings.query.get(1) - # Note to self: maybe we can use? @cached_property: - # https://werkzeug.palletsprojects.com/en/1.0.x/utils/#werkzeug.utils.cached_property + settings = Settings.get_settings() + websites = Website.get_websites() - return dict(settings=settings) + return dict(settings=settings, websites=websites) +@cache.cached(timeout=60, key_prefix="past_running_time") def is_past_running_time(): - end_date_time = Settings.query.get(1).running_time_to + end_date_time = Settings.get_settings().running_time_to current_date_time = datetime.utcnow() return current_date_time > end_date_time diff --git a/src/create_db.dev.py b/src/create_db.dev.py index 1776e89..f73aff6 100644 --- a/src/create_db.dev.py +++ b/src/create_db.dev.py @@ -26,7 +26,7 @@ db.session.add(web2) db.session.add(web3) - settings = Settings(websites=[web1, web2, web3], dummy=False) + settings = Settings(dummy=False) db.session.add(settings) @@ -42,9 +42,9 @@ root_hash="B" * 32, user_points=10, root_points=20, - os="Linux", + os="linux", ip="127.0.0.1", - hardness="Hard", + hardness="Easy", ) db.session.add(box) diff --git a/src/create_db.py b/src/create_db.py index 08bb5cb..c8cccef 100644 --- a/src/create_db.py +++ b/src/create_db.py @@ -4,7 +4,7 @@ from FlaskRTBCTF import db, bcrypt, create_app from FlaskRTBCTF import User, Machine, Logs from FlaskRTBCTF.main.models import Settings, Website -from FlaskRTBCTF.utils import handle_admin_pass +from FlaskRTBCTF.utils import handle_admin_pass, handle_admin_email app = create_app() @@ -21,16 +21,17 @@ root_hash="B" * 32, user_points=10, root_points=20, - os="Linux", + os="linux", ip="127.0.0.1", - hardness="Hard", + hardness="easy", ) db.session.add(box) passwd = handle_admin_pass() + email = handle_admin_email() admin_user = User( username="admin", - email="admin@admin.com", + email=email, password=bcrypt.generate_password_hash(passwd).decode("utf-8"), isAdmin=True, ) @@ -56,7 +57,7 @@ db.session.add(web2) db.session.add(web3) - settings = Settings(websites=[web1, web2, web3], dummy=True) + settings = Settings(dummy=True) db.session.add(settings) diff --git a/src/requirements.txt b/src/requirements.txt index 3998905..b24184c 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -24,3 +24,5 @@ SQLAlchemy==1.3.16 Werkzeug==1.0.1 WTForms==2.2.1 tablib==1.1.0 +Flask-Caching==1.8.0 +redis==3.4.1 \ No newline at end of file
    {{ loop.index }}{{ score['username'] }}{{ score['score'] }}{{ score[0] }}{{ score[1] }}
    {{ loop.index }}{{ score['username'] }}{{ score['score'] }}{{ score[0] }}{{ score[1] }}