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))