From f07e323c8f3e68f02b13aa6f25181ed920e14c39 Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Sun, 19 Apr 2020 19:37:33 +0530 Subject: [PATCH] merge gssoc20-dev into master (#53) * update to conform with flake8/PEP8 | #47 * Update README.md * Create CODE_OF_CONDUCT.md (#51) * 1. psf/black formatting with travis checks, 2. adjust docs, create CONTRIBUTING.md, 3. helper fns for creating admin pass and secret key by itself on run create admin pass and secret key by itself on run * Update README.md Co-authored-by: Aman-Codes <54680709+Aman-Codes@users.noreply.github.com> Co-authored-by: Ankur Chattopadhyay --- .travis.yml | 4 +- CODE_OF_CONDUCT.md | 71 +++++++++++++++ CONTRIBUTING.md | 83 +++++++++++++++++ README.md | 154 +++++++++++++------------------- app.json | 2 +- setup.cfg | 2 +- src/FlaskRTBCTF/__init__.py | 9 +- src/FlaskRTBCTF/admin/views.py | 9 +- src/FlaskRTBCTF/config.py | 44 ++++----- src/FlaskRTBCTF/ctf/forms.py | 22 ++--- src/FlaskRTBCTF/ctf/routes.py | 103 ++++++++++++--------- src/FlaskRTBCTF/helpers.py | 20 +++++ src/FlaskRTBCTF/main/routes.py | 17 ++-- src/FlaskRTBCTF/models.py | 42 ++++----- src/FlaskRTBCTF/users/forms.py | 75 +++++++--------- src/FlaskRTBCTF/users/routes.py | 123 ++++++++++++------------- src/FlaskRTBCTF/users/utils.py | 12 +-- src/create_db.py | 47 +++++----- src/requirements.txt | 2 + src/run.py | 4 +- 20 files changed, 495 insertions(+), 350 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 src/FlaskRTBCTF/helpers.py diff --git a/.travis.yml b/.travis.yml index 5fbd182..f265a73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,7 @@ install: - "pip install -r src/requirements.txt" - "python src/create_db.py" +before_script: + - black . --check script: - - pytest --flake8 + - flake8 . --count --max-line-length=88 --show-source --statistics diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2ec4f6e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,71 @@ +# Code of Conduct + +## Our Pledge + +As contributors and maintainers of the RTB-CTF-Framework project, and in the interest +of fostering an open and welcoming community, we pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at our Slack channel. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c274d21 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ + +# Contributing to RTB-CTF-Framework + +

+ + GitHub contributors + + + GitHub issues by-label + +

+ +

+ + GitHub issues by-label + + + GitHub issues by-label + + + GitHub issues by-label + +

+ +## This project makes use of the following Flask libraries + +* 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, + +## Style Guide + +Keeping to a consistent code style throughout the project makes it easier to contribute and collaborate. Please stick to the guidelines in PEP8, [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) and the Google Style Guide unless there’s a very good reason not to. + +## Contact + +##### 👨 Project Owner + +- Eshaan Bansal ([github](https://github.com/eshaan7),[linkedin](https://www.linkedin.com/in/eshaan7/)) + +##### 👬 Mentors + +- Sombuddha Chakravarty ([github](https://github.com/sammy1997),[linkedin](https://www.linkedin.com/in/sombuddha-chakravarty-9482b5131/)) + +Feel free to ask your queries!! 🙌 + +##### Slack Channel + +- [#proj_root-the-box-ctf-framework](https://app.slack.com/client/TRN1H1V43/CUC71PDD2) + +## Where to start ? + +See: [Issues](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues) and the following To-do list. Or just ping one of the mentors with new ideas. + +> Note: All PRs within the GSSoC'20 period will be merged in the `gssoc20-dev` branch. + +## To-do + +- [ ] Ideas for additional logging techniques to prevent flag sharing, cheating and such. (Issue: [#7](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/7)) +- [ ] Support for *n* number of boxes (accordions? seperate route?). (Issue: [#17](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/17)) +- [ ] Rating system: Average Box rating - input, calculate, output. (Issue: [#14](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/14)) +- [ ] Dark theme for `admin control` panel. (Issue: [#16](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/16)) +- [ ] Testing Password reset functionality, the mail-server setup, etc. +- [ ] More info on `home.html` +- [ ] Need to implement `account.html` +- [ ] Support for more hashes per box (not a priority) + +
+ +- [x] Freeze Scoreboard automatically past running time specified (Issue: [#3](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/3)) +- [x] Adding a `Deploy to Heroku` button. (Issue: [#15](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/15)) +- [x] Adding CI, Linting, Formatting specs. (Issue: [#18](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/18)) +- [x] db relationship between User and Score Tables (priority | issue: #5) +- [x] isAdmin column in User table and Admin views (priority) +- [x] Notifications +- [x] Use Flask Blueprints +- [x] Finalize black theme? +- [x] Error messages not appearing in `/submit` +- [x] Implement `machine.html` to server a page where one can download/serve machines \ No newline at end of file diff --git a/README.md b/README.md index 76f1fd9..2f767c0 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,24 @@ # RootTheBox CTF Framework -

- - Language grade: Python +

+ + Rawsec's CyberSecurity Inventory - + +

+

+ Build Status -

- -

- - Rawsec's CyberSecurity Inventory + + + Code style: black

-

- -

- -A lightweight, easy to deploy CTF framework(in Flask) for HackTheBox style machines. +A lightweight, easy to deploy CTF framework (in Flask) for HackTheBox style machines. The main purpose of this project is to serve as a scoring engine and CTF manager. @@ -27,10 +26,19 @@ The main purpose of this project is to serve as a scoring engine and CTF manager A live demo of the app is available at: . - You can login and mess around as 2 users: `admin:admin` and `test:test`(i.e. username:password combinations) + You can login and mess around as 2 users: `admin:admin` and `test:test` (i.e. username:password combinations) ## Features +##### For CTF hosters +* A page to show relevant details about the machine such as name, IP, OS, points and difficulty level. +* 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, +* Easily deployable on Heroku. + ##### For Developers & Contributors * Flask-blueprints for modularity and clean codebase, * Flask-admin for Admin views and easy realtime management, @@ -38,13 +46,35 @@ The main purpose of this project is to serve as a scoring engine and CTF manager * Flask-wtf for forms, * Flask-mail for mail service. -##### For CTF hosters -* A page to show relevant details about the machine such as name, IP, OS, points and difficulty level. -* 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 hashes: user and root), -* Real time scoreboard tracking, -* Easily deployable on Heroku. +## Deployment + +### Heroku + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +or do it manually, + +1. Create your heroku app using `heroku` cli tool. + + Follow the official guide by Heroku: https://devcenter.heroku.com/articles/getting-started-with-python#prepare-the-app + +2. Provision Database add-on. + + Add the following add on to your new app: https://elements.heroku.com/addons/heroku-postgresql + +3. Creating database instance. In your heroku app directory, + + ```bash + $ heroku run bash + [heroku]$ python create_db.py + ``` +4. Your app should be live now. You can run `heroku open` to open it in browser. + +### Docker + +```bash +$ docker-compose up +``` ## How To Use @@ -78,36 +108,13 @@ $ cd src/ [venv]$ python run.py ``` -### Deployment using Heroku +### Configuration For Your CTF -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) - -or do it manually, - -1. Create your heroku app using `heroku` cli tool. - - Follow the official guide by Heroku: https://devcenter.heroku.com/articles/getting-started-with-python#prepare-the-app - -2. Provision Database add-on. - - Add the following add on to your new app: https://elements.heroku.com/addons/heroku-postgresql - -3. Creating database instance. In your heroku app directory, - - ```bash - $ heroku run bash - [heroku]$ python create_db.py - ``` -4. Your app should be live now. You can run `heroku open` to open it in browser. - - -## For Your CTF - -Using this as simple as anything. +Using this as simple as anything. 1. Just configure your CTF settings in [`config.py`](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/blob/master/src/FlaskRTBCTF/config.py). -2. DO NOT FORGET to change admin credentials from [`create_db.py`](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/blob/master/src/create_db.py) +2. When you run [`create_db.py`](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/blob/master/src/create_db.py), a strong and random 16 char password for the **admin** user is created and set in the environment variable `ADMIN_PASS`. On Heroku, you can reveal this password from your application's dashboard settings. 3. See database instance creation steps under How To Use. @@ -117,7 +124,7 @@ Bonus: You can manage the database CRUD operations from admin views GUI as well ## Contributing -

+

GitHub contributors @@ -126,61 +133,20 @@ Bonus: You can manage the database CRUD operations from admin views GUI as well

-

- - GitHub issues by-label - - - GitHub issues by-label - - - GitHub issues by-label - -

- -Keeping to a consistent code style throughout the project makes it easier to contribute and collaborate. Please stick to the guidelines in PEP8 and the Google Style Guide unless there’s a very good reason not to. -Please see: [Issues](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues) and the following To-do list. - -> Note: All PRs within the GSSoC'20 period will be merged in the `gssoc20-dev` branch. ##### 👨 Project Owner -- Eshaan Bansal ([github](https://github.com/eshaan7),[linkedin](https://www.linkedin.com/in/eshaan7/)) +- Eshaan Bansal ([github](https://github.com/eshaan7), [linkedin](https://www.linkedin.com/in/eshaan7/)) ##### 👬 Mentors -- Sombuddha Chakravarty ([github](https://github.com/sammy1997),[linkedin](https://www.linkedin.com/in/sombuddha-chakravarty-9482b5131/)) +- Sombuddha Chakravarty ([github](https://github.com/sammy1997), [linkedin](https://www.linkedin.com/in/sombuddha-chakravarty-9482b5131/)) -Feel free to ask your queries!! 🙌 - -##### Slack Channel +##### Slack Channel for GSSoC 2020 - [#proj_root-the-box-ctf-framework](https://app.slack.com/client/TRN1H1V43/CUC71PDD2) -## To-do - -- [ ] Ideas for additional logging techniques to prevent flag sharing, cheating and such. (Issue: [#7](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/7)) -- [ ] Support for *n* number of boxes (accordions? seperate route?). (Issue: [#17](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/17)) -- [ ] Rating system: Average Box rating - input, calculate, output. (Issue: [#14](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/14)) -- [ ] Dark theme for `admin control` panel. (Issue: [#16](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/16)) -- [ ] Testing Password reset functionality, the mail-server setup, etc. -- [ ] More info on `home.html` -- [ ] Support for more hashes per box (not a priority) -- [ ] Need to implement `account.html` (not a priority) - -
- -- [x] Freeze Scoreboard automatically past running time specified (Issue: [#3](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/3)) -- [x] Adding a `Deploy to Heroku` button. (Issue: [#15](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/15)) -- [x] Adding CI, Linting, Formatting specs. (Issue: [#18](https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework/issues/18)) -- [x] db relationship between User and Score Tables (priority | issue: #5) -- [x] isAdmin column in User table and Admin views (priority) -- [x] Notifications -- [x] Use Flask Blueprints -- [x] Finalize black theme? -- [x] Error messages not appearing in `/submit` -- [x] Implement `machine.html` to server a page where one can download/serve machines - +For further guidelines, Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) ## Screenshots diff --git a/app.json b/app.json index 72a5e87..fad7aa1 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "name": "RootTheBox CTF Framework", - "description": "A lightweight, easy to deploy CTF framework(in Flask) for HackTheBox style machines.", + "description": "A lightweight, easy to deploy CTF framework (in Flask) for HackTheBox style machines.", "repository": "https://github.com/abs0lut3pwn4g3/RTB-CTF-Framework", "addons": [ { diff --git a/setup.cfg b/setup.cfg index 7586879..aaf258b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ -# content of setup.cfg +# content of setup.cfg (deprecated atm) [tool:pytest] flake8-ignore = W191 diff --git a/src/FlaskRTBCTF/__init__.py b/src/FlaskRTBCTF/__init__.py index 23e773a..c97b393 100644 --- a/src/FlaskRTBCTF/__init__.py +++ b/src/FlaskRTBCTF/__init__.py @@ -11,8 +11,8 @@ bcrypt = Bcrypt() login_manager = LoginManager() admin_manager = Admin() -login_manager.login_view = 'users.login' -login_manager.login_message_category = 'info' +login_manager.login_view = "users.login" +login_manager.login_message_category = "info" mail = Mail() @@ -27,6 +27,7 @@ def create_app(config_class=Config): # 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)) @@ -38,13 +39,15 @@ def create_app(config_class=Config): mail.init_app(app) from flask_sslify import SSLify + # only trigger SSLify if the app is running on Heroku - if 'DYNO' in os.environ: + if "DYNO" in os.environ: _ = SSLify(app) 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) diff --git a/src/FlaskRTBCTF/admin/views.py b/src/FlaskRTBCTF/admin/views.py index 2d72f9c..36cf1e6 100644 --- a/src/FlaskRTBCTF/admin/views.py +++ b/src/FlaskRTBCTF/admin/views.py @@ -1,4 +1,4 @@ -''' Admin Model Views ''' +""" Admin Model Views. """ from flask import abort from flask_login import current_user @@ -7,7 +7,7 @@ class MyModelView(ModelView): - column_exclude_list = ('password',) + column_exclude_list = ("password",) def is_accessible(self): if not current_user.is_authenticated or not current_user.isAdmin: @@ -18,9 +18,8 @@ def is_accessible(self): return False def _handle_view(self, name, **kwargs): - """ - Override builtin _handle_view in order to redirect users when a view is - not accessible. + """ Override builtin _handle_view in order to redirect users when a + view is not accessible. """ if not self.is_accessible(): if current_user.is_authenticated: diff --git a/src/FlaskRTBCTF/config.py b/src/FlaskRTBCTF/config.py index 70a78e3..5c05241 100644 --- a/src/FlaskRTBCTF/config.py +++ b/src/FlaskRTBCTF/config.py @@ -2,30 +2,28 @@ from datetime import datetime import pytz +from .helpers import handle_secret_key -''' Flask related Configurations - Note: DO NOT FORGET TO CHANGE 'SECRET_KEY' ! ''' +# Flask related Configurations +# Note: DO NOT FORGET TO CHANGE 'SECRET_KEY' ! class Config: - SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') \ - or 'sqlite:///site.db' + 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 - MAIL_SERVER = 'smtp.googlemail.com' + 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 ''' + 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 = { @@ -33,28 +31,22 @@ class Config: "name": "Abs0lut3Pwn4g3", "website": { "url": "https://Abs0lut3Pwn4g3.github.io/", - "name": "Official Abs0lut3Pwn4g3 Website" - }, - "website_2": { - "url": "https://twitter.com/abs0lut3pwn4g3", - "name": "Twitter" + "name": "Official Abs0lut3Pwn4g3 Website", }, - "website_3": { - "url": "https://github.com/abs0lut3pwn4g3", - "name": "Github" - } + "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(2030, 7, 8, 0, 00, 00, 0, pytz.utc), - "TimeZone": "UTC" -} # We do not recommend changing the Timezone. + "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 # We recommend to leave it on. It is more than just errors ;) - -# NOTE: CHANGE DEFAULT ADMIN CREDENTIALS in create_db.py !!! +LOGGING = True diff --git a/src/FlaskRTBCTF/ctf/forms.py b/src/FlaskRTBCTF/ctf/forms.py index 704b4ab..0dc20e2 100644 --- a/src/FlaskRTBCTF/ctf/forms.py +++ b/src/FlaskRTBCTF/ctf/forms.py @@ -4,20 +4,14 @@ class UserHashForm(FlaskForm): - userHash = StringField('User hash', - validators=[ - DataRequired(), - Length(min=32, max=32) - ] - ) - submit = SubmitField('Submit') + userHash = StringField( + "User hash", validators=[DataRequired(), Length(min=32, max=32)] + ) + submit = SubmitField("Submit") class RootHashForm(FlaskForm): - rootHash = StringField('Root hash', - validators=[ - DataRequired(), - Length(min=32, max=32) - ] - ) - submit = SubmitField('Submit') + rootHash = StringField( + "Root hash", validators=[DataRequired(), Length(min=32, max=32)] + ) + submit = SubmitField("Submit") diff --git a/src/FlaskRTBCTF/ctf/routes.py b/src/FlaskRTBCTF/ctf/routes.py index 96a66f4..c009128 100644 --- a/src/FlaskRTBCTF/ctf/routes.py +++ b/src/FlaskRTBCTF/ctf/routes.py @@ -1,4 +1,4 @@ -''' views / routes ''' +""" views / routes. """ from datetime import datetime @@ -15,10 +15,10 @@ from FlaskRTBCTF.models import Logs -ctf = Blueprint('ctf', __name__) +ctf = Blueprint("ctf", __name__) -''' Scoreboard ''' +# Scoreboard @ctf.route("/scoreboard") @@ -27,16 +27,16 @@ 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 - }) + userNameScoreList.append( + {"username": User.query.get(score.user_id).username, "score": score.points} + ) - return render_template('scoreboard.html', scores=userNameScoreList, - organization=organization) + return render_template( + "scoreboard.html", scores=userNameScoreList, organization=organization + ) -''' Machine Info ''' +# Machine Info @ctf.route("/machine") @@ -53,16 +53,21 @@ def machine(): 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( + "machine.html", + userHashForm=userHashForm, + rootHashForm=rootHashForm, + organization=organization, + box=box, + current=current_date_time, + end=end_date_time, + ) -''' Hash Submission Management ''' +# Hash Submission Management -@ctf.route("/validateRootHash", methods=['POST']) +@ctf.route("/validateRootHash", methods=["POST"]) @login_required def validateRootHash(): box = Machine.query.filter(Machine.ip == "127.0.0.1").first() @@ -85,25 +90,33 @@ def validateRootHash(): log = Logs.query.get(current_user.id) log.rootSubmissionIP = request.access_route[0] log.rootSubmissionTime = datetime.utcnow() - log.rootOwnTime = str( - log.rootSubmissionTime - log.machineVisitTime - ) + 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) + 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']) + 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() @@ -126,19 +139,27 @@ def validateUserHash(): log = Logs.query.get(current_user.id) log.userSubmissionIP = request.access_route[0] log.userSubmissionTime = datetime.utcnow() - log.userOwnTime = str( - log.userSubmissionTime - log.machineVisitTime - ) + 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) + 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) + return render_template( + "machine.html", + userHashForm=userHashForm, + rootHashForm=rootHashForm, + organization=organization, + box=box, + current=current_date_time, + end=end_date_time, + ) diff --git a/src/FlaskRTBCTF/helpers.py b/src/FlaskRTBCTF/helpers.py new file mode 100644 index 0000000..35a01a4 --- /dev/null +++ b/src/FlaskRTBCTF/helpers.py @@ -0,0 +1,20 @@ +""" Helper functions """ + +import os +import secrets + + +def handle_secret_key(): + sk = os.environ.get("SECRET_KEY", None) + if not sk: + sk = secrets.token_hex(16) + os.environ["SECRET_KEY"] = sk + return sk + + +def handle_admin_pass(): + passwd = os.environ.get("ADMIN_PASS", None) + if not passwd: + passwd = secrets.token_hex(16) + os.environ["ADMIN_PASS"] = passwd + return passwd diff --git a/src/FlaskRTBCTF/main/routes.py b/src/FlaskRTBCTF/main/routes.py index ebf7811..eb46acf 100644 --- a/src/FlaskRTBCTF/main/routes.py +++ b/src/FlaskRTBCTF/main/routes.py @@ -2,20 +2,25 @@ from FlaskRTBCTF.config import organization, RunningTime from FlaskRTBCTF.models import Notification -main = Blueprint('main', __name__) +main = Blueprint("main", __name__) -''' Index page ''' +""" Index page """ @main.route("/") @main.route("/home") def home(): - return render_template('home.html', organization=organization, - RunningTime=RunningTime) + return render_template( + "home.html", organization=organization, RunningTime=RunningTime + ) @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", + organization=organization, + title="Notifications", + notifs=notifs, + ) diff --git a/src/FlaskRTBCTF/models.py b/src/FlaskRTBCTF/models.py index dea8ed6..c81e444 100644 --- a/src/FlaskRTBCTF/models.py +++ b/src/FlaskRTBCTF/models.py @@ -1,4 +1,4 @@ -''' Models ''' +""" Models. """ from datetime import datetime @@ -15,7 +15,7 @@ def load_user(user_id): return User.query.get(int(user_id)) -''' Machine Table ''' +# Machine Table class Machine(db.Model): @@ -29,10 +29,10 @@ class Machine(db.Model): 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) + score = db.relationship("Score", backref="machine", lazy=True) -''' User Table ''' +# User Table class User(db.Model, UserMixin): @@ -41,21 +41,19 @@ class User(db.Model, UserMixin): email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) isAdmin = db.Column(db.Boolean, default=False) - score = db.relationship('Score', backref='user', lazy=True, - uselist=False) + score = db.relationship("Score", backref="user", lazy=True, uselist=False) if LOGGING: - logs = db.relationship('Logs', backref='user', lazy=True, - uselist=False) + 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') + 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']) + s = Serializer(current_app.config["SECRET_KEY"]) try: - user_id = s.loads(token)['user_id'] + user_id = s.loads(token)["user_id"] except Exception: return None return User.query.get(user_id) @@ -64,24 +62,24 @@ def __repr__(self): return f"User('{self.username}', '{self.email}'))" -''' Score Table ''' +# Score Table class Score(db.Model): - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, - primary_key=True) + 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) + 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 ''' +# Notifications Table class Notification(db.Model): @@ -94,13 +92,15 @@ def __repr__(self): return f"Notif('{self.title}', '{self.body}')" -''' Logging Table ''' +# Logging Table if LOGGING: + class Logs(db.Model): - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), - nullable=False, primary_key=True) + 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) diff --git a/src/FlaskRTBCTF/users/forms.py b/src/FlaskRTBCTF/users/forms.py index 864d7e5..d6fa371 100644 --- a/src/FlaskRTBCTF/users/forms.py +++ b/src/FlaskRTBCTF/users/forms.py @@ -2,61 +2,56 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField, BooleanField -from wtforms.validators import DataRequired, Length, Email, \ - EqualTo, ValidationError +from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from flask_login import current_user class RegistrationForm(FlaskForm): - username = StringField('Username', - validators=[DataRequired(), Length(min=4, max=20)]) - email = StringField('Email', - validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - confirm_password = PasswordField('Confirm Password', - validators=[ - DataRequired(), - EqualTo('password') - ] - ) - submit = SubmitField('Sign Up') + username = StringField( + "Username", validators=[DataRequired(), Length(min=4, max=20)] + ) + email = StringField("Email", validators=[DataRequired(), Email()]) + password = PasswordField("Password", validators=[DataRequired()]) + confirm_password = PasswordField( + "Confirm Password", validators=[DataRequired(), EqualTo("password")] + ) + submit = SubmitField("Sign Up") def validate_username(self, username): user = User.query.filter_by(username=username.data).first() if user: raise ValidationError( - 'That username is taken. Please choose a different one.' + "That username is taken. Please choose a different one." ) def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user: - raise ValidationError( - 'That email is taken. Please choose a different one.' - ) + raise ValidationError("That email is taken. Please choose a different one.") class LoginForm(FlaskForm): - username = StringField('Username', - validators=[DataRequired(), Length(min=4, max=20)]) - password = PasswordField('Password', validators=[DataRequired()]) - remember = BooleanField('Remember Me') - submit = SubmitField('Login') + username = StringField( + "Username", validators=[DataRequired(), Length(min=4, max=20)] + ) + password = PasswordField("Password", validators=[DataRequired()]) + remember = BooleanField("Remember Me") + submit = SubmitField("Login") class UpdateAccountForm(FlaskForm): - username = StringField('Username', - validators=[DataRequired(), Length(min=4, max=20)]) - email = StringField('Email', - validators=[DataRequired(), Email()]) - submit = SubmitField('Update') + username = StringField( + "Username", validators=[DataRequired(), Length(min=4, max=20)] + ) + email = StringField("Email", validators=[DataRequired(), Email()]) + submit = SubmitField("Update") def validate_username(self, username): if username.data != current_user.username: user = User.query.filter_by(username=username.data).first() if user: raise ValidationError( - 'That username is taken. Please choose a different one.' + "That username is taken. Please choose a different one." ) def validate_email(self, email): @@ -64,29 +59,25 @@ def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user: raise ValidationError( - 'That email is taken. Please choose a different one.' + "That email is taken. Please choose a different one." ) class RequestResetForm(FlaskForm): - email = StringField('Email', - validators=[DataRequired(), Email()]) - submit = SubmitField('Request Password Reset') + email = StringField("Email", validators=[DataRequired(), Email()]) + submit = SubmitField("Request Password Reset") def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user is None: raise ValidationError( - 'There is no account with that email. You must register first.' + "There is no account with that email. You must register first." ) class ResetPasswordForm(FlaskForm): - password = PasswordField('Password', validators=[DataRequired()]) - confirm_password = PasswordField('Confirm Password', - validators=[ - DataRequired(), - EqualTo('password') - ] - ) - submit = SubmitField('Reset Password') + password = PasswordField("Password", validators=[DataRequired()]) + confirm_password = PasswordField( + "Confirm Password", validators=[DataRequired(), EqualTo("password")] + ) + submit = SubmitField("Reset Password") diff --git a/src/FlaskRTBCTF/users/routes.py b/src/FlaskRTBCTF/users/routes.py index bf26c20..cd87022 100644 --- a/src/FlaskRTBCTF/users/routes.py +++ b/src/FlaskRTBCTF/users/routes.py @@ -1,8 +1,12 @@ from datetime import datetime import pytz -from FlaskRTBCTF.users.forms import (RegistrationForm, LoginForm, - RequestResetForm, ResetPasswordForm) +from FlaskRTBCTF.users.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 @@ -15,59 +19,58 @@ from FlaskRTBCTF.models import Logs -users = Blueprint('users', __name__) +users = Blueprint("users", __name__) -''' User management ''' +""" User management """ -@users.route("/register", methods=['GET', 'POST']) +@users.route("/register", methods=["GET", "POST"]) def register(): if current_user.is_authenticated: - flash('Already Authenticated', 'info') - return redirect(url_for('main.home')) + 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(): - hashed_password = bcrypt.generate_password_hash(form.password.data) \ - .decode('utf-8') - user = User( - username=form.username.data, email=form.email.data, - password=hashed_password + hashed_password = bcrypt.generate_password_hash(form.password.data).decode( + "utf-8" ) - score = Score( - user=user, userHash=False, - rootHash=False, points=0, machine=box + 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 + 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')) + 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, organization=organization + ) -@users.route("/login", methods=['GET', 'POST']) +@users.route("/login", methods=["GET", "POST"]) def login(): if current_user.is_authenticated: - flash('Already Authenticated', 'info') - return redirect(url_for('main.home')) + flash("Already Authenticated", "info") + return redirect(url_for("main.home")) form = LoginForm() if form.validate_on_submit(): @@ -75,16 +78,13 @@ def login(): pw_chk = bcrypt.check_password_hash(user.password, form.password.data) if user and pw_chk: login_user(user, remember=form.remember.data, force=True) - next_page = request.args.get('next') - return redirect(next_page) if next_page else \ - redirect(url_for('main.home')) + next_page = request.args.get("next") + 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) + flash("Login Unsuccessful. Please check username and password.", "danger") + return render_template( + "login.html", title="Login", form=form, organization=organization + ) @users.route("/logout") @@ -92,55 +92,56 @@ def login(): def logout(): logout_user() flash("Logged out.", "info") - return redirect(url_for('main.home')) + return redirect(url_for("main.home")) @users.route("/account") @login_required def account(): - return render_template('account.html', title='Account', - organization=organization) + return render_template("account.html", title="Account", organization=organization) -@users.route("/reset_password", methods=['GET', 'POST']) +@users.route("/reset_password", methods=["GET", "POST"]) def reset_request(): if current_user.is_authenticated: - return redirect(url_for('main.home')) + 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' + "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) + return redirect(url_for("users.login")) + return render_template( + "reset_request.html", + title="Reset Password", + form=form, + organization=organization, + ) -@users.route("/reset_password/", methods=['GET', 'POST']) +@users.route("/reset_password/", methods=["GET", "POST"]) def reset_token(token): if current_user.is_authenticated: - return redirect(url_for('main.home')) + return redirect(url_for("main.home")) user = User.verify_reset_token(token) if user is None: - flash('That is an invalid or expired token', 'warning') - return redirect(url_for('users.reset_request')) + flash("That is an invalid or expired token", "warning") + return redirect(url_for("users.reset_request")) form = ResetPasswordForm() if form.validate_on_submit(): - hashed_password = bcrypt.generate_password_hash(form.password.data) \ - .decode('utf-8') + hashed_password = bcrypt.generate_password_hash(form.password.data).decode( + "utf-8" + ) user.password = hashed_password db.session.commit() - flash( - 'Your password has been updated! You are now able to log in', - 'success' - ) - return redirect(url_for('users.login')) + 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, organization=organization + ) diff --git a/src/FlaskRTBCTF/users/utils.py b/src/FlaskRTBCTF/users/utils.py index f3ae0c9..b9969ae 100644 --- a/src/FlaskRTBCTF/users/utils.py +++ b/src/FlaskRTBCTF/users/utils.py @@ -1,4 +1,4 @@ -''' Utility Functions ''' +""" Utility Functions. """ from flask import url_for from flask_mail import Message @@ -7,13 +7,13 @@ 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: + 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/create_db.py b/src/create_db.py index 992c285..3d35944 100644 --- a/src/create_db.py +++ b/src/create_db.py @@ -1,9 +1,11 @@ -from datetime import datetime 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 @@ -18,49 +20,42 @@ box = Machine( name="My Awesome Pwnable Box", - user_hash='A' * 32, - root_hash='B' * 32, + user_hash="A" * 32, + root_hash="B" * 32, user_points=10, root_points=20, os="Linux", ip="127.0.0.1", - hardness="You tell" + hardness="You tell", ) db.session.add(box) - # NOTE: CHANGE DEFAULT CREDENTIALS !!! + passwd = handle_admin_pass() admin_user = User( - username='admin', - email='admin@admin.com', - password=bcrypt.generate_password_hash('admin').decode('utf-8'), - isAdmin=True + username="admin", + email="admin@admin.com", + 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 ) - 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!" + body="The CTF is live now. Please read rules!", ) db.session.add(notif) - test_user = User( - username='test', - email='test@test.com', - password=bcrypt.generate_password_hash('test').decode('utf-8') - ) - test_score = Score(user=test_user, userHash=False, - rootHash=False, points=0, machine=box) - db.session.add(test_user) - db.session.add(test_score) - if LOGGING: - admin_log = Logs(user=admin_user, accountCreationTime=default_time, - visitedMachine=True, machineVisitTime=default_time) + admin_log = Logs( + user=admin_user, + accountCreationTime=default_time, + visitedMachine=True, + machineVisitTime=default_time, + ) db.session.add(admin_log) - test_log = Logs(user=test_user, accountCreationTime=default_time) - db.session.add(test_log) db.session.commit() diff --git a/src/requirements.txt b/src/requirements.txt index 7449900..ecdd4c1 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ bcrypt==3.1.7 +black==19.10b0 blinker==1.4 cffi==1.14.0 click==7.1.1 @@ -10,6 +11,7 @@ Flask-Mail==0.9.1 Flask-SQLAlchemy==2.4.1 Flask-SSLify==0.1.5 Flask-WTF==0.14.3 +flake8==3.7.9 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.1 diff --git a/src/run.py b/src/run.py index 4149120..ff3783a 100644 --- a/src/run.py +++ b/src/run.py @@ -2,5 +2,5 @@ app = create_app() -if __name__ == '__main__': - app.run(debug=app.config.get('DEBUG', False)) +if __name__ == "__main__": + app.run(debug=app.config.get("DEBUG", False))