├── .codecov.yml ├── .dockerignore ├── .flaskenv ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CTFd ├── __init__.py ├── admin │ ├── __init__.py │ ├── challenges.py │ ├── notifications.py │ ├── pages.py │ ├── scoreboard.py │ ├── statistics.py │ ├── submissions.py │ ├── teams.py │ └── users.py ├── api │ ├── __init__.py │ └── v1 │ │ ├── __init__.py │ │ ├── awards.py │ │ ├── challenges.py │ │ ├── config.py │ │ ├── files.py │ │ ├── flags.py │ │ ├── hints.py │ │ ├── notifications.py │ │ ├── pages.py │ │ ├── scoreboard.py │ │ ├── statistics │ │ ├── __init__.py │ │ ├── challenges.py │ │ ├── submissions.py │ │ ├── teams.py │ │ └── users.py │ │ ├── submissions.py │ │ ├── tags.py │ │ ├── teams.py │ │ ├── tokens.py │ │ ├── unlocks.py │ │ └── users.py ├── auth.py ├── cache │ └── __init__.py ├── challenges.py ├── config.py ├── errors.py ├── events │ └── __init__.py ├── exceptions │ └── __init__.py ├── logs │ └── .gitkeep ├── models │ └── __init__.py ├── plugins │ ├── __init__.py │ ├── challenges │ │ ├── __init__.py │ │ └── assets │ │ │ ├── create.html │ │ │ ├── create.js │ │ │ ├── update.html │ │ │ ├── update.js │ │ │ ├── view.html │ │ │ └── view.js │ ├── dynamic_challenges │ │ ├── .gitignore │ │ ├── README.md │ │ ├── __init__.py │ │ ├── assets │ │ │ ├── create.html │ │ │ ├── create.js │ │ │ ├── update.html │ │ │ ├── update.js │ │ │ ├── view.html │ │ │ └── view.js │ │ └── function.png │ └── flags │ │ ├── __init__.py │ │ └── assets │ │ ├── regex │ │ ├── create.html │ │ └── edit.html │ │ └── static │ │ ├── create.html │ │ └── edit.html ├── schemas │ ├── __init__.py │ ├── awards.py │ ├── challenges.py │ ├── config.py │ ├── files.py │ ├── flags.py │ ├── hints.py │ ├── notifications.py │ ├── pages.py │ ├── submissions.py │ ├── tags.py │ ├── teams.py │ ├── tokens.py │ ├── unlocks.py │ └── users.py ├── scoreboard.py ├── teams.py ├── themes │ ├── admin │ │ ├── assets │ │ │ ├── css │ │ │ │ ├── admin.scss │ │ │ │ ├── challenge-board.scss │ │ │ │ ├── codemirror.scss │ │ │ │ └── includes │ │ │ │ │ └── sticky-footer.css │ │ │ └── js │ │ │ │ ├── challenges │ │ │ │ ├── challenge.js │ │ │ │ ├── files.js │ │ │ │ ├── flags.js │ │ │ │ ├── hints.js │ │ │ │ ├── new.js │ │ │ │ ├── requirements.js │ │ │ │ └── tags.js │ │ │ │ ├── pages │ │ │ │ ├── challenge.js │ │ │ │ ├── challenges.js │ │ │ │ ├── configs.js │ │ │ │ ├── editor.js │ │ │ │ ├── events.js │ │ │ │ ├── main.js │ │ │ │ ├── notifications.js │ │ │ │ ├── pages.js │ │ │ │ ├── reset.js │ │ │ │ ├── scoreboard.js │ │ │ │ ├── statistics.js │ │ │ │ ├── style.js │ │ │ │ ├── submissions.js │ │ │ │ ├── team.js │ │ │ │ ├── teams.js │ │ │ │ ├── user.js │ │ │ │ └── users.js │ │ │ │ ├── styles.js │ │ │ │ └── templates │ │ │ │ └── admin-flags-table.njk │ │ ├── static │ │ │ ├── css │ │ │ │ ├── admin.dev.css │ │ │ │ ├── admin.min.css │ │ │ │ ├── challenge-board.dev.css │ │ │ │ ├── challenge-board.min.css │ │ │ │ ├── codemirror.dev.css │ │ │ │ └── codemirror.min.css │ │ │ └── js │ │ │ │ ├── core.dev.js │ │ │ │ ├── core.min.js │ │ │ │ ├── graphs.dev.js │ │ │ │ ├── graphs.min.js │ │ │ │ ├── helpers.dev.js │ │ │ │ ├── helpers.min.js │ │ │ │ ├── pages │ │ │ │ ├── challenge.dev.js │ │ │ │ ├── challenge.min.js │ │ │ │ ├── configs.dev.js │ │ │ │ ├── configs.min.js │ │ │ │ ├── editor.dev.js │ │ │ │ ├── editor.min.js │ │ │ │ ├── main.dev.js │ │ │ │ ├── main.min.js │ │ │ │ ├── notifications.dev.js │ │ │ │ ├── notifications.min.js │ │ │ │ ├── pages.dev.js │ │ │ │ ├── pages.min.js │ │ │ │ ├── reset.dev.js │ │ │ │ ├── reset.min.js │ │ │ │ ├── scoreboard.dev.js │ │ │ │ ├── scoreboard.min.js │ │ │ │ ├── statistics.dev.js │ │ │ │ ├── statistics.min.js │ │ │ │ ├── submissions.dev.js │ │ │ │ ├── submissions.min.js │ │ │ │ ├── team.dev.js │ │ │ │ ├── team.min.js │ │ │ │ ├── teams.dev.js │ │ │ │ ├── teams.min.js │ │ │ │ ├── user.dev.js │ │ │ │ ├── user.min.js │ │ │ │ ├── users.dev.js │ │ │ │ └── users.min.js │ │ │ │ ├── plotly.bundle.dev.js │ │ │ │ ├── plotly.bundle.min.js │ │ │ │ ├── vendor.bundle.dev.js │ │ │ │ └── vendor.bundle.min.js │ │ └── templates │ │ │ ├── base.html │ │ │ ├── challenges │ │ │ ├── challenge.html │ │ │ ├── challenges.html │ │ │ └── new.html │ │ │ ├── config.html │ │ │ ├── configs │ │ │ ├── accounts.html │ │ │ ├── appearance.html │ │ │ ├── backup.html │ │ │ ├── email.html │ │ │ ├── mlc.html │ │ │ ├── settings.html │ │ │ └── time.html │ │ │ ├── editor.html │ │ │ ├── integrations.html │ │ │ ├── modals │ │ │ ├── awards │ │ │ │ └── create.html │ │ │ ├── challenges │ │ │ │ ├── challenges.html │ │ │ │ ├── files.html │ │ │ │ ├── flags.html │ │ │ │ ├── hints.html │ │ │ │ ├── requirements.html │ │ │ │ ├── solves.html │ │ │ │ └── tags.html │ │ │ ├── flags │ │ │ │ ├── create.html │ │ │ │ └── edit.html │ │ │ ├── hints │ │ │ │ └── edit.html │ │ │ ├── mail │ │ │ │ └── send.html │ │ │ ├── teams │ │ │ │ ├── captain.html │ │ │ │ ├── create.html │ │ │ │ └── edit.html │ │ │ └── users │ │ │ │ ├── create.html │ │ │ │ └── edit.html │ │ │ ├── notifications.html │ │ │ ├── page.html │ │ │ ├── pages.html │ │ │ ├── reset.html │ │ │ ├── scoreboard.html │ │ │ ├── statistics.html │ │ │ ├── submissions.html │ │ │ ├── teams │ │ │ ├── new.html │ │ │ ├── team.html │ │ │ └── teams.html │ │ │ └── users │ │ │ ├── new.html │ │ │ ├── user.html │ │ │ └── users.html │ └── core │ │ ├── assets │ │ ├── css │ │ │ ├── challenge-board.scss │ │ │ ├── codemirror.scss │ │ │ ├── core.scss │ │ │ ├── fonts.scss │ │ │ ├── includes │ │ │ │ ├── award-icons.scss │ │ │ │ ├── flag-icons.scss │ │ │ │ ├── jumbotron.css │ │ │ │ └── sticky-footer.css │ │ │ └── main.scss │ │ └── js │ │ │ ├── CTFd.js │ │ │ ├── api.js │ │ │ ├── config.js │ │ │ ├── events.js │ │ │ ├── ezq.js │ │ │ ├── fetch.js │ │ │ ├── graphs.js │ │ │ ├── helpers.js │ │ │ ├── pages │ │ │ ├── challenges.js │ │ │ ├── events.js │ │ │ ├── main.js │ │ │ ├── notifications.js │ │ │ ├── scoreboard.js │ │ │ ├── settings.js │ │ │ ├── setup.js │ │ │ ├── stats.js │ │ │ ├── style.js │ │ │ └── teams │ │ │ │ └── private.js │ │ │ ├── patch.js │ │ │ ├── styles.js │ │ │ ├── times.js │ │ │ └── utils.js │ │ ├── static │ │ ├── css │ │ │ ├── challenge-board.dev.css │ │ │ ├── challenge-board.min.css │ │ │ ├── codemirror.dev.css │ │ │ ├── codemirror.min.css │ │ │ ├── core.dev.css │ │ │ ├── core.min.css │ │ │ ├── fonts.dev.css │ │ │ ├── fonts.min.css │ │ │ ├── main.dev.css │ │ │ └── main.min.css │ │ ├── fonts │ │ │ ├── fa-brands-400.eot │ │ │ ├── fa-brands-400.svg │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.eot │ │ │ ├── fa-regular-400.svg │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.eot │ │ │ ├── fa-solid-900.svg │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff │ │ │ ├── fa-solid-900.woff2 │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── lato-latin-100.woff │ │ │ ├── lato-latin-100.woff2 │ │ │ ├── lato-latin-100italic.woff │ │ │ ├── lato-latin-100italic.woff2 │ │ │ ├── lato-latin-300.woff │ │ │ ├── lato-latin-300.woff2 │ │ │ ├── lato-latin-300italic.woff │ │ │ ├── lato-latin-300italic.woff2 │ │ │ ├── lato-latin-400.woff │ │ │ ├── lato-latin-400.woff2 │ │ │ ├── lato-latin-400italic.woff │ │ │ ├── lato-latin-400italic.woff2 │ │ │ ├── lato-latin-700.woff │ │ │ ├── lato-latin-700.woff2 │ │ │ ├── lato-latin-700italic.woff │ │ │ ├── lato-latin-700italic.woff2 │ │ │ ├── lato-latin-900.woff │ │ │ ├── lato-latin-900.woff2 │ │ │ ├── lato-latin-900italic.woff │ │ │ ├── lato-latin-900italic.woff2 │ │ │ ├── raleway-latin-100.woff │ │ │ ├── raleway-latin-100.woff2 │ │ │ ├── raleway-latin-100italic.woff │ │ │ ├── raleway-latin-100italic.woff2 │ │ │ ├── raleway-latin-200.woff │ │ │ ├── raleway-latin-200.woff2 │ │ │ ├── raleway-latin-200italic.woff │ │ │ ├── raleway-latin-200italic.woff2 │ │ │ ├── raleway-latin-300.woff │ │ │ ├── raleway-latin-300.woff2 │ │ │ ├── raleway-latin-300italic.woff │ │ │ ├── raleway-latin-300italic.woff2 │ │ │ ├── raleway-latin-400.woff │ │ │ ├── raleway-latin-400.woff2 │ │ │ ├── raleway-latin-400italic.woff │ │ │ ├── raleway-latin-400italic.woff2 │ │ │ ├── raleway-latin-500.woff │ │ │ ├── raleway-latin-500.woff2 │ │ │ ├── raleway-latin-500italic.woff │ │ │ ├── raleway-latin-500italic.woff2 │ │ │ ├── raleway-latin-600.woff │ │ │ ├── raleway-latin-600.woff2 │ │ │ ├── raleway-latin-600italic.woff │ │ │ ├── raleway-latin-600italic.woff2 │ │ │ ├── raleway-latin-700.woff │ │ │ ├── raleway-latin-700.woff2 │ │ │ ├── raleway-latin-700italic.woff │ │ │ ├── raleway-latin-700italic.woff2 │ │ │ ├── raleway-latin-800.woff │ │ │ ├── raleway-latin-800.woff2 │ │ │ ├── raleway-latin-800italic.woff │ │ │ ├── raleway-latin-800italic.woff2 │ │ │ ├── raleway-latin-900.woff │ │ │ ├── raleway-latin-900.woff2 │ │ │ ├── raleway-latin-900italic.woff │ │ │ └── raleway-latin-900italic.woff2 │ │ ├── img │ │ │ ├── ctfd.ai │ │ │ ├── ctfd.svg │ │ │ ├── ctfd_transfer.svg │ │ │ ├── favicon.ico │ │ │ ├── logo.png │ │ │ ├── logo_old.png │ │ │ └── scoreboard.png │ │ ├── js │ │ │ ├── core.dev.js │ │ │ ├── core.min.js │ │ │ ├── helpers.dev.js │ │ │ ├── helpers.min.js │ │ │ ├── pages │ │ │ │ ├── challenges.dev.js │ │ │ │ ├── challenges.min.js │ │ │ │ ├── main.dev.js │ │ │ │ ├── main.min.js │ │ │ │ ├── notifications.dev.js │ │ │ │ ├── notifications.min.js │ │ │ │ ├── scoreboard.dev.js │ │ │ │ ├── scoreboard.min.js │ │ │ │ ├── settings.dev.js │ │ │ │ ├── settings.min.js │ │ │ │ ├── setup.dev.js │ │ │ │ ├── setup.min.js │ │ │ │ ├── stats.dev.js │ │ │ │ ├── stats.min.js │ │ │ │ └── teams │ │ │ │ │ ├── private.dev.js │ │ │ │ │ └── private.min.js │ │ │ ├── plotly.bundle.dev.js │ │ │ ├── plotly.bundle.min.js │ │ │ ├── vendor.bundle.dev.js │ │ │ └── vendor.bundle.min.js │ │ └── sounds │ │ │ ├── notification.mp3 │ │ │ └── notification.webm │ │ └── templates │ │ ├── base.html │ │ ├── challenges.html │ │ ├── confirm.html │ │ ├── errors │ │ ├── 403.html │ │ ├── 404.html │ │ ├── 429.html │ │ ├── 500.html │ │ └── 502.html │ │ ├── login.html │ │ ├── notifications.html │ │ ├── page.html │ │ ├── register.html │ │ ├── reset_password.html │ │ ├── scoreboard.html │ │ ├── settings.html │ │ ├── setup.html │ │ ├── teams │ │ ├── join_team.html │ │ ├── new_team.html │ │ ├── private.html │ │ ├── public.html │ │ ├── team_enrollment.html │ │ └── teams.html │ │ └── users │ │ ├── private.html │ │ ├── public.html │ │ └── users.html ├── uploads │ └── .gitkeep ├── users.py ├── utils │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── integrations.py │ │ ├── pages.py │ │ └── visibility.py │ ├── countries │ │ └── __init__.py │ ├── crypto │ │ └── __init__.py │ ├── dates │ │ └── __init__.py │ ├── decorators │ │ ├── __init__.py │ │ ├── modes.py │ │ └── visibility.py │ ├── email │ │ ├── __init__.py │ │ ├── mailgun.py │ │ └── smtp.py │ ├── encoding │ │ └── __init__.py │ ├── events │ │ └── __init__.py │ ├── exports │ │ └── __init__.py │ ├── formatters │ │ └── __init__.py │ ├── helpers │ │ └── __init__.py │ ├── humanize │ │ ├── __init__.py │ │ └── numbers.py │ ├── initialization │ │ └── __init__.py │ ├── logging │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── modes │ │ └── __init__.py │ ├── notifications │ │ └── __init__.py │ ├── plugins │ │ └── __init__.py │ ├── scores │ │ └── __init__.py │ ├── security │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── csrf.py │ │ ├── passwords.py │ │ └── signing.py │ ├── sessions │ │ └── __init__.py │ ├── updates │ │ └── __init__.py │ ├── uploads │ │ ├── __init__.py │ │ └── uploaders.py │ ├── user │ │ └── __init__.py │ └── validators │ │ └── __init__.py └── views.py ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── development.txt ├── docker-compose.yml ├── docker-entrypoint.sh ├── docs ├── Makefile ├── api.rst ├── conf.py ├── configuration.rst ├── contributing.rst ├── deployment.rst ├── index.rst ├── make.bat ├── plugins.rst ├── scoring.rst └── themes.rst ├── export.py ├── import.py ├── manage.py ├── migrations ├── 1_2_0_upgrade_2_0_0.py ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 080d29b15cd3_add_tokens_table.py │ ├── 1093835a1051_add_default_email_templates.py │ ├── 4e4d5a9ea000_add_type_to_awards.py │ ├── 8369118943a1_initial_revision.py │ ├── a03403986a32_add_theme_code_injections_to_configs.py │ ├── b295b033364d_add_ondelete_cascade_to_foreign_keys.py │ └── b5551cd26764_add_captain_column_to_teams.py ├── package.json ├── populate.py ├── prepare.sh ├── requirements.txt ├── scripts └── install_docker.sh ├── serve.py ├── setup.cfg ├── tests ├── __init__.py ├── admin │ ├── __init__.py │ ├── test_challenges.py │ ├── test_config.py │ ├── test_export_csv.py │ ├── test_notifications.py │ ├── test_pages.py │ ├── test_scoreboard.py │ ├── test_statistics.py │ ├── test_submissions.py │ ├── test_teams.py │ ├── test_users.py │ └── test_views.py ├── api │ ├── __init__.py │ ├── test_tokens.py │ └── v1 │ │ ├── __init__.py │ │ ├── teams │ │ ├── __init__.py │ │ ├── test_scoring.py │ │ └── test_team_members.py │ │ ├── test_awards.py │ │ ├── test_challenges.py │ │ ├── test_config.py │ │ ├── test_csrf.py │ │ ├── test_files.py │ │ ├── test_flags.py │ │ ├── test_hints.py │ │ ├── test_notifications.py │ │ ├── test_pages.py │ │ ├── test_scoreboard.py │ │ ├── test_submissions.py │ │ ├── test_tags.py │ │ ├── test_teams.py │ │ ├── test_tokens.py │ │ ├── test_users.py │ │ ├── user │ │ ├── __init__.py │ │ ├── test_admin_access.py │ │ ├── test_challenges.py │ │ └── test_hints.py │ │ └── users │ │ ├── __init__.py │ │ └── test_scoring.py ├── challenges │ ├── __init__.py │ └── test_dynamic.py ├── helpers.py ├── oauth │ ├── __init__.py │ ├── test_redirect.py │ └── test_teams.py ├── teams │ ├── __init__.py │ ├── test_auth.py │ ├── test_challenges.py │ ├── test_hidden_team_scores.py │ ├── test_hints.py │ ├── test_scoreboard.py │ └── test_teams.py ├── test_config.py ├── test_plugin_utils.py ├── test_setup.py ├── test_themes.py ├── test_views.py ├── users │ ├── __init__.py │ ├── test_auth.py │ ├── test_challenges.py │ ├── test_hints.py │ ├── test_profile.py │ ├── test_scoreboard.py │ ├── test_settings.py │ ├── test_setup.py │ ├── test_submissions.py │ └── test_users.py └── utils │ ├── __init__.py │ ├── test_ctftime.py │ ├── test_email.py │ ├── test_encoding.py │ ├── test_events.py │ ├── test_exports.py │ ├── test_formatters.py │ ├── test_humanize.py │ ├── test_passwords.py │ ├── test_plugins.py │ ├── test_ratelimit.py │ ├── test_sessions.py │ ├── test_updates.py │ ├── test_uploaders.py │ └── test_validators.py ├── webpack.config.js ├── wsgi.py └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 1% 6 | threshold: 1 7 | patch: 8 | default: 9 | threshold: 1 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | CTFd/logs/*.log 2 | CTFd/static/uploads 3 | CTFd/uploads 4 | CTFd/*.db 5 | CTFd/uploads/**/* 6 | .ctfd_secret_key 7 | .data 8 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_ENV=development 2 | FLASK_RUN_PORT=4000 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Environment**: 8 | 9 | - CTFd Version/Commit: 10 | - Operating System: 11 | - Web Browser and Version: 12 | 13 | **What happened?** 14 | 15 | **What did you expect to happen?** 16 | 17 | **How to reproduce your issue** 18 | 19 | **Any associated stack traces or error logs** 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | .DS_Store 57 | 58 | *.db 59 | *.log 60 | .idea/ 61 | .vscode/ 62 | CTFd/static/uploads 63 | CTFd/uploads 64 | .data/ 65 | .ctfd_secret_key 66 | .*.swp 67 | 68 | # Vagrant 69 | .vagrant 70 | 71 | # CTFd Exports 72 | *.zip 73 | 74 | # JS 75 | node_modules/ 76 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CTFd/themes/**/vendor/ 2 | *.html 3 | *.njk 4 | *.png 5 | *.svg 6 | *.ico 7 | *.ai 8 | *.svg 9 | *.mp3 10 | *.webm 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | cache: 4 | - pip 5 | - yarn 6 | services: 7 | - mysql 8 | - postgresql 9 | - redis-server 10 | addons: 11 | apt: 12 | sources: 13 | - deadsnakes 14 | packages: 15 | - python3.6 16 | - python3-pip 17 | env: 18 | - TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd' 19 | - TESTING_DATABASE_URL='sqlite://' 20 | - TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd' 21 | python: 22 | - 2.7 23 | - 3.6 24 | before_install: 25 | - sudo rm -f /etc/boto.cfg 26 | - export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE 27 | - export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 28 | - python3.6 -m pip install black==19.3b0 29 | install: 30 | - pip install -r development.txt 31 | - yarn install --non-interactive 32 | - yarn global add prettier@1.17.0 33 | before_script: 34 | - psql -c 'create database ctfd;' -U postgres 35 | script: 36 | - make lint 37 | - make test 38 | after_success: 39 | - codecov 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to CTFd 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Do not open up a GitHub issue if the bug is a security vulnerability in CTFd**. Instead [email the details to us at support@ctfd.io](mailto:support@ctfd.io). 6 | 7 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/CTFd/CTFd/issues). 8 | 9 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/CTFd/CTFd/issues/new). Be sure to fill out the issue template with a **title and clear description**, and as much relevant information as possible (e.g. deployment setup, browser version, etc). 10 | 11 | #### **Did you write a patch that fixes a bug or implements a new feature?** 12 | 13 | * Open a new pull request with the patch. 14 | 15 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | * Ensure all status checks pass. PR's with test failures will not be merged. PR's with insufficient coverage may be merged depending on the situation. 18 | 19 | #### **Did you fix whitespace, format code, or make a purely cosmetic patch?** 20 | 21 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of CTFd will generally not be accepted. -------------------------------------------------------------------------------- /CTFd/admin/notifications.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | 3 | from CTFd.admin import admin 4 | from CTFd.models import Notifications 5 | from CTFd.utils.decorators import admins_only 6 | 7 | 8 | @admin.route("/admin/notifications") 9 | @admins_only 10 | def notifications(): 11 | notifs = Notifications.query.order_by(Notifications.id.desc()).all() 12 | return render_template("admin/notifications.html", notifications=notifs) 13 | -------------------------------------------------------------------------------- /CTFd/admin/pages.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request 2 | 3 | from CTFd.admin import admin 4 | from CTFd.models import Pages 5 | from CTFd.schemas.pages import PageSchema 6 | from CTFd.utils import markdown 7 | from CTFd.utils.decorators import admins_only 8 | 9 | 10 | @admin.route("/admin/pages") 11 | @admins_only 12 | def pages_listing(): 13 | pages = Pages.query.all() 14 | return render_template("admin/pages.html", pages=pages) 15 | 16 | 17 | @admin.route("/admin/pages/new") 18 | @admins_only 19 | def pages_new(): 20 | return render_template("admin/editor.html") 21 | 22 | 23 | @admin.route("/admin/pages/preview", methods=["POST"]) 24 | @admins_only 25 | def pages_preview(): 26 | data = request.form.to_dict() 27 | schema = PageSchema() 28 | page = schema.load(data) 29 | return render_template("page.html", content=markdown(page.data.content)) 30 | 31 | 32 | @admin.route("/admin/pages/") 33 | @admins_only 34 | def pages_detail(page_id): 35 | page = Pages.query.filter_by(id=page_id).first_or_404() 36 | page_op = request.args.get("operation") 37 | 38 | if request.method == "GET" and page_op == "preview": 39 | return render_template("page.html", content=markdown(page.content)) 40 | 41 | if request.method == "GET" and page_op == "create": 42 | return render_template("admin/editor.html") 43 | 44 | return render_template("admin/editor.html", page=page) 45 | -------------------------------------------------------------------------------- /CTFd/admin/scoreboard.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | 3 | from CTFd.admin import admin 4 | from CTFd.scoreboard import get_standings 5 | from CTFd.utils.decorators import admins_only 6 | 7 | 8 | @admin.route("/admin/scoreboard") 9 | @admins_only 10 | def scoreboard_listing(): 11 | standings = get_standings(admin=True) 12 | return render_template("admin/scoreboard.html", standings=standings) 13 | -------------------------------------------------------------------------------- /CTFd/admin/submissions.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request 2 | 3 | from CTFd.admin import admin 4 | from CTFd.models import Challenges, Submissions 5 | from CTFd.utils.decorators import admins_only 6 | from CTFd.utils.modes import get_model 7 | 8 | 9 | @admin.route("/admin/submissions", defaults={"submission_type": None}) 10 | @admin.route("/admin/submissions/") 11 | @admins_only 12 | def submissions_listing(submission_type): 13 | filters = {} 14 | if submission_type: 15 | filters["type"] = submission_type 16 | 17 | curr_page = abs(int(request.args.get("page", 1, type=int))) 18 | results_per_page = 50 19 | page_start = results_per_page * (curr_page - 1) 20 | page_end = results_per_page * (curr_page - 1) + results_per_page 21 | sub_count = Submissions.query.filter_by(**filters).count() 22 | page_count = int(sub_count / results_per_page) + (sub_count % results_per_page > 0) 23 | 24 | Model = get_model() 25 | 26 | submissions = ( 27 | Submissions.query.add_columns( 28 | Submissions.id, 29 | Submissions.type, 30 | Submissions.challenge_id, 31 | Submissions.provided, 32 | Submissions.account_id, 33 | Submissions.date, 34 | Challenges.name.label("challenge_name"), 35 | Model.name.label("team_name"), 36 | ) 37 | .filter_by(**filters) 38 | .join(Challenges) 39 | .join(Model) 40 | .order_by(Submissions.date.desc()) 41 | .slice(page_start, page_end) 42 | .all() 43 | ) 44 | 45 | return render_template( 46 | "admin/submissions.html", 47 | submissions=submissions, 48 | page_count=page_count, 49 | curr_page=curr_page, 50 | type=submission_type, 51 | ) 52 | -------------------------------------------------------------------------------- /CTFd/api/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/api/v1/__init__.py -------------------------------------------------------------------------------- /CTFd/api/v1/awards.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_restplus import Namespace, Resource 3 | 4 | from CTFd.cache import clear_standings 5 | from CTFd.models import Awards, db 6 | from CTFd.schemas.awards import AwardSchema 7 | from CTFd.utils.decorators import admins_only 8 | 9 | awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards") 10 | 11 | 12 | @awards_namespace.route("") 13 | class AwardList(Resource): 14 | @admins_only 15 | def post(self): 16 | req = request.get_json() 17 | schema = AwardSchema() 18 | 19 | response = schema.load(req, session=db.session) 20 | if response.errors: 21 | return {"success": False, "errors": response.errors}, 400 22 | 23 | db.session.add(response.data) 24 | db.session.commit() 25 | 26 | response = schema.dump(response.data) 27 | db.session.close() 28 | 29 | # Delete standings cache because awards can change scores 30 | clear_standings() 31 | 32 | return {"success": True, "data": response.data} 33 | 34 | 35 | @awards_namespace.route("/") 36 | @awards_namespace.param("award_id", "An Award ID") 37 | class Award(Resource): 38 | @admins_only 39 | def get(self, award_id): 40 | award = Awards.query.filter_by(id=award_id).first_or_404() 41 | response = AwardSchema().dump(award) 42 | if response.errors: 43 | return {"success": False, "errors": response.errors}, 400 44 | 45 | return {"success": True, "data": response.data} 46 | 47 | @admins_only 48 | def delete(self, award_id): 49 | award = Awards.query.filter_by(id=award_id).first_or_404() 50 | db.session.delete(award) 51 | db.session.commit() 52 | db.session.close() 53 | 54 | # Delete standings cache because awards can change scores 55 | clear_standings() 56 | 57 | return {"success": True} 58 | -------------------------------------------------------------------------------- /CTFd/api/v1/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Namespace 2 | 3 | statistics_namespace = Namespace( 4 | "statistics", description="Endpoint to retrieve Statistics" 5 | ) 6 | 7 | from CTFd.api.v1.statistics import challenges # noqa: F401 8 | from CTFd.api.v1.statistics import submissions # noqa: F401 9 | from CTFd.api.v1.statistics import teams # noqa: F401 10 | from CTFd.api.v1.statistics import users # noqa: F401 11 | -------------------------------------------------------------------------------- /CTFd/api/v1/statistics/submissions.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource 2 | from sqlalchemy import func 3 | 4 | from CTFd.api.v1.statistics import statistics_namespace 5 | from CTFd.models import Submissions 6 | from CTFd.utils.decorators import admins_only 7 | 8 | 9 | @statistics_namespace.route("/submissions/") 10 | class SubmissionPropertyCounts(Resource): 11 | @admins_only 12 | def get(self, column): 13 | if column in Submissions.__table__.columns.keys(): 14 | prop = getattr(Submissions, column) 15 | data = ( 16 | Submissions.query.with_entities(prop, func.count(prop)) 17 | .group_by(prop) 18 | .all() 19 | ) 20 | return {"success": True, "data": dict(data)} 21 | else: 22 | response = {"success": False, "errors": "That could not be found"}, 404 23 | return response 24 | -------------------------------------------------------------------------------- /CTFd/api/v1/statistics/teams.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource 2 | 3 | from CTFd.api.v1.statistics import statistics_namespace 4 | from CTFd.models import Teams 5 | from CTFd.utils.decorators import admins_only 6 | 7 | 8 | @statistics_namespace.route("/teams") 9 | class TeamStatistics(Resource): 10 | @admins_only 11 | def get(self): 12 | registered = Teams.query.count() 13 | data = {"registered": registered} 14 | return {"success": True, "data": data} 15 | -------------------------------------------------------------------------------- /CTFd/api/v1/statistics/users.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource 2 | from sqlalchemy import func 3 | 4 | from CTFd.api.v1.statistics import statistics_namespace 5 | from CTFd.models import Users 6 | from CTFd.utils.decorators import admins_only 7 | 8 | 9 | @statistics_namespace.route("/users") 10 | class UserStatistics(Resource): 11 | def get(self): 12 | registered = Users.query.count() 13 | confirmed = Users.query.filter_by(verified=True).count() 14 | data = {"registered": registered, "confirmed": confirmed} 15 | return {"success": True, "data": data} 16 | 17 | 18 | @statistics_namespace.route("/users/") 19 | class UserPropertyCounts(Resource): 20 | @admins_only 21 | def get(self, column): 22 | if column in Users.__table__.columns.keys(): 23 | prop = getattr(Users, column) 24 | data = ( 25 | Users.query.with_entities(prop, func.count(prop)).group_by(prop).all() 26 | ) 27 | return {"success": True, "data": dict(data)} 28 | else: 29 | return {"success": False, "message": "That could not be found"}, 404 30 | -------------------------------------------------------------------------------- /CTFd/cache/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_caching import Cache 3 | 4 | cache = Cache() 5 | 6 | 7 | def make_cache_key(path=None, key_prefix="view/%s"): 8 | """ 9 | This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses. 10 | Over time this function may be replaced with a cleaner custom cache implementation. 11 | :param path: 12 | :param key_prefix: 13 | :return: 14 | """ 15 | if path is None: 16 | path = request.endpoint 17 | cache_key = key_prefix % path 18 | return cache_key 19 | 20 | 21 | def clear_config(): 22 | from CTFd.utils import _get_config, get_app_config 23 | 24 | cache.delete_memoized(_get_config) 25 | cache.delete_memoized(get_app_config) 26 | 27 | 28 | def clear_standings(): 29 | from CTFd.utils.scores import get_standings, get_team_standings, get_user_standings 30 | from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList 31 | from CTFd.api import api 32 | 33 | cache.delete_memoized(get_standings) 34 | cache.delete_memoized(get_team_standings) 35 | cache.delete_memoized(get_user_standings) 36 | cache.delete(make_cache_key(path="scoreboard.listing")) 37 | cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint)) 38 | cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint)) 39 | cache.delete_memoized(ScoreboardList.get) 40 | 41 | 42 | def clear_pages(): 43 | from CTFd.utils.config.pages import get_page, get_pages 44 | 45 | cache.delete_memoized(get_pages) 46 | cache.delete_memoized(get_page) 47 | -------------------------------------------------------------------------------- /CTFd/challenges.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | from CTFd.utils import config, get_config 4 | from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf 5 | from CTFd.utils.decorators import ( 6 | during_ctf_time_only, 7 | require_team, 8 | require_verified_emails, 9 | ) 10 | from CTFd.utils.decorators.visibility import check_challenge_visibility 11 | from CTFd.utils.helpers import get_errors, get_infos 12 | 13 | challenges = Blueprint("challenges", __name__) 14 | 15 | 16 | @challenges.route("/challenges", methods=["GET"]) 17 | @during_ctf_time_only 18 | @require_verified_emails 19 | @check_challenge_visibility 20 | @require_team 21 | def listing(): 22 | infos = get_infos() 23 | errors = get_errors() 24 | start = get_config("start") or 0 25 | end = get_config("end") or 0 26 | 27 | if ctf_paused(): 28 | infos.append("{} is paused".format(config.ctf_name())) 29 | 30 | # CTF has ended but we want to allow view_after_ctf. Show error but let JS load challenges. 31 | if ctf_ended() and view_after_ctf(): 32 | infos.append("{} has ended".format(config.ctf_name())) 33 | 34 | return render_template( 35 | "challenges.html", infos=infos, errors=errors, start=int(start), end=int(end) 36 | ) 37 | -------------------------------------------------------------------------------- /CTFd/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | 3 | 4 | # 404 5 | def page_not_found(error): 6 | return render_template("errors/404.html", error=error.description), 404 7 | 8 | 9 | # 403 10 | def forbidden(error): 11 | return render_template("errors/403.html", error=error.description), 403 12 | 13 | 14 | # 500 15 | def general_error(error): 16 | return render_template("errors/500.html"), 500 17 | 18 | 19 | # 502 20 | def gateway_error(error): 21 | return render_template("errors/502.html", error=error.description), 502 22 | -------------------------------------------------------------------------------- /CTFd/events/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, Response, current_app, stream_with_context 2 | 3 | from CTFd.utils import get_app_config 4 | from CTFd.utils.decorators import authed_only, ratelimit 5 | 6 | events = Blueprint("events", __name__) 7 | 8 | 9 | @events.route("/events") 10 | @authed_only 11 | @ratelimit(method="GET", limit=150, interval=60) 12 | def subscribe(): 13 | @stream_with_context 14 | def gen(): 15 | for event in current_app.events_manager.subscribe(): 16 | yield str(event) 17 | 18 | enabled = get_app_config("SERVER_SENT_EVENTS") 19 | if enabled is False: 20 | return ("", 204) 21 | 22 | return Response(gen(), mimetype="text/event-stream") 23 | -------------------------------------------------------------------------------- /CTFd/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | class UserNotFoundException(Exception): 2 | pass 3 | 4 | 5 | class UserTokenExpiredException(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /CTFd/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/logs/.gitkeep -------------------------------------------------------------------------------- /CTFd/plugins/challenges/assets/create.js: -------------------------------------------------------------------------------- 1 | CTFd.plugin.run((_CTFd) => { 2 | const $ = _CTFd.lib.$ 3 | const md = _CTFd.lib.markdown() 4 | $('a[href="#new-desc-preview"]').on('shown.bs.tab', function (event) { 5 | if (event.target.hash == '#new-desc-preview') { 6 | var editor_value = $('#new-desc-editor').val(); 7 | $(event.target.hash).html( 8 | md.render(editor_value) 9 | ); 10 | } 11 | }); 12 | // $('#desc-edit').on('shown.bs.tab', function (event) { 13 | // if (event.target.hash == '#desc-preview') { 14 | // var editor_value = $('#desc-editor').val(); 15 | // $(event.target.hash).html( 16 | // window.challenge.render(editor_value) 17 | // ); 18 | // } 19 | // }); 20 | // $('#new-desc-edit').on('shown.bs.tab', function (event) { 21 | // if (event.target.hash == '#new-desc-preview') { 22 | // var editor_value = $('#new-desc-editor').val(); 23 | // $(event.target.hash).html( 24 | // window.challenge.render(editor_value) 25 | // ); 26 | // } 27 | // }); 28 | // $("#solve-attempts-checkbox").change(function () { 29 | // if (this.checked) { 30 | // $('#solve-attempts-input').show(); 31 | // } else { 32 | // $('#solve-attempts-input').hide(); 33 | // $('#max_attempts').val(''); 34 | // } 35 | // }); 36 | // $(document).ready(function () { 37 | // $('[data-toggle="tooltip"]').tooltip(); 38 | // }); 39 | }) 40 | -------------------------------------------------------------------------------- /CTFd/plugins/challenges/assets/update.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/plugins/challenges/assets/update.js -------------------------------------------------------------------------------- /CTFd/plugins/challenges/assets/view.js: -------------------------------------------------------------------------------- 1 | CTFd._internal.challenge.data = undefined 2 | 3 | CTFd._internal.challenge.renderer = CTFd.lib.markdown(); 4 | 5 | 6 | CTFd._internal.challenge.preRender = function () { } 7 | 8 | CTFd._internal.challenge.render = function (markdown) { 9 | return CTFd._internal.challenge.renderer.render(markdown) 10 | } 11 | 12 | 13 | CTFd._internal.challenge.postRender = function () { } 14 | 15 | 16 | CTFd._internal.challenge.submit = function (preview) { 17 | var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val()) 18 | var submission = CTFd.lib.$('#submission-input').val() 19 | 20 | var body = { 21 | 'challenge_id': challenge_id, 22 | 'submission': submission, 23 | } 24 | var params = {} 25 | if (preview) { 26 | params['preview'] = true 27 | } 28 | 29 | return CTFd.api.post_challenge_attempt(params, body).then(function (response) { 30 | if (response.status === 429) { 31 | // User was ratelimited but process response 32 | return response 33 | } 34 | if (response.status === 403) { 35 | // User is not logged in or CTF is paused. 36 | return response 37 | } 38 | return response 39 | }) 40 | }; 41 | -------------------------------------------------------------------------------- /CTFd/plugins/dynamic_challenges/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | -------------------------------------------------------------------------------- /CTFd/plugins/dynamic_challenges/assets/create.js: -------------------------------------------------------------------------------- 1 | // Markdown Preview 2 | $('#desc-edit').on('shown.bs.tab', function (event) { 3 | if (event.target.hash == '#desc-preview'){ 4 | var editor_value = $('#desc-editor').val(); 5 | $(event.target.hash).html( 6 | window.challenge.render(editor_value) 7 | ); 8 | } 9 | }); 10 | $('#new-desc-edit').on('shown.bs.tab', function (event) { 11 | if (event.target.hash == '#new-desc-preview'){ 12 | var editor_value = $('#new-desc-editor').val(); 13 | $(event.target.hash).html( 14 | window.challenge.render(editor_value) 15 | ); 16 | } 17 | }); 18 | $("#solve-attempts-checkbox").change(function() { 19 | if(this.checked) { 20 | $('#solve-attempts-input').show(); 21 | } else { 22 | $('#solve-attempts-input').hide(); 23 | $('#max_attempts').val(''); 24 | } 25 | }); 26 | 27 | $(document).ready(function(){ 28 | $('[data-toggle="tooltip"]').tooltip(); 29 | }); 30 | -------------------------------------------------------------------------------- /CTFd/plugins/dynamic_challenges/assets/update.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/plugins/dynamic_challenges/assets/update.js -------------------------------------------------------------------------------- /CTFd/plugins/dynamic_challenges/assets/view.js: -------------------------------------------------------------------------------- 1 | CTFd._internal.challenge.data = undefined 2 | 3 | CTFd._internal.challenge.renderer = CTFd.lib.markdown(); 4 | 5 | 6 | CTFd._internal.challenge.preRender = function () { } 7 | 8 | CTFd._internal.challenge.render = function (markdown) { 9 | return CTFd._internal.challenge.renderer.render(markdown) 10 | } 11 | 12 | 13 | CTFd._internal.challenge.postRender = function () { } 14 | 15 | 16 | CTFd._internal.challenge.submit = function (preview) { 17 | var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val()) 18 | var submission = CTFd.lib.$('#submission-input').val() 19 | 20 | var body = { 21 | 'challenge_id': challenge_id, 22 | 'submission': submission, 23 | } 24 | var params = {} 25 | if (preview) { 26 | params['preview'] = true 27 | } 28 | 29 | return CTFd.api.post_challenge_attempt(params, body).then(function (response) { 30 | if (response.status === 429) { 31 | // User was ratelimited but process response 32 | return response 33 | } 34 | if (response.status === 403) { 35 | // User is not logged in or CTF is paused. 36 | return response 37 | } 38 | return response 39 | }) 40 | }; 41 | -------------------------------------------------------------------------------- /CTFd/plugins/dynamic_challenges/function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/plugins/dynamic_challenges/function.png -------------------------------------------------------------------------------- /CTFd/plugins/flags/assets/regex/create.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 |
8 |
9 | 13 |
14 | -------------------------------------------------------------------------------- /CTFd/plugins/flags/assets/regex/edit.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 |
8 |
9 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /CTFd/plugins/flags/assets/static/create.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 |
8 |
9 | 13 |
14 | -------------------------------------------------------------------------------- /CTFd/plugins/flags/assets/static/edit.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 |
8 |
9 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /CTFd/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/schemas/__init__.py -------------------------------------------------------------------------------- /CTFd/schemas/awards.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Awards, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class AwardSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Awards 8 | include_fk = True 9 | dump_only = ("id", "date") 10 | 11 | views = { 12 | "admin": [ 13 | "category", 14 | "user_id", 15 | "name", 16 | "description", 17 | "value", 18 | "team_id", 19 | "user", 20 | "team", 21 | "date", 22 | "requirements", 23 | "id", 24 | "icon", 25 | ], 26 | "user": [ 27 | "category", 28 | "user_id", 29 | "name", 30 | "description", 31 | "value", 32 | "team_id", 33 | "user", 34 | "team", 35 | "date", 36 | "id", 37 | "icon", 38 | ], 39 | } 40 | 41 | def __init__(self, view=None, *args, **kwargs): 42 | if view: 43 | if isinstance(view, string_types): 44 | kwargs["only"] = self.views[view] 45 | elif isinstance(view, list): 46 | kwargs["only"] = view 47 | 48 | super(AwardSchema, self).__init__(*args, **kwargs) 49 | -------------------------------------------------------------------------------- /CTFd/schemas/challenges.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Challenges, ma 2 | 3 | 4 | class ChallengeSchema(ma.ModelSchema): 5 | class Meta: 6 | model = Challenges 7 | include_fk = True 8 | dump_only = ("id",) 9 | -------------------------------------------------------------------------------- /CTFd/schemas/config.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Configs, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class ConfigSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Configs 8 | include_fk = True 9 | dump_only = ("id",) 10 | 11 | views = {"admin": ["id", "key", "value"]} 12 | 13 | def __init__(self, view=None, *args, **kwargs): 14 | if view: 15 | if isinstance(view, string_types): 16 | kwargs["only"] = self.views[view] 17 | elif isinstance(view, list): 18 | kwargs["only"] = view 19 | 20 | super(ConfigSchema, self).__init__(*args, **kwargs) 21 | -------------------------------------------------------------------------------- /CTFd/schemas/files.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Files, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class FileSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Files 8 | include_fk = True 9 | dump_only = ("id", "type", "location") 10 | 11 | def __init__(self, view=None, *args, **kwargs): 12 | if view: 13 | if isinstance(view, string_types): 14 | kwargs["only"] = self.views[view] 15 | elif isinstance(view, list): 16 | kwargs["only"] = view 17 | 18 | super(FileSchema, self).__init__(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /CTFd/schemas/flags.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Flags, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class FlagSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Flags 8 | include_fk = True 9 | dump_only = ("id",) 10 | 11 | def __init__(self, view=None, *args, **kwargs): 12 | if view: 13 | if isinstance(view, string_types): 14 | kwargs["only"] = self.views[view] 15 | elif isinstance(view, list): 16 | kwargs["only"] = view 17 | 18 | super(FlagSchema, self).__init__(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /CTFd/schemas/hints.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Hints, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class HintSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Hints 8 | include_fk = True 9 | dump_only = ("id", "type") 10 | 11 | views = { 12 | "locked": ["id", "type", "challenge", "cost"], 13 | "unlocked": ["id", "type", "challenge", "content", "cost"], 14 | "admin": ["id", "type", "challenge", "content", "cost", "requirements"], 15 | } 16 | 17 | def __init__(self, view=None, *args, **kwargs): 18 | if view: 19 | if isinstance(view, string_types): 20 | kwargs["only"] = self.views[view] 21 | elif isinstance(view, list): 22 | kwargs["only"] = view 23 | 24 | super(HintSchema, self).__init__(*args, **kwargs) 25 | -------------------------------------------------------------------------------- /CTFd/schemas/notifications.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Notifications, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class NotificationSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Notifications 8 | include_fk = True 9 | dump_only = ("id", "date") 10 | 11 | def __init__(self, view=None, *args, **kwargs): 12 | if view: 13 | if isinstance(view, string_types): 14 | kwargs["only"] = self.views[view] 15 | elif isinstance(view, list): 16 | kwargs["only"] = view 17 | 18 | super(NotificationSchema, self).__init__(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /CTFd/schemas/pages.py: -------------------------------------------------------------------------------- 1 | from marshmallow import pre_load 2 | 3 | from CTFd.models import Pages, ma 4 | from CTFd.utils import string_types 5 | 6 | 7 | class PageSchema(ma.ModelSchema): 8 | class Meta: 9 | model = Pages 10 | include_fk = True 11 | dump_only = ("id",) 12 | 13 | @pre_load 14 | def validate_route(self, data): 15 | route = data.get("route") 16 | if route and route.startswith("/"): 17 | data["route"] = route.strip("/") 18 | 19 | def __init__(self, view=None, *args, **kwargs): 20 | if view: 21 | if isinstance(view, string_types): 22 | kwargs["only"] = self.views[view] 23 | elif isinstance(view, list): 24 | kwargs["only"] = view 25 | 26 | super(PageSchema, self).__init__(*args, **kwargs) 27 | -------------------------------------------------------------------------------- /CTFd/schemas/submissions.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields 2 | 3 | from CTFd.models import Submissions, ma 4 | from CTFd.schemas.challenges import ChallengeSchema 5 | from CTFd.utils import string_types 6 | 7 | 8 | class SubmissionSchema(ma.ModelSchema): 9 | challenge = fields.Nested(ChallengeSchema, only=["name", "category", "value"]) 10 | 11 | class Meta: 12 | model = Submissions 13 | include_fk = True 14 | dump_only = ("id",) 15 | 16 | views = { 17 | "admin": [ 18 | "provided", 19 | "ip", 20 | "challenge_id", 21 | "challenge", 22 | "user", 23 | "team", 24 | "date", 25 | "type", 26 | "id", 27 | ], 28 | "user": ["challenge_id", "challenge", "user", "team", "date", "type", "id"], 29 | } 30 | 31 | def __init__(self, view=None, *args, **kwargs): 32 | if view: 33 | if isinstance(view, string_types): 34 | kwargs["only"] = self.views[view] 35 | elif isinstance(view, list): 36 | kwargs["only"] = view 37 | 38 | super(SubmissionSchema, self).__init__(*args, **kwargs) 39 | -------------------------------------------------------------------------------- /CTFd/schemas/tags.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Tags, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class TagSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Tags 8 | include_fk = True 9 | dump_only = ("id",) 10 | 11 | views = {"admin": ["id", "challenge", "value"], "user": ["value"]} 12 | 13 | def __init__(self, view=None, *args, **kwargs): 14 | if view: 15 | if isinstance(view, string_types): 16 | kwargs["only"] = self.views[view] 17 | elif isinstance(view, list): 18 | kwargs["only"] = view 19 | 20 | super(TagSchema, self).__init__(*args, **kwargs) 21 | -------------------------------------------------------------------------------- /CTFd/schemas/tokens.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Tokens, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class TokenSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Tokens 8 | include_fk = True 9 | dump_only = ("id", "expiration", "type") 10 | 11 | views = { 12 | "admin": ["id", "type", "user_id", "created", "expiration", "value"], 13 | "user": ["id", "type", "created", "expiration"], 14 | } 15 | 16 | def __init__(self, view=None, *args, **kwargs): 17 | if view: 18 | if isinstance(view, string_types): 19 | kwargs["only"] = self.views[view] 20 | elif isinstance(view, list): 21 | kwargs["only"] = view 22 | 23 | super(TokenSchema, self).__init__(*args, **kwargs) 24 | -------------------------------------------------------------------------------- /CTFd/schemas/unlocks.py: -------------------------------------------------------------------------------- 1 | from CTFd.models import Unlocks, ma 2 | from CTFd.utils import string_types 3 | 4 | 5 | class UnlockSchema(ma.ModelSchema): 6 | class Meta: 7 | model = Unlocks 8 | include_fk = True 9 | dump_only = ("id", "date") 10 | 11 | views = { 12 | "admin": ["user_id", "target", "team_id", "date", "type", "id"], 13 | "user": ["target", "date", "type", "id"], 14 | } 15 | 16 | def __init__(self, view=None, *args, **kwargs): 17 | if view: 18 | if isinstance(view, string_types): 19 | kwargs["only"] = self.views[view] 20 | elif isinstance(view, list): 21 | kwargs["only"] = view 22 | 23 | super(UnlockSchema, self).__init__(*args, **kwargs) 24 | -------------------------------------------------------------------------------- /CTFd/scoreboard.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | from CTFd.cache import cache, make_cache_key 4 | from CTFd.utils import config 5 | from CTFd.utils.decorators.visibility import check_score_visibility 6 | from CTFd.utils.scores import get_standings 7 | 8 | scoreboard = Blueprint("scoreboard", __name__) 9 | 10 | 11 | @scoreboard.route("/scoreboard") 12 | @check_score_visibility 13 | @cache.cached(timeout=60, key_prefix=make_cache_key) 14 | def listing(): 15 | standings = get_standings() 16 | return render_template( 17 | "scoreboard.html", 18 | standings=standings, 19 | score_frozen=config.is_scoreboard_frozen(), 20 | ) 21 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/css/admin.scss: -------------------------------------------------------------------------------- 1 | @import "includes/sticky-footer.css"; 2 | 3 | #score-graph { 4 | height: 450px; 5 | display: block; 6 | clear: both; 7 | } 8 | 9 | #solves-graph { 10 | display: block; 11 | height: 350px; 12 | } 13 | 14 | #keys-pie-graph { 15 | height: 400px; 16 | display: block; 17 | } 18 | 19 | #categories-pie-graph { 20 | height: 400px; 21 | display: block; 22 | } 23 | 24 | #solve-percentages-graph { 25 | height: 400px; 26 | display: block; 27 | } 28 | 29 | .no-decoration { 30 | color: inherit !important; 31 | text-decoration: none !important; 32 | } 33 | 34 | .no-decoration:hover { 35 | color: inherit !important; 36 | text-decoration: none !important; 37 | } 38 | 39 | .table td, 40 | .table th { 41 | vertical-align: inherit; 42 | } 43 | 44 | pre { 45 | white-space: pre-wrap; 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | .form-control { 51 | position: relative; 52 | display: block; 53 | /*padding: 0.8em;*/ 54 | border-radius: 0; 55 | /*background: #f0f0f0;*/ 56 | /*color: #aaa;*/ 57 | font-weight: 400; 58 | font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; 59 | -webkit-appearance: none; 60 | } 61 | 62 | tbody tr:hover { 63 | background-color: rgba(0, 0, 0, 0.1) !important; 64 | } 65 | 66 | tr[data-href] { 67 | cursor: pointer; 68 | } 69 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/css/challenge-board.scss: -------------------------------------------------------------------------------- 1 | .chal-desc { 2 | padding-left: 30px; 3 | padding-right: 30px; 4 | font-size: 14px; 5 | } 6 | 7 | .chal-desc img { 8 | max-width: 100%; 9 | height: auto; 10 | } 11 | 12 | .modal-content { 13 | border-radius: 0px; 14 | max-width: 1000px; 15 | padding: 1em; 16 | margin: 0 auto; 17 | } 18 | 19 | .btn-info { 20 | background-color: #5b7290 !important; 21 | } 22 | 23 | .badge-info { 24 | background-color: #5b7290 !important; 25 | } 26 | 27 | .challenge-button { 28 | box-shadow: 3px 3px 3px grey; 29 | } 30 | 31 | .solved-challenge { 32 | background-color: #37d63e !important; 33 | opacity: 0.4; 34 | border: none; 35 | } 36 | 37 | .corner-button-check { 38 | margin-top: -10px; 39 | margin-right: 25px; 40 | position: absolute; 41 | right: 0; 42 | } 43 | 44 | .key-submit .btn { 45 | height: 51px; 46 | } 47 | 48 | #challenge-window .form-control { 49 | position: relative; 50 | display: block; 51 | padding: 0.8em; 52 | border-radius: 0; 53 | background: #f0f0f0; 54 | color: #aaa; 55 | font-weight: 400; 56 | font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; 57 | -webkit-appearance: none; 58 | height: auto !important; 59 | } 60 | 61 | #challenge-window .form-control:focus { 62 | background-color: transparent; 63 | border-color: #a3d39c; 64 | box-shadow: 0 0 0 0.2rem #a3d39c; 65 | transition: background-color 0.3s, border-color 0.3s; 66 | } 67 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/css/codemirror.scss: -------------------------------------------------------------------------------- 1 | @import "~codemirror/lib/codemirror.css"; 2 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/css/includes/sticky-footer.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | 8 | body { 9 | margin-bottom: 60px; /* Margin bottom by footer height */ 10 | } 11 | 12 | .footer { 13 | position: absolute; 14 | 15 | /* prevent scrollbars from showing on pages that don't use the full page height */ 16 | bottom: 1px; 17 | 18 | width: 100%; 19 | 20 | /* Set the fixed height of the footer here */ 21 | height: 60px; 22 | 23 | /* Override line-height from core because we have two lines in the admin panel */ 24 | line-height: normal !important; 25 | 26 | /* Avoid covering things */ 27 | z-index: -20; 28 | 29 | /*background-color: #f5f5f5;*/ 30 | } 31 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/challenges/files.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import CTFd from "core/CTFd"; 3 | import { default as helpers } from "core/helpers"; 4 | import { ezQuery } from "core/ezq"; 5 | 6 | export function addFile(event) { 7 | event.preventDefault(); 8 | let form = event.target; 9 | let data = { 10 | challenge: CHALLENGE_ID, 11 | type: "challenge" 12 | }; 13 | helpers.files.upload(form, data, function(response) { 14 | setTimeout(function() { 15 | window.location.reload(); 16 | }, 700); 17 | }); 18 | } 19 | 20 | export function deleteFile(event) { 21 | const file_id = $(this).attr("file-id"); 22 | const row = $(this) 23 | .parent() 24 | .parent(); 25 | ezQuery({ 26 | title: "Delete Files", 27 | body: "Are you sure you want to delete this file?", 28 | success: function() { 29 | CTFd.fetch("/api/v1/files/" + file_id, { 30 | method: "DELETE" 31 | }) 32 | .then(function(response) { 33 | return response.json(); 34 | }) 35 | .then(function(response) { 36 | if (response.success) { 37 | row.remove(); 38 | } 39 | }); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/challenges/requirements.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import CTFd from "core/CTFd"; 3 | 4 | export function addRequirement(event) { 5 | event.preventDefault(); 6 | const requirements = $("#prerequisite-add-form").serializeJSON(); 7 | 8 | // Shortcut if there's no prerequisite 9 | if (!requirements["prerequisite"]) { 10 | return; 11 | } 12 | 13 | CHALLENGE_REQUIREMENTS.prerequisites.push( 14 | parseInt(requirements["prerequisite"]) 15 | ); 16 | 17 | const params = { 18 | requirements: CHALLENGE_REQUIREMENTS 19 | }; 20 | 21 | CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, { 22 | method: "PATCH", 23 | credentials: "same-origin", 24 | headers: { 25 | Accept: "application/json", 26 | "Content-Type": "application/json" 27 | }, 28 | body: JSON.stringify(params) 29 | }) 30 | .then(function(response) { 31 | return response.json(); 32 | }) 33 | .then(function(data) { 34 | if (data.success) { 35 | // TODO: Make this refresh requirements 36 | window.location.reload(); 37 | } 38 | }); 39 | } 40 | 41 | export function deleteRequirement(event) { 42 | const challenge_id = $(this).attr("challenge-id"); 43 | const row = $(this) 44 | .parent() 45 | .parent(); 46 | 47 | CHALLENGE_REQUIREMENTS.prerequisites.pop(challenge_id); 48 | 49 | const params = { 50 | requirements: CHALLENGE_REQUIREMENTS 51 | }; 52 | CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, { 53 | method: "PATCH", 54 | credentials: "same-origin", 55 | headers: { 56 | Accept: "application/json", 57 | "Content-Type": "application/json" 58 | }, 59 | body: JSON.stringify(params) 60 | }) 61 | .then(function(response) { 62 | return response.json(); 63 | }) 64 | .then(function(data) { 65 | if (data.success) { 66 | row.remove(); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/challenges/tags.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import CTFd from "core/CTFd"; 3 | 4 | export function deleteTag(event) { 5 | const $elem = $(this); 6 | const tag_id = $elem.attr("tag-id"); 7 | 8 | CTFd.api.delete_tag({ tagId: tag_id }).then(response => { 9 | if (response.success) { 10 | $elem.parent().remove(); 11 | } 12 | }); 13 | } 14 | 15 | export function addTag(event) { 16 | if (event.keyCode != 13) { 17 | return; 18 | } 19 | 20 | const $elem = $(this); 21 | 22 | const tag = $elem.val(); 23 | const params = { 24 | value: tag, 25 | challenge: CHALLENGE_ID 26 | }; 27 | 28 | CTFd.api.post_tag_list({}, params).then(response => { 29 | if (response.success) { 30 | const tpl = 31 | "" + 32 | "{0}" + 33 | "×"; 34 | const tag = $(tpl.format(response.data.value, response.data.id)); 35 | $("#challenge-tags").append(tag); 36 | // TODO: tag deletion not working on freshly created tags 37 | tag.click(deleteTag); 38 | } 39 | }); 40 | 41 | $elem.val(""); 42 | } 43 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/challenges.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/admin/assets/js/pages/challenges.js -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/events.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import events from "core/events"; 3 | import CTFd from "core/CTFd"; 4 | 5 | $(() => { 6 | events(CTFd.config.urlRoot); 7 | }); 8 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/main.js: -------------------------------------------------------------------------------- 1 | import CTFd from "core/CTFd"; 2 | import $ from "jquery"; 3 | import Moment from "moment"; 4 | import nunjucks from "nunjucks"; 5 | import { Howl } from "howler"; 6 | import events from "core/events"; 7 | import times from "core/times"; 8 | import styles from "../styles"; 9 | import { default as helpers } from "core/helpers"; 10 | 11 | CTFd.init(window.init); 12 | window.CTFd = CTFd; 13 | window.helpers = helpers; 14 | window.$ = $; 15 | window.Moment = Moment; 16 | window.nunjucks = nunjucks; 17 | window.Howl = Howl; 18 | 19 | $(() => { 20 | styles(); 21 | times(); 22 | events(CTFd.config.urlRoot); 23 | }); 24 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/notifications.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import "core/utils"; 3 | import $ from "jquery"; 4 | import CTFd from "core/CTFd"; 5 | import { ezQuery, ezAlert } from "core/ezq"; 6 | 7 | function submit(event) { 8 | event.preventDefault(); 9 | const $form = $(this); 10 | const params = $form.serializeJSON(); 11 | 12 | // Disable button after click 13 | $form.find("button[type=submit]").attr("disabled", true); 14 | 15 | CTFd.api.post_notification_list({}, params).then(response => { 16 | // Admin should also see the notification sent out 17 | setTimeout(function() { 18 | $form.find("button[type=submit]").attr("disabled", false); 19 | }, 1000); 20 | if (!response.success) { 21 | ezAlert({ 22 | title: "Error", 23 | body: "Could not send notification. Please try again.", 24 | button: "OK" 25 | }); 26 | } 27 | }); 28 | } 29 | 30 | function deleteNotification(event) { 31 | event.preventDefault(); 32 | const $elem = $(this); 33 | const id = $elem.data("notif-id"); 34 | 35 | ezQuery({ 36 | title: "Delete Notification", 37 | body: "Are you sure you want to delete this notification?", 38 | success: function() { 39 | CTFd.api.delete_notification({ notificationId: id }).then(response => { 40 | if (response.success) { 41 | $elem.parent().remove(); 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | 48 | $(() => { 49 | $("#notifications_form").submit(submit); 50 | $(".delete-notification").click(deleteNotification); 51 | }); 52 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/pages.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import CTFd from "core/CTFd"; 3 | import $ from "jquery"; 4 | import { htmlEntities } from "core/utils"; 5 | import { ezQuery } from "core/ezq"; 6 | 7 | function deletePage(event) { 8 | const elem = $(this); 9 | const name = elem.attr("page-route"); 10 | const page_id = elem.attr("page-id"); 11 | ezQuery({ 12 | title: "Delete " + htmlEntities(name), 13 | body: "Are you sure you want to delete {0}?".format( 14 | "" + htmlEntities(name) + "" 15 | ), 16 | success: function() { 17 | CTFd.fetch("/api/v1/pages/" + page_id, { 18 | method: "DELETE" 19 | }) 20 | .then(function(response) { 21 | return response.json(); 22 | }) 23 | .then(function(response) { 24 | if (response.success) { 25 | elem 26 | .parent() 27 | .parent() 28 | .remove(); 29 | } 30 | }); 31 | } 32 | }); 33 | } 34 | 35 | $(() => { 36 | $(".delete-page").click(deletePage); 37 | }); 38 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/reset.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import $ from "jquery"; 3 | import { ezQuery } from "core/ezq"; 4 | 5 | function reset(event) { 6 | event.preventDefault(); 7 | ezQuery({ 8 | title: "Reset CTF?", 9 | body: "Are you sure you want to reset your CTFd instance?", 10 | success: function() { 11 | $("#reset-ctf-form") 12 | .off("submit") 13 | .submit(); 14 | } 15 | }); 16 | } 17 | 18 | $(() => { 19 | $("#reset-ctf-form").submit(reset); 20 | }); 21 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/scoreboard.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import CTFd from "core/CTFd"; 3 | import $ from "jquery"; 4 | 5 | const api_func = { 6 | users: (x, y) => CTFd.api.patch_user_public({ userId: x }, y), 7 | teams: (x, y) => CTFd.api.patch_team_public({ teamId: x }, y) 8 | }; 9 | 10 | function toggleAccount() { 11 | const $btn = $(this); 12 | const id = $btn.data("account-id"); 13 | const state = $btn.data("state"); 14 | let hidden = undefined; 15 | if (state === "visible") { 16 | hidden = true; 17 | } else if (state === "hidden") { 18 | hidden = false; 19 | } 20 | 21 | const params = { 22 | hidden: hidden 23 | }; 24 | 25 | api_func[CTFd.config.userMode](id, params).then(response => { 26 | if (response.success) { 27 | if (hidden) { 28 | $btn.data("state", "hidden"); 29 | $btn.addClass("btn-danger").removeClass("btn-success"); 30 | $btn.text("Hidden"); 31 | } else { 32 | $btn.data("state", "visible"); 33 | $btn.addClass("btn-success").removeClass("btn-danger"); 34 | $btn.text("Visible"); 35 | } 36 | } 37 | }); 38 | } 39 | 40 | $(() => { 41 | $(".scoreboard-toggle").click(toggleAccount); 42 | }); 43 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/style.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import styles from "../styles"; 3 | import times from "core/times"; 4 | 5 | $(() => { 6 | styles(); 7 | times(); 8 | }); 9 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/submissions.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import CTFd from "core/CTFd"; 3 | import $ from "jquery"; 4 | import { htmlEntities } from "core/utils"; 5 | import { ezQuery } from "core/ezq"; 6 | 7 | function deleteCorrectSubmission(event) { 8 | const key_id = $(this).data("submission-id"); 9 | const $elem = $(this) 10 | .parent() 11 | .parent(); 12 | const chal_name = $elem 13 | .find(".chal") 14 | .text() 15 | .trim(); 16 | const team_name = $elem 17 | .find(".team") 18 | .text() 19 | .trim(); 20 | 21 | const row = $(this) 22 | .parent() 23 | .parent(); 24 | 25 | ezQuery({ 26 | title: "Delete Submission", 27 | body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format( 28 | "" + htmlEntities(team_name) + "", 29 | "" + htmlEntities(chal_name) + "" 30 | ), 31 | success: function() { 32 | CTFd.api 33 | .delete_submission({ submissionId: key_id }) 34 | .then(function(response) { 35 | if (response.success) { 36 | row.remove(); 37 | } 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | $(() => { 44 | $(".delete-correct-submission").click(deleteCorrectSubmission); 45 | }); 46 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/teams.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/admin/assets/js/pages/teams.js -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/pages/users.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/styles.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/js/bootstrap.bundle"; 2 | import $ from "jquery"; 3 | 4 | export default () => { 5 | $(".form-control").bind({ 6 | focus: function() { 7 | $(this).addClass("input-filled-valid"); 8 | }, 9 | blur: function() { 10 | if ($(this).val() === "") { 11 | $(this).removeClass("input-filled-valid"); 12 | } 13 | } 14 | }); 15 | 16 | $(".modal").on("show.bs.modal", function(e) { 17 | $(".form-control").each(function() { 18 | if ($(this).val()) { 19 | $(this).addClass("input-filled-valid"); 20 | } 21 | }); 22 | }); 23 | 24 | $(function() { 25 | $(".form-control").each(function() { 26 | if ($(this).val()) { 27 | $(this).addClass("input-filled-valid"); 28 | } 29 | }); 30 | 31 | $("tr[data-href]").click(function() { 32 | var sel = getSelection().toString(); 33 | if (!sel) { 34 | var href = $(this).attr("data-href"); 35 | if (href) { 36 | window.location = href; 37 | } 38 | } 39 | return false; 40 | }); 41 | 42 | $("tr[data-href] a, tr[data-href] button").click(function(e) { 43 | // TODO: This is a hack to allow modal close buttons to work 44 | if (!$(this).attr("data-dismiss")) { 45 | e.stopPropagation(); 46 | } 47 | }); 48 | 49 | $('[data-toggle="tooltip"]').tooltip(); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /CTFd/themes/admin/assets/js/templates/admin-flags-table.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for flag in flags %} 11 | 12 | 13 | 14 | 19 | 20 | {% endfor %} 21 | 22 |
TypeKeySettings
{{flag.type}}
{{flag.content}}
15 | 16 | 17 | 18 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/static/css/admin.dev.css: -------------------------------------------------------------------------------- 1 | html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal !important;z-index:-20} 2 | 3 | #score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{height:400px;display:block}#categories-pie-graph{height:400px;display:block}#solve-percentages-graph{height:400px;display:block}.no-decoration{color:inherit !important;text-decoration:none !important}.no-decoration:hover{color:inherit !important;text-decoration:none !important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,0.1) !important}tr[data-href]{cursor:pointer} 4 | 5 | -------------------------------------------------------------------------------- /CTFd/themes/admin/static/css/admin.min.css: -------------------------------------------------------------------------------- 1 | html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal!important;z-index:-20}#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#categories-pie-graph,#keys-pie-graph,#solve-percentages-graph{height:400px;display:block}.no-decoration,.no-decoration:hover{color:inherit!important;text-decoration:none!important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:Avenir Next,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,.1)!important}tr[data-href]{cursor:pointer} -------------------------------------------------------------------------------- /CTFd/themes/admin/static/css/challenge-board.dev.css: -------------------------------------------------------------------------------- 1 | .chal-desc{padding-left:30px;padding-right:30px;font-size:14px}.chal-desc img{max-width:100%;height:auto}.modal-content{border-radius:0px;max-width:1000px;padding:1em;margin:0 auto}.btn-info{background-color:#5b7290 !important}.badge-info{background-color:#5b7290 !important}.challenge-button{box-shadow:3px 3px 3px grey}.solved-challenge{background-color:#37d63e !important;opacity:0.4;border:none}.corner-button-check{margin-top:-10px;margin-right:25px;position:absolute;right:0}.key-submit .btn{height:51px}#challenge-window .form-control{position:relative;display:block;padding:0.8em;border-radius:0;background:#f0f0f0;color:#aaa;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none;height:auto !important}#challenge-window .form-control:focus{background-color:transparent;border-color:#a3d39c;box-shadow:0 0 0 0.2rem #a3d39c;transition:background-color 0.3s, border-color 0.3s} 2 | 3 | -------------------------------------------------------------------------------- /CTFd/themes/admin/static/css/challenge-board.min.css: -------------------------------------------------------------------------------- 1 | .chal-desc{padding-left:30px;padding-right:30px;font-size:14px}.chal-desc img{max-width:100%;height:auto}.modal-content{border-radius:0;max-width:1000px;padding:1em;margin:0 auto}.badge-info,.btn-info{background-color:#5b7290!important}.challenge-button{box-shadow:3px 3px 3px grey}.solved-challenge{background-color:#37d63e!important;opacity:.4;border:none}.corner-button-check{margin-top:-10px;margin-right:25px;position:absolute;right:0}.key-submit .btn{height:51px}#challenge-window .form-control{position:relative;display:block;padding:.8em;border-radius:0;background:#f0f0f0;color:#aaa;font-weight:400;font-family:Avenir Next,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-appearance:none;height:auto!important}#challenge-window .form-control:focus{background-color:transparent;border-color:#a3d39c;box-shadow:0 0 0 .2rem #a3d39c;transition:background-color .3s,border-color .3s} -------------------------------------------------------------------------------- /CTFd/themes/admin/static/js/core.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/admin/static/js/core.min.js -------------------------------------------------------------------------------- /CTFd/themes/admin/static/js/graphs.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/admin/static/js/graphs.min.js -------------------------------------------------------------------------------- /CTFd/themes/admin/static/js/pages/teams.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="/themes/admin/static/js",o(o.s="./CTFd/themes/admin/assets/js/pages/teams.js")}({"./CTFd/themes/admin/assets/js/pages/teams.js":function(e,t,n){}}); -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/configs/mlc.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 11 | 14 |
15 | 16 |
17 | 23 | 26 |
27 | 28 | 29 |
30 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/integrations.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |
Integration enabled!
16 |

You may now close this tab.

17 |
18 |
19 |
20 | {% endblock %} 21 | 22 | {% block scripts %} 23 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/challenges.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for file in challenge.files %} 10 | 11 | 14 | 15 | 18 | 19 | {% endfor %} 20 | 21 |
FileSettings
12 | {{ file.location.split('/').pop() }} 13 | 16 | 17 |
22 | 23 |
24 |
25 |
26 | 27 | Attach multiple files using Control+Click or Cmd+Click 28 |
29 |
30 | 31 |
32 |
33 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/flags.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for flag in flags %} 11 | 12 | 13 | 16 | 20 | 21 | {% endfor %} 22 | 23 |
TypeFlagSettings
{{ flag.type }} 14 |
{{ flag.content }}
15 |
17 | 18 | 19 |
24 | 25 |
26 | 27 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/hints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for hint in challenge.hints %} 12 | 13 | 14 | 15 | 16 | 20 | 21 | {% endfor %} 22 | 23 |
IDHintCostSettings
{{ hint.type }}
{{ hint.content }}
{{ hint.cost }} 17 | 18 | 19 |
24 |
25 | 26 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/requirements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% set requirements = challenge.requirements %} 10 | {% if challenge.requirements %} 11 | {% for prereq in requirements['prerequisites'] %} 12 | 13 | 14 | 17 | 18 | {% endfor %} 19 | {% endif %} 20 | 21 |
RequirementSettings
{{ challenges[prereq] }} 15 | 16 |
22 | 23 |
24 |
25 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/solves.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% if solves is defined %} 10 | {% for solve in solves %} 11 | 12 | 17 | 20 | 21 | {% endfor %} 22 | {% endif %} 23 | 24 |
NameDate
13 | 14 | {{ solve.account.name }} 15 | 16 | 18 | 19 |
25 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/challenges/tags.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for tag in challenge.tags %} 4 | 5 | {{ tag.value }} 6 | × 7 | 8 | {% endfor %} 9 |
10 | 11 |
12 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/flags/create.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/flags/edit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/mail/send.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 9 |
10 |
11 | 12 |
13 | 16 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/modals/teams/captain.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 15 |
16 |
17 | 18 |
19 | 22 |
-------------------------------------------------------------------------------- /CTFd/themes/admin/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {{ content | safe }} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/reset.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Reset

10 |
11 |
12 |
13 |
14 |
15 |
16 | 29 | 30 | 31 | 32 | 35 |
36 |
37 |
38 |
39 | {% endblock %} 40 | 41 | {% block scripts %} 42 | {% endblock %} 43 | 44 | {% block entrypoint %} 45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/teams/new.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

10 | Create Team 11 |

12 |
13 |
14 | 15 |
16 |
17 |
18 | {% include "admin/modals/teams/create.html" %} 19 |
20 |
21 |
22 | {% endblock %} 23 | 24 | {% block scripts %} 25 | 26 | 27 | {% endblock %} 28 | 29 | {% block entrypoint %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /CTFd/themes/admin/templates/users/new.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

10 | Create User 11 |

12 |
13 |
14 | 15 |
16 |
17 |
18 | {% include "admin/modals/users/create.html" %} 19 |
20 |
21 |
22 | {% endblock %} 23 | 24 | {% block scripts %} 25 | 26 | 27 | {% endblock %} 28 | 29 | {% block entrypoint %} 30 | 31 | {% endblock %} -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/challenge-board.scss: -------------------------------------------------------------------------------- 1 | .chal-desc { 2 | padding-left: 30px; 3 | padding-right: 30px; 4 | font-size: 14px; 5 | } 6 | 7 | .chal-desc img { 8 | max-width: 100%; 9 | height: auto; 10 | } 11 | 12 | .modal-content { 13 | border-radius: 0px; 14 | max-width: 1000px; 15 | padding: 1em; 16 | margin: 0 auto; 17 | } 18 | 19 | .btn-info { 20 | background-color: #5b7290 !important; 21 | } 22 | 23 | .badge-info { 24 | background-color: #5b7290 !important; 25 | } 26 | 27 | .challenge-button { 28 | box-shadow: 3px 3px 3px grey; 29 | } 30 | 31 | .solved-challenge { 32 | background-color: #37d63e !important; 33 | opacity: 0.4; 34 | border: none; 35 | } 36 | 37 | .corner-button-check { 38 | margin-top: -10px; 39 | margin-right: 25px; 40 | position: absolute; 41 | right: 0; 42 | } 43 | 44 | .key-submit .btn { 45 | height: 51px; 46 | } 47 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/codemirror.scss: -------------------------------------------------------------------------------- 1 | @import "~codemirror/lib/codemirror.css"; 2 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/core.scss: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | background-color: #343a40; 3 | color: #fff; 4 | } 5 | 6 | .form-control { 7 | padding: 0.8em !important; 8 | background: #f0f0f0; 9 | color: #aaa; 10 | /* Behavior changed in Bootstrap v4.1.3. See https://github.com/twbs/bootstrap/issues/27629 */ 11 | height: auto !important; 12 | } 13 | 14 | select.form-control { 15 | height: auto !important; 16 | } 17 | 18 | .custom-select { 19 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") 20 | no-repeat right 0.75rem center/8px 10px !important; 21 | } 22 | 23 | .card { 24 | border-radius: 0 !important; 25 | } 26 | 27 | #score-graph { 28 | height: 450px; 29 | display: block; 30 | clear: both; 31 | } 32 | 33 | .form-control { 34 | position: relative; 35 | display: block; 36 | padding: 0.8em !important; 37 | border-radius: 0; 38 | background: #f0f0f0; 39 | color: #aaa; 40 | font-weight: 400; 41 | font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; 42 | -webkit-appearance: none; 43 | /* Behavior changed in Bootstrap v4.1.3. See https://github.com/twbs/bootstrap/issues/27629 */ 44 | height: auto !important; 45 | } 46 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url("https://use.fontawesome.com/releases/v5.9.0/css/all.css"); 2 | @import url("https://fonts.googleapis.com/css?family=Lato:400,400i,700,700i|Raleway:400,400i,700,700i&subset=latin-ext"); 3 | 4 | /* Handle offline font loading */ 5 | $fa-font-path: "~@fortawesome/fontawesome-free/webfonts" !default; 6 | $fa-font-display: auto !default; 7 | @import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; 8 | @import "~@fortawesome/fontawesome-free/scss/regular.scss"; 9 | @import "~@fortawesome/fontawesome-free/scss/solid.scss"; 10 | @import "~@fortawesome/fontawesome-free/scss/brands.scss"; 11 | 12 | @import "~typeface-lato/index.css"; 13 | @import "~typeface-raleway/index.css"; 14 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/includes/award-icons.scss: -------------------------------------------------------------------------------- 1 | .award-icon { 2 | font-family: "Font Awesome 5 Free", "Font Awesome 5 Free Offline"; 3 | font-weight: 900; 4 | -moz-osx-font-smoothing: grayscale; 5 | -webkit-font-smoothing: antialiased; 6 | display: inline-block; 7 | font-style: normal; 8 | font-variant: normal; 9 | text-rendering: auto; 10 | line-height: 1; 11 | } 12 | 13 | .award-shield:before { 14 | content: "\f3ed"; 15 | } 16 | .award-bug:before { 17 | content: "\f188"; 18 | } 19 | .award-crown:before { 20 | content: "\f521"; 21 | } 22 | .award-crosshairs:before { 23 | content: "\f05b"; 24 | } 25 | .award-ban:before { 26 | content: "\f05e"; 27 | } 28 | .award-lightning:before { 29 | content: "\f0e7"; 30 | } 31 | .award-code:before { 32 | content: "\f121"; 33 | } 34 | .award-cowboy:before { 35 | content: "\f8c0"; 36 | } 37 | .award-angry:before { 38 | content: "\f556"; 39 | } 40 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/includes/jumbotron.css: -------------------------------------------------------------------------------- 1 | /* Move down content because we have a fixed navbar that is 3.5rem tall */ 2 | body { 3 | padding-top: 3.5rem; 4 | } 5 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/css/includes/sticky-footer.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | 8 | body { 9 | /* Margin bottom by footer height */ 10 | margin-bottom: 60px; 11 | } 12 | 13 | .footer { 14 | position: absolute; 15 | 16 | /* prevent scrollbars from showing on pages that don't use the full page height */ 17 | bottom: 1px; 18 | 19 | width: 100%; 20 | 21 | /* Set the fixed height of the footer here */ 22 | height: 60px; 23 | 24 | /* Vertically center the text there */ 25 | line-height: 60px; 26 | 27 | /* Avoid covering things */ 28 | z-index: -20; 29 | 30 | /*background-color: #f5f5f5;*/ 31 | } 32 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/CTFd.js: -------------------------------------------------------------------------------- 1 | import fetch from "./fetch"; 2 | import config from "./config"; 3 | import { API } from "./api"; 4 | import "./patch"; 5 | import MarkdownIt from "markdown-it"; 6 | import $ from "jquery"; 7 | import ezq from "./ezq"; 8 | 9 | const api = new API("/"); 10 | const user = {}; 11 | const _internal = {}; 12 | const ui = { 13 | ezq 14 | }; 15 | const lib = { 16 | $, 17 | markdown 18 | }; 19 | 20 | let initialized = false; 21 | const init = data => { 22 | if (initialized) { 23 | return; 24 | } 25 | initialized = true; 26 | 27 | config.urlRoot = data.urlRoot || config.urlRoot; 28 | config.csrfNonce = data.csrfNonce || config.csrfNonce; 29 | config.userMode = data.userMode || config.userMode; 30 | api.domain = config.urlRoot + "/api/v1"; 31 | user.id = data.userId; 32 | }; 33 | const plugin = { 34 | run: f => { 35 | f(CTFd); 36 | } 37 | }; 38 | function markdown(config) { 39 | // Merge passed config with original. Default to original. 40 | const md_config = { ...{ html: true, linkify: true }, ...config }; 41 | const md = MarkdownIt(md_config); 42 | md.renderer.rules.link_open = function(tokens, idx, options, env, self) { 43 | tokens[idx].attrPush(["target", "_blank"]); 44 | return self.renderToken(tokens, idx, options); 45 | }; 46 | return md; 47 | } 48 | 49 | const CTFd = { 50 | init, 51 | config, 52 | fetch, 53 | user, 54 | ui, 55 | api, 56 | lib, 57 | _internal, 58 | plugin 59 | }; 60 | 61 | export default CTFd; 62 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | urlRoot: "", 3 | csrfNonce: "", 4 | userMode: "" 5 | }; 6 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/fetch.js: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import config from "./config"; 3 | 4 | const fetch = window.fetch; 5 | 6 | export default (url, options) => { 7 | if (options === undefined) { 8 | options = { 9 | method: "GET", 10 | credentials: "same-origin", 11 | headers: {} 12 | }; 13 | } 14 | url = config.urlRoot + url; 15 | 16 | if (options.headers === undefined) { 17 | options.headers = {}; 18 | } 19 | options.credentials = "same-origin"; 20 | options.headers["Accept"] = "application/json"; 21 | options.headers["Content-Type"] = "application/json"; 22 | options.headers["CSRF-Token"] = config.csrfNonce; 23 | 24 | return fetch(url, options); 25 | }; 26 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/helpers.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import jQuery from "jquery"; 3 | import { default as ezq } from "./ezq"; 4 | import { htmlEntities, colorHash, copyToClipboard } from "./utils"; 5 | 6 | const utils = { 7 | htmlEntities: htmlEntities, 8 | colorHash: colorHash, 9 | copyToClipboard: copyToClipboard 10 | }; 11 | 12 | const files = { 13 | upload: (form, extra_data, cb) => { 14 | const CTFd = window.CTFd; 15 | if (form instanceof jQuery) { 16 | form = form[0]; 17 | } 18 | var formData = new FormData(form); 19 | formData.append("nonce", CTFd.config.csrfNonce); 20 | for (let [key, value] of Object.entries(extra_data)) { 21 | formData.append(key, value); 22 | } 23 | 24 | var pg = ezq.ezProgressBar({ 25 | width: 0, 26 | title: "Upload Progress" 27 | }); 28 | $.ajax({ 29 | url: CTFd.config.urlRoot + "/api/v1/files", 30 | data: formData, 31 | type: "POST", 32 | cache: false, 33 | contentType: false, 34 | processData: false, 35 | xhr: function() { 36 | var xhr = $.ajaxSettings.xhr(); 37 | xhr.upload.onprogress = function(e) { 38 | if (e.lengthComputable) { 39 | var width = (e.loaded / e.total) * 100; 40 | pg = ezq.ezProgressBar({ 41 | target: pg, 42 | width: width 43 | }); 44 | } 45 | }; 46 | return xhr; 47 | }, 48 | success: function(data) { 49 | form.reset(); 50 | pg = ezq.ezProgressBar({ 51 | target: pg, 52 | width: 100 53 | }); 54 | setTimeout(function() { 55 | pg.modal("hide"); 56 | }, 500); 57 | 58 | if (cb) { 59 | cb(data); 60 | } 61 | } 62 | }); 63 | } 64 | }; 65 | 66 | const helpers = { 67 | files, 68 | utils, 69 | ezq 70 | }; 71 | 72 | export default helpers; 73 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/pages/events.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import events from "../events"; 3 | import config from "../config"; 4 | 5 | $(() => { 6 | events(config.urlRoot); 7 | }); 8 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/pages/main.js: -------------------------------------------------------------------------------- 1 | import CTFd from "../CTFd"; 2 | import $ from "jquery"; 3 | import Moment from "moment"; 4 | import nunjucks from "nunjucks"; 5 | import { Howl } from "howler"; 6 | import events from "../events"; 7 | import config from "../config"; 8 | import styles from "../styles"; 9 | import times from "../times"; 10 | import { default as helpers } from "../helpers"; 11 | 12 | CTFd.init(window.init); 13 | window.CTFd = CTFd; 14 | window.helpers = helpers; 15 | window.$ = $; 16 | window.Moment = Moment; 17 | window.nunjucks = nunjucks; 18 | window.Howl = Howl; 19 | 20 | $(() => { 21 | styles(); 22 | times(); 23 | events(config.urlRoot); 24 | }); 25 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/pages/notifications.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | import $ from "jquery"; 3 | import CTFd from "../CTFd"; 4 | import { clear_notification_counter } from "../utils"; 5 | 6 | $(() => { 7 | clear_notification_counter(); 8 | }); 9 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/pages/style.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import styles from "../styles"; 3 | import times from "../times"; 4 | 5 | $(() => { 6 | styles(); 7 | times(); 8 | }); 9 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/styles.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/js/bootstrap.bundle"; 2 | import $ from "jquery"; 3 | 4 | export default () => { 5 | $(".form-control").bind({ 6 | focus: function() { 7 | $(this).removeClass("input-filled-invalid"); 8 | $(this).addClass("input-filled-valid"); 9 | }, 10 | blur: function() { 11 | if ($(this).val() === "") { 12 | $(this).removeClass("input-filled-invalid"); 13 | $(this).removeClass("input-filled-valid"); 14 | } 15 | } 16 | }); 17 | 18 | $(".form-control").each(function() { 19 | if ($(this).val()) { 20 | $(this).addClass("input-filled-valid"); 21 | } 22 | }); 23 | 24 | $('[data-toggle="tooltip"]').tooltip(); 25 | }; 26 | -------------------------------------------------------------------------------- /CTFd/themes/core/assets/js/times.js: -------------------------------------------------------------------------------- 1 | import Moment from "moment"; 2 | import $ from "jquery"; 3 | 4 | export default () => { 5 | $("[data-time]").each((i, elem) => { 6 | elem.innerText = Moment($(elem).data("time")) 7 | .local() 8 | .format("MMMM Do, h:mm:ss A"); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /CTFd/themes/core/static/css/challenge-board.dev.css: -------------------------------------------------------------------------------- 1 | .chal-desc{padding-left:30px;padding-right:30px;font-size:14px}.chal-desc img{max-width:100%;height:auto}.modal-content{border-radius:0px;max-width:1000px;padding:1em;margin:0 auto}.btn-info{background-color:#5b7290 !important}.badge-info{background-color:#5b7290 !important}.challenge-button{box-shadow:3px 3px 3px grey}.solved-challenge{background-color:#37d63e !important;opacity:0.4;border:none}.corner-button-check{margin-top:-10px;margin-right:25px;position:absolute;right:0}.key-submit .btn{height:51px} 2 | 3 | -------------------------------------------------------------------------------- /CTFd/themes/core/static/css/challenge-board.min.css: -------------------------------------------------------------------------------- 1 | .chal-desc{padding-left:30px;padding-right:30px;font-size:14px}.chal-desc img{max-width:100%;height:auto}.modal-content{border-radius:0;max-width:1000px;padding:1em;margin:0 auto}.badge-info,.btn-info{background-color:#5b7290!important}.challenge-button{box-shadow:3px 3px 3px grey}.solved-challenge{background-color:#37d63e!important;opacity:.4;border:none}.corner-button-check{margin-top:-10px;margin-right:25px;position:absolute;right:0}.key-submit .btn{height:51px} -------------------------------------------------------------------------------- /CTFd/themes/core/static/css/core.dev.css: -------------------------------------------------------------------------------- 1 | .jumbotron{background-color:#343a40;color:#fff}.form-control{padding:0.8em !important;background:#f0f0f0;color:#aaa;height:auto !important}select.form-control{height:auto !important}.custom-select{background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px !important}.card{border-radius:0 !important}#score-graph{height:450px;display:block;clear:both}.form-control{position:relative;display:block;padding:0.8em !important;border-radius:0;background:#f0f0f0;color:#aaa;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none;height:auto !important} 2 | 3 | -------------------------------------------------------------------------------- /CTFd/themes/core/static/css/core.min.css: -------------------------------------------------------------------------------- 1 | .jumbotron{background-color:#343a40;color:#fff}select.form-control{height:auto!important}.custom-select{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px!important}.card{border-radius:0!important}#score-graph{height:450px;display:block;clear:both}.form-control{position:relative;display:block;padding:.8em!important;border-radius:0;background:#f0f0f0;color:#aaa;font-weight:400;font-family:Avenir Next,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-appearance:none;height:auto!important} -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-brands-400.eot -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-brands-400.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-regular-400.eot -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-regular-400.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-solid-900.eot -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-solid-900.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-100.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-100.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-100italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-100italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-100italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-100italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-300.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-300.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-300italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-300italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-300italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-400.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-400.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-400italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-400italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-400italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-400italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-700.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-700.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-700italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-700italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-900.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-900.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-900italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-900italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/lato-latin-900italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/lato-latin-900italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-100.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-100.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-100italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-100italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-100italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-100italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-200.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-200.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-200.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-200.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-200italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-200italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-200italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-200italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-300.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-300.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-300italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-300italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-300italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-400.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-400.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-400italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-400italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-400italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-400italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-500.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-500.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-500italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-500italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-500italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-500italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-600.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-600.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-600italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-600italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-600italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-600italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-700.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-700.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-700italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-700italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-800.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-800.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-800italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-800italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-800italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-800italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-900.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-900.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-900italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-900italic.woff -------------------------------------------------------------------------------- /CTFd/themes/core/static/fonts/raleway-latin-900italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/fonts/raleway-latin-900italic.woff2 -------------------------------------------------------------------------------- /CTFd/themes/core/static/img/ctfd.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/img/ctfd.ai -------------------------------------------------------------------------------- /CTFd/themes/core/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/img/favicon.ico -------------------------------------------------------------------------------- /CTFd/themes/core/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/img/logo.png -------------------------------------------------------------------------------- /CTFd/themes/core/static/img/logo_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/img/logo_old.png -------------------------------------------------------------------------------- /CTFd/themes/core/static/img/scoreboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/img/scoreboard.png -------------------------------------------------------------------------------- /CTFd/themes/core/static/js/core.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/js/core.min.js -------------------------------------------------------------------------------- /CTFd/themes/core/static/sounds/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/sounds/notification.mp3 -------------------------------------------------------------------------------- /CTFd/themes/core/static/sounds/notification.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/themes/core/static/sounds/notification.webm -------------------------------------------------------------------------------- /CTFd/themes/core/templates/challenges.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |

Challenges

12 |
13 |
14 | 15 | {% if infos %} 16 |
17 |
18 |
19 | {% for info in infos %} 20 |

{{ info }}

21 | {% endfor %} 22 |
23 |
24 |
25 | {% endif %} 26 | 27 | {% if errors %} 28 |
29 |
30 |
31 | {% for error in errors %} 32 |

{{ error }}

33 | {% endfor %} 34 |
35 |
36 |
37 | {% endif %} 38 | 39 | {% if admin or not errors %} 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 52 | {% endif %} 53 | {% endblock %} 54 | 55 | {% block scripts %} 56 | {% endblock %} 57 | 58 | {% block entrypoint %} 59 | {% if admin or not errors %} 60 | 61 | {% endif %} 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Confirm

10 |
11 |
12 |
13 |
14 |
15 | {% for info in infos %} 16 | 22 | {% endfor %} 23 | {% for error in errors %} 24 | 30 | {% endfor %} 31 | {% if user %} 32 |

33 | We've sent a confirmation email to {{ user.email }} 34 |

35 | 36 |
37 | 38 |

39 | Please click the link in that email to confirm your account. 40 |

41 | {% endif %} 42 | 43 |
44 | 45 | {% if name %} 46 |
47 |

48 | Need to resend the confirmation email? 49 |

50 |
51 | 52 |
53 | 54 |
55 | {% endif %} 56 |
57 |
58 |
59 | {% endblock %} 60 | 61 | {% block scripts %} 62 | {% endblock %} 63 | 64 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

Forbidden

9 |

{{ error }}

10 |
11 |
12 |
13 | 14 | {% endblock %} 15 | 16 | {% block scripts %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

404

9 |

Whoops, looks like we can't find that.

10 |

Sorry about that

11 |
12 |
13 |
14 | 15 | 16 | {% endblock %} 17 | 18 | {% block scripts %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/errors/429.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

429

9 |

Too many requests

10 |

Please slow down!

11 |
12 |
13 |
14 | 15 | 16 | {% endblock %} 17 | 18 | {% block scripts %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

500

9 |

An Internal Server Error has occurred

10 |
11 |
12 |
13 | 14 | 15 | {% endblock %} 16 | 17 | {% block scripts %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/errors/502.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

502

9 |

Bad Gateway

10 |
11 |
12 |
13 | 14 | 15 | {% endblock %} 16 | 17 | {% block scripts %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/notifications.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Notifications

7 |
8 |
9 |
10 | {% if not notifications %} 11 |

There are no notifications yet

12 | {% endif %} 13 | {% for notification in notifications %} 14 |
15 |
16 |

{{ notification.title }}

17 |
18 |

{{ notification.content | safe }}

19 | 20 |
21 |
22 |
23 | {% endfor %} 24 |
25 | {% endblock %} 26 | 27 | {% block entrypoint %} 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {{ content | safe }} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Reset Password

10 |
11 |
12 |
13 |
14 |
15 | {% for error in errors %} 16 | 21 | {% endfor %} 22 | {% if can_send_mail() %} 23 |
24 | 25 | {% if mode %} 26 |
27 | 30 | 31 |
32 | {% else %} 33 |
34 | 37 | 38 |
39 | {% endif %} 40 |
41 |
42 | 43 |
44 |
45 |
46 | {% else %} 47 |

Contact a CTF organizer

48 |

This CTF is not configured to send email.

49 |

Please contact an organizer to have your password reset

50 | {% endif %} 51 |
52 |
53 |
54 | {% endblock %} 55 | 56 | {% block scripts %} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/teams/join_team.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Join Team

10 |
11 |
12 |
13 |
14 |
15 | {% for info in infos %} 16 | 22 | {% endfor %} 23 | {% for error in errors %} 24 | 30 | {% endfor %} 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | {% endblock %} 53 | 54 | {% block scripts %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/teams/new_team.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Create Team

10 |
11 |
12 |
13 |
14 |
15 | {% for info in infos %} 16 | 22 | {% endfor %} 23 | {% for error in errors %} 24 | 30 | {% endfor %} 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 |

After creating your team, share the team name and password with your teammates so they can join your team.

44 | 47 |
48 |
49 |
50 |
51 |
52 |
53 | {% endblock %} 54 | 55 | {% block scripts %} 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /CTFd/themes/core/templates/teams/team_enrollment.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Team

10 |
11 |
12 |
13 |
14 |
15 |

Welcome to {{ get_ctf_name() }}!

16 |

17 | In order to participate you must either join or create a team. 18 |

19 |
20 |
21 | {% if integrations.mlc() %} 22 |
23 | 26 |
27 |
28 | 31 | 34 |
35 | {% else %} 36 |
37 |
38 | Join Team 39 |
40 |
41 | Create Team 42 |
43 |
44 | {% endif %} 45 |
46 | {% endblock %} 47 | 48 | {% block scripts %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /CTFd/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/uploads/.gitkeep -------------------------------------------------------------------------------- /CTFd/users.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request 2 | 3 | from CTFd.models import Users 4 | from CTFd.utils import config 5 | from CTFd.utils.decorators import authed_only 6 | from CTFd.utils.decorators.visibility import ( 7 | check_account_visibility, 8 | check_score_visibility, 9 | ) 10 | from CTFd.utils.user import get_current_user 11 | 12 | users = Blueprint("users", __name__) 13 | 14 | 15 | @users.route("/users") 16 | @check_account_visibility 17 | def listing(): 18 | page = abs(request.args.get("page", 1, type=int)) 19 | results_per_page = 50 20 | page_start = results_per_page * (page - 1) 21 | page_end = results_per_page * (page - 1) + results_per_page 22 | 23 | count = Users.query.filter_by(banned=False, hidden=False).count() 24 | users = ( 25 | Users.query.filter_by(banned=False, hidden=False) 26 | .slice(page_start, page_end) 27 | .all() 28 | ) 29 | 30 | pages = int(count / results_per_page) + (count % results_per_page > 0) 31 | return render_template("users/users.html", users=users, pages=pages, curr_page=page) 32 | 33 | 34 | @users.route("/profile") 35 | @users.route("/user") 36 | @authed_only 37 | def private(): 38 | user = get_current_user() 39 | 40 | solves = user.get_solves() 41 | awards = user.get_awards() 42 | 43 | place = user.place 44 | score = user.score 45 | 46 | return render_template( 47 | "users/private.html", 48 | solves=solves, 49 | awards=awards, 50 | user=user, 51 | score=score, 52 | place=place, 53 | score_frozen=config.is_scoreboard_frozen(), 54 | ) 55 | 56 | 57 | @users.route("/users/") 58 | @check_account_visibility 59 | @check_score_visibility 60 | def public(user_id): 61 | user = Users.query.filter_by(id=user_id, banned=False, hidden=False).first_or_404() 62 | return render_template("users/public.html", user=user) 63 | -------------------------------------------------------------------------------- /CTFd/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | import six 3 | from flask import current_app as app 4 | 5 | from CTFd.cache import cache 6 | 7 | if six.PY2: 8 | string_types = (str, unicode) # noqa: F821 9 | text_type = unicode # noqa: F821 10 | binary_type = str 11 | else: 12 | string_types = (str,) 13 | text_type = str 14 | binary_type = bytes 15 | 16 | markdown = mistune.Markdown() 17 | 18 | 19 | def get_app_config(key, default=None): 20 | value = app.config.get(key, default) 21 | return value 22 | 23 | 24 | @cache.memoize() 25 | def _get_config(key): 26 | config = Configs.query.filter_by(key=key).first() 27 | if config and config.value: 28 | value = config.value 29 | if value and value.isdigit(): 30 | return int(value) 31 | elif value and isinstance(value, six.string_types): 32 | if value.lower() == "true": 33 | return True 34 | elif value.lower() == "false": 35 | return False 36 | else: 37 | return value 38 | # Flask-Caching is unable to roundtrip a value of None. 39 | # Return an exception so that we can still cache and avoid the db hit 40 | return KeyError 41 | 42 | 43 | def get_config(key, default=None): 44 | value = _get_config(key) 45 | if value is KeyError: 46 | return default 47 | else: 48 | return value 49 | 50 | 51 | def set_config(key, value): 52 | config = Configs.query.filter_by(key=key).first() 53 | if config: 54 | config.value = value 55 | else: 56 | config = Configs(key=key, value=value) 57 | db.session.add(config) 58 | db.session.commit() 59 | cache.delete_memoized(_get_config, key) 60 | return config 61 | 62 | 63 | from CTFd.models import Configs, db # noqa: E402 64 | -------------------------------------------------------------------------------- /CTFd/utils/config/integrations.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils import get_config 2 | 3 | 4 | def mlc(): 5 | return get_config("oauth_client_id") and get_config("oauth_client_secret") 6 | -------------------------------------------------------------------------------- /CTFd/utils/config/pages.py: -------------------------------------------------------------------------------- 1 | from CTFd.cache import cache 2 | from CTFd.models import Pages 3 | 4 | 5 | @cache.memoize() 6 | def get_pages(): 7 | db_pages = Pages.query.filter( 8 | Pages.route != "index", Pages.draft.isnot(True), Pages.hidden.isnot(True) 9 | ).all() 10 | return db_pages 11 | 12 | 13 | @cache.memoize() 14 | def get_page(route): 15 | return Pages.query.filter(Pages.route == route, Pages.draft.isnot(True)).first() 16 | -------------------------------------------------------------------------------- /CTFd/utils/config/visibility.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils import get_config 2 | from CTFd.utils.user import authed, is_admin 3 | 4 | 5 | def challenges_visible(): 6 | v = get_config("challenge_visibility") 7 | if v == "public": 8 | return True 9 | elif v == "private": 10 | return authed() 11 | elif v == "admins": 12 | return is_admin() 13 | 14 | 15 | def scores_visible(): 16 | v = get_config("score_visibility") 17 | if v == "public": 18 | return True 19 | elif v == "private": 20 | return authed() 21 | elif v == "hidden": 22 | return False 23 | elif v == "admins": 24 | return is_admin() 25 | 26 | 27 | def accounts_visible(): 28 | v = get_config("account_visibility") 29 | if v == "public": 30 | return True 31 | elif v == "private": 32 | return authed() 33 | elif v == "admins": 34 | return is_admin() 35 | 36 | 37 | def registration_visible(): 38 | v = get_config("registration_visibility") 39 | if v == "public": 40 | return True 41 | elif v == "private": 42 | return False 43 | -------------------------------------------------------------------------------- /CTFd/utils/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from passlib.hash import bcrypt_sha256 4 | 5 | from CTFd.utils import string_types 6 | 7 | 8 | def hash_password(plaintext): 9 | return bcrypt_sha256.hash(str(plaintext)) 10 | 11 | 12 | def verify_password(plaintext, ciphertext): 13 | return bcrypt_sha256.verify(plaintext, ciphertext) 14 | 15 | 16 | def sha256(p): 17 | if isinstance(p, string_types): 18 | p = p.encode("utf-8") 19 | return hashlib.sha256(p).hexdigest() 20 | -------------------------------------------------------------------------------- /CTFd/utils/dates/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | from CTFd.utils import get_config 5 | 6 | 7 | def ctftime(): 8 | """ Checks whether it's CTF time or not. """ 9 | 10 | start = get_config("start") 11 | end = get_config("end") 12 | 13 | if start: 14 | start = int(start) 15 | else: 16 | start = 0 17 | if end: 18 | end = int(end) 19 | else: 20 | end = 0 21 | 22 | if start and end: 23 | if start < time.time() < end: 24 | # Within the two time bounds 25 | return True 26 | 27 | if start < time.time() and end == 0: 28 | # CTF starts on a date but never ends 29 | return True 30 | 31 | if start == 0 and time.time() < end: 32 | # CTF started but ends at a date 33 | return True 34 | 35 | if start == 0 and end == 0: 36 | # CTF has no time requirements 37 | return True 38 | 39 | return False 40 | 41 | 42 | def ctf_paused(): 43 | return get_config("paused") 44 | 45 | 46 | def ctf_started(): 47 | return time.time() > int(get_config("start") or 0) 48 | 49 | 50 | def ctf_ended(): 51 | if int(get_config("end") or 0): 52 | return time.time() > int(get_config("end") or 0) 53 | return False 54 | 55 | 56 | def view_after_ctf(): 57 | return get_config("view_after_ctf") 58 | 59 | 60 | def unix_time(dt): 61 | return int((dt - datetime.datetime(1970, 1, 1)).total_seconds()) 62 | 63 | 64 | def unix_time_millis(dt): 65 | return unix_time(dt) * 1000 66 | 67 | 68 | def unix_time_to_utc(t): 69 | return datetime.datetime.utcfromtimestamp(t) 70 | 71 | 72 | def isoformat(dt): 73 | return dt.isoformat() + "Z" 74 | -------------------------------------------------------------------------------- /CTFd/utils/decorators/modes.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from flask import abort 4 | 5 | from CTFd.utils import get_config 6 | from CTFd.utils.modes import TEAMS_MODE, USERS_MODE 7 | 8 | 9 | def require_team_mode(f): 10 | @functools.wraps(f) 11 | def _require_team_mode(*args, **kwargs): 12 | if get_config("user_mode") == USERS_MODE: 13 | abort(404) 14 | return f(*args, **kwargs) 15 | 16 | return _require_team_mode 17 | 18 | 19 | def require_user_mode(f): 20 | @functools.wraps(f) 21 | def _require_user_mode(*args, **kwargs): 22 | if get_config("user_mode") == TEAMS_MODE: 23 | abort(404) 24 | return f(*args, **kwargs) 25 | 26 | return _require_user_mode 27 | -------------------------------------------------------------------------------- /CTFd/utils/email/mailgun.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from CTFd.utils import get_app_config, get_config 4 | 5 | 6 | def sendmail(addr, text, subject): 7 | ctf_name = get_config("ctf_name") 8 | mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR") 9 | mailfrom_addr = "{} <{}>".format(ctf_name, mailfrom_addr) 10 | 11 | mailgun_base_url = get_config("mailgun_base_url") or get_app_config( 12 | "MAILGUN_BASE_URL" 13 | ) 14 | mailgun_api_key = get_config("mailgun_api_key") or get_app_config("MAILGUN_API_KEY") 15 | try: 16 | r = requests.post( 17 | mailgun_base_url + "/messages", 18 | auth=("api", mailgun_api_key), 19 | data={ 20 | "from": mailfrom_addr, 21 | "to": [addr], 22 | "subject": subject, 23 | "text": text, 24 | }, 25 | timeout=1.0, 26 | ) 27 | except requests.RequestException as e: 28 | return ( 29 | False, 30 | "{error} exception occured while handling your request".format( 31 | error=type(e).__name__ 32 | ), 33 | ) 34 | 35 | if r.status_code == 200: 36 | return True, "Email sent" 37 | else: 38 | return False, "Mailgun settings are incorrect" 39 | -------------------------------------------------------------------------------- /CTFd/utils/encoding/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import codecs 3 | 4 | import six 5 | 6 | from CTFd.utils import string_types 7 | 8 | 9 | def hexencode(s): 10 | if six.PY3 and isinstance(s, string_types): 11 | s = s.encode("utf-8") 12 | encoded = codecs.encode(s, "hex") 13 | if six.PY3: 14 | try: 15 | encoded = encoded.decode("utf-8") 16 | except UnicodeDecodeError: 17 | pass 18 | return encoded 19 | 20 | 21 | def hexdecode(s): 22 | decoded = codecs.decode(s, "hex") 23 | if six.PY3: 24 | try: 25 | decoded = decoded.decode("utf-8") 26 | except UnicodeDecodeError: 27 | pass 28 | return decoded 29 | 30 | 31 | def base64encode(s): 32 | if six.PY3 and isinstance(s, string_types): 33 | s = s.encode("utf-8") 34 | else: 35 | # Python 2 support because the base64 module doesnt like unicode 36 | s = str(s) 37 | 38 | encoded = base64.urlsafe_b64encode(s).rstrip(b"\n=") 39 | if six.PY3: 40 | try: 41 | encoded = encoded.decode("utf-8") 42 | except UnicodeDecodeError: 43 | pass 44 | return encoded 45 | 46 | 47 | def base64decode(s): 48 | if six.PY3 and isinstance(s, string_types): 49 | s = s.encode("utf-8") 50 | else: 51 | # Python 2 support because the base64 module doesnt like unicode 52 | s = str(s) 53 | 54 | decoded = base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b"=")) 55 | if six.PY3: 56 | try: 57 | decoded = decoded.decode("utf-8") 58 | except UnicodeDecodeError: 59 | pass 60 | return decoded 61 | -------------------------------------------------------------------------------- /CTFd/utils/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def safe_format(fmt, **kwargs): 5 | """ 6 | Function that safely formats strings with arbitrary potentially user-supplied format strings 7 | Looks for interpolation placeholders like {target} or {{ target }} 8 | """ 9 | return re.sub( 10 | r"\{?\{([^{}]*)\}\}?", lambda m: kwargs.get(m.group(1).strip(), m.group(0)), fmt 11 | ) 12 | -------------------------------------------------------------------------------- /CTFd/utils/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import current_app, flash, get_flashed_messages, request 4 | 5 | 6 | def info_for(endpoint, message): 7 | flash(message=message, category=endpoint + ".infos") 8 | 9 | 10 | def error_for(endpoint, message): 11 | flash(message=message, category=endpoint + ".errors") 12 | 13 | 14 | def get_infos(): 15 | return get_flashed_messages(category_filter=request.endpoint + ".infos") 16 | 17 | 18 | def get_errors(): 19 | return get_flashed_messages(category_filter=request.endpoint + ".errors") 20 | 21 | 22 | @current_app.url_defaults 23 | def env_asset_url_default(endpoint, values): 24 | """Create asset URLs dependent on the current env""" 25 | if endpoint == "views.themes": 26 | path = values.get("path", "") 27 | static_asset = path.endswith(".js") or path.endswith(".css") 28 | direct_access = ".dev" in path or ".min" in path 29 | if static_asset and not direct_access: 30 | env = values.get("env", current_app.env) 31 | mode = ".dev" if env == "development" else ".min" 32 | base, ext = os.path.splitext(path) 33 | values["path"] = base + mode + ext 34 | 35 | 36 | @current_app.url_defaults 37 | def asset_cache_url_default(endpoint, values): 38 | """Used to cache bust per server restarts""" 39 | if endpoint == "views.themes": 40 | values["d"] = current_app.run_id 41 | -------------------------------------------------------------------------------- /CTFd/utils/humanize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/utils/humanize/__init__.py -------------------------------------------------------------------------------- /CTFd/utils/humanize/numbers.py: -------------------------------------------------------------------------------- 1 | def ordinalize(n): 2 | """ 3 | http://codegolf.stackexchange.com/a/4712 4 | """ 5 | k = n % 10 6 | return "%d%s" % (n, "tsnrhtdd"[(n // 10 % 10 != 1) * (k < 4) * k :: 4]) 7 | -------------------------------------------------------------------------------- /CTFd/utils/logging/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import time 4 | 5 | from flask import session 6 | 7 | from CTFd.utils.user import get_ip 8 | 9 | 10 | def log(logger, format, **kwargs): 11 | logger = logging.getLogger(logger) 12 | props = { 13 | "id": session.get("id"), 14 | "name": session.get("name"), 15 | "email": session.get("email"), 16 | "type": session.get("type"), 17 | "date": time.strftime("%m/%d/%Y %X"), 18 | "ip": get_ip(), 19 | } 20 | props.update(kwargs) 21 | msg = format.format(**props) 22 | print(msg) 23 | logger.info(msg) 24 | -------------------------------------------------------------------------------- /CTFd/utils/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from alembic.migration import MigrationContext 4 | from flask import current_app as app 5 | from flask_migrate import Migrate, stamp 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.engine.url import make_url 8 | from sqlalchemy_utils import create_database as create_database_util 9 | from sqlalchemy_utils import database_exists as database_exists_util 10 | from sqlalchemy_utils import drop_database as drop_database_util 11 | 12 | migrations = Migrate() 13 | 14 | 15 | def create_database(): 16 | url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) 17 | if url.drivername == "postgres": 18 | url.drivername = "postgresql" 19 | 20 | if url.drivername.startswith("mysql"): 21 | url.query["charset"] = "utf8mb4" 22 | 23 | # Creates database if the database database does not exist 24 | if not database_exists_util(url): 25 | if url.drivername.startswith("mysql"): 26 | create_database_util(url, encoding="utf8mb4") 27 | else: 28 | create_database_util(url) 29 | return url 30 | 31 | 32 | def drop_database(): 33 | url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) 34 | if url.drivername == "postgres": 35 | url.drivername = "postgresql" 36 | drop_database_util(url) 37 | 38 | 39 | def get_current_revision(): 40 | engine = create_engine(app.config.get("SQLALCHEMY_DATABASE_URI")) 41 | conn = engine.connect() 42 | context = MigrationContext.configure(conn) 43 | current_rev = context.get_current_revision() 44 | return current_rev 45 | 46 | 47 | def stamp_latest_revision(): 48 | # Get proper migrations directory regardless of cwd 49 | directory = os.path.join(os.path.dirname(app.root_path), "migrations") 50 | stamp(directory=directory) 51 | -------------------------------------------------------------------------------- /CTFd/utils/modes/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | 3 | from CTFd.models import Teams, Users 4 | from CTFd.utils import get_config 5 | 6 | USERS_MODE = "users" 7 | TEAMS_MODE = "teams" 8 | 9 | 10 | def generate_account_url(account_id, admin=False): 11 | if get_config("user_mode") == USERS_MODE: 12 | if admin: 13 | return url_for("admin.users_detail", user_id=account_id) 14 | else: 15 | return url_for("users.public", user_id=account_id) 16 | elif get_config("user_mode") == TEAMS_MODE: 17 | if admin: 18 | return url_for("admin.teams_detail", team_id=account_id) 19 | else: 20 | return url_for("teams.public", team_id=account_id) 21 | 22 | 23 | def get_model(): 24 | if get_config("user_mode") == USERS_MODE: 25 | return Users 26 | elif get_config("user_mode") == TEAMS_MODE: 27 | return Teams 28 | 29 | 30 | def get_mode_as_word(plural=False, capitalize=False): 31 | if get_config("user_mode") == USERS_MODE: 32 | word = "user" 33 | else: 34 | word = "team" 35 | 36 | if plural: 37 | word += "s" 38 | if capitalize: 39 | word = word.title() 40 | return word 41 | -------------------------------------------------------------------------------- /CTFd/utils/notifications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/utils/notifications/__init__.py -------------------------------------------------------------------------------- /CTFd/utils/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from collections import namedtuple 4 | 5 | from flask import current_app as app 6 | 7 | 8 | def register_script(url): 9 | app.plugin_scripts.append(url) 10 | 11 | 12 | def register_stylesheet(url): 13 | app.plugin_stylesheets.append(url) 14 | 15 | 16 | def register_admin_script(url): 17 | app.admin_plugin_scripts.append(url) 18 | 19 | 20 | def register_admin_stylesheet(url): 21 | app.admin_plugin_stylesheets.append(url) 22 | 23 | 24 | def get_registered_scripts(): 25 | return app.plugin_scripts 26 | 27 | 28 | def get_registered_stylesheets(): 29 | return app.plugin_stylesheets 30 | 31 | 32 | def get_registered_admin_scripts(): 33 | return app.admin_plugin_scripts 34 | 35 | 36 | def get_registered_admin_stylesheets(): 37 | return app.admin_plugin_stylesheets 38 | 39 | 40 | def override_template(template, html): 41 | app.jinja_loader.overriden_templates[template] = html 42 | 43 | 44 | def get_configurable_plugins(): 45 | Plugin = namedtuple("Plugin", ["name", "route"]) 46 | 47 | plugins_path = os.path.join(app.root_path, "plugins") 48 | plugin_directories = os.listdir(plugins_path) 49 | 50 | plugins = [] 51 | 52 | for dir in plugin_directories: 53 | if os.path.isfile(os.path.join(plugins_path, dir, "config.json")): 54 | path = os.path.join(plugins_path, dir, "config.json") 55 | with open(path) as f: 56 | plugin_json_data = json.loads(f.read()) 57 | p = Plugin( 58 | name=plugin_json_data.get("name"), 59 | route=plugin_json_data.get("route"), 60 | ) 61 | plugins.append(p) 62 | elif os.path.isfile(os.path.join(plugins_path, dir, "config.html")): 63 | p = Plugin(name=dir, route="/admin/plugins/{}".format(dir)) 64 | plugins.append(p) 65 | 66 | return plugins 67 | -------------------------------------------------------------------------------- /CTFd/utils/security/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/CTFd/utils/security/__init__.py -------------------------------------------------------------------------------- /CTFd/utils/security/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from flask import session 5 | 6 | from CTFd.exceptions import UserNotFoundException, UserTokenExpiredException 7 | from CTFd.models import UserTokens, db 8 | from CTFd.utils.encoding import hexencode 9 | from CTFd.utils.security.csrf import generate_nonce 10 | 11 | 12 | def login_user(user): 13 | session["id"] = user.id 14 | session["name"] = user.name 15 | session["type"] = user.type 16 | session["email"] = user.email 17 | session["nonce"] = generate_nonce() 18 | 19 | 20 | def logout_user(): 21 | session.clear() 22 | 23 | 24 | def generate_user_token(user, expiration=None): 25 | temp_token = True 26 | while temp_token is not None: 27 | value = hexencode(os.urandom(32)) 28 | temp_token = UserTokens.query.filter_by(value=value).first() 29 | 30 | token = UserTokens( 31 | user_id=user.id, expiration=expiration, value=hexencode(os.urandom(32)) 32 | ) 33 | db.session.add(token) 34 | db.session.commit() 35 | return token 36 | 37 | 38 | def lookup_user_token(token): 39 | token = UserTokens.query.filter_by(value=token).first() 40 | if token: 41 | if datetime.datetime.utcnow() >= token.expiration: 42 | raise UserTokenExpiredException 43 | return token.user 44 | else: 45 | raise UserNotFoundException 46 | return None 47 | -------------------------------------------------------------------------------- /CTFd/utils/security/csrf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from CTFd.utils.encoding import hexencode 4 | 5 | 6 | def generate_nonce(): 7 | return hexencode(os.urandom(32)) 8 | -------------------------------------------------------------------------------- /CTFd/utils/security/passwords.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils.crypto import hash_password as hp 2 | from CTFd.utils.crypto import sha256 as sha 3 | from CTFd.utils.crypto import verify_password as vp 4 | 5 | 6 | def hash_password(p): 7 | print( 8 | "This function will be deprecated in a future release. Please update to CTFd.utils.crypto.hash_password" 9 | ) 10 | return hp(p) 11 | 12 | 13 | def check_password(p, hash): 14 | print( 15 | "This function will be deprecated in a future release. Please update to CTFd.utils.crypto.verify_password" 16 | ) 17 | return vp(p, hash) 18 | 19 | 20 | def sha256(p): 21 | print( 22 | "This function will be deprecated in a future release. Please update to CTFd.utils.crypto.sha256" 23 | ) 24 | return sha(p) 25 | -------------------------------------------------------------------------------- /CTFd/utils/security/signing.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from itsdangerous import Signer 3 | from itsdangerous.exc import ( # noqa: F401 4 | BadSignature, 5 | BadTimeSignature, 6 | SignatureExpired, 7 | ) 8 | from itsdangerous.url_safe import URLSafeTimedSerializer 9 | 10 | 11 | def serialize(data, secret=None): 12 | if secret is None: 13 | secret = current_app.config["SECRET_KEY"] 14 | s = URLSafeTimedSerializer(secret) 15 | return s.dumps(data) 16 | 17 | 18 | def unserialize(data, secret=None, max_age=432000): 19 | if secret is None: 20 | secret = current_app.config["SECRET_KEY"] 21 | s = URLSafeTimedSerializer(secret) 22 | return s.loads(data, max_age=max_age) 23 | 24 | 25 | def sign(data, secret=None): 26 | if secret is None: 27 | secret = current_app.config["SECRET_KEY"] 28 | s = Signer(secret) 29 | return s.sign(data) 30 | 31 | 32 | def unsign(data, secret=None): 33 | if secret is None: 34 | secret = current_app.config["SECRET_KEY"] 35 | s = Signer(secret) 36 | return s.unsign(data) 37 | -------------------------------------------------------------------------------- /CTFd/utils/uploads/__init__.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | from CTFd.models import ChallengeFiles, Files, PageFiles, db 4 | from CTFd.utils import get_app_config 5 | from CTFd.utils.uploads.uploaders import FilesystemUploader, S3Uploader 6 | 7 | UPLOADERS = {"filesystem": FilesystemUploader, "s3": S3Uploader} 8 | 9 | 10 | def get_uploader(): 11 | return UPLOADERS.get(get_app_config("UPLOAD_PROVIDER") or "filesystem")() 12 | 13 | 14 | def upload_file(*args, **kwargs): 15 | file_obj = kwargs.get("file") 16 | challenge_id = kwargs.get("challenge_id") or kwargs.get("challenge") 17 | page_id = kwargs.get("page_id") or kwargs.get("page") 18 | file_type = kwargs.get("type", "standard") 19 | 20 | model_args = {"type": file_type, "location": None} 21 | 22 | model = Files 23 | if file_type == "challenge": 24 | model = ChallengeFiles 25 | model_args["challenge_id"] = challenge_id 26 | if file_type == "page": 27 | model = PageFiles 28 | model_args["page_id"] = page_id 29 | 30 | uploader = get_uploader() 31 | location = uploader.upload(file_obj=file_obj, filename=file_obj.filename) 32 | 33 | model_args["location"] = location 34 | 35 | file_row = model(**model_args) 36 | db.session.add(file_row) 37 | db.session.commit() 38 | return file_row 39 | 40 | 41 | def delete_file(file_id): 42 | f = Files.query.filter_by(id=file_id).first_or_404() 43 | 44 | uploader = get_uploader() 45 | uploader.delete(filename=f.location) 46 | 47 | db.session.delete(f) 48 | db.session.commit() 49 | return True 50 | 51 | 52 | def rmdir(directory): 53 | shutil.rmtree(directory, ignore_errors=True) 54 | -------------------------------------------------------------------------------- /CTFd/utils/validators/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from flask import request 4 | from marshmallow import ValidationError 5 | from six.moves.urllib.parse import urljoin, urlparse 6 | 7 | from CTFd.models import Users 8 | from CTFd.utils.countries import lookup_country_code 9 | from CTFd.utils.user import get_current_user, is_admin 10 | 11 | EMAIL_REGEX = r"(^[^@\s]+@[^@\s]+\.[^@\s]+$)" 12 | 13 | 14 | def is_safe_url(target): 15 | ref_url = urlparse(request.host_url) 16 | test_url = urlparse(urljoin(request.host_url, target)) 17 | return test_url.scheme in ("http", "https") and ref_url.netloc == test_url.netloc 18 | 19 | 20 | def validate_url(url): 21 | return urlparse(url).scheme.startswith("http") 22 | 23 | 24 | def validate_email(email): 25 | return bool(re.match(EMAIL_REGEX, email)) 26 | 27 | 28 | def unique_email(email, model=Users): 29 | obj = model.query.filter_by(email=email).first() 30 | if is_admin(): 31 | if obj: 32 | raise ValidationError("Email address has already been used") 33 | if obj and obj.id != get_current_user().id: 34 | raise ValidationError("Email address has already been used") 35 | 36 | 37 | def validate_country_code(country_code): 38 | if country_code.strip() == "": 39 | return 40 | if lookup_country_code(country_code) is None: 41 | raise ValidationError("Invalid Country") 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | RUN apk update && \ 3 | apk add python python-dev linux-headers libffi-dev gcc make musl-dev py-pip mysql-client git openssl-dev 4 | RUN adduser -D -u 1001 -s /bin/bash ctfd 5 | 6 | WORKDIR /opt/CTFd 7 | RUN mkdir -p /opt/CTFd /var/log/CTFd /var/uploads 8 | 9 | COPY requirements.txt . 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | COPY . /opt/CTFd 14 | 15 | RUN for d in CTFd/plugins/*; do \ 16 | if [ -f "$d/requirements.txt" ]; then \ 17 | pip install -r $d/requirements.txt; \ 18 | fi; \ 19 | done; 20 | 21 | RUN chmod +x /opt/CTFd/docker-entrypoint.sh 22 | RUN chown -R 1001:1001 /opt/CTFd 23 | RUN chown -R 1001:1001 /var/log/CTFd /var/uploads 24 | 25 | USER 1001 26 | EXPOSE 8000 27 | ENTRYPOINT ["/opt/CTFd/docker-entrypoint.sh"] 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | flake8 --ignore=E402,E501,E712,W503,E203,I002 --exclude=CTFd/uploads CTFd/ migrations/ tests/ 3 | black --check --exclude=CTFd/uploads --exclude=node_modules . 4 | prettier --check 'CTFd/themes/**/assets/**/*' 5 | 6 | format: 7 | black --exclude=CTFd/uploads --exclude=node_modules . 8 | prettier --write 'CTFd/themes/**/assets/**/*' 9 | 10 | test: 11 | pytest --cov=CTFd --ignore=node_modules/ --disable-warnings -n auto 12 | bandit -r CTFd -x CTFd/uploads 13 | yarn verify 14 | 15 | serve: 16 | python serve.py 17 | 18 | shell: 19 | python manage.py shell 20 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==4.4.0 3 | pytest-randomly==1.2.3 4 | coverage==4.5.2 5 | mock==2.0.0 6 | flake8==3.7.7 7 | freezegun==0.3.11 8 | psycopg2==2.7.5 9 | psycopg2-binary==2.7.5 10 | codecov==2.0.15 11 | moto==1.3.7 12 | bandit==1.5.1 13 | flask_profiler==1.7 14 | pytest-xdist==1.28.0 15 | pytest-cov==2.6.1 16 | sphinx_rtd_theme==0.4.3 17 | flask-debugtoolbar==0.10.1 18 | flake8-isort==2.8.0 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | ctfd: 5 | build: . 6 | user: root 7 | restart: always 8 | ports: 9 | - "8000:8000" 10 | environment: 11 | - UPLOAD_FOLDER=/var/uploads 12 | - DATABASE_URL=mysql+pymysql://root:ctfd@db/ctfd 13 | - REDIS_URL=redis://cache:6379 14 | - WORKERS=1 15 | - LOG_FOLDER=/var/log/CTFd 16 | - ACCESS_LOG=- 17 | - ERROR_LOG=- 18 | volumes: 19 | - .data/CTFd/logs:/var/log/CTFd 20 | - .data/CTFd/uploads:/var/uploads 21 | - .:/opt/CTFd:ro 22 | depends_on: 23 | - db 24 | networks: 25 | default: 26 | internal: 27 | 28 | db: 29 | image: mariadb:10.4 30 | restart: always 31 | environment: 32 | - MYSQL_ROOT_PASSWORD=ctfd 33 | - MYSQL_USER=ctfd 34 | - MYSQL_PASSWORD=ctfd 35 | - MYSQL_DATABASE=ctfd 36 | volumes: 37 | - .data/mysql:/var/lib/mysql 38 | networks: 39 | internal: 40 | # This command is required to set important mariadb defaults 41 | command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --wait_timeout=28800, --log-warnings=0] 42 | 43 | cache: 44 | image: redis:4 45 | restart: always 46 | volumes: 47 | - .data/redis:/data 48 | networks: 49 | internal: 50 | 51 | networks: 52 | default: 53 | internal: 54 | internal: true 55 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eo pipefail 3 | 4 | WORKERS=${WORKERS:-1} 5 | WORKER_CLASS=${WORKER_CLASS:-gevent} 6 | ACCESS_LOG=${ACCESS_LOG:--} 7 | ERROR_LOG=${ERROR_LOG:--} 8 | WORKER_TEMP_DIR=${WORKER_TEMP_DIR:-/dev/shm} 9 | 10 | # Check that a .ctfd_secret_key file or SECRET_KEY envvar is set 11 | if [ ! -f .ctfd_secret_key ] && [ -z "$SECRET_KEY" ]; then 12 | if [ $WORKERS -gt 1 ]; then 13 | echo "[ ERROR ] You are configured to use more than 1 worker." 14 | echo "[ ERROR ] To do this, you must define the SECRET_KEY environment variable or create a .ctfd_secret_key file." 15 | echo "[ ERROR ] Exiting..." 16 | exit 1 17 | fi 18 | fi 19 | 20 | # Check that the database is available 21 | if [ -n "$DATABASE_URL" ] 22 | then 23 | url=`echo $DATABASE_URL | awk -F[@//] '{print $4}'` 24 | database=`echo $url | awk -F[:] '{print $1}'` 25 | port=`echo $url | awk -F[:] '{print $2}'` 26 | echo "Waiting for $database:$port to be ready" 27 | while ! mysqladmin ping -h "$database" -P "$port" --silent; do 28 | # Show some progress 29 | echo -n '.'; 30 | sleep 1; 31 | done 32 | echo "$database is ready" 33 | # Give it another second. 34 | sleep 1; 35 | fi 36 | 37 | # Initialize database 38 | python manage.py db upgrade 39 | 40 | # Start CTFd 41 | echo "Starting CTFd" 42 | exec gunicorn 'CTFd:create_app()' \ 43 | --bind '0.0.0.0:8000' \ 44 | --workers $WORKERS \ 45 | --worker-tmp-dir "$WORKER_TEMP_DIR" \ 46 | --worker-class "$WORKER_CLASS" \ 47 | --access-logfile "$ACCESS_LOG" \ 48 | --error-logfile "$ERROR_LOG" 49 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. CTFd documentation master file, created by 2 | sphinx-quickstart on Tue Apr 9 15:14:45 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | CTFd 7 | ==== 8 | 9 | .. image:: https://raw.githubusercontent.com/CTFd/CTFd/master/CTFd/themes/core/static/img/logo.png 10 | :width: 400 11 | :alt: CTFd Logo 12 | 13 | CTFd is a Capture The Flag framework focusing on ease of use and customizability. 14 | 15 | CTFd comes with most of the features needed by an event organizer to run a competition or workshop. In addition, if CTFd's feature set is insufficient, CTFd allows for the usage of plugins and themes to control almost every aspect of how it looks and behaves. 16 | 17 | CTFd is used by many different clubs, universities, and companies to host their own Capture The Flags. 18 | 19 | While available as open source, CTFd developers also provide a managed hosting service available at https://ctfd.io/. 20 | 21 | CTFd is written in Python and makes use of the Flask web framework. 22 | 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: Contents: 27 | 28 | deployment 29 | configuration 30 | scoring 31 | themes 32 | plugins 33 | contributing 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/themes.rst: -------------------------------------------------------------------------------- 1 | Themes 2 | ====== 3 | 4 | CTFds allows organizers to customize their CTFd instance with custom themes. The CTFd core routes will load templates from the theme folder specified by the theme configuration value. This value can be configured in the admin panel. 5 | 6 | .. Tip:: 7 | Official CTFd themes are available at https://ctfd.io/store. `Contact us `_ regarding custom themes and special projects. 8 | 9 | .. Tip:: 10 | Community themes are available at https://github.com/CTFd/themes. -------------------------------------------------------------------------------- /export.py: -------------------------------------------------------------------------------- 1 | from CTFd import create_app 2 | from CTFd.utils import config 3 | from CTFd.utils.exports import export_ctf 4 | 5 | import datetime 6 | import sys 7 | import shutil 8 | 9 | 10 | app = create_app() 11 | with app.app_context(): 12 | backup = export_ctf() 13 | 14 | if len(sys.argv) > 1: 15 | with open(sys.argv[1], "wb") as target: 16 | shutil.copyfileobj(backup, target) 17 | else: 18 | ctf_name = config.ctf_name() 19 | day = datetime.datetime.now().strftime("%Y-%m-%d") 20 | full_name = "{}.{}.zip".format(ctf_name, day) 21 | 22 | with open(full_name, "wb") as target: 23 | shutil.copyfileobj(backup, target) 24 | 25 | print("Exported {filename}".format(filename=full_name)) 26 | -------------------------------------------------------------------------------- /import.py: -------------------------------------------------------------------------------- 1 | """ 2 | python import.py export.zip 3 | """ 4 | from CTFd import create_app 5 | from CTFd.utils.exports import import_ctf 6 | 7 | import sys 8 | 9 | app = create_app() 10 | with app.app_context(): 11 | import_ctf(sys.argv[1]) 12 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_script import Manager 4 | from flask_migrate import Migrate, MigrateCommand 5 | from CTFd import create_app 6 | from CTFd.utils import get_config as get_config_util, set_config as set_config_util 7 | from CTFd.models import * 8 | 9 | app = create_app() 10 | 11 | manager = Manager(app) 12 | manager.add_command("db", MigrateCommand) 13 | 14 | 15 | @manager.command 16 | def get_config(key): 17 | with app.app_context(): 18 | print(get_config_util(key)) 19 | 20 | 21 | @manager.command 22 | def set_config(key, value): 23 | with app.app_context(): 24 | print(set_config_util(key, value).value) 25 | 26 | 27 | if __name__ == "__main__": 28 | manager.run() 29 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/080d29b15cd3_add_tokens_table.py: -------------------------------------------------------------------------------- 1 | """Add Tokens table to store user access tokens 2 | 3 | Revision ID: 080d29b15cd3 4 | Revises: b295b033364d 5 | Create Date: 2019-11-03 18:21:04.827015 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "080d29b15cd3" 13 | down_revision = "b295b033364d" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.create_table( 20 | "tokens", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.Column("type", sa.String(length=32), nullable=True), 23 | sa.Column("user_id", sa.Integer(), nullable=True), 24 | sa.Column("created", sa.DateTime(), nullable=True), 25 | sa.Column("expiration", sa.DateTime(), nullable=True), 26 | sa.Column("value", sa.String(length=128), nullable=True), 27 | sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), 28 | sa.PrimaryKeyConstraint("id"), 29 | sa.UniqueConstraint("value"), 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table("tokens") 35 | -------------------------------------------------------------------------------- /migrations/versions/4e4d5a9ea000_add_type_to_awards.py: -------------------------------------------------------------------------------- 1 | """Add type to awards 2 | 3 | Revision ID: 4e4d5a9ea000 4 | Revises: 8369118943a1 5 | Create Date: 2019-04-07 19:37:17.872128 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "4e4d5a9ea000" 13 | down_revision = "8369118943a1" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column( 21 | "awards", 22 | sa.Column( 23 | "type", sa.String(length=80), nullable=True, server_default="standard" 24 | ), 25 | ) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_column("awards", "type") 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/a03403986a32_add_theme_code_injections_to_configs.py: -------------------------------------------------------------------------------- 1 | """add theme code injections to configs 2 | 3 | Revision ID: a03403986a32 4 | Revises: 080d29b15cd3 5 | Create Date: 2020-02-13 01:10:16.430424 6 | 7 | """ 8 | from alembic import op 9 | from sqlalchemy.sql import column, table 10 | 11 | from CTFd.models import db 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "a03403986a32" 16 | down_revision = "080d29b15cd3" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | configs_table = table( 21 | "config", column("id", db.Integer), column("key", db.Text), column("value", db.Text) 22 | ) 23 | 24 | 25 | def upgrade(): 26 | connection = op.get_bind() 27 | css = connection.execute( 28 | configs_table.select().where(configs_table.c.key == "css").limit(1) 29 | ).fetchone() 30 | 31 | if css and css.value: 32 | new_css = "" 33 | config = connection.execute( 34 | configs_table.select().where(configs_table.c.key == "theme_header").limit(1) 35 | ).fetchone() 36 | if config: 37 | # Do not overwrite existing theme_header value 38 | pass 39 | else: 40 | connection.execute( 41 | configs_table.insert().values(key="theme_header", value=new_css) 42 | ) 43 | 44 | 45 | def downgrade(): 46 | pass 47 | -------------------------------------------------------------------------------- /migrations/versions/b5551cd26764_add_captain_column_to_teams.py: -------------------------------------------------------------------------------- 1 | """Add captain column to Teams 2 | 3 | Revision ID: b5551cd26764 4 | Revises: 4e4d5a9ea000 5 | Create Date: 2019-04-12 00:29:08.021141 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy.sql import column, table 11 | 12 | from CTFd.models import db 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "b5551cd26764" 16 | down_revision = "4e4d5a9ea000" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | teams_table = table("teams", column("id", db.Integer), column("captain_id", db.Integer)) 21 | 22 | users_table = table("users", column("id", db.Integer), column("team_id", db.Integer)) 23 | 24 | 25 | def upgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.add_column("teams", sa.Column("captain_id", sa.Integer(), nullable=True)) 28 | 29 | bind = op.get_bind() 30 | url = str(bind.engine.url) 31 | if url.startswith("sqlite") is False: 32 | op.create_foreign_key( 33 | "team_captain_id", "teams", "users", ["captain_id"], ["id"] 34 | ) 35 | 36 | connection = op.get_bind() 37 | for team in connection.execute(teams_table.select()): 38 | users = connection.execute( 39 | users_table.select() 40 | .where(users_table.c.team_id == team.id) 41 | .order_by(users_table.c.id) 42 | .limit(1) 43 | ) 44 | for user in users: 45 | connection.execute( 46 | teams_table.update() 47 | .where(teams_table.c.id == team.id) 48 | .values(captain_id=user.id) 49 | ) 50 | # ### end Alembic commands ### 51 | 52 | 53 | def downgrade(): 54 | # ### commands auto generated by Alembic - please adjust! ### 55 | op.drop_constraint("team_captain_id", "teams", type_="foreignkey") 56 | op.drop_column("teams", "captain_id") 57 | # ### end Alembic commands ### 58 | -------------------------------------------------------------------------------- /prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo apt-get update 3 | sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential python-dev python-pip libffi-dev 4 | pip install -r requirements.txt 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.1 2 | Werkzeug==0.16.0 3 | Flask-SQLAlchemy==2.4.1 4 | Flask-Caching==1.4.0 5 | Flask-Migrate==2.5.2 6 | Flask-Script==2.0.6 7 | SQLAlchemy==1.3.11 8 | SQLAlchemy-Utils==0.36.0 9 | passlib==1.7.2 10 | bcrypt==3.1.7 11 | six==1.13.0 12 | itsdangerous==1.1.0 13 | requests>=2.20.0 14 | PyMySQL==0.9.3 15 | gunicorn==19.9.0 16 | normality==2.0.0 17 | dataset==1.1.2 18 | mistune==0.8.4 19 | netaddr==0.7.19 20 | redis==3.3.11 21 | datafreeze==0.1.0 22 | gevent==1.4.0 23 | python-dotenv==0.10.3 24 | flask-restplus==0.13.0 25 | pathlib2==2.3.5 26 | flask-marshmallow==0.10.1 27 | marshmallow-sqlalchemy==0.17.0 28 | boto3==1.10.39 29 | marshmallow==2.20.2 30 | -------------------------------------------------------------------------------- /scripts/install_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to install docker in Debian Guest VM 4 | # per: https://docs.docker.com/engine/installation/linux/debian/#install-docker-ce 5 | 6 | # Install packages to allow apt to use a repository over HTTPS 7 | sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ 8 | python-pip \ 9 | apt-transport-https \ 10 | ca-certificates \ 11 | curl \ 12 | gnupg2 \ 13 | software-properties-common 14 | 15 | # Add Docker’s official GPG key 16 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 17 | 18 | # Set up the stable repository. 19 | sudo add-apt-repository \ 20 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 21 | $(lsb_release -cs) \ 22 | stable" 23 | 24 | # Update the apt package index 25 | sudo apt-get update 26 | 27 | # Install the latest version of Docker 28 | sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce 29 | 30 | # Add user to the docker group 31 | # Warning: The docker group grants privileges equivalent to the root user. 32 | sudo usermod -aG docker ${USER} 33 | 34 | # Configure Docker to start on boot 35 | sudo systemctl enable docker 36 | 37 | # Install docker-compose 38 | pip install docker-compose 39 | -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | from CTFd import create_app 2 | import argparse 3 | 4 | parser = argparse.ArgumentParser() 5 | parser.add_argument("--port", help="Port for debug server to listen on", default=4000) 6 | parser.add_argument( 7 | "--profile", help="Enable flask_profiler profiling", action="store_true" 8 | ) 9 | args = parser.parse_args() 10 | 11 | app = create_app() 12 | 13 | if args.profile: 14 | from flask_debugtoolbar import DebugToolbarExtension 15 | import flask_profiler 16 | 17 | app.config["flask_profiler"] = { 18 | "enabled": app.config["DEBUG"], 19 | "storage": {"engine": "sqlite"}, 20 | "basicAuth": {"enabled": False}, 21 | } 22 | flask_profiler.init_app(app) 23 | app.config["DEBUG_TB_PROFILER_ENABLED"] = True 24 | app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False 25 | 26 | toolbar = DebugToolbarExtension() 27 | toolbar.init_app(app) 28 | print(" * Flask profiling running at http://127.0.0.1:4000/flask-profiler/") 29 | 30 | app.run(debug=True, threaded=True, host="127.0.0.1", port=args.port) 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | stop=1 3 | verbosity=2 4 | with-coverage=1 5 | cover-package=CTFd -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/__init__.py -------------------------------------------------------------------------------- /tests/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/__init__.py -------------------------------------------------------------------------------- /tests/admin/test_export_csv.py: -------------------------------------------------------------------------------- 1 | from tests.helpers import create_ctfd, destroy_ctfd, gen_challenge, login_as_user 2 | 3 | 4 | def test_export_csv_works(): 5 | """Test that CSV exports work properly""" 6 | app = create_ctfd() 7 | with app.app_context(): 8 | gen_challenge(app.db) 9 | client = login_as_user(app, name="admin", password="password") 10 | 11 | csv_data = client.get("/admin/export/csv?table=challenges").get_data( 12 | as_text=True 13 | ) 14 | assert len(csv_data) > 0 15 | 16 | destroy_ctfd(app) 17 | -------------------------------------------------------------------------------- /tests/admin/test_notifications.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_notifications.py -------------------------------------------------------------------------------- /tests/admin/test_pages.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_pages.py -------------------------------------------------------------------------------- /tests/admin/test_scoreboard.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_scoreboard.py -------------------------------------------------------------------------------- /tests/admin/test_statistics.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_statistics.py -------------------------------------------------------------------------------- /tests/admin/test_submissions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_submissions.py -------------------------------------------------------------------------------- /tests/admin/test_teams.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/admin/test_teams.py -------------------------------------------------------------------------------- /tests/admin/test_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from tests.helpers import ( 5 | create_ctfd, 6 | destroy_ctfd, 7 | gen_tracking, 8 | gen_user, 9 | login_as_user, 10 | ) 11 | 12 | 13 | def test_admin_user_ip_search(): 14 | """Can an admin search user IPs""" 15 | app = create_ctfd() 16 | with app.app_context(): 17 | u1 = gen_user(app.db, name="user1", email="user1@ctfd.io") 18 | gen_tracking(app.db, user_id=u1.id, ip="1.1.1.1") 19 | 20 | u2 = gen_user(app.db, name="user2", email="user2@ctfd.io") 21 | gen_tracking(app.db, user_id=u2.id, ip="2.2.2.2") 22 | 23 | u3 = gen_user(app.db, name="user3", email="user3@ctfd.io") 24 | gen_tracking(app.db, user_id=u3.id, ip="3.3.3.3") 25 | 26 | u4 = gen_user(app.db, name="user4", email="user4@ctfd.io") 27 | gen_tracking(app.db, user_id=u4.id, ip="3.3.3.3") 28 | gen_tracking(app.db, user_id=u4.id, ip="4.4.4.4") 29 | 30 | with login_as_user(app, name="admin", password="password") as admin: 31 | r = admin.get("/admin/users?field=ip&q=1.1.1.1") 32 | resp = r.get_data(as_text=True) 33 | assert "user1" in resp 34 | assert "user2" not in resp 35 | assert "user3" not in resp 36 | 37 | r = admin.get("/admin/users?field=ip&q=2.2.2.2") 38 | resp = r.get_data(as_text=True) 39 | assert "user1" not in resp 40 | assert "user2" in resp 41 | assert "user3" not in resp 42 | 43 | r = admin.get("/admin/users?field=ip&q=3.3.3.3") 44 | resp = r.get_data(as_text=True) 45 | assert "user1" not in resp 46 | assert "user2" not in resp 47 | assert "user3" in resp 48 | assert "user4" in resp 49 | destroy_ctfd(app) 50 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/api/v1/__init__.py -------------------------------------------------------------------------------- /tests/api/v1/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/api/v1/teams/__init__.py -------------------------------------------------------------------------------- /tests/api/v1/teams/test_scoring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.models import Users 5 | from CTFd.utils import set_config 6 | from tests.helpers import create_ctfd, destroy_ctfd, gen_award, gen_team, login_as_user 7 | 8 | 9 | def test_api_team_place_hidden_if_scores_hidden(): 10 | """/api/v1/teams/me should not reveal team place if scores aren't visible""" 11 | app = create_ctfd(user_mode="teams") 12 | with app.app_context(): 13 | gen_team(app.db) 14 | app.db.session.commit() 15 | 16 | gen_award(app.db, user_id=2, team_id=1) 17 | 18 | u = Users.query.filter_by(id=2).first() 19 | 20 | with login_as_user(app, name=u.name) as client: 21 | r = client.get("/api/v1/teams/me", json="") 22 | resp = r.get_json() 23 | assert resp["data"]["place"] == "1st" 24 | 25 | set_config("score_visibility", "hidden") 26 | with login_as_user(app, name=u.name) as client: 27 | r = client.get("/api/v1/teams/me", json="") 28 | resp = r.get_json() 29 | assert resp["data"]["place"] is None 30 | 31 | set_config("score_visibility", "admins") 32 | with login_as_user(app, name=u.name) as client: 33 | r = client.get("/api/v1/teams/me", json="") 34 | resp = r.get_json() 35 | assert resp["data"]["place"] is None 36 | 37 | with login_as_user(app, name="admin") as client: 38 | r = client.get("/api/v1/teams/1", json="") 39 | resp = r.get_json() 40 | print(resp) 41 | assert resp["data"]["place"] == "1st" 42 | destroy_ctfd(app) 43 | -------------------------------------------------------------------------------- /tests/api/v1/test_csrf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask.testing import FlaskClient 5 | 6 | from tests.helpers import create_ctfd, destroy_ctfd, login_as_user 7 | 8 | 9 | def test_api_csrf_failure(): 10 | """Test that API requests require the CSRF-Token header""" 11 | app = create_ctfd() 12 | app.test_client_class = FlaskClient 13 | with app.app_context(): 14 | with login_as_user(app, "admin") as client: 15 | r = client.post( 16 | "/api/v1/challenges", 17 | json={ 18 | "name": "chal", 19 | "category": "cate", 20 | "description": "desc", 21 | "value": "100", 22 | "state": "hidden", 23 | "type": "standard", 24 | }, 25 | ) 26 | assert r.status_code == 403 27 | 28 | with client.session_transaction() as sess: 29 | nonce = sess.get("nonce") 30 | 31 | r = client.post( 32 | "/api/v1/challenges", 33 | headers={"CSRF-Token": nonce}, 34 | json={ 35 | "name": "chal", 36 | "category": "cate", 37 | "description": "desc", 38 | "value": "100", 39 | "state": "hidden", 40 | "type": "standard", 41 | }, 42 | ) 43 | assert r.status_code == 200 44 | destroy_ctfd(app) 45 | -------------------------------------------------------------------------------- /tests/api/v1/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/api/v1/user/__init__.py -------------------------------------------------------------------------------- /tests/api/v1/user/test_admin_access.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user 5 | 6 | 7 | def test_api_hint_404(): 8 | """Are admin protected resources accessible by admins/non-admins""" 9 | app = create_ctfd() 10 | endpoints = [ 11 | "/api/v1/configs/{}", 12 | "/api/v1/challenges/types", 13 | "/api/v1/statistics/teams", 14 | "/api/v1/flags/{}", 15 | "/api/v1/statistics/users/{}", 16 | "/api/v1/configs", 17 | "/api/v1/statistics/challenges/solves/percentages", 18 | "/api/v1/tags/{}", 19 | "/api/v1/pages", 20 | "/api/v1/files/{}", 21 | "/api/v1/challenges/{}/tags", 22 | "/api/v1/hints", 23 | "/api/v1/challenges/{}/files", 24 | "/api/v1/flags", 25 | "/api/v1/submissions/{}", 26 | "/api/v1/challenges/{}/flags", 27 | "/api/v1/awards/{}", 28 | "/api/v1/unlocks", 29 | "/api/v1/challenges/{}/hints", 30 | "/api/v1/statistics/submissions/{}", 31 | "/api/v1/flags/types/{}", 32 | "/api/v1/tags", 33 | "/api/v1/statistics/challenges/{}", 34 | "/api/v1/files", 35 | "/api/v1/flags/types", 36 | "/api/v1/submissions", 37 | "/api/v1/pages/{}", 38 | ] 39 | 40 | with app.app_context(): 41 | register_user(app) 42 | client = login_as_user(app) 43 | for endpoint in endpoints: 44 | r = client.get(endpoint.format(1)) 45 | assert r.status_code == 302 46 | assert r.location.startswith("http://localhost/login") 47 | destroy_ctfd(app) 48 | -------------------------------------------------------------------------------- /tests/api/v1/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/api/v1/users/__init__.py -------------------------------------------------------------------------------- /tests/api/v1/users/test_scoring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.models import Users 5 | from CTFd.utils import set_config 6 | from tests.helpers import ( 7 | create_ctfd, 8 | destroy_ctfd, 9 | login_as_user, 10 | register_user, 11 | simulate_user_activity, 12 | ) 13 | 14 | 15 | def test_api_user_place_hidden_if_scores_hidden(): 16 | """/api/v1/users/me should not reveal user place if scores aren't visible""" 17 | app = create_ctfd() 18 | with app.app_context(): 19 | register_user(app) 20 | user = Users.query.filter_by(id=2).first() 21 | simulate_user_activity(app.db, user=user) 22 | 23 | with login_as_user(app, name="user") as client: 24 | r = client.get("/api/v1/users/me", json="") 25 | resp = r.get_json() 26 | assert resp["data"]["place"] == "1st" 27 | 28 | set_config("score_visibility", "hidden") 29 | with login_as_user(app, name="user") as client: 30 | r = client.get("/api/v1/users/me", json="") 31 | resp = r.get_json() 32 | assert resp["data"]["place"] is None 33 | 34 | set_config("score_visibility", "admins") 35 | with login_as_user(app, name="user") as client: 36 | r = client.get("/api/v1/users/me", json="") 37 | resp = r.get_json() 38 | assert resp["data"]["place"] is None 39 | 40 | with login_as_user(app, name="admin") as client: 41 | r = client.get("/api/v1/users/2", json="") 42 | resp = r.get_json() 43 | assert resp["data"]["place"] == "1st" 44 | destroy_ctfd(app) 45 | -------------------------------------------------------------------------------- /tests/challenges/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/challenges/__init__.py -------------------------------------------------------------------------------- /tests/oauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/oauth/__init__.py -------------------------------------------------------------------------------- /tests/oauth/test_teams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from CTFd.models import Teams 4 | from CTFd.utils import set_config 5 | from tests.helpers import create_ctfd, destroy_ctfd, gen_team, login_with_mlc 6 | 7 | 8 | def test_team_size_limit(): 9 | """Only team_size amount of members can join a team even via MLC""" 10 | app = create_ctfd(user_mode="teams") 11 | app.config.update( 12 | { 13 | "OAUTH_CLIENT_ID": "ctfd_testing_client_id", 14 | "OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret", 15 | "OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize", 16 | "OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token", 17 | "OAUTH_API_ENDPOINT": "http://api.localhost/user", 18 | } 19 | ) 20 | with app.app_context(): 21 | set_config("team_size", 1) 22 | team = gen_team(app.db, member_count=1, oauth_id=1234) 23 | team_id = team.id 24 | login_with_mlc( 25 | app, team_name="team_name", team_oauth_id=1234, raise_for_error=False 26 | ) 27 | assert len(Teams.query.filter_by(id=team_id).first().members) == 1 28 | 29 | set_config("team_size", 2) 30 | login_with_mlc(app, team_name="team_name", team_oauth_id=1234) 31 | assert len(Teams.query.filter_by(id=team_id).first().members) == 2 32 | destroy_ctfd(app) 33 | -------------------------------------------------------------------------------- /tests/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/teams/__init__.py -------------------------------------------------------------------------------- /tests/teams/test_challenges.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.utils.scores import get_standings 5 | from tests.helpers import ( 6 | create_ctfd, 7 | destroy_ctfd, 8 | gen_challenge, 9 | gen_flag, 10 | gen_team, 11 | gen_user, 12 | login_as_user, 13 | ) 14 | 15 | 16 | def test_challenge_team_submit(): 17 | """Is a user's solved challenge reflected by other team members""" 18 | app = create_ctfd(user_mode="teams") 19 | with app.app_context(): 20 | user = gen_user(app.db) 21 | second_user = gen_user(app.db, name="user", email="second@ctfd.io") 22 | team = gen_team(app.db) 23 | user.team_id = team.id 24 | second_user.team_id = team.id 25 | team.members.append(user) 26 | team.members.append(second_user) 27 | gen_challenge(app.db) 28 | gen_flag(app.db, 1) 29 | app.db.session.commit() 30 | with login_as_user(app, name="user_name") as client: 31 | flag = {"challenge_id": 1, "submission": "flag"} 32 | client.post("/api/v1/challenges/attempt", json=flag) 33 | with login_as_user(app) as second_client: 34 | flag = {"challenge_id": 1, "submission": "flag"} 35 | r = second_client.post("/api/v1/challenges/attempt", json=flag) 36 | assert r.json["data"]["status"] == "already_solved" 37 | standings = get_standings() 38 | assert standings[0][2] == "team_name" 39 | assert standings[0][3] == 100 40 | destroy_ctfd(app) 41 | -------------------------------------------------------------------------------- /tests/teams/test_hints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.utils.scores import get_standings 5 | from tests.helpers import ( 6 | create_ctfd, 7 | destroy_ctfd, 8 | gen_award, 9 | gen_challenge, 10 | gen_hint, 11 | gen_team, 12 | gen_user, 13 | login_as_user, 14 | ) 15 | 16 | 17 | def test_hint_team_unlock(): 18 | """Is a user's unlocked hint reflected on other team members""" 19 | app = create_ctfd(user_mode="teams") 20 | with app.app_context(): 21 | user = gen_user(app.db) 22 | second_user = gen_user(app.db, name="user", email="second@ctfd.io") 23 | team = gen_team(app.db) 24 | user.team_id = team.id 25 | second_user.team_id = team.id 26 | team.members.append(user) 27 | team.members.append(second_user) 28 | chal = gen_challenge(app.db) 29 | gen_hint(app.db, chal.id, content="hint", cost=1, type="standard") 30 | gen_award(app.db, 2, team.id) 31 | app.db.session.commit() 32 | with login_as_user(app, name="user_name") as client: 33 | client.get("/api/v1/hints/1") 34 | client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"}) 35 | client.get("/api/v1/hints/1") 36 | with login_as_user(app) as second_client: 37 | second_client.get("/api/v1/hints/1") 38 | second_client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"}) 39 | r = second_client.get("/api/v1/hints/1") 40 | assert r.json["data"]["content"] == "hint" 41 | standings = get_standings() 42 | assert standings[0][2] == "team_name" 43 | assert standings[0][3] == 99 44 | destroy_ctfd(app) 45 | -------------------------------------------------------------------------------- /tests/teams/test_scoreboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.utils.scores import get_standings 5 | from tests.helpers import ( 6 | create_ctfd, 7 | destroy_ctfd, 8 | gen_challenge, 9 | gen_flag, 10 | gen_team, 11 | gen_user, 12 | login_as_user, 13 | ) 14 | 15 | 16 | def test_scoreboard_team_score(): 17 | """Is a user's submitted flag reflected on the team's score on /scoreboard""" 18 | app = create_ctfd(user_mode="teams") 19 | with app.app_context(): 20 | user = gen_user(app.db, name="user") 21 | team = gen_team(app.db) 22 | user.team_id = team.id 23 | team.members.append(user) 24 | gen_challenge(app.db) 25 | gen_flag(app.db, 1) 26 | app.db.session.commit() 27 | with login_as_user(app) as client: 28 | flag = {"challenge_id": 1, "submission": "flag"} 29 | client.post("/api/v1/challenges/attempt", json=flag) 30 | standings = get_standings() 31 | assert standings[0][2] == "team_name" 32 | assert standings[0][3] == 100 33 | destroy_ctfd(app) 34 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.config import TestingConfig 5 | from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user 6 | 7 | 8 | def test_reverse_proxy_config(): 9 | """Test that REVERSE_PROXY configuration behaves properly""" 10 | 11 | class ReverseProxyConfig(TestingConfig): 12 | REVERSE_PROXY = "1,2,3,4" 13 | 14 | app = create_ctfd(config=ReverseProxyConfig) 15 | with app.app_context(): 16 | assert app.wsgi_app.x_for == 1 17 | assert app.wsgi_app.x_proto == 2 18 | assert app.wsgi_app.x_host == 3 19 | assert app.wsgi_app.x_port == 4 20 | assert app.wsgi_app.x_prefix == 0 21 | destroy_ctfd(app) 22 | 23 | class ReverseProxyConfig(TestingConfig): 24 | REVERSE_PROXY = "true" 25 | 26 | app = create_ctfd(config=ReverseProxyConfig) 27 | with app.app_context(): 28 | assert app.wsgi_app.x_for == 1 29 | assert app.wsgi_app.x_proto == 1 30 | assert app.wsgi_app.x_host == 1 31 | assert app.wsgi_app.x_port == 1 32 | assert app.wsgi_app.x_prefix == 1 33 | destroy_ctfd(app) 34 | 35 | 36 | def test_server_sent_events_config(): 37 | """Test that SERVER_SENT_EVENTS configuration behaves properly""" 38 | 39 | class ServerSentEventsConfig(TestingConfig): 40 | SERVER_SENT_EVENTS = False 41 | 42 | app = create_ctfd(config=ServerSentEventsConfig) 43 | with app.app_context(): 44 | register_user(app) 45 | client = login_as_user(app) 46 | r = client.get("/events") 47 | assert r.status_code == 204 48 | destroy_ctfd(app) 49 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.utils import get_config 5 | from CTFd.utils.security.csrf import generate_nonce 6 | from CTFd.utils.security.signing import serialize 7 | from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user 8 | 9 | 10 | def test_setup_integrations(): 11 | app = create_ctfd() 12 | with app.app_context(): 13 | register_user(app) 14 | user = login_as_user(app) 15 | r = user.get("/setup/integrations") 16 | assert r.status_code == 403 17 | 18 | admin = login_as_user(app, "admin") 19 | r = admin.get("/setup/integrations") 20 | assert r.status_code == 403 21 | 22 | admin = login_as_user(app, "admin") 23 | 24 | url = "/setup/integrations?state={state}&mlc_client_id=client_id&mlc_client_secret=client_secret&name=mlc".format( 25 | state=serialize(generate_nonce()) 26 | ) 27 | r = admin.get(url) 28 | assert r.status_code == 200 29 | assert get_config("oauth_client_id") == "client_id" 30 | assert get_config("oauth_client_secret") == "client_secret" 31 | destroy_ctfd(app) 32 | -------------------------------------------------------------------------------- /tests/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glzjin/CTFd/02946650c99f5a607bb33f717556fed3609ceacd/tests/users/__init__.py -------------------------------------------------------------------------------- /tests/users/test_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.models import Users 5 | from CTFd.utils.crypto import verify_password 6 | from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user 7 | 8 | 9 | def test_email_cannot_be_changed_without_password(): 10 | """Test that a user can't update their email address without current password""" 11 | app = create_ctfd() 12 | with app.app_context(): 13 | register_user(app) 14 | client = login_as_user(app) 15 | 16 | data = {"name": "user", "email": "user2@ctfd.io"} 17 | 18 | r = client.patch("/api/v1/users/me", json=data) 19 | assert r.status_code == 400 20 | user = Users.query.filter_by(id=2).first() 21 | assert user.email == "user@ctfd.io" 22 | 23 | data = {"name": "user", "email": "user2@ctfd.io", "confirm": "asdf"} 24 | 25 | r = client.patch("/api/v1/users/me", json=data) 26 | assert r.status_code == 400 27 | user = Users.query.filter_by(id=2).first() 28 | assert user.email == "user@ctfd.io" 29 | 30 | data = {"name": "user", "email": "user2@ctfd.io", "confirm": "password"} 31 | 32 | r = client.patch("/api/v1/users/me", json=data) 33 | assert r.status_code == 200 34 | user = Users.query.filter_by(id=2).first() 35 | assert user.email == "user2@ctfd.io" 36 | assert verify_password(plaintext="password", ciphertext=user.password) 37 | destroy_ctfd(app) 38 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from CTFd.utils import get_config, set_config 5 | from tests.helpers import create_ctfd, destroy_ctfd 6 | 7 | 8 | def test_ctf_version_is_set(): 9 | """Does ctf_version get set correctly""" 10 | app = create_ctfd() 11 | with app.app_context(): 12 | assert get_config("ctf_version") == app.VERSION 13 | destroy_ctfd(app) 14 | 15 | 16 | def test_get_config_and_set_config(): 17 | """Does get_config and set_config work properly""" 18 | app = create_ctfd() 19 | with app.app_context(): 20 | assert get_config("setup") == True 21 | config = set_config("TEST_CONFIG_ENTRY", "test_config_entry") 22 | assert config.value == "test_config_entry" 23 | assert get_config("TEST_CONFIG_ENTRY") == "test_config_entry" 24 | destroy_ctfd(app) 25 | -------------------------------------------------------------------------------- /tests/utils/test_formatters.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils.formatters import safe_format 2 | 3 | 4 | def test_safe_format(): 5 | assert safe_format("Message from {ctf_name}", ctf_name="CTF") == "Message from CTF" 6 | assert ( 7 | safe_format("Message from {{ ctf_name }}", ctf_name="CTF") == "Message from CTF" 8 | ) 9 | assert safe_format("{{ ctf_name }} {{ctf_name}}", ctf_name="CTF") == "CTF CTF" 10 | assert ( 11 | safe_format("{ ctf_name } {ctf_name} {asdf}", ctf_name="CTF") 12 | == "CTF CTF {asdf}" 13 | ) 14 | -------------------------------------------------------------------------------- /tests/utils/test_humanize.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils.humanize.numbers import ordinalize 2 | 3 | 4 | def test_ordinalize(): 5 | tests = { 6 | 1: "1st", 7 | 2: "2nd", 8 | 3: "3rd", 9 | 4: "4th", 10 | 11: "11th", 11 | 12: "12th", 12 | 13: "13th", 13 | 101: "101st", 14 | 102: "102nd", 15 | 103: "103rd", 16 | 111: "111th", 17 | } 18 | for t, v in tests.items(): 19 | assert ordinalize(t) == v 20 | -------------------------------------------------------------------------------- /tests/utils/test_passwords.py: -------------------------------------------------------------------------------- 1 | from CTFd.utils.crypto import hash_password, sha256, verify_password 2 | 3 | 4 | def test_hash_password(): 5 | assert hash_password("asdf").startswith("$bcrypt-sha256") 6 | 7 | 8 | def test_verify_password(): 9 | assert verify_password( 10 | "asdf", 11 | "$bcrypt-sha256$2b,12$I0CNXRkGD2Bi/lbC4vZ7Y.$1WoilsadKpOjXa/be9x3dyu7p.mslZ6", 12 | ) 13 | 14 | 15 | def test_sha256(): 16 | assert ( 17 | sha256("asdf") 18 | == "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b" 19 | ) 20 | -------------------------------------------------------------------------------- /tests/utils/test_ratelimit.py: -------------------------------------------------------------------------------- 1 | from tests.helpers import create_ctfd, destroy_ctfd, register_user 2 | 3 | 4 | def test_ratelimit_on_auth(): 5 | """Test that ratelimiting function works properly""" 6 | app = create_ctfd() 7 | with app.app_context(): 8 | register_user(app) 9 | with app.test_client() as client: 10 | r = client.get("/login") 11 | with client.session_transaction() as sess: 12 | data = { 13 | "name": "user", 14 | "password": "wrong_password", 15 | "nonce": sess.get("nonce"), 16 | } 17 | for x in range(10): 18 | r = client.post("/login", data=data) 19 | assert r.status_code == 200 20 | 21 | for x in range(5): 22 | r = client.post("/login", data=data) 23 | assert r.status_code == 429 24 | destroy_ctfd(app) 25 | -------------------------------------------------------------------------------- /tests/utils/test_sessions.py: -------------------------------------------------------------------------------- 1 | from tests.helpers import create_ctfd, destroy_ctfd 2 | 3 | 4 | def test_sessions_set_httponly(): 5 | app = create_ctfd() 6 | with app.app_context(): 7 | with app.test_client() as client: 8 | r = client.get("/") 9 | cookie = dict(r.headers)["Set-Cookie"] 10 | assert "HttpOnly;" in cookie 11 | destroy_ctfd(app) 12 | 13 | 14 | def test_sessions_set_samesite(): 15 | app = create_ctfd() 16 | with app.app_context(): 17 | with app.test_client() as client: 18 | r = client.get("/") 19 | cookie = dict(r.headers)["Set-Cookie"] 20 | assert "SameSite=" in cookie 21 | destroy_ctfd(app) 22 | -------------------------------------------------------------------------------- /tests/utils/test_validators.py: -------------------------------------------------------------------------------- 1 | from marshmallow import ValidationError 2 | 3 | from CTFd.utils.validators import validate_country_code, validate_email 4 | 5 | 6 | def test_validate_country_code(): 7 | assert validate_country_code("") is None 8 | # TODO: This looks poor, when everything moves to pytest we should remove exception catches like this. 9 | try: 10 | validate_country_code("ZZ") 11 | except ValidationError: 12 | pass 13 | 14 | 15 | def test_validate_email(): 16 | """Test that the check_email_format() works properly""" 17 | assert validate_email("user@ctfd.io") is True 18 | assert validate_email("user+plus@gmail.com") is True 19 | assert validate_email("user.period1234@gmail.com") is True 20 | assert validate_email("user.period1234@b.c") is True 21 | assert validate_email("user.period1234@b") is False 22 | assert validate_email("no.ampersand") is False 23 | assert validate_email("user@") is False 24 | assert validate_email("@ctfd.io") is False 25 | assert validate_email("user.io@ctfd") is False 26 | assert validate_email("user\\@ctfd") is False 27 | 28 | for invalid_email in ["user.@ctfd.io", ".user@ctfd.io", "user@ctfd..io"]: 29 | try: 30 | assert validate_email(invalid_email) is False 31 | except AssertionError: 32 | print(invalid_email, "did not pass validation") 33 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Detect if we're running via `flask run` and don't monkey patch 4 | if not os.getenv("FLASK_RUN_FROM_CLI"): 5 | from gevent import monkey 6 | 7 | monkey.patch_all() 8 | 9 | from CTFd import create_app 10 | 11 | app = create_app() 12 | 13 | if __name__ == "__main__": 14 | app.run(debug=True, threaded=True, host="127.0.0.1", port=4000) 15 | --------------------------------------------------------------------------------