From 75a65f4ec24fb10ccf55382a27f785afd49eb83b Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:30:30 +0530 Subject: [PATCH 01/12] Added entries for mail config --- app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.json b/app.json index a71e2a3..3c4c287 100644 --- a/app.json +++ b/app.json @@ -20,6 +20,12 @@ "ADMIN_PASS": { "description": "Administrator password", "generator": "secret" + }, + "MAIL_USER": { + "description": "Username for mail service" + }, + "MAIL_PASS": { + "description": "Password for mail service" } }, "scripts": { From e7453b131f0f163235f28d3a34387bcc43239ed3 Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:31:07 +0530 Subject: [PATCH 02/12] for development purposes --- src/create_db.dev.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/create_db.dev.py diff --git a/src/create_db.dev.py b/src/create_db.dev.py new file mode 100644 index 0000000..1776e89 --- /dev/null +++ b/src/create_db.dev.py @@ -0,0 +1,68 @@ +import pytz +from datetime import datetime + +from FlaskRTBCTF import db, bcrypt, create_app +from FlaskRTBCTF import User, Notification, Machine, Settings, Website, Logs +from FlaskRTBCTF.utils import handle_admin_pass + + +app = create_app() + +# create_app().app_context().push() +with app.app_context(): + db.create_all() + + default_time = datetime.now(pytz.utc) + + web1 = Website( + name="Official Abs0lut3Pwn4g3 Website", url="https://Abs0lut3Pwn4g3.github.io/", + ) + web2 = Website(name="Twitter", url="https://twitter.com/Abs0lut3Pwn4g3",) + web3 = Website( + name="GitHub", url="https://github.com/Abs0lut3Pwn4g3/RTB-CTF-Framework" + ) + + db.session.add(web1) + db.session.add(web2) + db.session.add(web3) + + settings = Settings(websites=[web1, web2, web3], dummy=False) + + db.session.add(settings) + + notif = Notification( + title=f"Welcome to {settings.ctf_name}", + body="The CTF is live now. Please read rules!", + ) + db.session.add(notif) + + box = Machine( + name="My Awesome Pwnable Box", + user_hash="A" * 32, + root_hash="B" * 32, + user_points=10, + root_points=20, + os="Linux", + ip="127.0.0.1", + hardness="Hard", + ) + db.session.add(box) + + passwd = handle_admin_pass() + admin_user = User( + username="admin", + email="admin@admin.com", + password=bcrypt.generate_password_hash(passwd).decode("utf-8"), + isAdmin=True, + ) + db.session.add(admin_user) + + admin_log = Logs( + user=admin_user, + accountCreationTime=default_time, + visitedMachine=True, + machineVisitTime=default_time, + ) + db.session.add(admin_log) + + db.session.commit() From 3b2a50c7f330e2a5fe461b6879ad4f530fbd91ed Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:32:01 +0530 Subject: [PATCH 03/12] moved to flask factory app method :) --- src/FlaskRTBCTF/utils/__init__.py | 12 +++++++ src/FlaskRTBCTF/utils/admin_manager.py | 7 ++++ src/FlaskRTBCTF/utils/bcrypt.py | 3 ++ src/FlaskRTBCTF/utils/helpers.py | 49 ++++++++++++++++++++++++++ src/FlaskRTBCTF/utils/login_manager.py | 26 ++++++++++++++ src/FlaskRTBCTF/utils/mail.py | 18 ++++++++++ src/FlaskRTBCTF/utils/models.py | 4 +++ 7 files changed, 119 insertions(+) create mode 100644 src/FlaskRTBCTF/utils/__init__.py create mode 100644 src/FlaskRTBCTF/utils/admin_manager.py create mode 100644 src/FlaskRTBCTF/utils/bcrypt.py create mode 100644 src/FlaskRTBCTF/utils/helpers.py create mode 100644 src/FlaskRTBCTF/utils/login_manager.py create mode 100644 src/FlaskRTBCTF/utils/mail.py create mode 100644 src/FlaskRTBCTF/utils/models.py diff --git a/src/FlaskRTBCTF/utils/__init__.py b/src/FlaskRTBCTF/utils/__init__.py new file mode 100644 index 0000000..fd66ce4 --- /dev/null +++ b/src/FlaskRTBCTF/utils/__init__.py @@ -0,0 +1,12 @@ +from .models import db +from .admin_manager import admin_manager +from .bcrypt import bcrypt +from .helpers import ( + handle_admin_pass, + 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/admin_manager.py b/src/FlaskRTBCTF/utils/admin_manager.py new file mode 100644 index 0000000..9dc0c25 --- /dev/null +++ b/src/FlaskRTBCTF/utils/admin_manager.py @@ -0,0 +1,7 @@ +from flask_admin import Admin +from flask_admin.menu import MenuLink + + +admin_manager = Admin(template_mode="bootstrap3") +admin_manager.add_link(MenuLink(name="CTF Setup", url="/setup")) +admin_manager.add_link(MenuLink(name="Go Back", url="/")) diff --git a/src/FlaskRTBCTF/utils/bcrypt.py b/src/FlaskRTBCTF/utils/bcrypt.py new file mode 100644 index 0000000..ac60495 --- /dev/null +++ b/src/FlaskRTBCTF/utils/bcrypt.py @@ -0,0 +1,3 @@ +from flask_bcrypt import Bcrypt + +bcrypt = Bcrypt() diff --git a/src/FlaskRTBCTF/utils/helpers.py b/src/FlaskRTBCTF/utils/helpers.py new file mode 100644 index 0000000..98e3d14 --- /dev/null +++ b/src/FlaskRTBCTF/utils/helpers.py @@ -0,0 +1,49 @@ +""" Helper functions """ + +import os +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 + + +def handle_secret_key(default="you-will-never-guess"): + sk = os.environ.get("SECRET_KEY", default) + if not sk: + sk = secrets.token_hex(16) + os.environ["SECRET_KEY"] = sk + return sk + + +def handle_admin_pass(default="admin"): + passwd = os.environ.get("ADMIN_PASS", default) + if not passwd: + passwd = secrets.token_hex(16) + os.environ["ADMIN_PASS"] = passwd + return passwd + + +def inject_app_context(): + settings = Settings.query.get(1) + toasts = [{"title": "CTF", "msg": "Correct User Hash!"}] + + # Note to self: maybe we can use? @cached_property: + # https://werkzeug.palletsprojects.com/en/1.0.x/utils/#werkzeug.utils.cached_property + + return dict(settings=settings, toasts=toasts) + + +def is_past_running_time(): + end_date_time = Settings.query.get(1).running_time_to + current_date_time = datetime.utcnow() + return current_date_time > end_date_time diff --git a/src/FlaskRTBCTF/utils/login_manager.py b/src/FlaskRTBCTF/utils/login_manager.py new file mode 100644 index 0000000..24649a5 --- /dev/null +++ b/src/FlaskRTBCTF/utils/login_manager.py @@ -0,0 +1,26 @@ +from functools import wraps + +from flask import flash, redirect +from flask_login import LoginManager, current_user + + +login_manager = LoginManager() +login_manager.login_view = "users.login" +login_manager.login_message_category = "info" + + +def admin_only(f): + """ + Route decorator to require admin access. + http://flask.pocoo.org/docs/0.12/patterns/viewdecorators/ + """ + + @wraps(f) + def decorated_function(*args, **kwargs): + if current_user.is_authenticated and current_user.isAdmin: + return f(*args, **kwargs) + else: + flash("You are not authorized to perform this operation.", "danger") + return redirect("/") + + return decorated_function diff --git a/src/FlaskRTBCTF/utils/mail.py b/src/FlaskRTBCTF/utils/mail.py new file mode 100644 index 0000000..5bb8bac --- /dev/null +++ b/src/FlaskRTBCTF/utils/mail.py @@ -0,0 +1,18 @@ +from flask import url_for +from flask_mail import Mail, Message + +mail = Mail() + + +def send_reset_email(user): + token = user.get_reset_token() + msg = Message( + "Password Reset Request", sender="noreply@demo.com", recipients=[user.email] + ) + msg.body = f"""To reset your password, visit the following link: +{url_for('users.reset_token', token=token, _external=True)} + +If you did not make this request then simply ignore this email +and no changes will be made. +""" + mail.send(msg) diff --git a/src/FlaskRTBCTF/utils/models.py b/src/FlaskRTBCTF/utils/models.py new file mode 100644 index 0000000..f606adc --- /dev/null +++ b/src/FlaskRTBCTF/utils/models.py @@ -0,0 +1,4 @@ +from flask_sqlalchemy import SQLAlchemy + + +db = SQLAlchemy() From 6e94ee850a70c448dbf4a6bd569ba663e0a1d25d Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:36:57 +0530 Subject: [PATCH 04/12] models moved in their respective folders --- src/FlaskRTBCTF/ctf/models.py | 16 +++++ src/FlaskRTBCTF/main/models.py | 75 +++++++++++++++++++++ src/FlaskRTBCTF/models.py | 115 -------------------------------- src/FlaskRTBCTF/users/models.py | 63 +++++++++++++++++ 4 files changed, 154 insertions(+), 115 deletions(-) create mode 100644 src/FlaskRTBCTF/ctf/models.py create mode 100644 src/FlaskRTBCTF/main/models.py delete mode 100644 src/FlaskRTBCTF/models.py create mode 100644 src/FlaskRTBCTF/users/models.py diff --git a/src/FlaskRTBCTF/ctf/models.py b/src/FlaskRTBCTF/ctf/models.py new file mode 100644 index 0000000..688e205 --- /dev/null +++ b/src/FlaskRTBCTF/ctf/models.py @@ -0,0 +1,16 @@ +from FlaskRTBCTF.utils import db + +# Machine Table + + +class Machine(db.Model): + __tablename__ = "machine" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), nullable=False) + user_hash = db.Column(db.String(32), nullable=False) + 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") diff --git a/src/FlaskRTBCTF/main/models.py b/src/FlaskRTBCTF/main/models.py new file mode 100644 index 0000000..a4a2565 --- /dev/null +++ b/src/FlaskRTBCTF/main/models.py @@ -0,0 +1,75 @@ +""" Main Application Models. """ + + +from datetime import datetime, date, time, timedelta + +from sqlalchemy.ext.hybrid import hybrid_property + +from FlaskRTBCTF.utils import db + + +# Notifications Table + + +class Notification(db.Model): + __tablename__ = "notification" + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(64), nullable=False) + body = db.Column(db.TEXT(), nullable=False) + timestamp = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f"Notif('{self.title}', '{self.body}')" + + +# Settings Table + + +class Settings(db.Model): + __tablename__ = "settings" + id = db.Column(db.Integer, primary_key=True) + dummy = db.Column(db.Boolean, nullable=False, default=True) + ctf_name = db.Column(db.String(64), nullable=False, default="RootTheBox CTF") + organization_name = db.Column( + db.String(80), nullable=False, default="Abs0lut3Pwn4g3" + ) + + from_date = db.Column(db.Date, nullable=True, default=date.today()) + from_time = db.Column(db.Time, nullable=True, default=time()) + to_date = db.Column( + db.Date, nullable=False, default=date.today() + timedelta(days=2) + ) + to_time = db.Column(db.Time, nullable=False, default=time()) + + websites = db.relationship("Website", backref="settings", lazy=True, uselist=True) + + @hybrid_property + def running_time_from(self): + return datetime.combine(self.from_date, self.from_time) + + @hybrid_property + def running_time_to(self): + return datetime.combine(self.to_date, self.to_time) + + def __repr__(self): + return f"CTF('{self.ctf_name},'{self.organization_name}')" + + +# Websites Table + + +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/" + ) + name = db.Column( + db.TEXT(), nullable=False, default="Official Abs0lut3Pwn4g3 Website" + ) + + def __repr__(self): + return f"Website('{self.name}','{self.url}')" diff --git a/src/FlaskRTBCTF/models.py b/src/FlaskRTBCTF/models.py deleted file mode 100644 index 2d72387..0000000 --- a/src/FlaskRTBCTF/models.py +++ /dev/null @@ -1,115 +0,0 @@ -""" Models. """ - - -from datetime import datetime -from itsdangerous import TimedJSONWebSignatureSerializer as Serializer - -from flask import current_app -from FlaskRTBCTF.config import LOGGING -from FlaskRTBCTF import db, login_manager -from flask_login import UserMixin - - -@login_manager.user_loader -def load_user(user_id): - return User.query.get(int(user_id)) - - -# Machine Table - - -class Machine(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), nullable=False) - user_hash = db.Column(db.String(32), nullable=False) - 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") - - score = db.relationship("Score", backref="machine", lazy=True) - - -# User Table - - -class User(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(24), unique=True, nullable=False) - email = db.Column(db.String(88), unique=True, nullable=False) - password = db.Column(db.String(64), nullable=False) - isAdmin = db.Column(db.Boolean, default=False) - score = db.relationship("Score", backref="user", lazy=True, uselist=False) - if LOGGING: - logs = db.relationship("Logs", backref="user", lazy=True, uselist=False) - - def get_reset_token(self, expires_sec=1800): - s = Serializer(current_app.config["SECRET_KEY"], expires_sec) - return s.dumps({"user_id": self.id}).decode("utf-8") - - @staticmethod - def verify_reset_token(token): - s = Serializer(current_app.config["SECRET_KEY"]) - try: - user_id = s.loads(token)["user_id"] - except Exception: - return None - return User.query.get(user_id) - - def __repr__(self): - return f"User('{self.username}', '{self.email}'))" - - -# Score Table - - -class Score(db.Model): - user_id = db.Column( - db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True - ) - userHash = db.Column(db.Boolean, default=False) - rootHash = db.Column(db.Boolean, default=False) - points = db.Column(db.Integer) - timestamp = db.Column(db.DateTime(), default=datetime.utcnow) - machine_id = db.Column(db.Integer, db.ForeignKey("machine.id"), nullable=False) - - def __repr__(self): - return f"Score('{self.user_id}', '{self.points}')" - - -# Notifications Table - - -class Notification(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(30), nullable=False) - body = db.Column(db.TEXT(), nullable=False) - timestamp = db.Column(db.DateTime, default=datetime.utcnow) - - def __repr__(self): - return f"Notif('{self.title}', '{self.body}')" - - -# Logging Table - - -if LOGGING: - - 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) - visitedMachine = db.Column(db.Boolean, default=False) - machineVisitTime = db.Column(db.DateTime, nullable=True) - userSubmissionTime = db.Column(db.DateTime, nullable=True) - rootSubmissionTime = db.Column(db.DateTime, nullable=True) - userOwnTime = db.Column(db.String, nullable=True) - rootOwnTime = db.Column(db.String, nullable=True) - userSubmissionIP = db.Column(db.String, nullable=True) - rootSubmissionIP = db.Column(db.String, nullable=True) - - def __repr__(self): - return f"Logs('{self.user_id}','{self.visitedMachine}'" diff --git a/src/FlaskRTBCTF/users/models.py b/src/FlaskRTBCTF/users/models.py new file mode 100644 index 0000000..7091031 --- /dev/null +++ b/src/FlaskRTBCTF/users/models.py @@ -0,0 +1,63 @@ +from itsdangerous import TimedJSONWebSignatureSerializer as Serializer + +from flask import current_app +from flask_login import UserMixin + +from FlaskRTBCTF import db, login_manager + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +# User Table + + +class User(db.Model, UserMixin): + __tablename__ = "user" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(24), unique=True, nullable=False) + email = db.Column(db.String(88), unique=True, nullable=False) + password = db.Column(db.String(64), nullable=False) + isAdmin = db.Column(db.Boolean, default=False) + points = db.Column(db.Integer, nullable=False, default=0) + logs = db.relationship("Logs", backref="user", lazy=True, uselist=False) + + def get_reset_token(self, expires_sec=1800): + s = Serializer(current_app.config["SECRET_KEY"], expires_sec) + return s.dumps({"user_id": self.id}).decode("utf-8") + + @staticmethod + def verify_reset_token(token): + s = Serializer(current_app.config["SECRET_KEY"]) + try: + user_id = s.loads(token)["user_id"] + except Exception: + return None + return User.query.get(user_id) + + def __repr__(self): + return f"User('{self.username}', '{self.email}'))" + + +# Logging Table + + +class Logs(db.Model): + __tablename__ = "logs" + user_id = db.Column( + db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True + ) + accountCreationTime = db.Column(db.DateTime, nullable=False) + visitedMachine = db.Column(db.Boolean, default=False) + machineVisitTime = db.Column(db.DateTime, nullable=True) + userSubmissionTime = db.Column(db.DateTime, nullable=True) + rootSubmissionTime = db.Column(db.DateTime, nullable=True) + userOwnTime = db.Column(db.String, nullable=True) + rootOwnTime = db.Column(db.String, nullable=True) + userSubmissionIP = db.Column(db.String, nullable=True) + rootSubmissionIP = db.Column(db.String, nullable=True) + + def __repr__(self): + return f"Logs('{self.user_id}','{self.visitedMachine}'" From 87a7d8f7fd7198de3c9dc68bbd03f22b50c740ff Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:37:18 +0530 Subject: [PATCH 05/12] update requirements.txt --- src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirements.txt b/src/requirements.txt index ecdd4c1..3998905 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -23,4 +23,4 @@ six==1.14.0 SQLAlchemy==1.3.16 Werkzeug==1.0.1 WTForms==2.2.1 -pytest-flake8==1.0.4 +tablib==1.1.0 From f438beb5574baea37513a7ac7ce458f5ac0c4dfc Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:38:01 +0530 Subject: [PATCH 06/12] new theme for admin views, search functionality --- src/FlaskRTBCTF/admin/views.py | 28 +++++++++++++++++++++++---- src/FlaskRTBCTF/config.py | 35 +++------------------------------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/FlaskRTBCTF/admin/views.py b/src/FlaskRTBCTF/admin/views.py index 36cf1e6..34b5b90 100644 --- a/src/FlaskRTBCTF/admin/views.py +++ b/src/FlaskRTBCTF/admin/views.py @@ -1,13 +1,16 @@ """ Admin Model Views. """ -from flask import abort +from flask import abort, redirect, flash from flask_login import current_user +from flask_admin import expose +from flask_admin.form import SecureForm from flask_admin.contrib.sqla import ModelView -class MyModelView(ModelView): - - column_exclude_list = ("password",) +class BaseModelView(ModelView): + export_types = ("csv", "json") + can_export = True + form_base_class = SecureForm def is_accessible(self): if not current_user.is_authenticated or not current_user.isAdmin: @@ -25,3 +28,20 @@ def _handle_view(self, name, **kwargs): if current_user.is_authenticated: # permission denied abort(403) + + +class UserAdminView(BaseModelView): + column_exclude_list = ("password",) + column_searchable_list = ("username", "email") + + @expose("/new/") + def create_view(self): + flash("Please use registration form for creating new users.", "info") + return redirect("/admin/user") + + +class MachineAdminView(BaseModelView): + column_searchable_list = ("name", "ip") + form_choices = { + "hardness": [("easy", "Easy"), ("medium", "Medium"), ("hard", "Hard")] + } diff --git a/src/FlaskRTBCTF/config.py b/src/FlaskRTBCTF/config.py index 5c05241..51da949 100644 --- a/src/FlaskRTBCTF/config.py +++ b/src/FlaskRTBCTF/config.py @@ -1,8 +1,6 @@ import os -from datetime import datetime -import pytz -from .helpers import handle_secret_key +from .utils import handle_secret_key # Flask related Configurations # Note: DO NOT FORGET TO CHANGE 'SECRET_KEY' ! @@ -14,39 +12,12 @@ class Config: # 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 + # TEMPLATES_AUTO_RELOAD = True MAIL_SERVER = "smtp.googlemail.com" MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = os.environ.get("EMAIL_USER") MAIL_PASSWORD = os.environ.get("EMAIL_PASS") - - -# CTF related Configuration -# Add some information about organization and specify CTF name - -organization = { - "ctfname": "RootTheBox CTF", - "name": "Abs0lut3Pwn4g3", - "website": { - "url": "https://Abs0lut3Pwn4g3.github.io/", - "name": "Official Abs0lut3Pwn4g3 Website", - }, - "website_2": {"url": "https://twitter.com/abs0lut3pwn4g3", "name": "Twitter"}, - "website_3": {"url": "https://github.com/abs0lut3pwn4g3", "name": "GitHub"}, -} - -# Specify CTFs Running Time -# We do not recommend changing the Timezone. - -RunningTime = { - "from": datetime(2019, 7, 7, 15, 00, 00, 0, pytz.utc), - "to": datetime(2023, 7, 8, 0, 00, 00, 0, pytz.utc), - "TimeZone": "UTC", -} - -# Logging: Set to 'True' to enable Logging in Admin Views. -# We recommend to leave it on. It is more than just errors ;) - -LOGGING = True From 471a18a1c9b2a3388428aee06ea4994d8a43d547 Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:39:30 +0530 Subject: [PATCH 07/12] macros and form macros for reusability --- src/FlaskRTBCTF/__init__.py | 69 +++++---- src/FlaskRTBCTF/ctf/forms.py | 35 ++++- src/FlaskRTBCTF/ctf/routes.py | 210 ++++++++++---------------- src/FlaskRTBCTF/main/forms.py | 83 ++++++++++ src/FlaskRTBCTF/main/routes.py | 73 +++++++-- src/FlaskRTBCTF/templates/forms.html | 181 ++++++++++++++++++++++ src/FlaskRTBCTF/templates/macros.html | 16 ++ src/FlaskRTBCTF/users/forms.py | 4 +- src/FlaskRTBCTF/users/routes.py | 86 +++++------ src/create_db.py | 54 +++---- 10 files changed, 545 insertions(+), 266 deletions(-) create mode 100644 src/FlaskRTBCTF/main/forms.py create mode 100644 src/FlaskRTBCTF/templates/forms.html create mode 100644 src/FlaskRTBCTF/templates/macros.html diff --git a/src/FlaskRTBCTF/__init__.py b/src/FlaskRTBCTF/__init__.py index c97b393..998d750 100644 --- a/src/FlaskRTBCTF/__init__.py +++ b/src/FlaskRTBCTF/__init__.py @@ -1,19 +1,27 @@ -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_bcrypt import Bcrypt -from flask_login import LoginManager -from flask_admin import Admin -from flask_mail import Mail -from FlaskRTBCTF.config import Config, LOGGING import os -db = SQLAlchemy() -bcrypt = Bcrypt() -login_manager = LoginManager() -admin_manager = Admin() -login_manager.login_view = "users.login" -login_manager.login_message_category = "info" -mail = Mail() +from flask import Flask + +from FlaskRTBCTF.config import Config +from FlaskRTBCTF.admin.views import BaseModelView, UserAdminView, MachineAdminView +from FlaskRTBCTF.utils import ( + db, + bcrypt, + login_manager, + admin_manager, + mail, + inject_app_context, +) + +from FlaskRTBCTF.users.models import User, Logs +from FlaskRTBCTF.main.models import Notification +from FlaskRTBCTF.ctf.models import Machine + +from FlaskRTBCTF.users.routes import users +from FlaskRTBCTF.ctf.routes import ctf +from FlaskRTBCTF.main.routes import main + +_blueprints = (users, ctf, main) def create_app(config_class=Config): @@ -24,32 +32,23 @@ def create_app(config_class=Config): bcrypt.init_app(app) login_manager.init_app(app) admin_manager.init_app(app) - # Add model views - from FlaskRTBCTF.admin.views import MyModelView - from FlaskRTBCTF.models import User, Score, Notification, Machine - - if LOGGING: - from FlaskRTBCTF.models import Logs - admin_manager.add_view(MyModelView(User, db.session)) - admin_manager.add_view(MyModelView(Score, db.session)) - admin_manager.add_view(MyModelView(Notification, db.session)) - admin_manager.add_view(MyModelView(Machine, db.session)) - if LOGGING: - admin_manager.add_view(MyModelView(Logs, db.session)) mail.init_app(app) - from flask_sslify import SSLify + app.context_processor(inject_app_context) + + # 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(BaseModelView(Logs, db.session)) + + for _bp in _blueprints: + app.register_blueprint(_bp) # only trigger SSLify if the app is running on Heroku if "DYNO" in os.environ: - _ = SSLify(app) + from flask_sslify import SSLify - from FlaskRTBCTF.users.routes import users - from FlaskRTBCTF.ctf.routes import ctf - from FlaskRTBCTF.main.routes import main - - app.register_blueprint(users) - app.register_blueprint(ctf) - app.register_blueprint(main) + _ = SSLify(app) return app diff --git a/src/FlaskRTBCTF/ctf/forms.py b/src/FlaskRTBCTF/ctf/forms.py index 0dc20e2..984079a 100644 --- a/src/FlaskRTBCTF/ctf/forms.py +++ b/src/FlaskRTBCTF/ctf/forms.py @@ -1,17 +1,36 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired, Length +from wtforms import StringField, SubmitField, HiddenField +from wtforms.validators import DataRequired, Length, ValidationError +from .models import Machine class UserHashForm(FlaskForm): - userHash = StringField( - "User hash", validators=[DataRequired(), Length(min=32, max=32)] + machine_id = HiddenField("Machine ID", validators=[DataRequired()]) + user_hash = StringField( + "User Hash", validators=[DataRequired(), Length(min=32, max=32)] ) - submit = SubmitField("Submit") + submit_user_hash = SubmitField("Submit") + + def validate_user_hash(self, user_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(user_hash.data): + raise ValidationError("Incorrect User Hash") class RootHashForm(FlaskForm): - rootHash = StringField( - "Root hash", validators=[DataRequired(), Length(min=32, max=32)] + machine_id = HiddenField("Machine ID", validators=[DataRequired()]) + root_hash = StringField( + "Root Hash", validators=[DataRequired(), Length(min=32, max=32)] ) - submit = SubmitField("Submit") + submit_root_hash = SubmitField("Submit") + + 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): + pass + else: + raise ValidationError("Incorrect Root Hash.") diff --git a/src/FlaskRTBCTF/ctf/routes.py b/src/FlaskRTBCTF/ctf/routes.py index c009128..400b580 100644 --- a/src/FlaskRTBCTF/ctf/routes.py +++ b/src/FlaskRTBCTF/ctf/routes.py @@ -2,164 +2,108 @@ from datetime import datetime -import pytz -from flask import Blueprint, render_template, flash, request +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.config import organization, LOGGING, RunningTime -from FlaskRTBCTF.models import User, Score, Machine -from FlaskRTBCTF.ctf.forms import UserHashForm, RootHashForm -if LOGGING: - from FlaskRTBCTF.models import Logs +from FlaskRTBCTF import db +from FlaskRTBCTF.users.models import User, Logs +from FlaskRTBCTF.utils import is_past_running_time +from .models import Machine +from .forms import UserHashForm, RootHashForm 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 def scoreboard(): - scores = Score.query.order_by(Score.points.desc(), Score.timestamp).all() + users_score = User.query.order_by(User.points.desc()).all() userNameScoreList = [] - for score in scores: - userNameScoreList.append( - {"username": User.query.get(score.user_id).username, "score": score.points} - ) + for u in users_score: + userNameScoreList.append({"username": u.username, "score": u.points}) - return render_template( - "scoreboard.html", scores=userNameScoreList, organization=organization - ) + return render_template("scoreboard.html", scores=userNameScoreList) -# Machine Info +# Machines Info -@ctf.route("/machine") +@ctf.route("/machines", methods=["GET", "POST"]) @login_required -def machine(): - box = Machine.query.filter(Machine.ip == "127.0.0.1").first() - if LOGGING: +def machines(): + userHashForm = UserHashForm() + rootHashForm = RootHashForm() + + if request.method == "GET": log = Logs.query.get(current_user.id) + + # check if it is the first visit to machine page for user if log.visitedMachine is False: log.visitedMachine = True log.machineVisitTime = datetime.utcnow() db.session.commit() - userHashForm = UserHashForm() - rootHashForm = RootHashForm() - end_date_time = RunningTime["to"] - current_date_time = datetime.now(pytz.utc) - return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - organization=organization, - box=box, - current=current_date_time, - end=end_date_time, - ) - - -# Hash Submission Management - -@ctf.route("/validateRootHash", methods=["POST"]) -@login_required -def validateRootHash(): - box = Machine.query.filter(Machine.ip == "127.0.0.1").first() - userHashForm = UserHashForm() - rootHashForm = RootHashForm() - end_date_time = RunningTime["to"] - current_date_time = datetime.now(pytz.utc) - if rootHashForm.validate_on_submit(): - if current_date_time > end_date_time: - flash("Sorry! Contest has ended", "danger") - elif rootHashForm.rootHash.data == box.root_hash: - score = Score.query.get(current_user.id) - if score.rootHash: - flash("You already own System.", "success") - else: - score.rootHash = True - score.points += box.root_points - score.timestamp = datetime.now(pytz.utc) - if LOGGING: - 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() - flash("Congrats! correct system hash.", "success") - else: - flash("Sorry! Wrong system hash", "danger") - return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - box=box, - organization=organization, - current=current_date_time, - end=end_date_time, - ) - else: - return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - box=box, - organization=organization, - current=current_date_time, - end=end_date_time, - ) - - -@ctf.route("/validateUserHash", methods=["POST"]) -@login_required -def validateUserHash(): - box = Machine.query.filter(Machine.ip == "127.0.0.1").first() - userHashForm = UserHashForm() - rootHashForm = RootHashForm() - end_date_time = RunningTime["to"] - current_date_time = datetime.now(pytz.utc) - if userHashForm.validate_on_submit(): - if current_date_time > end_date_time: - flash("Sorry! Contest has ended", "danger") - elif userHashForm.userHash.data == box.user_hash: - score = Score.query.get(current_user.id) - if score.userHash: - flash("You already own User.", "success") - else: - score.userHash = True - score.points += box.user_points - score.timestamp = datetime.now(pytz.utc) - if LOGGING: - 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() - flash("Congrats! correct user hash.", "success") - else: - flash("Sorry! Wrong user hash", "danger") - return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - organization=organization, - box=box, - current=current_date_time, - end=end_date_time, - ) else: - return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - organization=organization, - box=box, - current=current_date_time, - end=end_date_time, - ) + if is_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(): + box = Machine.query.get(int(userHashForm.machine_id.data)) + 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() + 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 + 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() + flash("Congrats! correct root hash.", "success") + + return render_template( + "machine.html", userHashForm=userHashForm, rootHashForm=rootHashForm, + ) diff --git a/src/FlaskRTBCTF/main/forms.py b/src/FlaskRTBCTF/main/forms.py new file mode 100644 index 0000000..fb36e10 --- /dev/null +++ b/src/FlaskRTBCTF/main/forms.py @@ -0,0 +1,83 @@ +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.validators import DataRequired, Length, URL +from sqlalchemy.exc import SQLAlchemyError + +from FlaskRTBCTF import db +from FlaskRTBCTF.utils import admin_only +from .models import Settings, Website + + +class SettingsForm(FlaskForm): + ctf_name = StringField( + "CTF Name", validators=[DataRequired(), Length(min=3, max=64)] + ) + 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()) + + submit = SubmitField("Next") + + @admin_only + def setup(self): + if self.is_submitted(): + 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 + + db.session.commit() + + return redirect(url_for("main.setup", step=3)) + else: + return redirect(url_for("main.setup", step=2)) + + +class WebsiteForm(FlaskForm): + names = FieldList( + StringField("Label", validators=[DataRequired(), Length(min=2, max=64)]), + min_entries=1, + max_entries=3, + ) + urls = FieldList( + StringField("URL", validators=[DataRequired(), URL()], widget=URLInput()), + min_entries=1, + max_entries=3, + ) + submit = SubmitField("Finish Setup") + + @admin_only + def setup(self): + if self.is_submitted(): + 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]) + db.session.add(obj) + db.session.commit() + flash( + "CTF setup was successful! \ + You can use admin controls for managing database tables.", + "success", + ) + return redirect(url_for("main.home")) + + except SQLAlchemyError: + db.session.rollback() + flash("Transaction failed. Please try again.", "danger") + return redirect(url_for("main.setup"), step=3) + + else: + flash("Error: Couldn't save form data.", "danger") + return redirect(url_for("main.setup", step=3)) diff --git a/src/FlaskRTBCTF/main/routes.py b/src/FlaskRTBCTF/main/routes.py index eb46acf..86684a3 100644 --- a/src/FlaskRTBCTF/main/routes.py +++ b/src/FlaskRTBCTF/main/routes.py @@ -1,26 +1,75 @@ -from flask import render_template, Blueprint -from FlaskRTBCTF.config import organization, RunningTime -from FlaskRTBCTF.models import Notification +from flask import render_template, Blueprint, redirect, url_for, request, flash +from flask_login import login_required + +from .models import Notification, Settings, Website +from .forms import SettingsForm, WebsiteForm +from FlaskRTBCTF.utils import admin_only main = Blueprint("main", __name__) + """ Index page """ +@main.before_request +def needs_setup(): + settings = Settings.query.get(1) + if settings.dummy: + if request.endpoint not in ("main.setup", "users.login"): + flash("Please setup the CTF, before accessing any routes.", "info") + return redirect(url_for("main.setup")) + else: + return + + @main.route("/") @main.route("/home") def home(): - return render_template( - "home.html", organization=organization, RunningTime=RunningTime - ) + settings = Settings.query.get(1) + running_time = { + "from": settings.running_time_from, + "to": settings.running_time_to, + } + + return render_template("home.html", RunningTime=running_time) @main.route("/notifications") def notifications(): notifs = Notification.query.order_by(Notification.timestamp.desc()).all() - return render_template( - "notifications.html", - organization=organization, - title="Notifications", - notifs=notifs, - ) + + return render_template("notifications.html", title="Notifications", notifs=notifs) + + +@main.route("/setup", methods=["GET", "POST"]) +@login_required +@admin_only +def setup(): + website_form_data = {"names": list(), "urls": list()} + for w in Website.query.all(): + website_form_data["names"].append(w.name) + website_form_data["urls"].append(w.url) + + settings_form_data = Settings.query.get(1) + + settings_form = SettingsForm(obj=settings_form_data) + website_form = WebsiteForm(data=website_form_data) + + if request.method == "GET": + return render_template( + "setup.html", + title="Setup", + settingsForm=settings_form, + websitesForm=website_form, + ) + + else: + + step = int(request.args.get("step", 2)) + + if step == 2: + return settings_form.setup() + elif step == 3: + return website_form.setup() + else: + return redirect(url_for("main.setup")) diff --git a/src/FlaskRTBCTF/templates/forms.html b/src/FlaskRTBCTF/templates/forms.html new file mode 100644 index 0000000..d82acf1 --- /dev/null +++ b/src/FlaskRTBCTF/templates/forms.html @@ -0,0 +1,181 @@ +{% from 'macros.html' import form_field_group_macro %} + + +{% macro login_form_macro(form, url) %} +
+
+ {{ form.hidden_tag() }} +
+ Log In +
+ {{ form_field_group_macro(field=form.username) }} +
+
+ {{ form_field_group_macro(field=form.password) }} +
+
+ {{ form.remember(class="custom-control-input") }} + {{ form.remember.label(class="custom-control-label") }} +
+
+
+ {{ form.submit(class="btn btn-dark btn-hover-red") }} +
+ + Forgot Password? + +
+
+ +{% endmacro %} + + +{% macro register_form_macro(form) %} +
+
+ {{ form.hidden_tag() }} +
+ Join Today +
+ {{ form_field_group_macro(field=form.username) }} +
+
+ {{ form_field_group_macro(field=form.email) }} +
+
+ {{ form_field_group_macro(field=form.password) }} +
+
+ {{ form_field_group_macro(field=form.confirm_password) }} +
+
+
+ {{ form.submit(class="btn btn-dark btn-hover-red") }} +
+
+
+
+ + Already Have An Account? Sign In + +
+{% endmacro %} + + + +{% macro settings_form_macro(form, url) %} +
+
+ {{ form.hidden_tag() }} +
+ CTF Settings +
+ {{ form_field_group_macro(field=form.ctf_name) }} +
+
+ {{ form_field_group_macro(field=form.organization_name) }} +
+ +
Running Time
+ + Date & Time should be in UTC Time Zone. +
+ If you don't need to specify running time, make the end date's year far, far away in the future. +
+
+ {{ form_field_group_macro(field=form.from_date) }} + {{ form_field_group_macro(field=form.from_time) }} +
+
+ {{ form_field_group_macro(field=form.to_date) }} + {{ form_field_group_macro(field=form.to_time) }} +
+
+
+ {{ form.submit(class="btn btn-dark btn-hover-red") }} +
+
+
+ +{% endmacro %} + + +{% macro website_form_macro(form, url) %} +
+
+
+ Add Websites for your organization +
+ {{ form.names.label(class="form-control-label") }} + {{ form.names(class="is-invalid", autocomplete="off") }} +
+
+ {{ form.urls.label(class="form-control-label text-uppercase") }} + {{ form.urls(class="is-invalid", autocomplete="off") }} +
+
+
+ Go Back + {{ form.submit(class="btn btn-dark btn-hover-red") }} +
+
+
+{% endmacro %} + + + +{% macro user_hash_form_macro(form, machine_id) %} + +
+ {{ 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.submit_user_hash(class="btn btn-dark btn-hover-red") }} +
+
+ +{% endmacro %} + + + +{% macro root_hash_form_macro(form, machine_id) %} + +
+ {{ 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.submit_root_hash(class="btn btn-dark btn-hover-red") }} +
+
+ +{% endmacro %} diff --git a/src/FlaskRTBCTF/templates/macros.html b/src/FlaskRTBCTF/templates/macros.html new file mode 100644 index 0000000..d456c49 --- /dev/null +++ b/src/FlaskRTBCTF/templates/macros.html @@ -0,0 +1,16 @@ + +{% macro form_field_group_macro(field) %} + + {{ field.label(class="form-control-label") }} + {% if field.errors %} + {{ field(class="form-control is-invalid", autocomplete="off") }} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ field(class="form-control", autocomplete="off") }} + {% endif %} + +{% endmacro %} \ No newline at end of file diff --git a/src/FlaskRTBCTF/users/forms.py b/src/FlaskRTBCTF/users/forms.py index a1c0ab4..8fd9b8b 100644 --- a/src/FlaskRTBCTF/users/forms.py +++ b/src/FlaskRTBCTF/users/forms.py @@ -1,10 +1,10 @@ -from FlaskRTBCTF.models import User - from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField, BooleanField from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from flask_login import current_user +from .models import User + class RegistrationForm(FlaskForm): username = StringField( diff --git a/src/FlaskRTBCTF/users/routes.py b/src/FlaskRTBCTF/users/routes.py index cd87022..0492157 100644 --- a/src/FlaskRTBCTF/users/routes.py +++ b/src/FlaskRTBCTF/users/routes.py @@ -1,22 +1,17 @@ from datetime import datetime import pytz -from FlaskRTBCTF.users.forms import ( +from flask import render_template, url_for, flash, redirect, request, Blueprint +from flask_login import login_user, current_user, logout_user, login_required + +from FlaskRTBCTF.utils import db, bcrypt, send_reset_email +from .forms import ( RegistrationForm, LoginForm, RequestResetForm, ResetPasswordForm, ) -from FlaskRTBCTF.users.utils import send_reset_email -from FlaskRTBCTF.config import organization, LOGGING -from FlaskRTBCTF.models import User, Score, Machine - -from flask import render_template, url_for, flash, redirect, request, Blueprint -from flask_login import login_user, current_user, logout_user, login_required -from FlaskRTBCTF import db, bcrypt - -if LOGGING: - from FlaskRTBCTF.models import Logs +from .models import User, Logs users = Blueprint("users", __name__) @@ -28,10 +23,9 @@ @users.route("/register", methods=["GET", "POST"]) def register(): if current_user.is_authenticated: - flash("Already Authenticated", "info") + flash("Already Authenticated.", "info") return redirect(url_for("main.home")) - box = Machine.query.filter(Machine.ip == "127.0.0.1").first() form = RegistrationForm() if form.validate_on_submit(): @@ -41,29 +35,24 @@ def register(): user = User( username=form.username.data, email=form.email.data, password=hashed_password ) - score = Score(user=user, userHash=False, rootHash=False, points=0, machine=box) - if LOGGING: - log = Logs( - user=user, - accountCreationTime=datetime.now(pytz.utc), - visitedMachine=False, - machineVisitTime=None, - userSubmissionTime=None, - rootSubmissionTime=None, - userSubmissionIP=None, - rootSubmissionIP=None, - ) - db.session.add(log) + log = Logs( + user=user, + accountCreationTime=datetime.now(pytz.utc), + visitedMachine=False, + machineVisitTime=None, + userSubmissionTime=None, + rootSubmissionTime=None, + userSubmissionIP=None, + rootSubmissionIP=None, + ) + db.session.add(log) db.session.add(user) - db.session.add(score) db.session.commit() flash("Your account has been created! You are now able to log in.", "success") return redirect(url_for("users.login")) - return render_template( - "register.html", title="Register", form=form, organization=organization - ) + return render_template("register.html", title="Register", form=form) @users.route("/login", methods=["GET", "POST"]) @@ -82,9 +71,7 @@ def login(): return redirect(next_page) if next_page else redirect(url_for("main.home")) else: flash("Login Unsuccessful. Please check username and password.", "danger") - return render_template( - "login.html", title="Login", form=form, organization=organization - ) + return render_template("login.html", title="Login", form=form) @users.route("/logout") @@ -98,7 +85,7 @@ def logout(): @users.route("/account") @login_required def account(): - return render_template("account.html", title="Account", organization=organization) + return render_template("account.html", title="Account") @users.route("/reset_password", methods=["GET", "POST"]) @@ -107,18 +94,18 @@ def reset_request(): return redirect(url_for("main.home")) form = RequestResetForm() if form.validate_on_submit(): - user = User.query.filter_by(email=form.email.data).first() - send_reset_email(user) - flash( - "An email has been sent with instructions to reset your password.", "info" - ) - return redirect(url_for("users.login")) - return render_template( - "reset_request.html", - title="Reset Password", - form=form, - organization=organization, - ) + try: + user = User.query.filter_by(email=form.email.data).first() + send_reset_email(user) + flash( + "An email has been sent with instructions to reset your password.", + "info", + ) + return redirect(url_for("users.login")) + except Exception: + flash("Mail is not setup. Please contact admin directly.", "info") + return redirect(request.url) + return render_template("reset_request.html", title="Reset Password", form=form) @users.route("/reset_password/", methods=["GET", "POST"]) @@ -129,8 +116,9 @@ def reset_token(token): user = User.verify_reset_token(token) if user is None: - flash("That is an invalid or expired token", "warning") + flash("That is an invalid or expired token", "danger") return redirect(url_for("users.reset_request")) + form = ResetPasswordForm() if form.validate_on_submit(): @@ -142,6 +130,4 @@ def reset_token(token): flash("Your password has been updated! You are now able to log in", "success") return redirect(url_for("users.login")) - return render_template( - "reset_token.html", title="Reset Password", form=form, organization=organization - ) + return render_template("reset_token.html", title="Reset Password", form=form) diff --git a/src/create_db.py b/src/create_db.py index 3d35944..08bb5cb 100644 --- a/src/create_db.py +++ b/src/create_db.py @@ -1,18 +1,15 @@ import pytz from datetime import datetime -from FlaskRTBCTF import create_app, db, bcrypt -from FlaskRTBCTF.helpers import handle_admin_pass -from FlaskRTBCTF.models import User, Score, Notification, Machine -from FlaskRTBCTF.config import organization, LOGGING - -if LOGGING: - from FlaskRTBCTF.models import Logs +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 app = create_app() -# create_app().app_context().push() + with app.app_context(): db.create_all() @@ -26,7 +23,7 @@ root_points=20, os="Linux", ip="127.0.0.1", - hardness="You tell", + hardness="Hard", ) db.session.add(box) @@ -37,25 +34,30 @@ password=bcrypt.generate_password_hash(passwd).decode("utf-8"), isAdmin=True, ) - admin_score = Score( - user=admin_user, userHash=False, rootHash=False, points=0, machine=box - ) db.session.add(admin_user) - db.session.add(admin_score) - notif = Notification( - title=f"Welcome to {organization['ctfname']}", - body="The CTF is live now. Please read rules!", + admin_log = Logs( + user=admin_user, + accountCreationTime=default_time, + visitedMachine=True, + machineVisitTime=default_time, + ) + db.session.add(admin_log) + + web1 = Website( + name="Official Abs0lut3Pwn4g3 Website", url="https://Abs0lut3Pwn4g3.github.io/", ) - db.session.add(notif) - - if LOGGING: - admin_log = Logs( - user=admin_user, - accountCreationTime=default_time, - visitedMachine=True, - machineVisitTime=default_time, - ) - db.session.add(admin_log) + web2 = Website(name="Twitter", url="https://twitter.com/Abs0lut3Pwn4g3",) + web3 = Website( + name="GitHub", url="https://github.com/Abs0lut3Pwn4g3/RTB-CTF-Framework" + ) + + db.session.add(web1) + db.session.add(web2) + db.session.add(web3) + + settings = Settings(websites=[web1, web2, web3], dummy=True) + + db.session.add(settings) db.session.commit() From 4cf05cb5ac4845913ea390fdbac53c5f2bb5fd2b Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Mon, 27 Apr 2020 00:40:08 +0530 Subject: [PATCH 08/12] setup page, multiple machines, add new btns --- .travis.yml | 2 +- src/FlaskRTBCTF/.gitignore | 1 - src/FlaskRTBCTF/helpers.py | 20 --- src/FlaskRTBCTF/static/main.css | 98 +++++++---- src/FlaskRTBCTF/templates/account.html | 4 +- src/FlaskRTBCTF/templates/home.html | 21 ++- src/FlaskRTBCTF/templates/layout.html | 55 +++++-- src/FlaskRTBCTF/templates/login.html | 48 +----- src/FlaskRTBCTF/templates/machine.html | 164 +++++++++---------- src/FlaskRTBCTF/templates/notifications.html | 3 + src/FlaskRTBCTF/templates/register.html | 74 +-------- src/FlaskRTBCTF/templates/setup.html | 72 ++++++++ src/FlaskRTBCTF/users/utils.py | 19 --- src/FlaskRTBCTF/utils/helpers.py | 4 +- 14 files changed, 282 insertions(+), 303 deletions(-) delete mode 100644 src/FlaskRTBCTF/.gitignore delete mode 100644 src/FlaskRTBCTF/helpers.py create mode 100644 src/FlaskRTBCTF/templates/setup.html delete mode 100644 src/FlaskRTBCTF/users/utils.py diff --git a/.travis.yml b/.travis.yml index f265a73..2154ac0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,4 +24,4 @@ install: before_script: - black . --check script: - - flake8 . --count --max-line-length=88 --show-source --statistics + - flake8 . --count --max-line-length=88 --exclude="src/FlaskRTBCTF/utils/__init__.py" --show-source --statistics \ No newline at end of file diff --git a/src/FlaskRTBCTF/.gitignore b/src/FlaskRTBCTF/.gitignore deleted file mode 100644 index c18dd8d..0000000 --- a/src/FlaskRTBCTF/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ diff --git a/src/FlaskRTBCTF/helpers.py b/src/FlaskRTBCTF/helpers.py deleted file mode 100644 index 5240d5e..0000000 --- a/src/FlaskRTBCTF/helpers.py +++ /dev/null @@ -1,20 +0,0 @@ -""" Helper functions """ - -import os -import secrets - - -def handle_secret_key(default="you-will-never-guess"): - sk = os.environ.get("SECRET_KEY", default) - if not sk: - sk = secrets.token_hex(16) - os.environ["SECRET_KEY"] = sk - return sk - - -def handle_admin_pass(default="admin"): - passwd = os.environ.get("ADMIN_PASS", default) - if not passwd: - passwd = secrets.token_hex(16) - os.environ["ADMIN_PASS"] = passwd - return passwd diff --git a/src/FlaskRTBCTF/static/main.css b/src/FlaskRTBCTF/static/main.css index c386809..131bf8e 100644 --- a/src/FlaskRTBCTF/static/main.css +++ b/src/FlaskRTBCTF/static/main.css @@ -1,8 +1,15 @@ @import url("https://fonts.googleapis.com/css?family=Raleway:200,300,400,500,600"); +:root { + --primary-color: #ce1b28; + --secondary-color: #2c2f36; + --dark-color: #101010; + --light-color: #C9D3E7; +} + body { - background: #2c2f36; - color: #C9D3E7 !important; + background: var(--secondary-color); + color: var(--light-color) !important; margin-top: 5rem; font-family: "Raleway", Arial, Helvetica, sans-serif; line-height: 1.65; @@ -10,15 +17,19 @@ body { } h1, h2, h3, h4, h5, h6 { - color: #C9D3E7; + color: var(--light-color); } hr { - background-color: #C9D3E7; + background-color: var(--light-color); +} + +.bg-dark { + background-color: var(--dark-color) !important; } -.bg-steel { - background-color: #101010; +.bg-primary { + background-color: var(--primary-color) !important; } .site-header .navbar-nav .nav-link { @@ -26,7 +37,7 @@ hr { } .site-header .navbar-nav .nav-link:hover { - color: #ce1b28; + color: var(--primary-color); } .site-header .navbar-nav .nav-link.active { @@ -41,7 +52,8 @@ hr { } a.red-link { - color: #ce1b28 !important; + cursor: pointer; + color: var(--primary-color) !important; font-size: 17px; } @@ -50,43 +62,51 @@ a.red-link:hover { } a.red-underlined-link { - color: #ce1b28 !important; + color: var(--primary-color) !important; text-decoration: underline; } a:hover, .btn-hover-red:hover { - color: #ce1b28 !important; + color: var(--primary-color) !important; text-decoration: none !important; } .title-heading { - color: #C9D3E7 !important; + color: var(--light-color) !important; } +/* machine.html */ + .machine-heading { font-size: 2.5rem; - background: #101010; + background: var(--dark-color); text-align: center; - box-shadow: 0 7px 16px 0 #ce1b28; + box-shadow: 0 7px 16px 0 var(--primary-color); box-sizing: border-box; } + +.btn-sm { + border-radius: 10%; + cursor: pointer; +} + /* custom bootstrap */ .table-responsive { - box-shadow: 0px 9px 18px 0px #101010 !important; + box-shadow: 0px 9px 18px 0px var(--dark-color) !important; } .table-row-odd { - background-color: #2c2f36; + background-color: var(--secondary-color); color: #fff; } .table-row-even { - background: #101010; + background: var(--dark-color); color: #fff; padding: 10px !important; - box-shadow: 69px 10px 18px 0 #ce1b28 !important; + box-shadow: 69px 10px 18px 0 var(--primary-color) !important; box-sizing: border-box; border-collapse: collapse; } @@ -94,8 +114,8 @@ a:hover, .btn-hover-red:hover { .content-section, .jumbotron { background: rgba(35,36,39,0.95); padding: 10px 20px; - color: #C9D3E7; - border: 1px solid #2c2f36; + color: var(--light-color); + border: 1px solid var(--secondary-color); box-shadow: 0 9px 18px 0 rgba(0,0,0,0.25); box-sizing: border-box; border-radius: 3px; @@ -108,17 +128,17 @@ a:hover, .btn-hover-red:hover { } .card { - background: #101010; + background: var(--dark-color); padding: 10px 20px; - color: #C9D3E7; - border: 1px solid #2c2f36; - box-shadow: 0 5px 18px 0 #ce1b28; + color: var(--light-color); + border: 1px solid var(--secondary-color); + box-shadow: 0 5px 18px 0 var(--primary-color); box-sizing: border-box; margin-bottom: 20px; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{ - background-color: #ce1b28 !important; + background-color: var(--primary-color) !important; } .form-control { @@ -134,28 +154,40 @@ a:hover, .btn-hover-red:hover { } ul.list-group a.list-group-item { - background: #2c2f36; - color: #C9D3E7; + background: var(--secondary-color); + color: var(--light-color); box-shadow: 0 9px 18px 0 rgba(0,0,0,0.25); box-sizing: border-box; } .alert-success { - color: #ce1b28 !important; - background-color: #101010 !important; - border: 1px solid #2c2f36 !important; + color: var(--primary-color) !important; + background-color: var(--dark-color) !important; + border: 1px solid var(--secondary-color) !important; box-sizing: border-box; border-radius: 3px; box-shadow: 0 9px 18px 0 rgba(0,0,0,0.25) !important; } -.alert-info, .alert-danger{ - color: #101010 !important; - background-color: #ce1b28 !important; +.alert-info, .alert-danger { + color: var(--dark-color) !important; + background-color: var(--primary-color) !important; opacity: 0.8; filter: alpha(opacity=30); - border: 1px solid #2c2f36 !important; + border: 1px solid var(--secondary-color) !important; box-sizing: border-box; border-radius: 3px; box-shadow: 0 9px 18px 0 rgba(0,0,0,0.25) !important; } + + +/* Setup.html */ + +.line { + background-color: #bbbbbb !important; +} + +.active .bs-stepper-circle { + background-color: var(--light-color) !important; + color: var(--dark-color) !important; +} \ No newline at end of file diff --git a/src/FlaskRTBCTF/templates/account.html b/src/FlaskRTBCTF/templates/account.html index e4b374f..3d9c717 100644 --- a/src/FlaskRTBCTF/templates/account.html +++ b/src/FlaskRTBCTF/templates/account.html @@ -4,9 +4,9 @@ {% block content %}
-

Username:

{{ current_user.username }} +

Username:

{{ current_user.username }}
-

Email:

+

Email:

{{ current_user.email }}
diff --git a/src/FlaskRTBCTF/templates/home.html b/src/FlaskRTBCTF/templates/home.html index 855f9d7..477a161 100644 --- a/src/FlaskRTBCTF/templates/home.html +++ b/src/FlaskRTBCTF/templates/home.html @@ -4,9 +4,14 @@
-

Welcome to {{ organization['ctfname'] }}

+

Welcome to {{ settings.ctf_name }}

{% if current_user.is_authenticated %} -

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

+

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

{% else %}

You need to login first.

{% endif %} @@ -18,12 +23,18 @@

Welcome to {{ organization['ctfname'] }}

Rules


    -
  • Running time: {{ RunningTime['from'].strftime("%Y-%m-%d %I:%M %p") }} to {{ RunningTime['to'].strftime("%Y-%m-%d %I:%M %p") }} (All times in {{ RunningTime['TimeZone'] }})
  • +
  • Running time: + {{ RunningTime['from'].strftime("%Y-%m-%d %I:%M %p") }} + to + {{ RunningTime['to'].strftime("%Y-%m-%d %I:%M %p") }} + (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)
  • +
  • Automated vulnerability scanners will get you nowhere (we know 'cause we made the box)
  • Attacking this CTF infrastructure website is unnecessary. There are no hints. This website is for the sole purpose of registration, hash submission and management.
  • -
  • We have tested the machine multiple, I repeat multiple times so if you are receiving a status code of 404 or 301. Figure it out yourself!
  • +
  • We have tested the machine multiple, I repeat multiple times so if you are receiving a status code of 404 or 301. + Figure it out yourself!
  • Have fun! :)
diff --git a/src/FlaskRTBCTF/templates/layout.html b/src/FlaskRTBCTF/templates/layout.html index 6e468fb..0a79e9a 100644 --- a/src/FlaskRTBCTF/templates/layout.html +++ b/src/FlaskRTBCTF/templates/layout.html @@ -8,24 +8,26 @@ - + - + - + {% if title %} - {{ organization['ctfname'] }} - {{ title }} + {{ settings.ctf_name }} - {{ title }} {% else %} - {{ organization['ctfname'] }} + {{ settings.ctf_name }} {% endif %}