diff --git a/.travis.yml b/.travis.yml index f265a73..5adb050 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,9 +19,9 @@ before_install: install: - "pip install -r src/requirements.txt" - - "python src/create_db.py" + - "python src/create_db.test.py" before_script: - black . --check script: - - flake8 . --count --max-line-length=88 --show-source --statistics + - flake8 . --count --max-line-length=88 --show-source --statistics \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13be93d..75108c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ $ black . ``` ```bash -$ flake8 src/ ---max-line-length=88 --show-source --statistics +$ flake8 src/ --max-line-length=88 --show-source --statistics ``` if flake8 shows any errors or warnings, please fix the changes in a new commit and squash all the commits into one before submitting the PR. diff --git a/Dockerfile b/Dockerfile index 225088d..5ede1e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,28 @@ -FROM python:3 +FROM python:3.8.2-alpine3.11 +MAINTAINER eshaan7bansal@gmail.com + +# Env +RUN export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@postgres:${DB_PORT}/${DB_NAME}" \ + && export REDIS_URL="redis://redis:6379/0" + +# update and install packages +RUN apk update \ + && apk add libpq postgresql-dev \ + && apk add build-base \ + && apk add --no-cache git libssl1.1 g++ make libffi-dev + +# Add a new low-privileged user +RUN adduser --shell /sbin/login www-data -DH + +# Install RTB-CTF-Framework WORKDIR /usr/src/app COPY src ./ -RUN pip install --no-cache-dir -r requirements.txt -EXPOSE 8080 -RUN chown -R 1001:1001 . -USER 1001 +RUN pip install --no-cache-dir -r requirements.txt \ + && chown -R www-data ./ + +USER www-data + +EXPOSE 8000 RUN chmod +x /usr/src/app/docker-entrypoint.sh ENTRYPOINT [ "/usr/src/app/docker-entrypoint.sh" ] \ No newline at end of file diff --git a/README.md b/README.md index 5845f7f..c286edd 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Build Status - + Code style: black @@ -30,50 +30,34 @@ The main purpose of this project is to serve as a scoring engine and CTF manager ## Features -##### For CTF hosters -* A page to show relevant details about the machine such as name, IP, OS, points and difficulty level. +* Machines listing name, IP, OS, points and difficulty level. +* Challenges listing with title, description, URL, points. +* Totally configurable settings such running time, organization details, CTF name. * Automatic strong password for administrator * Well implemented controls for administrators providing features such as issuing notifications, database CRUD operations, full fledged logging, * Simple User Registration/login process, account management, Forgot password functionalities, * Flag submission (currently 2 flags: user and root), * Real time scoreboard tracking, +* Efficient caching so it's fast * Easily deployable on Heroku. -##### For Developers & Contributors -* Flask-blueprints for modularity and clean codebase, -* Flask-admin for Admin views and easy realtime management, -* Flask-SQLAlchemy for SQL models, -* Flask-login for session handling, -* Flask-wtf for responsive forms, -* Flask-mail for mail service, -* Flask-bcrypt for password hashing and security, - ## Build locally Please see [INSTALLATION.md](INSTALLATION.md). -## Host Your Own CTF In 5 minutes with Heroku - -Using this is as simple as anything. - -1. Fork the `master` branch and clone your fork, - -```bash -$ git clone https://github.com//RTB-CTF-Framework -$ cd RTB-CTF-Framework/ -``` - -2. Configure your CTF settings (such as name, running time) in [`config.py`](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/blob/master/src/FlaskRTBCTF/config.py). +## Host Your Own CTF in a minute with Heroku -3. In the `app.json`, change the `repository` key's value to match your fork's URL. +1. Sign up on [Heroku](https://heroku.com), if you haven't already and click on the below "Deploy to Heroku" button. -4. Push these changes to the remote of your fork. + [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) -5. Visit your Fork's GitHub URL in the browser and click on the following **Deploy to Heroku** button. +2. Give your application an awesome name and _optionally_ specify mail environment variables. > Note: A psuedo-random password for the **admin** user would be created and set in the config variable `ADMIN_PASS`. On Heroku, you can reveal this password from your application's dashboard settings. Same for the Flask application's `SECRET_KEY`. - [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) +3. Open your newly deployed application in the browser, you'll be redirected to login as the `admin` user and do so. + +4. Finally, you'll want to `/setup` the CTF Settings and, #### Yay! Now you have a customized instance of the RTB-CTF-Framework live on Heroku. 🎉 @@ -104,11 +88,6 @@ $ cd RTB-CTF-Framework/ For further guidelines, Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) -## Screenshots - -> Why look at static pictures, when you can use a demo ? Visit: . - - - - +## Live Demo +** Live Demo: (login with `admin:admin`) diff --git a/app.json b/app.json index a71e2a3..ba2207e 100644 --- a/app.json +++ b/app.json @@ -5,6 +5,9 @@ "addons": [ { "plan": "heroku-postgresql" + }, + { + "plan": "heroku-redis" } ], "buildpacks": [ @@ -20,6 +23,17 @@ "ADMIN_PASS": { "description": "Administrator password", "generator": "secret" + }, + "ADMIN_EMAIL": { + "description": "Administrator email" + }, + "MAIL_USER": { + "description": "Username for mail service", + "required": false + }, + "MAIL_PASS": { + "description": "Password for mail service", + "required": false } }, "scripts": { diff --git a/docker-compose.yml b/docker-compose.yml index 64139fa..09d3152 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,55 @@ version: "3" services: - rtbd: + rtbctf: build: . + container_name: rtb_gunicorn + restart: unless-stopped ports: - - 80:8080 - restart: unless-stopped \ No newline at end of file + - "80:8000" + expose: + - 8000 + environment: + - DEBUG=False + - SECRET_KEY=changeme + - DB_USER=eshaan + - DB_PASSWORD=eshaan + - DB_NAME=rtbctf + - DB_PORT=5432 + - WORKERS=8 + - ADMIN_PASS=admin + depends_on: + - postgres + - redis + + postgres: + image: library/postgres:12.1-alpine + container_name: rtb_postgres + restart: unless-stopped + expose: + - "5432" + environment: + - POSTGRES_USER=eshaan + - POSTGRES_PASSWORD=eshaan + - POSTGRES_DB=rtbctf + + redis: + image: redis:6.0-rc4-alpine + container_name: rtb_redis + restart: unless-stopped + expose: + - "6379" + + + # nginx: + # image: library/nginx:1.16.1-alpine + # container_name: rtb_nginx + # restart: unless-stopped + # hostname: nginx + # volumes: + # - ./rtb_nginx_http:/etc/nginx/conf.d/default.conf + # ports: + # - "80:80" + # - "443:443" + # depends_on: + # - rtbctf diff --git a/rtb_nginx_http b/rtb_nginx_http new file mode 100644 index 0000000..223a789 --- /dev/null +++ b/rtb_nginx_http @@ -0,0 +1,16 @@ +# the upstream component nginx needs to connect to +upstream flask { + server rtbctf:8000 fail_timeout=30s; +} + + +server { + listen 80; + + server_name rtbctf.com; + + location / { + proxy_pass http://localhost:8000/ + } + +} diff --git a/screenshots/home_ss.png b/screenshots/home_ss.png deleted file mode 100644 index 5687268..0000000 Binary files a/screenshots/home_ss.png and /dev/null differ diff --git a/screenshots/machine_ss.png b/screenshots/machine_ss.png deleted file mode 100644 index d933c62..0000000 Binary files a/screenshots/machine_ss.png and /dev/null differ diff --git a/screenshots/scoreboard_ss.png b/screenshots/scoreboard_ss.png deleted file mode 100644 index 1b8de1c..0000000 Binary files a/screenshots/scoreboard_ss.png and /dev/null differ 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/__init__.py b/src/FlaskRTBCTF/__init__.py index c97b393..413a938 100644 --- a/src/FlaskRTBCTF/__init__.py +++ b/src/FlaskRTBCTF/__init__.py @@ -1,55 +1,42 @@ -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.utils import ( + db, + bcrypt, + cache, + login_manager, + admin_manager, + mail, + inject_app_context, +) +from FlaskRTBCTF.users.routes import users +from FlaskRTBCTF.ctf.routes import ctf +from FlaskRTBCTF.main.routes import main + + +_blueprints = (users, ctf, main) + +_extensions = (db, bcrypt, cache, login_manager, admin_manager, mail) def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(Config) + app.context_processor(inject_app_context) + + for _ext in _extensions: + _ext.init_app(app) - db.init_app(app) - 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 + 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/admin/views.py b/src/FlaskRTBCTF/admin/views.py index 36cf1e6..86704c7 100644 --- a/src/FlaskRTBCTF/admin/views.py +++ b/src/FlaskRTBCTF/admin/views.py @@ -1,13 +1,21 @@ """ Admin Model Views. """ -from flask import abort +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 from flask_admin.contrib.sqla import ModelView +from ..utils.cache import cache +from ..utils.helpers import clear_points_cache -class MyModelView(ModelView): - column_exclude_list = ("password",) +class BaseModelView(ModelView): + export_types = ("csv", "json") + can_export = True + form_base_class = SecureForm + column_display_pk = True # optional, but I like to see the IDs in the list + form_excluded_columns = ("created_on", "updated_on") def is_accessible(self): if not current_user.is_authenticated or not current_user.isAdmin: @@ -25,3 +33,95 @@ def _handle_view(self, name, **kwargs): if current_user.is_authenticated: # permission denied abort(403) + + +class UserAdminView(BaseModelView): + can_view_details = True + column_exclude_list = ("password",) + form_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") + + @staticmethod + def after_model_delete(model): + cache.delete(key="scoreboard") + return + + +class MachineAdminView(BaseModelView): + can_view_details = True + column_searchable_list = ("name", "ip") + + @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 ChallengeAdminView(BaseModelView): + can_view_details = True + column_searchable_list = ("title", "url") + form_choices = { + "difficulty": [ + ("easy", "Easy"), + ("medium", "Medium"), + ("hard", "Hard"), + ("insane", "Insane"), + ] + } + + @staticmethod + def after_model_change(form, model, is_created): + cache.delete(key="challenges") + return + + @staticmethod + def after_model_delete(model): + cache.delete(key="challenges") + return + + +class UserChallengeAdminView(BaseModelView): + column_filters = ("completed",) + column_list = ("user_id", "challenge_id", "completed") + + @staticmethod + def after_model_change(form, model, is_created): + if form.completed != model.completed: + clear_points_cache(userId=model.user_id, mode="c") + return + + @staticmethod + def after_model_delete(model): + clear_points_cache(userId=model.user_id, mode="c") + return + + +class UserMachineAdminView(BaseModelView): + column_filters = ("owned_user", "owned_root") + column_list = ("user_id", "machine_id", "owned_user", "owned_root") + + @staticmethod + def after_model_change(form, model, is_created): + if (form.owned_user != model.owned_user) or ( + form.owned_root != model.owned_root + ): + clear_points_cache(userId=model.user_id, mode="m") + return + + @staticmethod + def after_model_delete(model): + clear_points_cache(userId=model.user_id, mode="m") + return + + +class NotificationAdminView(BaseModelView): + column_searchable_list = ("title",) diff --git a/src/FlaskRTBCTF/config.py b/src/FlaskRTBCTF/config.py index 5c05241..34cdfcb 100644 --- a/src/FlaskRTBCTF/config.py +++ b/src/FlaskRTBCTF/config.py @@ -1,52 +1,23 @@ 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' ! 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')` 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 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 diff --git a/src/FlaskRTBCTF/ctf/forms.py b/src/FlaskRTBCTF/ctf/forms.py index 0dc20e2..64ba097 100644 --- a/src/FlaskRTBCTF/ctf/forms.py +++ b/src/FlaskRTBCTF/ctf/forms.py @@ -1,17 +1,82 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired, Length +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, Challenge -class UserHashForm(FlaskForm): - userHash = StringField( - "User hash", validators=[DataRequired(), Length(min=32, max=32)] +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()] ) + difficulty = 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( + "User Hash", validators=[DataRequired(), Length(min=32, max=32)] + ) + 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.root_hash == str(root_hash.data): + pass + else: + raise ValidationError("Incorrect Root Hash.") + + +class ChallengeFlagForm(FlaskForm): + challenge_id = HiddenField("Challenge ID", validators=[DataRequired()]) + flag = StringField("Flag", validators=[DataRequired(), Length(min=4)]) + submit_flag = SubmitField("Submit") + + def validate_flag(self, flag): + ch = Challenge.query.get(int(self.challenge_id.data)) + if not ch: + raise ValidationError("No challenge with that ID exists") + elif ch.flag != str(flag.data): + raise ValidationError("Incorrect flag.") diff --git a/src/FlaskRTBCTF/ctf/models.py b/src/FlaskRTBCTF/ctf/models.py new file mode 100644 index 0000000..0634557 --- /dev/null +++ b/src/FlaskRTBCTF/ctf/models.py @@ -0,0 +1,157 @@ +from sqlalchemy.orm import joinedload + +from FlaskRTBCTF.utils.models import db, TimeMixin, ReprMixin +from FlaskRTBCTF.utils.cache import cache + + +# Machine Table +class Machine(TimeMixin, ReprMixin, db.Model): + __tablename__ = "machine" + __repr_fields__ = ( + "name", + "os", + ) + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), nullable=False, unique=True) + 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, nullable=False, default="linux") + ip = db.Column(db.String(64), nullable=False) + difficulty = db.Column(db.String, nullable=False, default="Easy") + + @staticmethod + @cache.cached(timeout=3600 * 3, key_prefix="machines") + def get_all(): + return Machine.query.all() + + +# UserMachine: N to N relationship +class UserMachine(TimeMixin, db.Model): + __tablename__ = "user_machine" + user_id = db.Column( + db.Integer, + db.ForeignKey("user.id"), + nullable=False, + primary_key=True, + index=True, + ) + machine_id = db.Column( + db.Integer, + db.ForeignKey("machine.id"), + nullable=False, + primary_key=True, + index=True, + ) + owned_user = db.Column(db.Boolean, nullable=False, default=False) + owned_root = db.Column(db.Boolean, nullable=False, default=False) + + @classmethod + @cache.memoize(timeout=3600 * 3) + def completed_machines(cls, user_id): + completed = dict() + _ids1 = ( + cls.query.with_entities(cls.machine_id) + .filter_by(user_id=user_id, owned_user=True) + .all() + ) + _ids2 = ( + cls.query.with_entities(cls.machine_id) + .filter_by(user_id=user_id, owned_root=True) + .all() + ) + completed["user"] = [int(id[0]) for id in _ids1] + completed["root"] = [int(id[0]) for id in _ids2] + return completed + + +# Tag Model +class Tag(ReprMixin, db.Model): + __tablename__ = "tag" + __repr_fields__ = ("label",) + id = db.Column(db.Integer, primary_key=True) + label = db.Column(db.String(32), nullable=False) + color = db.Column(db.String(16), nullable=False) + + +# Tags table +tags = db.Table( + "tags", + db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True), + db.Column( + "challenge_id", db.Integer, db.ForeignKey("challenge.id"), primary_key=True + ), +) + + +# Challenges Model +class Challenge(TimeMixin, ReprMixin, db.Model): + __tablename__ = "challenge" + __repr_fields__ = ("title", "category") + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(64), nullable=False, unique=True) + description = db.Column(db.TEXT, nullable=True) + flag = db.Column(db.TEXT, nullable=False) + points = db.Column(db.Integer, nullable=False, default=0) + url = db.Column(db.TEXT, nullable=True) + difficulty = db.Column(db.String, nullable=True) + + category_id = db.Column(db.Integer, db.ForeignKey("category.id"), nullable=False) + category = db.relationship("Category", backref=db.backref("challenges", lazy=True)) + + tags = db.relationship( + "Tag", + secondary=tags, + lazy="subquery", + backref=db.backref("challenges", lazy="noload"), + ) + + +# UserChallenge: N to N relationship +class UserChallenge(TimeMixin, db.Model): + __tablename__ = "user_challenge" + user_id = db.Column( + db.Integer, + db.ForeignKey("user.id"), + nullable=False, + primary_key=True, + index=True, + ) + challenge_id = db.Column( + db.Integer, + db.ForeignKey("challenge.id"), + nullable=False, + primary_key=True, + index=True, + ) + completed = db.Column(db.Boolean, nullable=False, default=False) + + @classmethod + @cache.memoize(timeout=3600 * 3) + def completed_challenges(cls, user_id): + _ids = ( + cls.query.with_entities(cls.challenge_id) + .filter_by(user_id=user_id, completed=True) + .all() + ) + _ids = [int(id[0]) for id in _ids] + return _ids + + +# Category Model +class Category(ReprMixin, db.Model): + __tablename__ = "category" + __repr_fields__ = ("name",) + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(32), nullable=False) + + @staticmethod + @cache.cached(timeout=3600 * 3, key_prefix="challenges") + def get_challenges(): + categories = ( + Category.query.options(joinedload("challenges")) + .filter(Category.challenges) + .all() + ) + return categories diff --git a/src/FlaskRTBCTF/ctf/routes.py b/src/FlaskRTBCTF/ctf/routes.py index c009128..24581d5 100644 --- a/src/FlaskRTBCTF/ctf/routes.py +++ b/src/FlaskRTBCTF/ctf/routes.py @@ -2,164 +2,203 @@ 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.users.models import User, Logs +from FlaskRTBCTF.utils import ( + db, + cache, + is_past_running_time, + admin_only, + clear_points_cache, +) +from .models import Machine, Category, UserChallenge, UserMachine +from .forms import UserHashForm, RootHashForm, MachineForm, ChallengeFlagForm ctf = Blueprint("ctf", __name__) # Scoreboard - - @ctf.route("/scoreboard") -@login_required +@cache.cached(timeout=120, key_prefix="scoreboard") def scoreboard(): - scores = Score.query.order_by(Score.points.desc(), Score.timestamp).all() - userNameScoreList = [] - for score in scores: - userNameScoreList.append( - {"username": User.query.get(score.user_id).username, "score": score.points} - ) + usersScores = User.query.all() + usersScores.sort(reverse=True, key=lambda user: user.points(id=user.id)) + + return render_template("scoreboard.html", scores=usersScores) - return render_template( - "scoreboard.html", scores=userNameScoreList, organization=organization - ) +# Machines Info +@ctf.route("/machines", methods=["GET", "POST"]) +@login_required +def machines(): + userHashForm = UserHashForm() + rootHashForm = RootHashForm() -# Machine Info + is_finished = is_past_running_time() + if request.method == "GET": + boxes = Machine.get_all() + completed = UserMachine.completed_machines(user_id=current_user.id) -@ctf.route("/machine") -@login_required -def machine(): - box = Machine.query.filter(Machine.ip == "127.0.0.1").first() - if LOGGING: 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, - ) + return render_template( + "machines.html", + boxes=boxes, + completed=completed, + is_finished=is_finished, + userHashForm=userHashForm, + rootHashForm=rootHashForm, + ) -# Hash Submission Management + else: + if is_finished: + flash("Sorry! CTF has ended.", "danger") + return redirect(url_for("ctf.machines")) + machine_id = int(userHashForm.machine_id.data or rootHashForm.machine_id.data) -@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: + user_machine = UserMachine.query.filter_by( + user_id=current_user.id, machine_id=machine_id + ).first() + + if not user_machine: + user_machine = UserMachine(user_id=current_user.id, machine_id=machine_id) + db.session.add(user_machine) + db.session.commit() + + if userHashForm.submit_user_hash.data and userHashForm.validate_on_submit(): + if user_machine.owned_user: + flash("You already own User.", "success") + return redirect(url_for("ctf.machines")) + + user_machine.owned_user = True + 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() + clear_points_cache(userId=current_user.id, mode="m") + flash("Congrats! correct user hash.", "success") + + elif rootHashForm.submit_root_hash.data and rootHashForm.validate_on_submit(): + if user_machine.owned_root: 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") + return redirect(url_for("ctf.machines")) + + user_machine.owned_root = True + 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() + clear_points_cache(userId=current_user.id, mode="m") + flash("Congrats! correct root hash.", "success") + else: - flash("Sorry! Wrong system hash", "danger") + errors = userHashForm.user_hash.errors or rootHashForm.root_hash.errors + for e in errors: + flash(e, "danger") + + return redirect(url_for("ctf.machines")) + + +# New machine form +@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( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - box=box, - organization=organization, - current=current_date_time, - end=end_date_time, + "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) + + +# Edit machine form +@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( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - box=box, - organization=organization, - current=current_date_time, - end=end_date_time, + "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) -@ctf.route("/validateUserHash", methods=["POST"]) +# Challenges Info +@ctf.route("/challenges", methods=["GET", "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") +def challenges(): + form = ChallengeFlagForm() + + if request.method == "GET": + categories = Category.get_challenges() + completed = UserChallenge.completed_challenges(user_id=current_user.id) + return render_template( - "machine.html", - userHashForm=userHashForm, - rootHashForm=rootHashForm, - organization=organization, - box=box, - current=current_date_time, - end=end_date_time, + "challenges.html", + categories=categories, + completed=completed, + form=form, + is_finished=is_past_running_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") + + elif form.validate_on_submit(): + ch_id = int(form.challenge_id.data) + user_ch = UserChallenge.query.filter_by( + user_id=current_user.id, challenge_id=ch_id + ).first() + if not user_ch: + user_ch = UserChallenge(user_id=current_user.id, challenge_id=ch_id) + db.session.add(user_ch) + elif user_ch.completed: + flash( + "You've already submitted the flag for this challenge.", "success" + ) + return redirect(request.url) + + user_ch.completed = True + db.session.commit() + clear_points_cache(userId=current_user.id, mode="c") + flash("Congrats! correct flag.", "success") + + else: + err = ", ".join(*form.errors.values()) + flash(err, "danger") + + return redirect(url_for("ctf.challenges")) 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/main/forms.py b/src/FlaskRTBCTF/main/forms.py new file mode 100644 index 0000000..42d024d --- /dev/null +++ b/src/FlaskRTBCTF/main/forms.py @@ -0,0 +1,93 @@ +from flask import url_for, redirect, flash +from flask_wtf import FlaskForm +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.utils import db, admin_only, cache +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") + from_time = TimeField("Start Time") + to_date = DateField("End Date", format="%Y-%m-%d") + to_time = TimeField("End Time") + + submit = SubmitField("Save & Next") + + @admin_only + def setup(self): + if self.is_submitted(): + 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 + + db.session.commit() + + cache.delete(key="past_running_time") + cache.delete(key="settings") + return redirect(url_for("main.setup", step=3)) + + except SQLAlchemyError: + db.session.rollback() + flash("Transaction failed. Please try again.", "danger") + return redirect(url_for("main.setup"), step=2) + + else: + flash("Form validation failed. Please try again.", "danger") + 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( + URLField("URL", validators=[DataRequired(), URL()]), + 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(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.", + "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/models.py b/src/FlaskRTBCTF/main/models.py new file mode 100644 index 0000000..7dd15f5 --- /dev/null +++ b/src/FlaskRTBCTF/main/models.py @@ -0,0 +1,68 @@ +""" Main Application Models. """ + + +from datetime import datetime, date, time, timedelta + +from sqlalchemy.ext.hybrid import hybrid_property + +from FlaskRTBCTF.utils.models import db, TimeMixin, ReprMixin +from FlaskRTBCTF.utils.cache import cache + + +# Notifications Model +class Notification(TimeMixin, ReprMixin, db.Model): + __tablename__ = "notification" + __repr_fields__ = ("title",) + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(64), nullable=False) + body = db.Column(db.TEXT(), nullable=False) + + +# Settings Model +class Settings(ReprMixin, db.Model): + __tablename__ = "settings" + __repr_fields__ = ("ctf_name", "organization_name") + 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()) + + @staticmethod + @cache.cached(timeout=3600 * 6, key_prefix="settings") + def get_settings(): + return Settings.query.get(1) + + @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) + + +# Websites Model +class Website(ReprMixin, db.Model): + __tablename__ = "website" + __repr_fields__ = ("id", "name", "url") + id = db.Column(db.Integer, primary_key=True) + url = db.Column( + db.TEXT(), + nullable=False, + default="https://github.com/Abs0lut3Pwn4g3/RTB-CTF-Framework", + ) + name = db.Column(db.TEXT(), nullable=False, default="Source code on GitHub") + + @staticmethod + @cache.cached(timeout=3600 * 6, key_prefix="websites") + def get_websites(): + return Website.query.all() diff --git a/src/FlaskRTBCTF/main/routes.py b/src/FlaskRTBCTF/main/routes.py index eb46acf..dd3dbc1 100644 --- a/src/FlaskRTBCTF/main/routes.py +++ b/src/FlaskRTBCTF/main/routes.py @@ -1,26 +1,76 @@ -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 .models import Notification, Settings, Website +from .forms import SettingsForm, WebsiteForm +from FlaskRTBCTF.utils import admin_only main = Blueprint("main", __name__) + +""" Before app request processor """ + + +@main.before_app_request +def needs_setup(): + settings = Settings.get_settings() + if settings.dummy: + if request.endpoint not in ("main.setup", "users.login", "static"): + flash("Please setup the CTF, before accessing any routes.", "info") + return redirect(url_for("main.setup")) + else: + return + + """ Index page """ @main.route("/") @main.route("/home") def home(): - return render_template( - "home.html", organization=organization, RunningTime=RunningTime - ) + settings = Settings.get_settings() + 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, - ) + notifs = Notification.query.order_by(Notification.updated_on.desc()).all() + + return render_template("notifications.html", title="Notifications", notifs=notifs) + + +@main.route("/setup", methods=["GET", "POST"]) +@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 and settings_form.validate_on_submit(): + return settings_form.setup() + elif step == 3: + return website_form.setup() + else: + return redirect(url_for("main.setup")) 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/static/main.css b/src/FlaskRTBCTF/static/main.css index c386809..45a0400 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-steel { - background-color: #101010; +.bg-dark { + background-color: var(--dark-color) !important; +} + +.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,59 @@ 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; +} + +/* challenges.html */ + +.tags-container { + display: flex; + justify-items: center; + align-items: center; +} + +.tags-container .tag { + margin: 4px 4px; + border-radius: 2px; + padding: 1px 2.5px; +} + +.card-top { + display: flex !important; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/FlaskRTBCTF/static/main.js b/src/FlaskRTBCTF/static/main.js new file mode 100644 index 0000000..1b3acbc --- /dev/null +++ b/src/FlaskRTBCTF/static/main.js @@ -0,0 +1,33 @@ +$(document).ready( function() { + + // Tooltip + $('[data-toggle="tooltip"]').tooltip( + { + 'delay': { show: 50, hide: 50 } + } + ); + + // machine user hash submission modal + $('#m-form-user').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); + }) + + // machine root hash submission modal + $('#m-form-root').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-root').val(boxId); + }) + + // challenge flag submission + $('#c-form').on('show.bs.modal', function (event) { + const button = $(event.relatedTarget); + const challengeId = button.data('chid'); // Extract info from data-* attributes + $(this).find('#challenge-id').val(challengeId); + }) + +}); \ 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/challenges.html b/src/FlaskRTBCTF/templates/challenges.html new file mode 100644 index 0000000..319185b --- /dev/null +++ b/src/FlaskRTBCTF/templates/challenges.html @@ -0,0 +1,92 @@ +{% extends "layout.html" %} +{% from "forms.html" import challenge_flag_submission_form %} +{% from "macros.html" import render_modal %} +{% block content %} + +{% if current_user.isAdmin %} +Add new +{% endif %} + + +{% for category in categories %} + +

{{ category.name }}

+ + {% with challenges = category.challenges %} + {% for chal in challenges %} +
+ +
+ + + + {{ chal['title'] }} + + + {% if chal['id'] in completed %} + + {% elif is_finished %} + + {% else %} + + + + + {% endif %} + + + + + +
+ + +
+
+ +
#{{ chal['id'] }}
+
+  - {{ chal['updated_on'].strftime("%Y-%m-%d %I:%M %p %Z") }} +
+
+ + {% for tag in chal['tags'] %} + {{ tag['label'] }} + + {% endfor %} + +
+
+

{{ chal['description'] |e }}

+
+
+
+ + {{ chal['url'] }} +
+
{{ chal['points'] }} points
+
{{ chal['difficulty'] }}
+
+
+ + +
+
+ +
+ + {% endfor %} + {% endwith %} + +{% endfor %} + + + +{{ render_modal( + modalId="c-form", title="Submit Flag", form_macro1=challenge_flag_submission_form, form1=form + ) +}} + +{% endblock content %} \ No newline at end of file diff --git a/src/FlaskRTBCTF/templates/forms.html b/src/FlaskRTBCTF/templates/forms.html new file mode 100644 index 0000000..2743131 --- /dev/null +++ b/src/FlaskRTBCTF/templates/forms.html @@ -0,0 +1,191 @@ +{% 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) %} + +
+ {{ form.csrf_token() }} + {{ form.machine_id(id="machine-id-user") }} +
+ {{ 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") }} +
+
+ +{% endmacro %} + + + +{% macro root_hash_form_macro(form) %} + +
+ {{ form.csrf_token() }} + {{ form.machine_id(id="machine-id-root") }} +
+ {{ 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") }} +
+
+ +{% endmacro %} + + + +{% macro challenge_flag_submission_form(form) %} + +
+ {{ form.csrf_token() }} + {{ form.challenge_id(id="challenge-id") }} +
+ {{ form.flag.label(class="form-control-label") }} + {{ form.flag( + class="ml-3 mr-3 is-invalid", + size=28, + minlength=4, + autocomplete="off" + ) + }} + {{ form.submit_flag(class="btn btn-sm btn-dark btn-hover-red") }} +
+
+ +{% endmacro %} diff --git a/src/FlaskRTBCTF/templates/home.html b/src/FlaskRTBCTF/templates/home.html index 855f9d7..6cb3356 100644 --- a/src/FlaskRTBCTF/templates/home.html +++ b/src/FlaskRTBCTF/templates/home.html @@ -4,9 +4,12 @@
-

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 +21,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..488bc29 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 %}