├── .envrc ├── .gitignore ├── Vagrantfile ├── config.json ├── db ├── Vagrantfile ├── conf.d │ └── easyctf.cnf └── init.d │ └── init.sql ├── deploy ├── .gitignore ├── config.yml.exmaple └── deploy.py ├── docker-compose.yml ├── docs ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ └── setup.md ├── filestore ├── .gitignore ├── Dockerfile ├── app.py ├── cloud-provision.sh ├── default.conf ├── entrypoint.sh ├── env.example ├── poetry.lock ├── pyproject.toml └── systemd │ └── filestore.service ├── flake.lock ├── flake.nix ├── judge ├── .gitignore ├── api.py ├── cloud-provision.sh ├── config.py ├── confine ├── executor.py ├── judge.py ├── languages.py ├── models.py ├── nsjail ├── output └── systemd │ └── judge.service ├── nginx ├── Dockerfile ├── easyctf.conf └── nginx.conf ├── provision.sh ├── server ├── .dockerignore ├── .gitignore ├── Dockerfile ├── app.py ├── cloud-provision.sh ├── easyctf │ ├── __init__.py │ ├── assets │ │ ├── css │ │ │ ├── bootstrap.min.css │ │ │ ├── font-awesome.css │ │ │ ├── font-awesome.min.css │ │ │ ├── main.css │ │ │ └── selectize.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── OFL.txt │ │ │ ├── SourceSansPro-Black.ttf │ │ │ ├── SourceSansPro-BlackItalic.ttf │ │ │ ├── SourceSansPro-Bold.ttf │ │ │ ├── SourceSansPro-BoldItalic.ttf │ │ │ ├── SourceSansPro-ExtraLight.ttf │ │ │ ├── SourceSansPro-ExtraLightItalic.ttf │ │ │ ├── SourceSansPro-Italic.ttf │ │ │ ├── SourceSansPro-Light.ttf │ │ │ ├── SourceSansPro-LightItalic.ttf │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ ├── SourceSansPro-SemiBold.ttf │ │ │ ├── SourceSansPro-SemiBoldItalic.ttf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── game.json │ │ ├── images │ │ │ ├── digitalocean.png │ │ │ └── game │ │ │ │ ├── 090939.png │ │ │ │ ├── background.png │ │ │ │ ├── background_rain.gif │ │ │ │ ├── char_A_scale.png │ │ │ │ ├── char_B_scale.png │ │ │ │ ├── char_C_scale.png │ │ │ │ ├── char_D_scale.png │ │ │ │ └── char_E_scale.png │ │ └── js │ │ │ ├── bootstrap.min.js │ │ │ ├── bootstrap3-typeahead.min.js │ │ │ ├── jquery-2.1.4.min.js │ │ │ ├── livestamp.min.js │ │ │ ├── moment.min.js │ │ │ ├── selectize.min.js │ │ │ ├── smooth-scroll.min.js │ │ │ └── vn.js │ ├── config.py │ ├── constants.py │ ├── decorators.py │ ├── forms │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── chals.py │ │ ├── classroom.py │ │ ├── game.py │ │ ├── teams.py │ │ ├── users.py │ │ └── validators.py │ ├── models │ │ ├── __init__.py │ │ ├── others.py │ │ ├── team.py │ │ └── user.py │ ├── objects.py │ ├── py.typed │ ├── templates │ │ ├── admin │ │ │ ├── problems.html │ │ │ └── settings.html │ │ ├── base │ │ │ ├── about.html │ │ │ ├── easter.html │ │ │ ├── index.html │ │ │ ├── prizes.html │ │ │ ├── rules.html │ │ │ ├── scoreboard.html │ │ │ ├── sponsors.html │ │ │ ├── team.html │ │ │ └── updates.html │ │ ├── chals │ │ │ ├── list.html │ │ │ ├── programming.html │ │ │ ├── shell.html │ │ │ ├── solves.html │ │ │ ├── status.html │ │ │ └── submission.html │ │ ├── classroom │ │ │ ├── index.html │ │ │ ├── new.html │ │ │ └── view.html │ │ ├── footer.html │ │ ├── game │ │ │ └── game.html │ │ ├── layout.html │ │ ├── navbar.html │ │ ├── teams │ │ │ ├── create.html │ │ │ ├── profile.html │ │ │ └── settings.html │ │ ├── templates.html │ │ └── users │ │ │ ├── forgot.html │ │ │ ├── login.html │ │ │ ├── profile.html │ │ │ ├── register.html │ │ │ ├── reset.html │ │ │ ├── settings.html │ │ │ └── two_factor │ │ │ └── setup.html │ ├── utils.py │ └── views │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api │ │ ├── __init__.py │ │ └── admin.py │ │ ├── base.py │ │ ├── chals.py │ │ ├── classroom.py │ │ ├── game.py │ │ ├── judge.py │ │ ├── teams.py │ │ └── users.py ├── entrypoint.sh ├── env.example ├── forgot.mail ├── import_problems.sh ├── manage.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 59f8fa2f0c98_.py ├── poetry.lock ├── pyproject.toml └── registration.mail └── shell ├── .gitignore ├── Vagrantfile ├── include ├── bin │ └── addctfuser └── etc │ ├── adduser.conf │ ├── pam.d │ └── login │ ├── security │ └── limits.conf │ └── sudoers └── setup.sh /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .data 2 | .env 3 | .vagrant 4 | .vscode 5 | .digitalocean-token 6 | .idea 7 | 8 | __pycache__ 9 | *.pyc 10 | ubuntu-xenial-16.04-cloudimg-console.log 11 | 12 | ctf-data 13 | .direnv 14 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | Vagrant.configure("2") do |config| 4 | config.vm.box = "ubuntu/xenial64" 5 | config.vm.network "forwarded_port", guest: 80, host: 7000 6 | config.vm.network "forwarded_port", guest: 8000, host: 8000 7 | 8 | config.vm.synced_folder "../problems", "/problems" 9 | config.vm.provider "virtualbox" do |vb| 10 | end 11 | 12 | # config.vm.provision "shell", path: "provision.sh" 13 | end 14 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource_prefix": "staging_", 3 | "region": "nyc3", 4 | "load_balancer": { 5 | "name": "load_balancer" 6 | } 7 | } -------------------------------------------------------------------------------- /db/Vagrantfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/db/Vagrantfile -------------------------------------------------------------------------------- /db/conf.d/easyctf.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set=utf8mb4 3 | 4 | [mysql] 5 | default-character-set=utf8mb4 6 | 7 | [mysqld] 8 | innodb_buffer_pool_size=20M 9 | init_connect='SET collation_connection = utf8_unicode_ci' 10 | init_connect='SET NAMES utf8mb4' 11 | character-set-server=utf8mb4 12 | collation-server=utf8_unicode_ci 13 | skip-character-set-client-handshake 14 | max_allowed_packet=512M 15 | wait_timeout=31536000 16 | -------------------------------------------------------------------------------- /db/init.d/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE `app`; 2 | CREATE USER `app`@'%' IDENTIFIED BY 'hellosu'; 3 | GRANT ALL PRIVILEGES ON app.* TO app@'%'; 4 | 5 | CREATE DATABASE `minio`; 6 | CREATE USER minio@'%' IDENTIFIED BY 'hellosu'; 7 | GRANT ALL PRIVILEGES ON minio.* TO minio@'%'; 8 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | -------------------------------------------------------------------------------- /deploy/config.yml.exmaple: -------------------------------------------------------------------------------- 1 | private_key: "~/.ssh/id_rsa" 2 | -------------------------------------------------------------------------------- /deploy/deploy.py: -------------------------------------------------------------------------------- 1 | import paramiko 2 | import yaml 3 | import os 4 | import sys 5 | 6 | 7 | def read_config(): 8 | with open(os.path.join(os.path.dirname(__file__), "config.yml")) as f: 9 | data = yaml.load(f) 10 | return data 11 | 12 | 13 | def get_client(): 14 | client = paramiko.SSHClient() 15 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 16 | return client 17 | 18 | 19 | def update_web_server(client, host, pkey): 20 | client.connect(host, username="root", pkey=pkey) 21 | (sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/easyctf-platform && git reset --hard && git pull origin master && systemctl restart easyctf'") 22 | print(sout.read(), serr.read()) 23 | client.close() 24 | 25 | 26 | def reimport_problems(client, host, pkey): 27 | client.connect(host, username="root", pkey=pkey) 28 | (sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/problems && git reset --hard && git pull origin master && cd /root/easyctf-platform/server && dotenv /var/easyctf/env python3 manage.py import /root/problems'") 29 | print(sout.read(), serr.read()) 30 | client.close() 31 | 32 | 33 | def update_judge(client, host, pkey): 34 | client.connect(host, username="root", pkey=pkey) 35 | (sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/easyctf-platform && git reset --hard && git pull origin master && systemctl restart judge'") 36 | print(sout.read(), serr.read()) 37 | client.close() 38 | 39 | 40 | if __name__ == "__main__": 41 | service = None 42 | if len(sys.argv) > 1: 43 | service = sys.argv[1] 44 | config = read_config() 45 | key_path = os.path.expanduser(config.get("private_key")) 46 | pkey = paramiko.RSAKey.from_private_key_file(key_path) 47 | client = get_client() 48 | if not service or service == "web": 49 | for host in config.get("web"): 50 | update_web_server(client, host, pkey) 51 | reimport_problems(client, host, pkey) 52 | if not service or service == "judge": 53 | for host in config.get("judge"): 54 | update_judge(client, host, pkey) 55 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | nginx: 5 | image: nginx 6 | ports: [3000:80] 7 | volumes: 8 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 9 | - ./nginx/easyctf.conf:/etc/nginx/conf.d/default.conf 10 | - ./ctf-data/filestore:/var/opt/filestore 11 | 12 | app: 13 | build: server 14 | depends_on: [db, files] 15 | volumes: 16 | - ./server:/app 17 | environment: 18 | - SECRET_KEY=ad88fec19a7641e5de308e45dd4fa1c5 19 | - DATABASE_URL=mysql://app:hellosu@db:3306/app 20 | - S3_RESOURCE=http://minio:9000 21 | - FILESTORE_SAVE_ENDPOINT=http://files:5000/save 22 | - CACHE_REDIS_HOST=redis 23 | 24 | - WAIT_HOSTS=db:3306 25 | - WAIT_HOSTS_TIMEOUT=300 26 | - WAIT_SLEEP_INTERVAL=10 27 | - WAIT_HOST_CONNECT_TIMEOUT=30 28 | 29 | db: 30 | image: mariadb 31 | expose: [3306] 32 | volumes: 33 | - ./db/init.d:/docker-entrypoint-initdb.d 34 | - ./ctf-data/mariadb:/var/lib/mysql 35 | environment: 36 | - MARIADB_ROOT_PASSWORD=45694fd9e39afc4a3597bc2797620e15 37 | 38 | files: 39 | build: filestore 40 | expose: [5000] 41 | volumes: 42 | - ./filestore:/app 43 | - ./ctf-data/filestore:/data 44 | environment: 45 | - UPLOAD_FOLDER=/data/static 46 | - FILESTORE_PORT=5000 47 | 48 | minio: 49 | image: minio/minio 50 | ports: [9000:9000, 9001:9001] 51 | volumes: 52 | - ./ctf-data/minio:/data 53 | command: server --address 0.0.0.0:9000 /data 54 | 55 | redis: 56 | image: redis 57 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Michael Zhang"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "LibreCTF" 7 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Setup](./setup.md) 4 | -------------------------------------------------------------------------------- /docs/src/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow these steps to get a CTF running locally. 4 | 5 | - Make sure you have Docker and Docker Compose installed. 6 | - Clone the repo. 7 | 8 | ```bash 9 | git clone https://github.com/easyctf/librectf 10 | ``` 11 | - Start the Docker services. 12 | 13 | ```bash 14 | docker compose up -d 15 | ``` 16 | - Visit http://localhost:3000 17 | 18 | (TODO: Instructions about MinIO, etc.) 19 | -------------------------------------------------------------------------------- /filestore/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /filestore/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | ENV FLASK_DEBUG=1 3 | 4 | RUN apt-get update -y && apt-get install -y --no-install-recommends \ 5 | libmariadb-dev \ 6 | ; 7 | RUN pip install poetry 8 | 9 | ENV WAIT_VERSION 2.7.2 10 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait 11 | RUN chmod +x /wait 12 | 13 | RUN mkdir -p /app 14 | WORKDIR /app 15 | 16 | COPY poetry.lock . 17 | COPY pyproject.toml . 18 | RUN poetry install 19 | 20 | CMD ["sh", "-c", "/wait && poetry run flask run --host 0.0.0.0"] 21 | -------------------------------------------------------------------------------- /filestore/app.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import logging 4 | import os 5 | 6 | from flask import Flask, abort, request, send_file 7 | 8 | app = Flask(__name__) 9 | app.config["UPLOAD_FOLDER"] = os.getenv("UPLOAD_FOLDER", "/usr/share/nginx/html") 10 | 11 | if not os.path.exists(app.config["UPLOAD_FOLDER"]): 12 | os.makedirs(app.config["UPLOAD_FOLDER"]) 13 | 14 | 15 | @app.route("/") 16 | def index(): 17 | return "You shouldn't be here." 18 | 19 | 20 | @app.route("/save", methods=["POST"]) 21 | def save(): 22 | if "file" not in request.files: 23 | return "no file uploaded", 400 24 | file = request.files["file"] 25 | if file.filename == "": 26 | return "no filename found", 400 27 | filename = hashlib.sha256(file.read()).hexdigest() 28 | file.seek(0) 29 | if "filename" in request.form: 30 | name, ext = json.loads(request.form["filename"]) 31 | filename = "%s.%s.%s" % (name, filename, ext) 32 | else: 33 | if "prefix" in request.form: 34 | filename = "%s%s" % (request.form["prefix"], filename) 35 | if "suffix" in request.form: 36 | filename = "%s%s" % (filename, request.form["suffix"]) 37 | file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) 38 | return filename 39 | 40 | 41 | # This route should be used for debugging filestore locally. 42 | @app.route("/static/") 43 | def serve(path): 44 | path = os.path.join(app.config["UPLOAD_FOLDER"], path) 45 | if not os.path.exists(path): 46 | return abort(404) 47 | return send_file(path) 48 | 49 | 50 | if __name__ == "__main__": 51 | logging.warning("Uploading to {}".format(app.config["UPLOAD_FOLDER"])) 52 | port = int(os.getenv("FILESTORE_PORT", "8001")) 53 | app.run(use_debugger=True, use_reloader=True, port=port, host="0.0.0.0") 54 | -------------------------------------------------------------------------------- /filestore/cloud-provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # run this to set up the server 3 | # only do this the first time 4 | set -e 5 | set -o xtrace 6 | 7 | PROJECT_DIRECTORY="/var/filestore/src" 8 | PYTHON=python3 9 | 10 | echo "installing system dependencies..." 11 | if [ ! -f $HOME/.installdep.filestore.apt ]; then 12 | apt-get update && apt-get install -y \ 13 | git \ 14 | nginx \ 15 | python3 \ 16 | python3-dev \ 17 | python3-nose \ 18 | python3-pip \ 19 | realpath \ 20 | systemd 21 | touch $HOME/.installdep.filestore.apt 22 | fi 23 | 24 | mkdir -p /var/filestore 25 | mkdir -p /var/log/filestore 26 | 27 | if [ ! -d $PROJECT_DIRECTORY ]; then 28 | b=`realpath $(basename $0)` 29 | c=`dirname $b` 30 | # cp -r $d $PROJECT_DIRECTORY 31 | ln -s $c $PROJECT_DIRECTORY 32 | else 33 | (cd $PROJECT_DIRECTORY; git pull origin master || true) 34 | fi 35 | 36 | mkdir -p /usr/share/nginx/html/static 37 | touch /usr/share/nginx/html/static/index.html 38 | echo "" > /usr/share/nginx/html/static/index.html 39 | rm -rf /etc/nginx/conf.d/* /etc/nginx/sites-enabled/* 40 | cp $PROJECT_DIRECTORY/default.conf /etc/nginx/sites-enabled/filestore 41 | 42 | service nginx reload 43 | service nginx restart 44 | 45 | echo "installing python dependencies..." 46 | if [ ! -f $HOME/.installdep.filestore.pip ]; then 47 | $PYTHON -m pip install -U pip 48 | $PYTHON -m pip install gunicorn 49 | $PYTHON -m pip install -r $PROJECT_DIRECTORY/requirements.txt 50 | touch $HOME/.installdep.filestore.pip 51 | fi 52 | 53 | # dirty hack 54 | KILL=/bin/kill 55 | eval "echo \"$(< $PROJECT_DIRECTORY/systemd/filestore.service)\"" > /etc/systemd/system/filestore.service 56 | 57 | echo "Filestore has been deployed!" 58 | echo "Modify the env file at /var/filestore/env." 59 | echo "Then run" 60 | echo 61 | echo "systemctl --system daemon-reload && systemctl restart filestore" 62 | echo "gucci gang" 63 | 64 | cp env.example /var/filestore/env 65 | systemctl --system daemon-reload && systemctl restart filestore 66 | -------------------------------------------------------------------------------- /filestore/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.html; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /filestore/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd /var/filestore/src 4 | 5 | PYTHON=/usr/bin/python3 6 | 7 | echo "determining bind location..." 8 | BIND_PORT=${FILESTORE_PORT:-8000} 9 | PRIVATE_BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/private/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0") 10 | PRIVATE_BIND_ADDR=$(echo $BIND_ADDR_ | xargs) 11 | PUBLIC_BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0") 12 | PUBLIC_BIND_ADDR=$(echo $BIND_ADDR_ | xargs) 13 | 14 | WORKERS=${WORKERS:-4} 15 | ENVIRONMENT=${ENVIRONMENT:-production} 16 | service nginx start 17 | if [ "$ENVIRONMENT" == "development" ]; then 18 | $PYTHON app.py 19 | else 20 | exec gunicorn --bind="$PRIVATE_BIND_ADDR:$BIND_PORT" --bind="$PUBLIC_BIND_ADDR:$BIND_PORT" -w $WORKERS app:app 21 | fi 22 | -------------------------------------------------------------------------------- /filestore/env.example: -------------------------------------------------------------------------------- 1 | UPLOAD_FOLDER=/usr/share/nginx/html 2 | FILESTORE_PORT=8001 3 | -------------------------------------------------------------------------------- /filestore/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "filestore" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Michael Zhang "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | Flask = "^2.2.2" 10 | 11 | [tool.poetry.dev-dependencies] 12 | 13 | [build-system] 14 | requires = ["poetry-core>=1.0.0"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /filestore/systemd/filestore.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=easyctf static file server 3 | After=network.target 4 | 5 | [Service] 6 | EnvironmentFile=/var/filestore/env 7 | PIDFile=/run/filestore/pid 8 | User=root 9 | WorkingDirectory=$PROJECT_DIRECTORY 10 | ExecStart=$PROJECT_DIRECTORY/entrypoint.sh 11 | ExecReload=$KILL -s HUP \$MAINPID 12 | ExecStop=$KILL -s TERM \$MAINPID 13 | PrivateTmp=true 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1667395993, 6 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "flake-utils", 14 | "type": "indirect" 15 | } 16 | }, 17 | "nixpkgs": { 18 | "locked": { 19 | "lastModified": 1653936696, 20 | "narHash": "sha256-M6bJShji9AIDZ7Kh7CPwPBPb/T7RiVev2PAcOi4fxDQ=", 21 | "owner": "nixos", 22 | "repo": "nixpkgs", 23 | "rev": "ce6aa13369b667ac2542593170993504932eb836", 24 | "type": "github" 25 | }, 26 | "original": { 27 | "id": "nixpkgs", 28 | "type": "indirect" 29 | } 30 | }, 31 | "root": { 32 | "inputs": { 33 | "flake-utils": "flake-utils", 34 | "nixpkgs": "nixpkgs" 35 | } 36 | } 37 | }, 38 | "root": "root", 39 | "version": 7 40 | } 41 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputs = { self, nixpkgs, flake-utils }: 3 | flake-utils.lib.eachDefaultSystem (system: 4 | let 5 | pkgs = import nixpkgs { inherit system; }; 6 | pythonPackages = pkgs.python310Packages; 7 | in { 8 | devShell = pkgs.mkShell { 9 | buildInputs = with pkgs; [ 10 | (python310.withPackages (p: with p; [ black poetry ])) 11 | libmysqlclient 12 | mdbook 13 | ]; 14 | 15 | SECRET_KEY = "ad88fec19a7641e5de308e45dd4fa1c5"; 16 | }; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /judge/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | # ideally these should all be in a subfolder 3 | generator.py 4 | grader.py 5 | program.py 6 | case_number 7 | input 8 | report 9 | error 10 | .idea 11 | -------------------------------------------------------------------------------- /judge/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | 5 | from languages import languages, Python3 6 | from models import Job, Problem 7 | 8 | 9 | class API(object): 10 | def __init__(self, key, base_url): 11 | self.key = key 12 | self.base_url = base_url 13 | 14 | def api_call(self, url, method="GET", data=None, headers=None): 15 | if headers is None: 16 | headers = dict() 17 | headers.update({"API-Key": self.key}) 18 | r = requests.request(method, url, data=data, headers=headers) 19 | return r 20 | 21 | def claim(self): 22 | r = self.api_call(self.base_url + "/jobs") 23 | print("text:", repr(r.text)) 24 | if not r.text: 25 | return None 26 | required_fields = [ 27 | "id", 28 | "language", 29 | "source", 30 | "pid", 31 | "test_cases", 32 | "time_limit", 33 | "memory_limit", 34 | "generator_code", 35 | "grader_code", 36 | "source_verifier_code", 37 | ] 38 | # create job object 39 | obj = r.json() 40 | if not all(field in obj for field in required_fields): 41 | return None 42 | problem = Problem( 43 | obj["pid"], 44 | obj["test_cases"], 45 | obj["time_limit"], 46 | obj["memory_limit"], 47 | obj["generator_code"], 48 | Python3, 49 | obj["grader_code"], 50 | Python3, 51 | obj["source_verifier_code"], 52 | Python3, 53 | ) 54 | language = languages.get(obj["language"]) 55 | if not language: 56 | return None # TODO: should definitely not do this 57 | return Job(obj["id"], problem, obj["source"], language) 58 | 59 | def submit(self, result): 60 | verdict = result.verdict 61 | data = dict( 62 | id=result.job.id, 63 | verdict=result.verdict.value if verdict else "JE", 64 | last_ran_case=result.last_ran_case, 65 | execution_time=result.execution_time, 66 | execution_memory=result.execution_memory, 67 | ) 68 | r = self.api_call(self.base_url + "/jobs", method="POST", data=data) 69 | return r.status_code // 100 == 2 70 | -------------------------------------------------------------------------------- /judge/cloud-provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | declare API_KEY=$1 3 | declare JUDGE_URL=$2 4 | 5 | if [ ! $API_KEY ]; then 6 | echo "please provide a key." 7 | exit 1 8 | fi 9 | 10 | PROJECT_DIRECTORY="/var/judge/src" 11 | PYTHON=$(which python3) 12 | mkdir -p /var/judge 13 | mkdir -p /var/log/judge 14 | 15 | echo "installing system dependencies..." 16 | if [ ! -f $HOME/.installdep.judge.apt ]; then 17 | apt-get update && apt-get install -y software-properties-common && \ 18 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5BB92C09DB82666C && \ 19 | add-apt-repository -y ppa:fkrull/deadsnakes && \ 20 | add-apt-repository -y ppa:openjdk-r/ppa && \ 21 | apt-get install -y \ 22 | build-essential \ 23 | openjdk-7-jdk \ 24 | pkg-config \ 25 | python2.7 \ 26 | python3.5 \ 27 | python3 \ 28 | python3-pip 29 | touch $HOME/.installdep.judge.apt 30 | fi 31 | 32 | if [ ! -d $PROJECT_DIRECTORY ]; then 33 | b=`realpath $(basename $0)` 34 | c=`dirname $b` 35 | d=`dirname $c` 36 | ln -s $c $PROJECT_DIRECTORY 37 | else 38 | (cd $PROJECT_DIRECTORY; git pull origin master || true) 39 | fi 40 | 41 | echo "installing python dependencies..." 42 | if [ ! -f $HOME/.installdep.judge.pip ]; then 43 | $PYTHON -m pip install -U pip 44 | $PYTHON -m pip install requests 45 | touch $HOME/.installdep.judge.pip 46 | fi 47 | 48 | # dirty hack 49 | echo "writing systemd entry..." 50 | PYTHON=$(which python3) 51 | eval "echo \"$(< $PROJECT_DIRECTORY/systemd/judge.service)\"" > /etc/systemd/system/judge.service 52 | 53 | systemctl daemon-reload 54 | systemctl enable judge 55 | systemctl start judge 56 | -------------------------------------------------------------------------------- /judge/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | from typing import Dict 4 | 5 | 6 | APP_ROOT = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) 7 | CONFINE_PATH = APP_ROOT / "confine" 8 | 9 | COMPILATION_TIME_LIMIT = 10 10 | GRADER_TIME_LIMIT = 10 11 | 12 | PARTIAL_JOB_SUBMIT_TIME_THRESHOLD = 2 # Seconds 13 | PARTIAL_JOB_SUBMIT_CASES_THRESHOLD = 10 14 | -------------------------------------------------------------------------------- /judge/confine: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/judge/confine -------------------------------------------------------------------------------- /judge/judge.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | import signal 5 | import sys 6 | import time 7 | import tempfile 8 | import traceback 9 | 10 | import executor 11 | from api import API 12 | from models import Job 13 | 14 | logger = logging.getLogger(__name__) 15 | logger.setLevel(logging.INFO) 16 | logging.info("Starting up") 17 | 18 | 19 | api = None 20 | judge_url = None 21 | current_job = None # type: Job 22 | 23 | 24 | def loop(): 25 | global current_job 26 | job = api.claim() 27 | current_job = job 28 | if not job: 29 | logger.debug("No jobs available.") 30 | return False 31 | logger.info("Got job %d.", job.id) 32 | 33 | tempdir = tempfile.mkdtemp(prefix="jury-") 34 | try: 35 | for execution_result in executor.run_job(job, tempdir): 36 | # execution_result is partial here 37 | 38 | logger.info( 39 | "Job %d partially judged; case: %d, time: %.2f, memory: %d", 40 | job.id, 41 | execution_result.last_ran_case, 42 | execution_result.execution_time, 43 | execution_result.execution_memory, 44 | ) 45 | 46 | if execution_result.verdict: 47 | # This should be the last value returned by run_job 48 | logger.info( 49 | "Job %d finished with verdict %s." 50 | % (job.id, execution_result.verdict.value) 51 | ) 52 | 53 | if api.submit(execution_result): 54 | logger.info("Job %d successfully partially submitted." % job.id) 55 | else: 56 | logger.info("Job %d failed to partially submit." % job.id) 57 | except: 58 | traceback.print_exc(file=sys.stderr) 59 | shutil.rmtree(tempdir, ignore_errors=True) 60 | finally: 61 | shutil.rmtree(tempdir, ignore_errors=True) 62 | 63 | return True 64 | 65 | 66 | if __name__ == "__main__": 67 | api_key = os.getenv("API_KEY") 68 | if not api_key: 69 | print("no api key", file=sys.stderr) 70 | sys.exit(1) 71 | judge_url = os.getenv("JUDGE_URL") 72 | if not judge_url: 73 | print("no judge url", file=sys.stderr) 74 | sys.exit(1) 75 | api = API(api_key, judge_url) 76 | while True: 77 | try: 78 | if not loop(): 79 | time.sleep(3) 80 | except KeyboardInterrupt: 81 | sys.exit(0) 82 | except: 83 | traceback.print_exc(file=sys.stderr) 84 | -------------------------------------------------------------------------------- /judge/models.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Problem: 5 | def __init__( 6 | self, 7 | id: int, 8 | test_cases: int, 9 | time_limit: float, 10 | memory_limit: int, 11 | generator_code: str, 12 | generator_language, 13 | grader_code: str, 14 | grader_language, 15 | source_verifier_code: str = None, 16 | source_verifier_language=None, 17 | ): 18 | self.id = id 19 | self.test_cases = test_cases 20 | self.time_limit = time_limit 21 | self.memory_limit = memory_limit 22 | self.generator_code = generator_code 23 | self.generator_language = generator_language 24 | self.grader_code = grader_code 25 | self.grader_language = grader_language 26 | self.source_verifier_code = source_verifier_code 27 | self.source_verifier_language = source_verifier_language 28 | 29 | 30 | class Job: 31 | def __init__(self, id: int, problem: Problem, code: str, language): 32 | self.id = id 33 | self.problem = problem 34 | self.code = code 35 | self.language = language 36 | 37 | 38 | class JobVerdict(enum.Enum): 39 | accepted = "AC" 40 | ran = "RAN" 41 | invalid_source = "IS" 42 | wrong_answer = "WA" 43 | time_limit_exceeded = "TLE" 44 | memory_limit_exceeded = "MLE" 45 | runtime_error = "RTE" 46 | illegal_syscall = "ISC" 47 | compilation_error = "CE" 48 | judge_error = "JE" 49 | 50 | 51 | class ExecutionResult: 52 | def __init__( 53 | self, 54 | job: Job, 55 | verdict: JobVerdict, 56 | last_ran_case: int, 57 | execution_time: float, 58 | execution_memory: int, 59 | ): 60 | self.job = job 61 | self.verdict = verdict 62 | self.last_ran_case = last_ran_case 63 | self.execution_time = execution_time 64 | self.execution_memory = execution_memory 65 | -------------------------------------------------------------------------------- /judge/nsjail: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/judge/nsjail -------------------------------------------------------------------------------- /judge/output: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/judge/output -------------------------------------------------------------------------------- /judge/systemd/judge.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=indepedent judging unit 3 | 4 | [Service] 5 | Restart=always 6 | Environment=\"API_KEY=$API_KEY\" 7 | Environment=\"JUDGE_URL=$JUDGE_URL\" 8 | ExecStart=$PYTHON $PROJECT_DIRECTORY/judge.py 9 | ExecStop=: 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | COPY easyctf.conf /etc/nginx/conf.d/default.conf 5 | -------------------------------------------------------------------------------- /nginx/easyctf.conf: -------------------------------------------------------------------------------- 1 | upstream srv { 2 | server app:5000; 3 | } 4 | 5 | server { 6 | listen 80 default_server; 7 | server_name localhost localhost.easyctf.com; 8 | 9 | access_log /var/log/nginx/access.log; 10 | error_log /var/log/nginx/error.log error; 11 | 12 | underscores_in_headers on; 13 | 14 | location /static/ { 15 | root /var/opt/filestore; 16 | autoindex off; 17 | } 18 | 19 | location / { 20 | proxy_set_header HOST $host; 21 | proxy_set_header X-Forwarded-Proto $scheme; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_pass http://srv/; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile off; 22 | #tcp_nopush on; 23 | 24 | keepalive_timeout 65; 25 | 26 | gzip on; 27 | gzip_proxied any; 28 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | # include /etc/nginx/sites-enabled/easyctf.conf; 32 | } 33 | -------------------------------------------------------------------------------- /provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apt-get update && apt-get install -y \ 4 | build-essential \ 5 | git \ 6 | libffi-dev \ 7 | libjpeg-dev \ 8 | libmysqlclient-dev \ 9 | libpng-dev \ 10 | libssl-dev \ 11 | mysql-client \ 12 | mysql-server \ 13 | nginx \ 14 | openssh-client \ 15 | pkg-config \ 16 | python2.7 \ 17 | python3 \ 18 | python3-dev \ 19 | python3-nose \ 20 | python3-pip \ 21 | realpath \ 22 | redis-server \ 23 | systemd \ 24 | 25 | (cd server; ./cloud-provision.sh) 26 | 27 | (cd filestore; ./cloud-provision.sh) 28 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/.dockerignore -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | easyctf.db 4 | secret.sh -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | ENV FLASK_DEBUG=1 3 | 4 | RUN apt-get update -y && apt-get install -y --no-install-recommends \ 5 | libmariadb-dev \ 6 | ; 7 | RUN pip install poetry 8 | 9 | ENV WAIT_VERSION 2.7.2 10 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait 11 | RUN chmod +x /wait 12 | 13 | RUN mkdir -p /app 14 | WORKDIR /app 15 | 16 | COPY poetry.lock . 17 | COPY pyproject.toml . 18 | RUN poetry install 19 | 20 | CMD ["sh", "-c", "/wait && poetry run flask db upgrade && poetry run flask run --host 0.0.0.0"] 21 | -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- 1 | from easyctf import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /server/cloud-provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # run this to set up the server 3 | # only do this the first time 4 | set -e 5 | # set -o xtrace 6 | 7 | REPOSITORY="git@github.com:iptq/easyctf-platform.git" 8 | PROJECT_DIRECTORY="/var/easyctf/src" 9 | PYTHON=python3 10 | 11 | echo "installing system dependencies..." 12 | if [ ! -f $HOME/.installdep.server.apt ]; then 13 | apt-get update && apt-get install -y \ 14 | git \ 15 | libffi-dev \ 16 | libjpeg-dev \ 17 | libmysqlclient-dev \ 18 | libpng-dev \ 19 | libssl-dev \ 20 | mysql-client \ 21 | openssh-client \ 22 | python3 \ 23 | python3-dev \ 24 | python3-nose \ 25 | python3-pip \ 26 | realpath \ 27 | systemd 28 | touch $HOME/.installdep.server.apt 29 | fi 30 | 31 | mkdir -p /var/easyctf 32 | mkdir -p /var/log/easyctf 33 | 34 | if [ ! -d $PROJECT_DIRECTORY ]; then 35 | # why the fuck shoul i clone if i already hav this file LMAO 36 | b=`realpath $(basename $0)` 37 | c=`dirname $b` 38 | d=`dirname $c` 39 | # cp -r $d $PROJECT_DIRECTORY 40 | ln -s $c $PROJECT_DIRECTORY 41 | else 42 | (cd $PROJECT_DIRECTORY; git pull origin master || true) 43 | fi 44 | 45 | echo "installing python dependencies..." 46 | if [ ! -f $HOME/.installdep.server.pip ]; then 47 | $PYTHON -m pip install -U pip 48 | $PYTHON -m pip install gunicorn 49 | $PYTHON -m pip install -r $PROJECT_DIRECTORY/requirements.txt 50 | touch $HOME/.installdep.server.pip 51 | fi 52 | 53 | # dirty hack 54 | KILL=/bin/kill 55 | eval "echo \"$(< $PROJECT_DIRECTORY/systemd/easyctf.service)\"" > /etc/systemd/system/easyctf.service 56 | 57 | echo "EasyCTF has been deployed!" 58 | echo "Modify the env file at /var/easyctf/env." 59 | echo "Then run" 60 | echo 61 | echo "systemctl --system daemon-reload && systemctl restart easyctf" 62 | echo "gucci gang" 63 | 64 | cp env.example /var/easyctf/env 65 | systemctl --system daemon-reload && systemctl restart easyctf 66 | -------------------------------------------------------------------------------- /server/easyctf/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import time 3 | import logging 4 | import socket 5 | 6 | from flask import Flask, request 7 | from flask_login import current_user 8 | 9 | 10 | def create_app(config=None): 11 | app = Flask(__name__, static_folder="assets", static_url_path="/assets") 12 | hostname = socket.gethostname() 13 | 14 | if not config: 15 | from easyctf.config import Config 16 | 17 | config = Config() 18 | app.config.from_object(config) 19 | 20 | from easyctf.objects import cache, db, login_manager, sentry, migrate, s3 21 | import easyctf.models 22 | 23 | cache.init_app(app) 24 | db.init_app(app) 25 | migrate.init_app(app, db) 26 | login_manager.init_app(app) 27 | s3.init_app(app) 28 | if app.config.get("ENVIRONMENT") != "development": 29 | sentry.init_app(app, logging=True, level=logging.WARNING) 30 | 31 | from easyctf.utils import filestore, to_place_str, to_timestamp 32 | 33 | app.jinja_env.globals.update(filestore=filestore) 34 | app.jinja_env.filters["to_timestamp"] = to_timestamp 35 | app.jinja_env.filters["to_place_str"] = to_place_str 36 | 37 | from easyctf.models import Config 38 | 39 | def get_competition_running(): 40 | configs = Config.get_many(["start_time", "end_time"]) 41 | if "start_time" not in configs or "end_time" not in configs: 42 | return None, None, False 43 | start_time_str = configs["start_time"] 44 | end_time_str = configs["end_time"] 45 | 46 | start_time = datetime.fromtimestamp(float(start_time_str)) 47 | end_time = datetime.fromtimestamp(float(end_time_str)) 48 | now = datetime.utcnow() 49 | 50 | competition_running = start_time < now and now < end_time 51 | return start_time, end_time, competition_running 52 | 53 | @app.after_request 54 | def easter_egg_link(response): 55 | if not request.cookies.get("easter_egg_enabled"): 56 | response.set_cookie("easter_egg_enabled", "0") 57 | return response 58 | 59 | # TODO: actually finish this 60 | @app.context_processor 61 | def inject_config(): 62 | ( 63 | competition_start, 64 | competition_end, 65 | competition_running, 66 | ) = get_competition_running() 67 | easter_egg_enabled = False 68 | if competition_running and current_user.is_authenticated: 69 | try: 70 | easter_egg_enabled = int(request.cookies.get("easter_egg_enabled")) == 1 71 | except: 72 | pass 73 | config = dict( 74 | admin_email="", 75 | hostname=hostname, 76 | competition_running=competition_running, 77 | competition_start=competition_start, 78 | competition_end=competition_end, 79 | ctf_name=Config.get("ctf_name", "OpenCTF"), 80 | easter_egg_enabled=easter_egg_enabled, 81 | environment=app.config.get("ENVIRONMENT", "production"), 82 | ) 83 | return config 84 | 85 | from easyctf.views import admin, base, classroom, chals, game, judge, teams, users 86 | 87 | app.register_blueprint(admin.blueprint, url_prefix="/admin") 88 | app.register_blueprint(base.blueprint) 89 | app.register_blueprint(classroom.blueprint, url_prefix="/classroom") 90 | app.register_blueprint(chals.blueprint, url_prefix="/chals") 91 | app.register_blueprint(game.blueprint, url_prefix="/game") 92 | app.register_blueprint(judge.blueprint, url_prefix="/judge") 93 | app.register_blueprint(teams.blueprint, url_prefix="/teams") 94 | app.register_blueprint(users.blueprint, url_prefix="/users") 95 | 96 | return app 97 | -------------------------------------------------------------------------------- /server/easyctf/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Source Sans Pro"; 3 | font-weight: 300; 4 | src: url("../fonts/SourceSansPro-Light.ttf") format("truetype"); 5 | } 6 | 7 | @font-face { 8 | font-family: "Source Sans Pro"; 9 | font-weight: 400; 10 | src: url("../fonts/SourceSansPro-Regular.ttf") format("truetype"); 11 | } 12 | 13 | html, 14 | body { 15 | font-family: "Source Sans Pro"; 16 | font-weight: 300 !important; 17 | } 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6, 25 | .btn { 26 | font-weight: 300 !important; 27 | } 28 | 29 | #main-content { 30 | min-height: 85vh; 31 | } 32 | 33 | .navbar { 34 | margin-bottom: 0; 35 | border-radius: 0; 36 | -moz-border-radius: 0; 37 | -webkit-border-radius: 0; 38 | } 39 | 40 | .navatar { 41 | width: 20px; 42 | height: 20px; 43 | border-radius: 2px; 44 | margin-right: 4px; 45 | } 46 | 47 | .section { 48 | padding: 30px 0 50px 0; 49 | } 50 | 51 | .logo-table { 52 | margin: 10px; 53 | } 54 | 55 | .logo-table td { 56 | padding: 10px; 57 | vertical-align: middle; 58 | } 59 | 60 | .logo-table a:hover { 61 | text-decoration: none; 62 | } 63 | 64 | .logo-table h2 { 65 | margin: 0; 66 | } 67 | 68 | .logo { 69 | max-height: 100px; 70 | } 71 | 72 | .badge a { 73 | color: white; 74 | text-decoration: none; 75 | } 76 | 77 | .gradient { 78 | color: #fff; 79 | background: #31abc6; 80 | background: linear-gradient(45deg, #660099 0%, #cc3399 100%); 81 | } 82 | 83 | #masthead { 84 | min-height: 40vh; 85 | position: relative; 86 | margin: 0; 87 | padding-top: 34px; 88 | padding-bottom: 42px; 89 | 90 | color: #fff; 91 | } 92 | 93 | #title h1 { 94 | color: rgba(255, 255, 255, 1); 95 | font-size: 6.5em; 96 | } 97 | 98 | #title h2 { 99 | color: rgba(255, 255, 255, 0.8); 100 | font-size: 2em; 101 | } 102 | 103 | #title a { 104 | color: rgba(255, 255, 255, 0.6); 105 | font-size: 1.2em; 106 | text-decoration: none; 107 | } 108 | 109 | #links a { 110 | margin: 0 12px 0 12px; 111 | color: #555; 112 | font-size: 0.75em; 113 | } 114 | 115 | .site-footer { 116 | /*margin-top: 90px;*/ 117 | background-color: #eee; 118 | color: #999; 119 | padding: 3em 0 0; 120 | font-size: 14.3px; 121 | position: relative; 122 | } 123 | 124 | .site-footer .footer-heading { 125 | color: #555; 126 | } 127 | 128 | .footer-copyright { 129 | margin-top: 2em; 130 | padding: 2em 2em; 131 | border-top: 1px solid rgba(0, 0, 0, 0.1); 132 | text-align: center; 133 | } 134 | 135 | .site-footer h5, 136 | .site-footer .h5 { 137 | font-size: 1.2em; 138 | } 139 | 140 | .container.jumbotron { 141 | background-color: transparent; 142 | } 143 | 144 | html, 145 | body, 146 | h1, 147 | h2, 148 | h3, 149 | h4, 150 | h5, 151 | h6 { 152 | font-family: "Source Sans Pro", Arial, sans-serif; 153 | font-weight: 300; 154 | } 155 | 156 | b { 157 | font-weight: 400; 158 | } 159 | 160 | .large-text { 161 | font-size: 1.6em; 162 | } 163 | 164 | .tab-content { 165 | padding: 15px; 166 | } 167 | 168 | .tab-content > .tab-pane { 169 | display: none; 170 | } 171 | 172 | .tab-content > .active { 173 | display: block; 174 | } 175 | 176 | .navbar { 177 | border-radius: 0; 178 | } 179 | 180 | .site-footer { 181 | background-color: #eee; 182 | color: #999; 183 | padding: 3em 0 0; 184 | font-size: 14.3px; 185 | position: relative; 186 | } 187 | 188 | .site-footer .footer-heading { 189 | color: #555; 190 | } 191 | 192 | .footer-copyright { 193 | margin-top: 2em; 194 | padding: 2em 2em; 195 | border-top: 1px solid rgba(0, 0, 0, 0.1); 196 | text-align: center; 197 | } 198 | 199 | h5, 200 | .h5 { 201 | font-size: 1.2em; 202 | } 203 | 204 | #links a { 205 | margin: 0 12px 0 12px; 206 | color: #555; 207 | font-size: 0.75em; 208 | } 209 | 210 | .selectize-input { 211 | height: 34px; 212 | padding: 6px 12px; 213 | font-size: 14px; 214 | -webkit-transition: border-color ease-in-out 0.15s, 215 | -webkit-box-shadow ease-in-out 0.15s; 216 | -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 217 | transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 218 | } 219 | 220 | .selectize-control { 221 | margin-top: 8px !important; 222 | } 223 | 224 | .selectize-input .item { 225 | padding: 1px 6px !important; 226 | } 227 | -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-Black.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-BlackItalic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-Bold.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-BoldItalic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-ExtraLight.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-Italic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-Light.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-LightItalic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-SemiBold.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/SourceSansPro-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/SourceSansPro-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /server/easyctf/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /server/easyctf/assets/images/digitalocean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/digitalocean.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/090939.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/090939.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/background.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/background_rain.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/background_rain.gif -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/char_A_scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/char_A_scale.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/char_B_scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/char_B_scale.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/char_C_scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/char_C_scale.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/char_D_scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/char_D_scale.png -------------------------------------------------------------------------------- /server/easyctf/assets/images/game/char_E_scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/assets/images/game/char_E_scale.png -------------------------------------------------------------------------------- /server/easyctf/assets/js/livestamp.min.js: -------------------------------------------------------------------------------- 1 | // Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License 2 | (function(d,g){var h=1E3,i=!1,e=d([]),j=function(b,a){var c=b.data("livestampdata");"number"==typeof a&&(a*=1E3);b.removeAttr("data-livestamp").removeData("livestamp");a=g(a);g.isMoment(a)&&!isNaN(+a)&&(c=d.extend({},{original:b.contents()},c),c.moment=g(a),b.data("livestampdata",c).empty(),e.push(b[0]))},k=function(){i||(f.update(),setTimeout(k,h))},f={update:function(){d("[data-livestamp]").each(function(){var a=d(this);j(a,a.data("livestamp"))});var b=[];e.each(function(){var a=d(this),c=a.data("livestampdata"); 3 | if(void 0===c)b.push(this);else if(g.isMoment(c.moment)){var e=a.html(),c=c.moment.fromNow();if(e!=c){var f=d.Event("change.livestamp");a.trigger(f,[e,c]);f.isDefaultPrevented()||a.html(c)}}});e=e.not(b)},pause:function(){i=!0},resume:function(){i=!1;k()},interval:function(b){if(void 0===b)return h;h=b}},l={add:function(b,a){"number"==typeof a&&(a*=1E3);a=g(a);g.isMoment(a)&&!isNaN(+a)&&(b.each(function(){j(d(this),a)}),f.update());return b},destroy:function(b){e=e.not(b);b.each(function(){var a= 4 | d(this),c=a.data("livestampdata");if(void 0===c)return b;a.html(c.original?c.original:"").removeData("livestampdata")});return b},isLivestamp:function(b){return void 0!==b.data("livestampdata")}};d.livestamp=f;d(function(){f.resume()});d.fn.livestamp=function(b,a){l[b]||(a=b,b="add");return l[b](this,a)}})(jQuery,moment); 5 | -------------------------------------------------------------------------------- /server/easyctf/assets/js/smooth-scroll.min.js: -------------------------------------------------------------------------------- 1 | /*! smooth-scroll v7.1.1 | (c) 2015 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */ 2 | !function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.smoothScroll=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var t,n,o,r,a={},u="querySelector"in document&&"addEventListener"in e,c={selector:"[data-scroll]",selectorHeader:"[data-scroll-header]",speed:500,easing:"easeInOutCubic",offset:0,updateURL:!0,callback:function(){}},i=function(){var e={},t=!1,n=0,o=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],n++);for(var r=function(n){for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t&&"[object Object]"===Object.prototype.toString.call(n[o])?e[o]=i(!0,e[o],n[o]):e[o]=n[o])};o>n;n++){var a=arguments[n];r(a)}return e},s=function(e){return Math.max(e.scrollHeight,e.offsetHeight,e.clientHeight)},l=function(e,t){var n,o,r=t.charAt(0),a="classList"in document.documentElement;for("["===r&&(t=t.substr(1,t.length-2),n=t.split("="),n.length>1&&(o=!0,n[1]=n[1].replace(/"/g,"").replace(/'/g,"")));e&&e!==document;e=e.parentNode){if("."===r)if(a){if(e.classList.contains(t.substr(1)))return e}else if(new RegExp("(^|\\s)"+t.substr(1)+"(\\s|$)").test(e.className))return e;if("#"===r&&e.id===t.substr(1))return e;if("["===r&&e.hasAttribute(n[0])){if(!o)return e;if(e.getAttribute(n[0])===n[1])return e}if(e.tagName.toLowerCase()===t)return e}return null},f=function(e){for(var t,n=String(e),o=n.length,r=-1,a="",u=n.charCodeAt(0);++r=1&&31>=t||127==t||0===r&&t>=48&&57>=t||1===r&&t>=48&&57>=t&&45===u?"\\"+t.toString(16)+" ":t>=128||45===t||95===t||t>=48&&57>=t||t>=65&&90>=t||t>=97&&122>=t?n.charAt(r):"\\"+n.charAt(r)}return a},d=function(e,t){var n;return"easeInQuad"===e&&(n=t*t),"easeOutQuad"===e&&(n=t*(2-t)),"easeInOutQuad"===e&&(n=.5>t?2*t*t:-1+(4-2*t)*t),"easeInCubic"===e&&(n=t*t*t),"easeOutCubic"===e&&(n=--t*t*t+1),"easeInOutCubic"===e&&(n=.5>t?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1),"easeInQuart"===e&&(n=t*t*t*t),"easeOutQuart"===e&&(n=1- --t*t*t*t),"easeInOutQuart"===e&&(n=.5>t?8*t*t*t*t:1-8*--t*t*t*t),"easeInQuint"===e&&(n=t*t*t*t*t),"easeOutQuint"===e&&(n=1+--t*t*t*t*t),"easeInOutQuint"===e&&(n=.5>t?16*t*t*t*t*t:1+16*--t*t*t*t*t),n||t},m=function(e,t,n){var o=0;if(e.offsetParent)do o+=e.offsetTop,e=e.offsetParent;while(e);return o=o-t-n,o>=0?o:0},h=function(){return Math.max(e.document.body.scrollHeight,e.document.documentElement.scrollHeight,e.document.body.offsetHeight,e.document.documentElement.offsetHeight,e.document.body.clientHeight,e.document.documentElement.clientHeight)},p=function(e){return e&&"object"==typeof JSON&&"function"==typeof JSON.parse?JSON.parse(e):{}},g=function(t,n){e.history.pushState&&(n||"true"===n)&&"file:"!==e.location.protocol&&e.history.pushState(null,null,[e.location.protocol,"//",e.location.host,e.location.pathname,e.location.search,t].join(""))},b=function(e){return null===e?0:s(e)+e.offsetTop};a.animateScroll=function(t,n,a){var u=p(t?t.getAttribute("data-options"):null),s=i(s||c,a||{},u);n="#"+f(n.substr(1));var l="#"===n?e.document.documentElement:e.document.querySelector(n),v=e.pageYOffset;o||(o=e.document.querySelector(s.selectorHeader)),r||(r=b(o));var y,O,S,I=m(l,r,parseInt(s.offset,10)),H=I-v,E=h(),L=0;g(n,s.updateURL);var j=function(o,r,a){var u=e.pageYOffset;(o==r||u==r||e.innerHeight+u>=E)&&(clearInterval(a),l.focus(),s.callback(t,n))},w=function(){L+=16,O=L/parseInt(s.speed,10),O=O>1?1:O,S=v+H*d(s.easing,O),e.scrollTo(0,Math.floor(S)),j(S,I,y)},C=function(){y=setInterval(w,16)};0===e.pageYOffset&&e.scrollTo(0,0),C()};var v=function(e){var n=l(e.target,t.selector);n&&"a"===n.tagName.toLowerCase()&&(e.preventDefault(),a.animateScroll(n,n.hash,t))},y=function(e){n||(n=setTimeout(function(){n=null,r=b(o)},66))};return a.destroy=function(){t&&(e.document.removeEventListener("click",v,!1),e.removeEventListener("resize",y,!1),t=null,n=null,o=null,r=null)},a.init=function(n){u&&(a.destroy(),t=i(c,n||{}),o=e.document.querySelector(t.selectorHeader),r=b(o),e.document.addEventListener("click",v,!1),o&&e.addEventListener("resize",y,!1))},a}); -------------------------------------------------------------------------------- /server/easyctf/config.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import os 3 | import sys 4 | import logging 5 | 6 | import pathlib 7 | from cachelib import RedisCache 8 | 9 | 10 | class CTFCache(RedisCache): 11 | def dump_object(self, value): 12 | value_type = type(value) 13 | if value_type in (int, int): 14 | return str(value).encode("ascii") 15 | return b"!" + pickle.dumps(value, -1) 16 | 17 | 18 | def cache(app, config, args, kwargs): 19 | kwargs["host"] = app.config.get("CACHE_REDIS_HOST", "localhost") 20 | return CTFCache(*args, **kwargs) 21 | 22 | 23 | class Config(object): 24 | def __init__(self, app_root=None, testing=False): 25 | if app_root is None: 26 | self.app_root = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) 27 | else: 28 | self.app_root = pathlib.Path(app_root) 29 | 30 | self.TESTING = False 31 | self.SECRET_KEY = self._load_secret_key() 32 | self.SQLALCHEMY_DATABASE_URI = self._get_database_url() 33 | self.SQLALCHEMY_TRACK_MODIFICATIONS = False 34 | self.PREFERRED_URL_SCHEME = "https" 35 | 36 | self.CACHE_TYPE = "easyctf.config.cache" 37 | self.CACHE_REDIS_HOST = os.getenv("CACHE_REDIS_HOST", "redis") 38 | 39 | self.ENVIRONMENT = os.getenv("ENVIRONMENT", "production") 40 | self.EMAIL_VERIFICATION_REQUIRED = 0 41 | # self.EMAIL_VERIFICATION_REQUIRED = int(os.getenv( 42 | # "EMAIL_VERIFICATION_REQUIRED", "1" if self.ENVIRONMENT == "production" else "0")) 43 | 44 | self.S3_RESOURCE = os.getenv("S3_RESOURCE", "") 45 | self.FILESTORE_SAVE_ENDPOINT = os.getenv( 46 | "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save" 47 | ) 48 | self.FILESTORE_STATIC = os.getenv("FILESTORE_STATIC", "/static") 49 | 50 | self.JUDGE_URL = os.getenv("JUDGE_URL", "http://127.0.0.1/") 51 | self.JUDGE_API_KEY = os.getenv("JUDGE_API_KEY", "") 52 | self.SHELL_HOST = os.getenv("SHELL_HOST", "") 53 | 54 | self.ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "") 55 | self.MAILGUN_URL = os.getenv("MAILGUN_URL", "") 56 | self.MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "") 57 | 58 | if self.ENVIRONMENT == "development": 59 | self.DEBUG = True 60 | self.TEMPLATES_AUTO_RELOAD = True 61 | 62 | if testing or self.ENVIRONMENT == "testing": 63 | test_db_path = os.path.join(os.path.dirname(__file__), "test.db") 64 | self.SQLALCHEMY_DATABASE_URI = "sqlite:///%s" % test_db_path 65 | if not os.path.exists(test_db_path): 66 | with open(test_db_path, "a"): 67 | os.utime(test_db_path, None) 68 | self.TESTING = True 69 | self.WTF_CSRF_ENABLED = False 70 | 71 | def _load_secret_key(self): 72 | key = os.environ.get("SECRET_KEY") 73 | if key: 74 | return key 75 | logging.fatal("No SECRET_KEY specified. Exiting...") 76 | sys.exit(1) 77 | 78 | @staticmethod 79 | def _get_database_url(): 80 | url = os.getenv("DATABASE_URL") 81 | if url: 82 | return url 83 | return "mysql://root:%s@db/%s" % ( 84 | os.getenv("MYSQL_ROOT_PASSWORD"), 85 | os.getenv("MYSQL_DATABASE"), 86 | ) 87 | -------------------------------------------------------------------------------- /server/easyctf/constants.py: -------------------------------------------------------------------------------- 1 | FORGOT_EMAIL_TEMPLATE = open("forgot.mail").read() 2 | REGISTRATION_EMAIL_TEMPLATE = open("registration.mail").read() 3 | 4 | USER_LEVELS = ["Administrator", "Student", "Observer", "Teacher"] 5 | USER_REGULAR = 1 6 | USER_OBSERVER = 2 7 | USER_TEACHER = 3 8 | 9 | SUPPORTED_LANGUAGES = { 10 | "cxx": "C++", 11 | "python2": "Python 2", 12 | "python3": "Python 3", 13 | "java": "Java", 14 | } 15 | -------------------------------------------------------------------------------- /server/easyctf/decorators.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import wraps, update_wrapper 3 | 4 | from flask import abort, flash, redirect, url_for, session, make_response, current_app 5 | from flask_login import current_user, login_required 6 | 7 | from easyctf.models import Config 8 | 9 | 10 | def email_verification_required(func): 11 | @wraps(func) 12 | def wrapper(*args, **kwargs): 13 | if current_app.config.get("EMAIL_VERIFICATION_REQUIRED", 0): 14 | if not (current_user.is_authenticated and current_user.email_verified): 15 | session.pop("_flashes", None) 16 | flash("You need to verify your email first.", "warning") 17 | return redirect(url_for("users.settings")) 18 | return func(*args, **kwargs) 19 | 20 | return wrapper 21 | 22 | 23 | def admin_required(func): 24 | @wraps(func) 25 | def wrapper(*args, **kwargs): 26 | if not (current_user.is_authenticated and current_user.admin): 27 | abort(403) 28 | return func(*args, **kwargs) 29 | 30 | return wrapper 31 | 32 | 33 | def teacher_required(func): 34 | @wraps(func) 35 | def wrapper(*args, **kwargs): 36 | if not (current_user.is_authenticated and current_user.level == 3): 37 | abort(403) 38 | return func(*args, **kwargs) 39 | 40 | return wrapper 41 | 42 | 43 | def block_before_competition(func): 44 | @wraps(func) 45 | def wrapper(*args, **kwargs): 46 | start_time = Config.get("start_time") 47 | if not current_user.is_authenticated or not ( 48 | current_user.admin 49 | or ( 50 | start_time 51 | and current_user.is_authenticated 52 | and datetime.utcnow() >= datetime.fromtimestamp(int(start_time)) 53 | ) 54 | ): 55 | abort(403) 56 | return func(*args, **kwargs) 57 | 58 | return wrapper 59 | 60 | 61 | def block_after_competition(func): 62 | @wraps(func) 63 | def wrapper(*args, **kwargs): 64 | end_time = Config.get("end_time") 65 | if not current_user.is_authenticated or not ( 66 | current_user.admin 67 | or ( 68 | end_time 69 | and current_user.is_authenticated 70 | and datetime.utcnow() <= datetime.fromtimestamp(int(end_time)) 71 | ) 72 | ): 73 | abort(403) 74 | return func(*args, **kwargs) 75 | 76 | return wrapper 77 | 78 | 79 | def team_required(func): 80 | @wraps(func) 81 | @login_required 82 | def wrapper(*args, **kwargs): 83 | if not hasattr(current_user, "team") or not current_user.tid: 84 | flash("You need a team to view this page!", "info") 85 | return redirect(url_for("teams.create")) 86 | return func(*args, **kwargs) 87 | 88 | return wrapper 89 | 90 | 91 | def is_team_captain(func): 92 | @wraps(func) 93 | def wrapper(*args, **kwargs): 94 | if not ( 95 | current_user.is_authenticated 96 | and current_user.tid 97 | and current_user.team.owner == current_user.uid 98 | ): 99 | return abort(403) 100 | return func(*args, **kwargs) 101 | 102 | return wrapper 103 | 104 | 105 | def no_cache(func): 106 | @wraps(func) 107 | def wrapper(*args, **kwargs): 108 | response = make_response(func(*args, **kwargs)) 109 | response.headers["Last-Modified"] = datetime.now() 110 | response.headers[ 111 | "Cache-Control" 112 | ] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" 113 | response.headers["Pragma"] = "no-cache" 114 | response.headers["Expires"] = "-1" 115 | return response 116 | 117 | return update_wrapper(wrapper, func) 118 | -------------------------------------------------------------------------------- /server/easyctf/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /server/easyctf/forms/admin.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import time 3 | from datetime import datetime 4 | 5 | from flask_wtf import FlaskForm 6 | from sqlalchemy import and_ 7 | from wtforms import ValidationError 8 | from wtforms.fields import ( 9 | BooleanField, 10 | FloatField, 11 | HiddenField, 12 | IntegerField, 13 | StringField, 14 | SubmitField, 15 | TextAreaField, 16 | DateTimeLocalField, 17 | ) 18 | from wtforms.validators import InputRequired, NumberRange, Optional 19 | 20 | from easyctf.models import Problem 21 | from easyctf.utils import VALID_PROBLEM_NAME, generate_string 22 | 23 | 24 | class DateTimeField(DateTimeLocalField): 25 | def _value(self): 26 | if not self.data: 27 | return "" 28 | return datetime.fromtimestamp(float(self.data)).strftime("%Y-%m-%dT%H:%M") 29 | 30 | def process_formdata(self, valuelist): 31 | value = valuelist[0] 32 | obj = datetime.strptime(value, "%Y-%m-%dT%H:%M") 33 | self.data = time.mktime(obj.timetuple()) 34 | 35 | 36 | class ProblemForm(FlaskForm): 37 | author = StringField( 38 | "Problem Author", validators=[InputRequired("Please enter the author.")] 39 | ) 40 | title = StringField( 41 | "Problem Title", validators=[InputRequired("Please enter a problem title.")] 42 | ) 43 | name = StringField( 44 | "Problem Name (slug)", 45 | validators=[InputRequired("Please enter a problem name.")], 46 | ) 47 | category = StringField( 48 | "Problem Category", 49 | validators=[InputRequired("Please enter a problem category.")], 50 | ) 51 | description = TextAreaField( 52 | "Description", validators=[InputRequired("Please enter a description.")] 53 | ) 54 | value = IntegerField("Value", validators=[InputRequired("Please enter a value.")]) 55 | programming = BooleanField(default=False, validators=[Optional()]) 56 | 57 | autogen = BooleanField("Autogen", validators=[Optional()]) 58 | grader = TextAreaField( 59 | "Grader", validators=[InputRequired("Please enter a grader.")] 60 | ) 61 | generator = TextAreaField("Generator", validators=[Optional()]) 62 | source_verifier = TextAreaField("Source Verifier", validators=[Optional()]) 63 | 64 | test_cases = IntegerField("Test Cases", validators=[Optional()]) 65 | time_limit = FloatField("Time Limit", validators=[Optional()]) 66 | memory_limit = IntegerField("Memory Limit", validators=[Optional()]) 67 | 68 | submit = SubmitField("Submit") 69 | 70 | def validate_name(self, field): 71 | if not VALID_PROBLEM_NAME.match(field.data): 72 | raise ValidationError( 73 | "Problem name must be an all-lowercase, slug-style string." 74 | ) 75 | # if Problem.query.filter(Problem.name == field.data).count(): 76 | # raise ValidationError("That problem name already exists.") 77 | 78 | def validate_grader(self, field): 79 | grader = imp.new_module("grader") 80 | if self.programming.data: 81 | # TODO validation 82 | pass 83 | else: 84 | try: 85 | exec(field.data, grader.__dict__) 86 | assert hasattr(grader, "grade"), "Grader is missing a 'grade' function." 87 | if self.autogen.data: 88 | assert hasattr( 89 | grader, "generate" 90 | ), "Grader is missing a 'generate' function." 91 | seed1 = generate_string() 92 | import random 93 | 94 | random.seed(seed1) 95 | data = grader.generate(random) 96 | assert type(data) is dict, "'generate' must return dict" 97 | else: 98 | result = grader.grade(None, "") 99 | assert ( 100 | type(result) is tuple 101 | ), "'grade' must return (correct, message)" 102 | correct, message = result 103 | assert type(correct) is bool, "'correct' must be a boolean." 104 | assert type(message) is str, "'message' must be a string." 105 | except Exception as e: 106 | raise ValidationError("%s: %s" % (e.__class__.__name__, str(e))) 107 | 108 | 109 | class SettingsForm(FlaskForm): 110 | team_size = IntegerField( 111 | "Team Size", 112 | default=5, 113 | validators=[NumberRange(min=1), InputRequired("Please enter a max team size.")], 114 | ) 115 | ctf_name = StringField( 116 | "CTF Name", 117 | default="OpenCTF", 118 | validators=[InputRequired("Please enter a CTF name.")], 119 | ) 120 | start_time = DateTimeField( 121 | "Start Time", validators=[InputRequired("Please enter a CTF start time.")] 122 | ) 123 | end_time = DateTimeField( 124 | "End Time", validators=[InputRequired("Please enter a CTF end time.")] 125 | ) 126 | judge_api_key = StringField("Judge API Key", validators=[Optional()]) 127 | 128 | submit = SubmitField("Save Settings") 129 | 130 | def validate_start_time(self, field): 131 | import logging 132 | 133 | logging.error("lol {}".format(field.data)) 134 | -------------------------------------------------------------------------------- /server/easyctf/forms/chals.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import ValidationError 3 | from wtforms.fields import HiddenField, StringField, TextAreaField 4 | from wtforms.validators import InputRequired 5 | 6 | from easyctf.constants import SUPPORTED_LANGUAGES 7 | 8 | 9 | class ProblemSubmitForm(FlaskForm): 10 | pid = HiddenField("Problem ID") 11 | flag = StringField("Flag", validators=[InputRequired("Please enter a flag.")]) 12 | 13 | 14 | class ProgrammingSubmitForm(FlaskForm): 15 | pid = HiddenField() 16 | code = TextAreaField("Code", validators=[InputRequired("Please enter code.")]) 17 | language = HiddenField() 18 | 19 | def validate_language(self, field): 20 | if field.data not in SUPPORTED_LANGUAGES: 21 | raise ValidationError("Invalid language.") 22 | 23 | def validate_code(self, field): 24 | if len(field.data) > 65536: 25 | raise ValidationError("Code too large! (64KB max)") 26 | -------------------------------------------------------------------------------- /server/easyctf/forms/classroom.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from sqlalchemy import func 3 | from wtforms import ValidationError 4 | from wtforms.fields import * 5 | from wtforms.validators import * 6 | 7 | from easyctf.models import Team 8 | 9 | 10 | class NewClassroomForm(FlaskForm): 11 | name = StringField("Classroom Name", validators=[InputRequired()]) 12 | submit = SubmitField("Create") 13 | 14 | 15 | class AddTeamForm(FlaskForm): 16 | name = StringField("Team Name", validators=[InputRequired()]) 17 | submit = SubmitField("Add Team") 18 | 19 | def validate_name(self, field): 20 | if not Team.query.filter( 21 | func.lower(Team.teamname) == field.data.lower() 22 | ).count(): 23 | raise ValidationError("Team does not exist!") 24 | -------------------------------------------------------------------------------- /server/easyctf/forms/game.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import ValidationError 5 | from wtforms.fields import StringField 6 | from wtforms.validators import Length 7 | 8 | 9 | class GameStateUpdateForm(FlaskForm): 10 | state = StringField("state", validators=[Length(max=4096)]) 11 | 12 | def validate_state(self, field): 13 | try: 14 | json.loads(field.data) 15 | except: 16 | raise ValidationError("invalid json!") 17 | -------------------------------------------------------------------------------- /server/easyctf/forms/teams.py: -------------------------------------------------------------------------------- 1 | from flask_login import current_user 2 | from flask_wtf import FlaskForm 3 | from sqlalchemy import func, and_ 4 | from wtforms import ValidationError 5 | from wtforms.fields import BooleanField, FileField, StringField, SubmitField 6 | from wtforms.validators import InputRequired, Length 7 | 8 | from easyctf.forms.validators import TeamLengthValidator 9 | from easyctf.models import Config, Team, User 10 | 11 | 12 | class AddMemberForm(FlaskForm): 13 | username = StringField( 14 | "Username", 15 | validators=[ 16 | InputRequired( 17 | "Please enter the username of the person you would like to add." 18 | ) 19 | ], 20 | ) 21 | submit = SubmitField("Add") 22 | 23 | def get_user(self): 24 | query = User.query.filter( 25 | func.lower(User.username) == self.username.data.lower() 26 | ) 27 | return query.first() 28 | 29 | def validate_username(self, field): 30 | if not current_user.team: 31 | raise ValidationError("You must belong to a team.") 32 | if current_user.team.owner != current_user.uid: 33 | raise ValidationError("Only the team captain can invite new members.") 34 | if len(current_user.team.outgoing_invitations) >= Config.get_team_size(): 35 | raise ValidationError( 36 | "You've already sent the maximum number of invitations." 37 | ) 38 | user = User.query.filter( 39 | func.lower(User.username) == field.data.lower() 40 | ).first() 41 | if user is None: 42 | raise ValidationError("This user doesn't exist.") 43 | if user.tid is not None: 44 | raise ValidationError("This user is already a part of a team.") 45 | if user in current_user.team.outgoing_invitations: 46 | raise ValidationError("You've already invited this member.") 47 | 48 | 49 | class CreateTeamForm(FlaskForm): 50 | teamname = StringField( 51 | "Team Name", 52 | validators=[InputRequired("Please create a team name."), TeamLengthValidator], 53 | ) 54 | school = StringField( 55 | "School", 56 | validators=[ 57 | InputRequired("Please enter your school."), 58 | Length( 59 | 3, 60 | 36, 61 | "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", 62 | ), 63 | ], 64 | ) 65 | submit = SubmitField("Create Team") 66 | 67 | def validate_teamname(self, field): 68 | if current_user.tid is not None: 69 | raise ValidationError("You are already in a team.") 70 | if Team.query.filter(func.lower(Team.teamname) == field.data.lower()).count(): 71 | raise ValidationError("Team name is taken.") 72 | 73 | 74 | class DisbandTeamForm(FlaskForm): 75 | teamname = StringField("Confirm Team Name") 76 | submit = SubmitField("Delete Team") 77 | 78 | def validate_teamname(self, field): 79 | if not current_user.team: 80 | raise ValidationError("You must belong to a team.") 81 | if current_user.team.owner != current_user.uid: 82 | raise ValidationError("Only the team captain can disband the team.") 83 | if field.data != current_user.team.teamname: 84 | raise ValidationError("Incorrect confirmation.") 85 | 86 | 87 | class ManageTeamForm(FlaskForm): 88 | teamname = StringField( 89 | "Team Name", 90 | validators=[InputRequired("Please create a team name."), TeamLengthValidator], 91 | ) 92 | school = StringField( 93 | "School", 94 | validators=[ 95 | InputRequired("Please enter your school."), 96 | Length( 97 | 3, 98 | 36, 99 | "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", 100 | ), 101 | ], 102 | ) 103 | submit = SubmitField("Update") 104 | 105 | def __init__(self, *args, **kwargs): 106 | super().__init__(*args, **kwargs) 107 | self.tid = kwargs.get("tid", None) 108 | 109 | def validate_teamname(self, field): 110 | if Team.query.filter( 111 | and_(func.lower(Team.teamname) == field.data.lower(), Team.tid != self.tid) 112 | ).count(): 113 | raise ValidationError("Team name is taken.") 114 | 115 | 116 | class ProfileEditForm(FlaskForm): 117 | teamname = StringField( 118 | "Team Name", 119 | validators=[InputRequired("Please enter a team name."), TeamLengthValidator], 120 | ) 121 | school = StringField( 122 | "School", 123 | validators=[ 124 | InputRequired("Please enter your school."), 125 | Length( 126 | 3, 127 | 36, 128 | "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", 129 | ), 130 | ], 131 | ) 132 | avatar = FileField("Avatar") 133 | remove_avatar = BooleanField("Remove Avatar") 134 | submit = SubmitField("Update Profile") 135 | -------------------------------------------------------------------------------- /server/easyctf/forms/validators.py: -------------------------------------------------------------------------------- 1 | from wtforms.validators import Length 2 | 3 | UsernameLengthValidator = Length( 4 | 3, 16, message="Usernames must be between 3 to 16 characters long." 5 | ) 6 | TeamLengthValidator = Length( 7 | 3, 32, message="Usernames must be between 3 to 32 characters long." 8 | ) 9 | -------------------------------------------------------------------------------- /server/easyctf/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .others import * 2 | from .team import Team 3 | from .user import User 4 | -------------------------------------------------------------------------------- /server/easyctf/models/user.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import time 4 | from io import BytesIO 5 | from datetime import datetime 6 | 7 | import onetimepass 8 | from passlib.hash import bcrypt 9 | from sqlalchemy.ext.hybrid import hybrid_property 10 | 11 | import easyctf.models as models 12 | from easyctf.objects import cache, db, login_manager 13 | from easyctf.utils import generate_identicon, save_file 14 | 15 | 16 | class User(db.Model): 17 | """ 18 | User model. 19 | """ 20 | 21 | __tablename__ = "users" 22 | uid = db.Column(db.Integer, index=True, primary_key=True) 23 | tid = db.Column(db.Integer, db.ForeignKey("teams.tid")) 24 | name = db.Column(db.Unicode(32)) 25 | easyctf = db.Column(db.Boolean, index=True, default=False) 26 | username = db.Column(db.String(16), unique=True, index=True) 27 | email = db.Column(db.String(128), unique=True) 28 | _password = db.Column("password", db.String(128)) 29 | admin = db.Column(db.Boolean, default=False) 30 | level = db.Column(db.Integer) 31 | _register_time = db.Column("register_time", db.DateTime, default=datetime.utcnow) 32 | reset_token = db.Column(db.String(32)) 33 | otp_secret = db.Column(db.String(16)) 34 | otp_confirmed = db.Column(db.Boolean, default=False) 35 | email_token = db.Column(db.String(32)) 36 | email_verified = db.Column(db.Boolean, default=False) 37 | 38 | team = db.relationship("Team", back_populates="members") 39 | solves = db.relationship("Solve", backref="user", lazy=True) 40 | jobs = db.relationship("Job", backref="user", lazy=True) 41 | _avatar = db.Column("avatar", db.String(128)) 42 | 43 | outgoing_invitations = db.relationship( 44 | "Team", 45 | secondary=models.player_team_invitation, 46 | lazy="subquery", 47 | backref=db.backref("incoming_invitations", lazy=True), 48 | ) 49 | 50 | @property 51 | def avatar(self): 52 | if not self._avatar: 53 | avatar_file = BytesIO() 54 | avatar = generate_identicon("user%s" % self.uid) 55 | avatar.save(avatar_file, format="PNG") 56 | avatar_file.seek(0) 57 | response = save_file(avatar_file, prefix="team_avatar_", suffix=".png") 58 | if response.status_code == 200: 59 | self._avatar = response.text 60 | db.session.add(self) 61 | db.session.commit() 62 | return self._avatar or "" # just so the frontend doesnt 500 63 | 64 | def __eq__(self, other): 65 | if isinstance(other, User): 66 | return self.uid == other.uid 67 | return NotImplemented 68 | 69 | def __str__(self): 70 | return "" % self.uid 71 | 72 | def check_password(self, password): 73 | return bcrypt.verify(password, self.password) 74 | 75 | def get_id(self): 76 | return str(self.uid) 77 | 78 | @property 79 | def is_anonymous(self): 80 | return False 81 | 82 | @staticmethod 83 | @login_manager.user_loader 84 | def get_by_id(id): 85 | query_results = User.query.filter_by(uid=id) 86 | return query_results.first() 87 | 88 | @property 89 | def is_active(self): 90 | # TODO This will be based off account standing. 91 | return True 92 | 93 | @property 94 | def is_authenticated(self): 95 | return True 96 | 97 | @hybrid_property 98 | def password(self): 99 | return self._password 100 | 101 | @password.setter 102 | def password_setter(self, password): 103 | self._password = bcrypt.encrypt(password, rounds=10) 104 | 105 | @hybrid_property 106 | def register_time(self): 107 | return int(time.mktime(self._register_time.timetuple())) 108 | 109 | @hybrid_property 110 | def username_lower(self): 111 | return self.username.lower() 112 | 113 | def get_totp_uri(self): 114 | if self.otp_secret is None: 115 | # TODO: Replace with secrets library 116 | secret = base64.b32encode(os.urandom(10)).decode("utf-8").lower() 117 | self.otp_secret = secret 118 | db.session.add(self) 119 | db.session.commit() 120 | service_name = models.Config.get("ctf_name") 121 | return "otpauth://totp/%s:%s?secret=%s&issuer=%s" % ( 122 | service_name, 123 | self.username, 124 | self.otp_secret, 125 | service_name, 126 | ) 127 | 128 | def verify_totp(self, token): 129 | return onetimepass.valid_totp(token, self.otp_secret) 130 | 131 | @cache.memoize(timeout=120) 132 | def points(self): 133 | points = 0 134 | for solve in self.solves: 135 | points += solve.problem.value 136 | return points 137 | -------------------------------------------------------------------------------- /server/easyctf/objects.py: -------------------------------------------------------------------------------- 1 | from random import SystemRandom 2 | 3 | import boto3 4 | from flask_caching import Cache 5 | from flask_migrate import Migrate 6 | from flask_login import LoginManager 7 | from flask_sqlalchemy import SQLAlchemy 8 | from raven.contrib.flask import Sentry 9 | 10 | 11 | class S3Wrapper: 12 | def __init__(self): 13 | self.client = None 14 | 15 | def init_app(self, app): 16 | s3_resource = app.config.get("S3_RESOURCE") 17 | self.client = boto3.resource( 18 | "s3", 19 | endpoint_url=s3_resource, 20 | ) 21 | 22 | 23 | random = SystemRandom() 24 | cache = Cache() 25 | login_manager = LoginManager() 26 | db = SQLAlchemy() 27 | sentry = Sentry() 28 | migrate = Migrate() 29 | s3 = S3Wrapper() 30 | -------------------------------------------------------------------------------- /server/easyctf/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/easyctf/py.typed -------------------------------------------------------------------------------- /server/easyctf/templates/admin/problems.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field, render_editor %} 2 | {% extends "layout.html" %} 3 | {% block title %}Problem Editor{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 13 |
14 |
15 |

Problem Editor

16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 33 |
34 |
35 |
36 |

{% if not current_problem %}New Problem{% else %}Editing 37 | {{ current_problem.title }}{% endif %}

38 |
39 |
40 |
41 |
42 | {{ problem_form.csrf_token }} 43 |
44 | {{ render_field(problem_form.author) }} 45 | {{ render_field(problem_form.title) }} 46 | {{ render_field(problem_form.name) }} 47 | {{ render_field(problem_form.category) }} 48 | {{ render_field(problem_form.value) }} 49 | {{ render_editor(problem_form.description, "markdown") }} 50 | {{ problem_form.programming(style="display: none;") }} 51 |
52 |
53 | 63 |
64 |
65 | {{ render_generic_field(problem_form.autogen) }} 66 |
67 |
68 | {{ render_field(problem_form.test_cases) }} 69 | {{ render_field(problem_form.time_limit) }} 70 | {{ render_field(problem_form.memory_limit) }} 71 | {{ render_editor(problem_form.generator, "python") }} 72 |
73 | {{ render_editor(problem_form.grader, "python") }} 74 |
75 |
76 | {{ problem_form.submit(class_="btn btn-primary") }} 77 |
78 | {% if current_problem %} 79 |
80 | 81 |
82 | {% endif %} 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 96 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/admin/settings.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}CTF Settings{% endblock %} 4 | 5 | {% block content %} 6 |
7 | {{ settings_form.csrf_token }} 8 |
9 |
10 |
11 | {{ settings_form.submit(class_="btn btn-lg btn-success") }} 12 |
13 |

Settings

14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |

General Settings

23 |
24 |
25 |
26 |
27 |
28 | {{ render_field(settings_form.ctf_name) }} 29 | {{ render_field(settings_form.team_size) }} 30 | {{ render_field(settings_form.start_time) }} 31 | {{ render_field(settings_form.end_time) }} 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/base/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}About{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

About

8 |
9 |
10 |
11 |
12 |
13 |
14 | 17 | 18 |

Yup. You heard that right. EasyCTF is a online hacking/cybersecurity contest targeted at middle and high school students. Like many similar CTF competitions, participants will have to crack, decompile, decrypt, etc. through many defenses in order to find a secret message, known as the "flag". The challenges presented are designed with the intent of being hacked, making it an excellent and legal way for students to get some great hands-on experience.

19 | 20 | 23 | 24 |

Teams can consist of up to 5 members. There's no advantage to having less people on your team, but it's definitely to your advantage to have more people helping you, so invite your friends to play along! Only teams that consist of middle or high school students are eligible for prizes. Please see our rules page to review what constitutes an eligible team.

25 | 26 | 29 | 30 |

That's completely fine! Like its name suggests, EasyCTF is an intro-level CTF contest, but can be enjoyable for new and veteran hackers alike. We don't expect you to know anything, but we do expect you to learn! With the "programming" category, you can practice some of the foundational skills required to solve some of the later challenges.

31 | 32 | 35 | 36 |

Absolutely. In fact, we encourage you to use whatever resources you have, as long as it complies with our rules. CTF contests are created with the intention of allowing participants to look up information about topics they aren't familiar with. Our goal here is to open the door to cybersecurity and computer science for young and prospective hackers, and being able to find the information you want through searching the Internet is a great skill to have. Please see our rules page to review what resources you are allowed to use.

37 | 38 | 41 | 42 |

You don't need any background experience to play EasyCTF! That being said, if you still want to know what kind of problems are going to show up in the contest, try reading some of these writeups from last year, or practicing with challenges from other past CTFs.

43 |
44 |
45 |
46 |
47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/easter.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Easter Eggs{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Easter Eggs

8 |
9 |
10 |
11 |
12 |

Here's how the Easter Egg game works:

13 |
    14 |
  • Easter eggs are scattered throughout the site, challenges, and other places that are related to EasyCTF
  • 15 |
  • You don't get to know how many easter eggs there are
  • 16 |
  • You don't know how many easter eggs other people have
  • 17 |
  • Whoever finds the most eggs gets a special bonus ;)
  • 18 |
19 |

Good luck! Remember, don't spoil the fun of finding this page by telling other people!

20 | 21 | {% if current_user.admin %} 22 |

Admin Panel

23 |

Add easter eggs here. Go to the database to remove eggs.

24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |

37 |
    38 | {% for egg in eggs %} 39 |
  • {{ egg.flag }}
  • 40 | {% endfor %} 41 |
42 | {% else %} 43 |

Submit Easter Egg

44 |

For getting this far, here's an easter egg: egg{backdoor_from_the_90's}.

45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 |

58 |
    59 | {% for solve in eggs %} 60 |
  • {{ solve.egg.flag }}
  • 61 | {% endfor %} 62 |
63 | {% endif %} 64 |
65 |
66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Home{% endblock %} 3 | 4 | {% block content %} 5 | 20 |
21 |
22 | 26 |
27 |
28 |

29 |

Play

30 |

Whether you're a seasoned CTFer, or you've never written a line of code, you'll be able to get involved with EasyCTF!

31 |
32 |
33 |

34 |

Learn

35 |

Our new and improved classrooms feature makes it easier for teachers to integrate EasyCTF into classroom instruction!

36 |
37 |
38 |

39 |

Compete

40 |

Solve challenges faster than other teams to earn your spot on the leaderboard, whether you're eligible or not.

41 |
42 |
43 |

44 |

Win

45 |

Teams that consist of high school students from US schools are eligible to win prizes! Click here to learn more.

46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 57 |
58 |

Intense Cybersecurity Contest

59 |

No, we're not running around in a gym, tagging other players. Capture the flag contests, or CTFs for short, are intense cybersecurity contests where participants try to capture pieces of information, called flags.

60 |
61 |
62 |

Fun, Competitive Style

63 |

The race to the top is on! Submit flags and watch your team go up on the leaderboard. You'll get a full week to solve as many challenges as possible.

64 |
65 |
66 |
67 |
68 |
69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/prizes.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Prizes{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Prizes

8 |
9 |
10 |
11 |
12 |

Confirmed Prizes

13 |
    14 |
  • Top 10 eligible teams will receive EasyCTF stickers.
  • 15 |
16 |

More prizes incoming!

17 |

Join us on Discord to get notified when this information is available.

18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/rules.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Rules{% endblock %} 3 | {% block content %} 4 |
5 |
6 |

Rules

7 |
8 |
9 |
10 |
11 |
12 |
13 |

Please follow these rules to give everyone a fair chance at winning.

14 |
    15 |
  1. Use common sense. If you feel like what you're doing is disrupting the competition in any way, STOP doing it. Don't wait for us to yell at you.
  2. 16 |
  3. Don't cheat. We're encouraging you to do your own work. While it's acceptable to use resources on the internet such as documentation or forums, it's not acceptable to ask someone other than the 1-5 members of your team to directly help you on the problem. Don't ask questions on public forums involving specific details of the problems. Don't share flags or methods with other teams.
  4. 17 |
  5. Don't disrupt others. Don't attack other teams, and don't attack the contest infrastructure in order to prevent others from solving problems or submitting flags.
  6. 18 |
  7. Don't make many accounts. Making multiple accounts for any purpose ("multi-accounting") is not allowed. If you are a teacher and want to set up a class, please ask your students to create accounts for themselves and then you can add them to your classroom. Any hints of multi-accounting is basis for disqualification, no questions asked.
  8. 19 |
  9. Be nice. Since this is an educational environment, don't use profanity in your username or team names. Inappropriate names are subject to account deletion or immediate disqualification at the discretion of the organizers. Also, don't spam or harass people in chat.
  10. 20 |
21 |

If you break any of these rules, your team may be converted into an observer team (see below), disqualifying you from winning prizes. In severe cases, we may decide to remove you from the competition altogether. Decisions made by competition organizers are final.

22 | 23 | 26 | 27 |

Anyone is welcome to play in EasyCTF! However, since EasyCTF is targeted at students enrolled in middle schools and high schools in the US, only those students will be eligible for prizes. People who don't fall in the category of students enrolled in a middle or high school in the US are observers. While observers are allowed to place on the scoreboard, they will not be eligible for winning prizes. Teams with at least one observer member will be considered an observer team.

28 |

After the contest is over, we will contact the winning teams to verify that their team meets the conditions before distributing prizes. If a winning team does not meet these conditions, their team will be considered an observer team, and the prizes will be given to the next highest team.

29 | 30 | 33 | 34 |

The flags that you find will usually follow the format easyctf{flag}. This way, you will know you have the flag when you find it. Problems that don't follow this format will indicate that they don't follow the format next to their problem statement. If you believe you've found a flag but the scoring server is not accepting it, please contact the competition organizers.

35 | 36 | 39 | 40 |

In EasyCTF, every problem has an assigned point value, which is usually representative of the difficulty of the problem. Your team's total score is the sum of the points you obtain from every problem you solve. Problems may have speed bonuses, allowing you to earn extra points for solving it faster. The speed bonus varies per problem and will be indicated in the problem listing.

41 |

Teams with a higher score will outrank teams with a lower score. The latest listing of every team's score will appear on their team's profile, and an estimate of the latest ranking will appear on the scoreboard page. Ties between teams that have an equal amount of points will be settled by their performance on higher-valued problems. For example, if two teams tied at 100 points by solving a 20-point problem and an 80-point problem, the team that solved the 80-point problem first will outrank the other.

42 |
43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/scoreboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Scoreboard{% endblock %} 3 | 4 | {% block content %} 5 | 8 |
9 |
10 |

Scoreboard

11 |
12 |
13 | 14 |
15 |
16 | Show Observer Teams 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% set rank = 1 %} 29 | {% set unrank = 1 %} 30 | {% for team, tid, score, date in scoreboard %} 31 | 36 | 37 | 38 | 39 | 40 | 41 | {% set unrank = unrank + 1 %} 42 | {% if not team.observer %} 43 | {% set rank = rank + 1 %} 44 | {% endif %} 45 | 46 | {% endfor %} 47 | 48 |
RankTeam NameSchoolPointsLast Solve
33 | 34 | {{ rank }} 35 | {{ team.teamname }}{{ team.school }}{{ score }}
49 |
50 |
51 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/sponsors.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Sponsors{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Sponsors

8 |
9 |
10 |
11 |
12 |

Shout out to these awesome people for helping make EasyCTF happen!

13 |
14 |
15 | 16 | 17 | 18 | 22 | 23 |
19 |

DigitalOcean

20 |

Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.

21 |
24 |
25 |
26 |

If you're interested in sponsoring, shoot us an email at team@easyctf.com!

27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/team.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}The Team{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

The Team

8 |
9 |
10 |
11 |
12 |
13 |

We're the people directly responsible for EasyCTF! Want to talk to us? Find us on Discord!

14 | {% for user in easyctf_team %} 15 |
16 |
17 |
18 | 19 | 20 | 21 | 26 | 32 | 33 | 34 |
22 | 23 | 24 | 25 | 27 |

{{ user.name }}

28 |

29 | @{{ user.username }} 30 |

31 |
35 |
36 |
37 |
38 | {% endfor %} 39 |
40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /server/easyctf/templates/base/updates.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Updates{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Discord Chat

8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |

We're using Discord this year

16 |

Discord is a friendly text + voice chat service for gamers, with more features as well as moderation options. We're using Discord as our primary method of communication. Therefore, updates and announcements will most likely be posted into the #announcements channel in Discord first. Unfortunately, Discord doesn't have any kind of built-in IRC bridge, so in order to join the chat, you must use their client.

17 | 18 |

Rules for Discord Chat

19 |

These rules will be posted into the chat room as well.

20 |
    21 |
  • Use common sense.
  • 22 |
  • Everything that Admins say goes.
  • 23 |
  • Don't cheat.
  • 24 |
  • Don't violate Discord's Terms of Service.
  • 25 |
  • Remember this is still a school environment; many of the other members may be students and teachers you know.
  • 26 |
27 | 28 |

Have Fun and Make Friends!

29 |

We hope you stay even after the competition ends. Many of the people in our chat were veterans who joined the old EasyCTF Slack, which we then migrated to this Discord server.

30 | 31 |
32 | Join the Chat 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/chals/shell.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Shell{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Shell

9 |
10 |
11 |
12 |
13 |

The shell server allows you to connect to a live Linux server and use it to help you 14 | as you solve challenges. You'll need to log in with a different set of credentials, 15 | which you can view by clicking the button below. Note that (1) in order to paste into 16 | the terminal emulator below, you must right click and select "paste from browser", and 17 | (2) the password will not be displayed as you type it in.

18 |

Our server supports mosh login!

19 |
20 |
21 |

EasyCTF Shell Server

22 |
23 |
24 | 25 | 26 | 29 | 32 | 33 |
27 | Reveal Credentials 28 | 30 | Full Screen 31 |
34 |
35 | 36 |
37 | Abuse is not tolerated and will lead to immediate disqualification and removal from the competition. 38 |
39 |
40 |
41 |
42 | 50 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/chals/solves.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Solves{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Recent Solves on {{ problem.title }}

9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for solve in problem.solves|sort(attribute="date", reverse=True) %} 22 | 23 | 24 | 27 | 28 | {% endfor %} 29 | 30 |
Team NameDate
{{ solve.team.teamname }} 25 | 26 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /server/easyctf/templates/chals/status.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Submissions{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 | 10 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for job in jobs %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% endfor %} 45 | 46 |
Submission IDSubmittedJudgedUserProblemLanguageVerdictTimeMemory
{{ job.id }}{{ job.user.username}}{{ job.problem.title }}{{ job.language }}{{ job.verdict }}{{ job.execution_time }}{{ job.execution_memory }}
47 |
48 |
49 |
50 |
51 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/chals/submission.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Submission #{{ job.id }} on {{ problem.title }}{% endblock %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 | 11 | 16 | 17 |
18 |
19 | 20 |

21 | Submitted by {{ user.username }} 22 | . 23 |

24 | 25 |
{{ job.contents }}
26 | 27 |

28 | Status: {{ job.status }}
29 | {% if job.status == 2 %} 30 | Verdict: {{ job.verdict }}
31 | Time: {{ job.execution_time }}
32 | Memory: {{ job.execution_memory }}
33 | {% endif %} 34 |

35 |

Verdict Abbreviation

36 |
    37 |
  • AC = Accepted
  • 38 |
  • IS = Invalid Source
  • 39 |
  • WA = Wrong Answer
  • 40 |
  • TLE = Time Limit Exceeded
  • 41 |
  • RTE = RunTime Error
  • 42 |
  • ISC = Illegal SysCall
  • 43 |
  • CE = Compilation Error
  • 44 |
  • JE = Judge Error (you should probably report this)
  • 45 |
46 |
47 |
48 |
49 |
50 | 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /server/easyctf/templates/classroom/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Classrooms{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% if current_user.level == 3 %} 8 |
9 | New Class 10 |
11 | {% endif %} 12 |

Classrooms

13 |
14 |
15 |
16 |
17 | {% if current_user.level == 3 %} 18 |

These are the classrooms that you manage. You can create a new one by clicking 19 | here.

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for class in classes %} 29 | 30 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
NameMembers
31 | {{ class.name }} 32 | {{ class.size }}
38 | {% else %} 39 |
40 |
41 |

These are the classrooms that you are a part of.

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% for class in classes %} 51 | 52 | 55 | 56 | 57 | {% endfor %} 58 | 59 |
NameMembers
53 | {{ class.name }} 54 | {{ class.size }}
60 |
61 |
62 |

The teacher of these classrooms have invited you to join their classroom.

63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {% for class in invites %} 73 | 74 | 75 | 76 | 79 | 80 | {% endfor %} 81 | 82 |
NameMembers
{{ class.name }}{{ class.size }} 77 | Join » 78 |
83 |
84 |
85 | {% endif %} 86 |
87 |
88 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/classroom/new.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, %} 2 | {% extends "layout.html" %} 3 | {% block title %}New Classroom{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

11 | « Back 12 |

13 |
14 |
15 |

New Classroom

16 |
17 |
18 |
19 | {{ new_classroom_form.csrf_token }} 20 |
21 | {{ render_field(new_classroom_form.name) }} 22 |
23 | {{ new_classroom_form.submit(class_="btn btn-primary") }} 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |

EasyCTF is a national, online, student-run high school hacking competition that opens the door to computer science and cybersecurity for students all over the world.

7 |
    8 |
9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 | 20 | 27 |
28 |
29 |
30 |
31 | 34 |
35 | -------------------------------------------------------------------------------- /server/easyctf/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import flashes %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | {% block title %}{% endblock %} - {{ ctf_name }}, High School CTF Competition 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if keywords %} 23 | {% endif %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% if current_user.admin %} 38 |
39 |
40 | competition status: 41 | {{ "not " if not competition_running }}running. 42 | (window: to ) 43 |
44 |
45 | {% endif %} 46 | 49 | {% with messages = get_flashed_messages(with_categories=true) %} 50 | {% if messages %} 51 |
52 | {% for category, message in messages %} 53 |
54 |
{{ message }}
55 |
56 | {% endfor %} 57 |
58 | {% endif %} 59 | {% endwith %} 60 |
61 | {% block content %}{% endblock %} 62 |
63 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /server/easyctf/templates/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 11 | 70 |
71 | -------------------------------------------------------------------------------- /server/easyctf/templates/teams/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "templates.html" import render_field, render_generic_field %} 3 | {% block title %}Create or Join Team{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 |
12 |

Create Team

13 |
14 |
15 | {% if current_user.level == 3 %} 16 |
17 | Hey there! Even though you're a teacher, we're going to ask that you create an account simply because of how the system works. In terms of scoring, your team will be considered an observer team. 18 |
19 | {% endif %} 20 |
21 | {{ create_team_form.csrf_token }} 22 |
23 | {{ render_field(create_team_form.teamname) }} 24 | {{ render_field(create_team_form.school) }} 25 |
26 | {{ create_team_form.submit(class_="btn btn-primary") }} 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |

Join Team

35 |
36 | {% if current_user.level == 3 %} 37 |
38 | A word of warning: if you join a team with your students, the team will be marked as an observer team and will be disqualified from winning prizes. If you want to keep track of your students' progress, please use our classrooms feature. 39 |
40 | {% endif %} 41 | {% set incoming_invitations = current_user.incoming_invitations %} 42 | {% if incoming_invitations %} 43 |
44 | Here are your current invitations. Click "Accept" to join the team. 45 |
46 |
47 | {% for team in incoming_invitations %} 48 |
49 | Accept 50 | {{ team.teamname }} 51 |
52 | {% endfor %} 53 |
54 | {% else %} 55 |
56 | To join a team, you must have an invitation from that team. Contact your team to send you an 57 | invitation, or find their team profile page and request to join their team. 58 |
59 | {% endif %} 60 |
61 | {% set outgoing_invitations = current_user.outgoing_invitations %} 62 | {% if outgoing_invitations %} 63 |
64 |
65 |

Sent Requests

66 |
67 |
68 | {% for req in outgoing_invitations %} 69 | 73 | {% endfor %} 74 |
75 |
76 | {% endif %} 77 |
78 |
79 |
80 |
81 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/templates.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
3 | {{ field.label(class_="control-label") }} 4 | {{ field(class_="form-control", autocomplete="off", **kwargs.get("options", {})) }} 5 | {% if field.errors %} 6 |

7 | {% for error in field.errors %} 8 | {{ error }}
9 | {% endfor %} 10 |

11 | {% endif %} 12 |
13 | {% endmacro %} 14 | 15 | {% macro render_generic_field(field) %} 16 |
17 | {{ field.label(class_="control-label") }} 18 | {{ field(autocomplete="off") }} 19 | {% if field.errors %} 20 |

21 | {% for error in field.errors %} 22 | {{ error }}
23 | {% endfor %} 24 |

25 | {% endif %} 26 |
27 | {% endmacro %} 28 | 29 | {% macro render_editor(field, language) %} 30 |
31 | {{ field.label(class_="control-label") }} 32 | {{ field(style="display:none;") }} 33 |
34 | {% if field.errors %} 35 |

36 | {% for error in field.errors %} 37 | {{ error }}
38 | {% endfor %} 39 |

40 | {% endif %} 41 |
42 | 60 | {% endmacro %} -------------------------------------------------------------------------------- /server/easyctf/templates/users/forgot.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Forgot Password{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 |
12 |

Forgot Password

13 |
14 |
15 |
16 | {{ forgot_form.csrf_token }} 17 |
18 | {{ render_field(forgot_form.email) }} 19 |
20 | {{ forgot_form.submit(class_="btn btn-primary") }} 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Login

9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

Login

18 |
19 |
20 |
21 |
22 | {{ login_form.csrf_token }} 23 |
24 | {{ render_field(login_form.username) }} 25 | {{ render_field(login_form.password) }} 26 | 29 |
30 | {{ login_form.remember(autocomplete="off") }} 31 | {{ login_form.remember.label(class_="control-label") }} 32 |
33 |
34 | {{ login_form.submit(class_="btn btn-primary") }} 35 | 36 | Forgot Password 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /server/easyctf/templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Register{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Register

9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

Register

18 |
19 |
20 |
21 |
22 | {{ register_form.csrf_token }} 23 |
24 | {{ render_field(register_form.name) }} 25 | {{ render_field(register_form.email) }} 26 | {{ render_field(register_form.username) }} 27 |
28 |
29 | {{ render_field(register_form.password) }} 30 |
31 |
32 | {{ render_field(register_form.confirm_password) }} 33 |
34 |
35 | {{ render_generic_field(register_form.level) }} 36 |
37 | {{ register_form.submit(class_="btn btn-primary") }} 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/users/reset.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field %} 2 | {% extends "layout.html" %} 3 | {% block title %}Reset Password{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 |
12 |

Reset Password

13 |
14 |
15 |
16 | {{ reset_form.csrf_token }} 17 |
18 | {{ render_field(reset_form.password) }} 19 | {{ render_field(reset_form.confirm_password) }} 20 |
21 | {{ reset_form.submit(class_="btn btn-primary") }} 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /server/easyctf/templates/users/two_factor/setup.html: -------------------------------------------------------------------------------- 1 | {% from "templates.html" import render_field, render_generic_field, flashes %} 2 | {% extends "layout.html" %} 3 | {% block title %}Setup Two-Factor Authentication{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 |
12 |

Two-Factor Authentication

13 |
14 |
15 |

Two-factor authentication protects your account by requiring you to verify your identity using a means other than your password. We'll use the app Authy to moderate this process. To begin, scan the following QR code with your app, and then enter the 6-digit code shown on your screen.

16 |
17 | {{ two_factor_form.csrf_token }} 18 |
19 |
20 |
21 | 22 |
23 |
24 | {{ render_field(two_factor_form.code) }} 25 | {{ render_field(two_factor_form.password) }} 26 |
27 | {{ two_factor_form.submit(class_="btn btn-primary") }} 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /server/easyctf/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | import time 4 | from io import BytesIO 5 | from string import hexdigits 6 | from urllib.parse import urljoin, urlparse 7 | 8 | import requests 9 | from flask import current_app, redirect, request, url_for 10 | from PIL import Image, ImageDraw, ImageOps 11 | 12 | from easyctf.objects import random 13 | 14 | VALID_USERNAME = re.compile(r"^[A-Za-z_][A-Za-z\d_]*$") 15 | VALID_PROBLEM_NAME = re.compile(r"^[a-z_][a-z\-\d_]*$") 16 | 17 | 18 | def generate_string(length=32, alpha=hexdigits): 19 | characters = [random.choice(alpha) for x in range(length)] 20 | return "".join(characters) 21 | 22 | 23 | def generate_short_string(): 24 | return generate_string(length=16) 25 | 26 | 27 | def send_mail(recipient, subject, body): 28 | data = {"from": current_app.config["ADMIN_EMAIL"], "subject": subject, "html": body} 29 | data["bcc" if type(recipient) == list else "to"] = recipient 30 | auth = ("api", current_app.config["MAILGUN_API_KEY"]) 31 | url = "{}/messages".format(current_app.config["MAILGUN_URL"]) 32 | return requests.post(url, auth=auth, data=data) 33 | 34 | 35 | def filestore(name): 36 | prefix = current_app.config.get("FILESTORE_STATIC", "/static") 37 | return prefix + "/" + name 38 | 39 | 40 | def save_file(file, **params): 41 | url = current_app.config.get( 42 | "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save" 43 | ) 44 | return requests.post(url, data=params, files=dict(file=file)) 45 | 46 | 47 | def to_timestamp(date): 48 | if date is None: 49 | return "" 50 | return int(time.mktime(date.timetuple())) 51 | 52 | 53 | def to_place_str(n): 54 | k = n % 10 55 | return "%d%s" % (n, "tsnrhtdd"[(n / 10 % 10 != 1) * (k < 4) * k :: 4]) 56 | 57 | 58 | def is_safe_url(target): 59 | ref_url = urlparse(request.host_url) 60 | test_url = urlparse(urljoin(request.host_url, target)) 61 | return test_url.scheme in ("http", "https") and ref_url.netloc == test_url.netloc 62 | 63 | 64 | def get_redirect_target(): 65 | for target in request.values.get("next"), request.referrer: 66 | if not target: 67 | continue 68 | if is_safe_url(target): 69 | return target 70 | 71 | 72 | def redirect_back(endpoint, **values): 73 | target = request.form["next"] 74 | if not target or not is_safe_url(target): 75 | target = url_for(endpoint, **values) 76 | return redirect(target) 77 | 78 | 79 | def sanitize_avatar(f): 80 | try: 81 | im = Image.open(f) 82 | im2 = ImageOps.fit(im, (512, 512), Image.ANTIALIAS) 83 | 84 | buf = BytesIO() 85 | im2.save(buf, format="png") 86 | buf.seek(0) 87 | return buf 88 | except: 89 | return None 90 | 91 | 92 | def generate_identicon(seed): 93 | seed = seed.strip().lower().encode("utf-8") 94 | h = hashlib.sha1(seed).hexdigest() 95 | size = 256 96 | margin = 0.08 97 | base_margin = int(size * margin) 98 | cell = int((size - base_margin * 2.0) / 5) 99 | margin = int((size - cell * 5.0) / 2) 100 | image = Image.new("RGB", (size, size)) 101 | draw = ImageDraw.Draw(image) 102 | 103 | def hsl2rgb(h, s, b): 104 | h *= 6 105 | s1 = [] 106 | s *= b if b < 0.5 else 1 - b 107 | b += s 108 | s1.append(b) 109 | s1.append(b - h % 1 * s * 2) 110 | s *= 2 111 | b -= s 112 | s1.append(b) 113 | s1.append(b) 114 | s1.append(b + h % 1 * s) 115 | s1.append(b + s) 116 | 117 | return [s1[~~h % 6], s1[(h | 16) % 6], s1[(h | 8) % 6]] 118 | 119 | rgb = hsl2rgb(int(h[-7:], 16) & 0xFFFFFFF, 0.5, 0.7) 120 | bg = (255, 255, 255) 121 | fg = (int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)) 122 | draw.rectangle([(0, 0), (size, size)], fill=bg) 123 | 124 | for i in range(15): 125 | c = bg if int(h[i], 16) % 2 == 1 else fg 126 | if i < 5: 127 | draw.rectangle( 128 | [ 129 | (2 * cell + margin, i * cell + margin), 130 | (3 * cell + margin, (i + 1) * cell + margin), 131 | ], 132 | fill=c, 133 | ) 134 | elif i < 10: 135 | draw.rectangle( 136 | [ 137 | (1 * cell + margin, (i - 5) * cell + margin), 138 | (2 * cell + margin, (i - 4) * cell + margin), 139 | ], 140 | fill=c, 141 | ) 142 | draw.rectangle( 143 | [ 144 | (3 * cell + margin, (i - 5) * cell + margin), 145 | (4 * cell + margin, (i - 4) * cell + margin), 146 | ], 147 | fill=c, 148 | ) 149 | elif i < 15: 150 | draw.rectangle( 151 | [ 152 | (0 * cell + margin, (i - 10) * cell + margin), 153 | (1 * cell + margin, (i - 9) * cell + margin), 154 | ], 155 | fill=c, 156 | ) 157 | draw.rectangle( 158 | [ 159 | (4 * cell + margin, (i - 10) * cell + margin), 160 | (5 * cell + margin, (i - 9) * cell + margin), 161 | ], 162 | fill=c, 163 | ) 164 | 165 | return image 166 | -------------------------------------------------------------------------------- /server/easyctf/views/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /server/easyctf/views/admin.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, abort, flash, redirect, render_template, request, url_for 2 | from wtforms_components import read_only 3 | 4 | from easyctf.decorators import admin_required 5 | from easyctf.forms.admin import ProblemForm, SettingsForm 6 | from easyctf.models import AutogenFile, Config, Problem, JudgeKey 7 | from easyctf.objects import db 8 | 9 | blueprint = Blueprint("admin", __name__, template_folder="templates") 10 | 11 | DEFAULT_GRADER = """def grade(random, submission): 12 | if "correct_flag" in submission: 13 | return True, "Nice!" 14 | return False, "Nope." 15 | """ 16 | 17 | 18 | @blueprint.route("/problems//delete", methods=["POST"]) 19 | @admin_required 20 | def delete_problem(pid): 21 | problem = Problem.get_by_id(pid) 22 | if problem is None: 23 | abort(404) 24 | db.session.delete(problem) 25 | db.session.commit() 26 | flash("Problem {} has been deleted!".format(repr(problem.name)), "info") 27 | return redirect(url_for("admin.problems")) 28 | 29 | 30 | @blueprint.route("/problems", methods=["GET", "POST"]) 31 | @blueprint.route("/problems/", methods=["GET", "POST"]) 32 | @admin_required 33 | def problems(pid=None): 34 | problem = None 35 | problem_form = ProblemForm() 36 | if problem_form.validate_on_submit(): 37 | new_problem = False 38 | if pid is None: 39 | p = Problem.query.filter_by(name=problem_form.name.data).first() 40 | if p: 41 | flash("Please choose a unique name for this problem.", "warning") 42 | return redirect(url_for("admin.problems")) 43 | new_problem = True 44 | problem = Problem() 45 | else: 46 | problem = Problem.get_by_id(pid) 47 | if problem is None: 48 | abort(404) 49 | problem_form.populate_obj(problem) 50 | db.session.add(problem) 51 | db.session.flush() 52 | autogen_files = AutogenFile.query.filter_by(pid=pid) 53 | if autogen_files.count(): 54 | autogen_files.delete() 55 | db.session.commit() 56 | if new_problem: 57 | flash("Problem {} has been created!".format(repr(problem.name)), "info") 58 | return redirect(url_for("admin.problems", pid=problem.pid)) 59 | problems = Problem.query.order_by(Problem.value).all() 60 | if pid is not None: 61 | problem = Problem.get_by_id(pid) 62 | if not problem: 63 | return abort(404) 64 | if request.method != "POST": 65 | problem_form = ProblemForm(obj=problem) 66 | # if problem.programming: 67 | # judge_problem = judge_api.problems_get(pid) 68 | # if judge_problem.status_code == 404: 69 | # abort(500) 70 | # problem_form.grader.data = judge_problem.data['grader_code'] 71 | # problem_form.generator.data = judge_problem.data['generator_code'] 72 | else: 73 | problem_form.grader.data = DEFAULT_GRADER 74 | return render_template( 75 | "admin/problems.html", 76 | current_problem=problem, 77 | problems=problems, 78 | problem_form=problem_form, 79 | ) 80 | 81 | 82 | @blueprint.route("/settings/judge/key") 83 | @admin_required 84 | def judge_key(): 85 | key = JudgeKey() 86 | db.session.add(key) 87 | db.session.commit() 88 | flash("Key created: {}. This won't be shown again.".format(key.key), "success") 89 | return redirect(url_for("admin.settings")) 90 | 91 | 92 | @blueprint.route("/settings", methods=["GET", "POST"]) 93 | @admin_required 94 | def settings(): 95 | settings_form = SettingsForm() 96 | if settings_form.validate_on_submit(): 97 | configs = dict() 98 | for field in settings_form: 99 | if field.short_name in ["csrf_token", "submit"]: 100 | continue 101 | configs.update(**{field.short_name: field.data}) 102 | Config.set_many(configs) 103 | flash("CTF settings updated!", "success") 104 | return redirect(url_for("admin.settings")) 105 | else: 106 | configs = Config.get_many([field.short_name for field in settings_form]) 107 | for field in settings_form: 108 | if field.short_name == "csrf_token": 109 | continue 110 | if field.short_name in configs: 111 | field.data = configs.get(field.short_name, "") 112 | return render_template("admin/settings.html", settings_form=settings_form) 113 | -------------------------------------------------------------------------------- /server/easyctf/views/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | import admin 4 | 5 | blueprint = Blueprint("api", __name__) 6 | 7 | blueprint.register_blueprint(admin.blueprint, url_prefix="/admin") 8 | -------------------------------------------------------------------------------- /server/easyctf/views/api/admin.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | blueprint = Blueprint("admin", __name__) 4 | -------------------------------------------------------------------------------- /server/easyctf/views/base.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, abort, redirect, render_template, request, url_for, flash 2 | from flask_login import current_user, login_required 3 | from sqlalchemy import func 4 | 5 | from easyctf.models import Egg, WrongEgg, Team, User, EggSolve 6 | from easyctf.objects import cache, db 7 | 8 | blueprint = Blueprint("base", __name__, template_folder="templates") 9 | 10 | 11 | @blueprint.route("/") 12 | def index(): 13 | return render_template("base/index.html") 14 | 15 | 16 | @blueprint.route("/about") 17 | def about(): 18 | return render_template("base/about.html") 19 | 20 | 21 | @blueprint.route("/rules") 22 | def rules(): 23 | return render_template("base/rules.html") 24 | 25 | 26 | @blueprint.route("/prizes") 27 | def prizes(): 28 | return render_template("base/prizes.html") 29 | 30 | 31 | @blueprint.route("/sponsors") 32 | def sponsors(): 33 | return render_template("base/sponsors.html") 34 | 35 | 36 | @blueprint.route("/team") 37 | # @cache.cached(timeout=0) 38 | def team(): 39 | easyctf_team = db.session.query(User).filter(User.easyctf == True).all() 40 | return render_template("base/team.html", easyctf_team=easyctf_team) 41 | 42 | 43 | @blueprint.route("/updates") 44 | def updates(): 45 | return render_template("base/updates.html") 46 | 47 | 48 | @blueprint.route("/scoreboard") 49 | @login_required 50 | def scoreboard(): 51 | scoreboard = Team.scoreboard() 52 | return render_template("base/scoreboard.html", scoreboard=scoreboard) 53 | 54 | 55 | @blueprint.route("/shibboleet", methods=["GET", "POST"]) 56 | def easter(): 57 | if not ( 58 | current_user.is_authenticated and (current_user.admin or current_user.team) 59 | ): 60 | return abort(404) 61 | eggs = [] 62 | if request.method == "POST": 63 | if current_user.admin and request.form.get("submit"): 64 | newegg_str = request.form.get("egg") 65 | if newegg_str: 66 | newegg = Egg(flag=newegg_str) 67 | db.session.add(newegg) 68 | db.session.commit() 69 | flash("New egg has been added!", "success") 70 | else: 71 | cand = request.form.get("egg") 72 | egg = Egg.query.filter_by(flag=cand).first() 73 | if egg: 74 | solve = EggSolve.query.filter_by( 75 | eid=egg.eid, tid=current_user.tid 76 | ).first() 77 | if solve: 78 | flash("You already got this one", "info") 79 | else: 80 | solve = EggSolve( 81 | eid=egg.eid, tid=current_user.tid, uid=current_user.uid 82 | ) 83 | db.session.add(solve) 84 | db.session.commit() 85 | flash("Congrats!", "success") 86 | else: 87 | submission = WrongEgg.query.filter_by( 88 | tid=current_user.tid, submission=cand 89 | ).first() 90 | if submission: 91 | flash("You've already tried that egg", "info") 92 | else: 93 | submission = WrongEgg( 94 | tid=current_user.tid, uid=current_user.uid, submission=cand 95 | ) 96 | db.session.add(submission) 97 | db.session.commit() 98 | flash("Nope, sorry", "danger") 99 | return redirect(url_for("base.easter")) 100 | if current_user.admin: 101 | eggs = Egg.query.all() 102 | else: 103 | eggs = EggSolve.query.filter_by(tid=current_user.tid).all() 104 | return render_template("base/easter.html", eggs=eggs) 105 | -------------------------------------------------------------------------------- /server/easyctf/views/classroom.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, abort, flash, redirect, render_template, url_for 2 | from flask_login import current_user, login_required 3 | from sqlalchemy import func 4 | 5 | from easyctf.decorators import teacher_required, team_required 6 | from easyctf.forms.classroom import AddTeamForm, NewClassroomForm 7 | from easyctf.models import Classroom, Team, classroom_invitation, team_classroom 8 | from easyctf.objects import db 9 | 10 | blueprint = Blueprint("classroom", __name__) 11 | 12 | 13 | @blueprint.route("/") 14 | @team_required 15 | @login_required 16 | def index(): 17 | invites = None 18 | if current_user.level == 3: 19 | classes = Classroom.query.filter_by(owner=current_user.uid).all() 20 | elif not current_user.tid: 21 | flash("You must be a part of a team to join classes.", "info") 22 | return redirect(url_for("teams.create")) 23 | else: 24 | classes = current_user.team.classrooms 25 | invites = current_user.team.classroom_invites 26 | return render_template("classroom/index.html", classes=classes, invites=invites) 27 | 28 | 29 | @blueprint.route("/new", methods=["GET", "POST"]) 30 | @login_required 31 | @teacher_required 32 | def new(): 33 | new_classroom_form = NewClassroomForm() 34 | if new_classroom_form.validate_on_submit(): 35 | classroom = Classroom(name=new_classroom_form.name.data, owner=current_user.uid) 36 | db.session.add(classroom) 37 | db.session.commit() 38 | flash("Created classroom.", "success") 39 | return redirect(url_for("classroom.view", id=classroom.id)) 40 | return render_template("classroom/new.html", new_classroom_form=new_classroom_form) 41 | 42 | 43 | @blueprint.route("/delete/") 44 | @login_required 45 | @teacher_required 46 | def delete(id): 47 | classroom = Classroom.query.filter_by(id=id).first() 48 | if not classroom: 49 | abort(404) 50 | if current_user.uid != classroom.owner: 51 | abort(403) 52 | db.session.delete(classroom) 53 | db.session.commit() 54 | return redirect(url_for("classroom.index")) 55 | 56 | 57 | @blueprint.route("/accept/") 58 | @login_required 59 | def accept(id): 60 | invitation = db.session.query(classroom_invitation).filter_by( 61 | team_id=current_user.tid, classroom_id=id 62 | ) 63 | if not invitation: 64 | abort(404) 65 | classroom = Classroom.query.filter_by(id=id).first() 66 | if not classroom: 67 | abort(404) 68 | classroom.teams.append(current_user.team) 69 | if current_user.team in classroom.invites: 70 | classroom.invites.remove(current_user.team) 71 | db.session.commit() 72 | flash("Joined classroom.", "success") 73 | return redirect(url_for("classroom.view", id=id)) 74 | 75 | 76 | @blueprint.route("/remove//") 77 | @login_required 78 | @teacher_required 79 | def remove(cid, tid): 80 | classroom = Classroom.query.filter_by(id=cid).first() 81 | if not classroom: 82 | abort(404) 83 | if current_user.uid != classroom.owner: 84 | abort(403) 85 | team = Team.query.filter_by(tid=tid).first() 86 | if not team: 87 | abort(404) 88 | if team not in classroom: 89 | abort(403) 90 | classroom.teams.remove(team) 91 | db.session.commit() 92 | return redirect(url_for("classroom.view", id=cid)) 93 | 94 | 95 | @blueprint.route("/", methods=["GET", "POST"]) 96 | @login_required 97 | def view(id): 98 | classroom = Classroom.query.filter_by(id=id).first() 99 | if not classroom: 100 | return redirect("classroom.index") 101 | if not ( 102 | current_user.uid == classroom.owner 103 | or db.session.query(team_classroom) 104 | .filter_by(team_id=current_user.tid, classroom_id=classroom.id) 105 | .count() 106 | ): 107 | abort(403) 108 | add_team_form = AddTeamForm(prefix="addteam") 109 | if add_team_form.validate_on_submit(): 110 | if current_user.uid != classroom.owner: 111 | abort(403) 112 | team = Team.query.filter( 113 | func.lower(Team.teamname) == add_team_form.name.data.lower() 114 | ).first() 115 | classroom.invites.append(team) 116 | flash("Team invited.", "success") 117 | db.session.commit() 118 | return redirect(url_for("classroom.view", id=id)) 119 | users = [user for _team in classroom.teams for user in _team.members] 120 | return render_template( 121 | "classroom/view.html", 122 | classroom=classroom, 123 | users=users, 124 | add_team_form=add_team_form, 125 | ) 126 | -------------------------------------------------------------------------------- /server/easyctf/views/game.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from functools import wraps 4 | 5 | from flask import ( 6 | Blueprint, 7 | abort, 8 | current_app, 9 | flash, 10 | make_response, 11 | render_template, 12 | request, 13 | url_for, 14 | ) 15 | from flask_login import current_user, login_required 16 | 17 | from easyctf.decorators import block_before_competition, team_required 18 | from easyctf.forms.chals import ProblemSubmitForm, ProgrammingSubmitForm 19 | from easyctf.forms.game import GameStateUpdateForm 20 | from easyctf.models import AutogenFile, GameState, Job, Problem, Solve, User, WrongFlag 21 | from easyctf.objects import cache, db, sentry 22 | 23 | blueprint = Blueprint("game", __name__, template_folder="templates") 24 | 25 | 26 | def api_view(f): 27 | @wraps(f) 28 | def wrapper(*args, **kwargs): 29 | status, result = f(*args, **kwargs) 30 | return make_response( 31 | json.dumps(result or dict()), 32 | status, 33 | {"Content-Type": "application/json; charset=utf-8"}, 34 | ) 35 | 36 | return wrapper 37 | 38 | 39 | @blueprint.route("/", methods=["GET"]) 40 | @login_required 41 | @team_required 42 | @block_before_competition 43 | def game(): 44 | problem_submit_form = ProblemSubmitForm() 45 | 46 | return render_template("game/game.html", problem_submit_form=problem_submit_form) 47 | 48 | 49 | @blueprint.route("/problems", methods=["GET"]) 50 | @login_required 51 | @team_required 52 | @block_before_competition 53 | @api_view 54 | def problems(): 55 | if current_user.admin: 56 | problems = Problem.query.order_by(Problem.value).all() 57 | else: 58 | problems = current_user.team.get_unlocked_problems() 59 | formatted_problems = {problem.pid: problem.api_summary() for problem in problems} 60 | return 200, formatted_problems 61 | 62 | 63 | @blueprint.route("/submit", methods=["POST"]) 64 | @login_required 65 | @team_required 66 | @block_before_competition 67 | @api_view 68 | def submit(): 69 | problem_submit_form = ProblemSubmitForm(**request.get_json()) 70 | if problem_submit_form.validate(): 71 | problem = Problem.query.get_or_404(int(problem_submit_form.pid.data)) 72 | result, message = problem.try_submit(problem_submit_form.flag.data) 73 | 74 | return 200, { 75 | "result": result, 76 | "message": message, 77 | } 78 | return 400, None # TODO: actually return validation error 79 | 80 | 81 | @blueprint.route("/state", methods=["GET", "POST"]) 82 | @login_required 83 | @team_required 84 | @block_before_competition 85 | def game_state_get(): 86 | game_state = GameState.query.filter_by(uid=current_user.uid).first() 87 | if game_state is None: 88 | game_state = GameState(uid=current_user.uid) 89 | # TODO: proper upserting 90 | db.session.add(game_state) 91 | db.session.commit() 92 | return make_response( 93 | game_state.state, 200, {"Content-Type": "application/json; charset=utf-8"} 94 | ) 95 | 96 | 97 | @blueprint.route("/state/update", methods=["POST"]) 98 | @login_required 99 | @team_required 100 | @block_before_competition 101 | @api_view 102 | def game_state_update(): 103 | game_state_update_form = GameStateUpdateForm(**request.get_json()) 104 | if game_state_update_form.validate(): 105 | state = game_state_update_form.state.data 106 | game_state = GameState.query.filter_by(uid=current_user.uid).first() 107 | if game_state is None: 108 | game_state = GameState(uid=current_user.uid) 109 | # TODO: proper upserting 110 | db.session.add(game_state) 111 | game_state.state = state 112 | db.session.commit() 113 | return 200, None 114 | return 400, None # TODO: actually return validation error 115 | -------------------------------------------------------------------------------- /server/easyctf/views/judge.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import json 3 | import traceback 4 | from datetime import datetime, timedelta 5 | from functools import wraps 6 | 7 | from flask import Blueprint, abort, make_response, request 8 | 9 | from sqlalchemy import and_, or_ 10 | from easyctf.models import Job, JudgeKey, Solve, Problem 11 | from easyctf.objects import db 12 | 13 | blueprint = Blueprint("judge", __name__) 14 | 15 | 16 | def api_view(f): 17 | @wraps(f) 18 | def wrapper(*args, **kwargs): 19 | api_key = request.headers.get("API-Key") 20 | if not api_key: 21 | return abort(403) 22 | key = JudgeKey.query.filter_by(key=api_key).first() 23 | if not key: 24 | return abort(403) 25 | status, result = f(*args, **kwargs) 26 | return make_response( 27 | json.dumps(result or dict()), 28 | status, 29 | {"Content-Type": "application/json; charset=utf-8"}, 30 | ) 31 | 32 | return wrapper 33 | 34 | 35 | @blueprint.route("/jobs", methods=["GET", "POST"]) 36 | @api_view 37 | def jobs(): 38 | if request.method == "GET": 39 | # implement language preference later 40 | available = ( 41 | Job.query.filter( 42 | or_( 43 | Job.status == 0, 44 | and_( 45 | Job.status == 1, 46 | Job.claimed < datetime.utcnow() - timedelta(minutes=5), 47 | ), 48 | ) 49 | ) 50 | .order_by(Job.submitted) 51 | .first() 52 | ) 53 | if not available: 54 | return 204, [] 55 | # assign job to current judge 56 | info = dict( 57 | # job info 58 | id=available.id, 59 | language=available.language, 60 | source=available.contents, 61 | # problem info 62 | pid=available.problem.pid, 63 | test_cases=available.problem.test_cases, 64 | time_limit=available.problem.time_limit, 65 | memory_limit=available.problem.memory_limit, 66 | generator_code=available.problem.generator, 67 | grader_code=available.problem.grader, 68 | source_verifier_code=available.problem.source_verifier or "", 69 | ) 70 | available.status = 1 71 | available.claimed = datetime.utcnow() 72 | db.session.add(available) 73 | db.session.commit() 74 | return 202, info 75 | elif request.method == "POST": 76 | # expect 77 | id_raw = request.form.get("id") 78 | print("id = ", id_raw) 79 | if not (id_raw and id_raw.isdigit()): 80 | return 400, None 81 | id = int(id_raw) 82 | job = Job.query.filter_by(id=id).first() 83 | if not job: 84 | return 404, None 85 | try: 86 | job.status = 2 87 | job.verdict = request.form.get("verdict") 88 | job.execution_time = float(request.form.get("execution_time")) 89 | job.execution_memory = int(request.form.get("execution_memory")) 90 | job.completed = datetime.utcnow() 91 | db.session.add(job) 92 | if job.verdict == "AC": 93 | solve = Solve.query.filter_by(pid=job.pid, tid=job.tid).first() 94 | if not solve: 95 | solve = Solve( 96 | pid=job.pid, uid=job.uid, tid=job.tid, _date=job.completed 97 | ) 98 | db.session.add(solve) 99 | db.session.commit() 100 | return 202, None 101 | except Exception: 102 | # failed 103 | job.status = 3 104 | job.feedback = traceback.format_exc() 105 | db.session.add(job) 106 | db.session.commit() 107 | return 400, None 108 | -------------------------------------------------------------------------------- /server/easyctf/views/teams.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from io import BytesIO 3 | 4 | from flask import Blueprint, abort, flash, redirect, render_template, url_for 5 | from flask_login import current_user, login_required 6 | 7 | from easyctf.decorators import is_team_captain, email_verification_required 8 | from easyctf.forms.teams import AddMemberForm, CreateTeamForm, ProfileEditForm 9 | from easyctf.models import Config, Team, User 10 | from easyctf.objects import db 11 | from easyctf.utils import sanitize_avatar, save_file 12 | from easyctf.constants import USER_TEACHER 13 | 14 | blueprint = Blueprint("teams", __name__, template_folder="templates") 15 | 16 | 17 | @blueprint.route("/accept/") 18 | @is_team_captain 19 | @login_required 20 | def accept(id): 21 | return "" 22 | 23 | 24 | @blueprint.route("/cancel/") 25 | @is_team_captain 26 | @login_required 27 | def cancel(id): 28 | current_team = current_user.team 29 | target_user = User.get_by_id(id) 30 | try: 31 | assert target_user != None, "User not found." 32 | assert ( 33 | target_user in current_team.outgoing_invitations 34 | ), "No invitation for this user found." 35 | current_team.outgoing_invitations.remove(target_user) 36 | db.session.add(current_team) 37 | db.session.commit() 38 | flash( 39 | "Invitation to %s successfully withdrawn." % target_user.username, "success" 40 | ) 41 | except AssertionError as e: 42 | flash(str(e), "danger") 43 | return redirect(url_for("teams.settings")) 44 | 45 | 46 | @blueprint.route("/create", methods=["GET", "POST"]) 47 | @email_verification_required 48 | @login_required 49 | def create(): 50 | if current_user.tid: 51 | return redirect(url_for("teams.profile")) 52 | create_team_form = CreateTeamForm(prefix="create") 53 | if create_team_form.validate_on_submit(): 54 | new_team = create_team(create_team_form) 55 | logging.info("Created team '%s' (id=%s)!" % (new_team.teamname, new_team.tid)) 56 | return redirect(url_for("teams.profile")) 57 | return render_template("teams/create.html", create_team_form=create_team_form) 58 | 59 | 60 | @blueprint.route("/evict/") 61 | @is_team_captain 62 | @login_required 63 | def evict(id): 64 | current_team = current_user.team 65 | target_user = User.get_by_id(id) 66 | try: 67 | assert target_user != None, "User not found." 68 | assert target_user in current_team.members, "This user isn't in your team!" 69 | assert target_user.uid != current_team.owner, "You can't evict the captain!" 70 | current_team.members.remove(target_user) 71 | db.session.add(target_user) 72 | db.session.commit() 73 | flash("Removed %s from the team." % target_user.username, "success") 74 | except AssertionError as e: 75 | flash(str(e), "danger") 76 | return redirect(url_for("teams.settings")) 77 | 78 | 79 | @blueprint.route("/profile", methods=["GET", "POST"]) 80 | @blueprint.route("/profile/", methods=["GET", "POST"]) 81 | def profile(tid=None): 82 | if tid is None and current_user.is_authenticated: 83 | if current_user.tid is None: 84 | return redirect(url_for("teams.create")) 85 | else: 86 | return redirect(url_for("teams.profile", tid=current_user.tid)) 87 | team = Team.get_by_id(tid) 88 | if team is None: 89 | abort(404) 90 | if not current_user.is_authenticated: 91 | flash("Please login to view team profiles!", "warning") 92 | return redirect(url_for("users.login")) 93 | if not current_user.admin and (team.banned and current_user.tid != team.tid): 94 | abort(404) 95 | return render_template("teams/profile.html", team=team) 96 | 97 | 98 | @blueprint.route("/settings", methods=["GET", "POST"]) 99 | @is_team_captain 100 | @login_required 101 | def settings(): 102 | current_team = current_user.team 103 | add_member_form = AddMemberForm(prefix="add-member") 104 | profile_edit_form = ProfileEditForm(prefix="profile-edit") 105 | if add_member_form.submit.data and add_member_form.validate_on_submit(): 106 | target_user = add_member_form.get_user() 107 | current_team.outgoing_invitations.append(target_user) 108 | db.session.add(current_team) 109 | db.session.commit() 110 | flash("Invitation to %s sent!" % target_user.username, "info") 111 | return redirect(url_for("teams.settings")) 112 | elif profile_edit_form.submit.data and profile_edit_form.validate_on_submit(): 113 | for field in profile_edit_form: 114 | if field.short_name == "avatar": 115 | if hasattr(field.data, "read") and len(field.data.read()) > 0: 116 | field.data.seek(0) 117 | f = BytesIO(field.data.read()) 118 | new_avatar = sanitize_avatar(f) 119 | if new_avatar: 120 | response = save_file( 121 | new_avatar, prefix="team_avatar", suffix=".png" 122 | ) 123 | if response.status_code == 200: 124 | current_team._avatar = response.text 125 | continue 126 | if hasattr(current_team, field.short_name): 127 | setattr(current_team, field.short_name, field.data) 128 | if profile_edit_form.remove_avatar.data: 129 | current_team._avatar = None 130 | db.session.add(current_team) 131 | db.session.commit() 132 | flash("Profile updated.", "success") 133 | return redirect(url_for("teams.settings")) 134 | else: 135 | for field in profile_edit_form: 136 | if hasattr(current_team, field.short_name): 137 | field.data = getattr(current_team, field.short_name, "") 138 | return render_template( 139 | "teams/settings.html", 140 | team=current_team, 141 | profile_edit_form=profile_edit_form, 142 | add_member_form=add_member_form, 143 | ) 144 | 145 | 146 | def create_team(form): 147 | new_team = Team(owner=current_user.uid) 148 | db.session.add(new_team) 149 | db.session.commit() 150 | current_user.tid = new_team.tid 151 | form.populate_obj(current_user.team) 152 | db.session.add(current_user) 153 | db.session.commit() 154 | return new_team 155 | -------------------------------------------------------------------------------- /server/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd /var/easyctf/src 4 | PYTHON=python3 5 | 6 | echo "determining bind location..." 7 | BIND_PORT=8000 8 | BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/private/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0") 9 | BIND_ADDR=$(echo $BIND_ADDR_ | xargs) 10 | 11 | echo "starting EasyCTF..." 12 | COMMAND=${1:-runserver} 13 | ENVIRONMENT=${ENVIRONMENT:-production} 14 | WORKERS=${WORKERS:-4} 15 | $PYTHON manage.py db upgrade 16 | if [ "$COMMAND" == "runserver" ]; then 17 | if [ "$ENVIRONMENT" == "development" ]; then 18 | $PYTHON manage.py runserver 19 | else 20 | exec gunicorn --bind="$BIND_ADDR:$BIND_PORT" -w $WORKERS 'easyctf:create_app()' 21 | fi 22 | fi 23 | -------------------------------------------------------------------------------- /server/env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://username:password@127.0.0.1/easyctf 2 | SECRET_KEY=key 3 | FILESTORE_SAVE_ENDPOINT=http://example.com/save 4 | FILESTORE_STATIC=http://example.com 5 | CACHE_REDIS_HOST=127.0.0.1 6 | ENVIRONMENT=development 7 | ADMIN_EMAIL=admin@example.com 8 | MAILGUN_URL=https://api.mailgun.net/v3/example.com 9 | MAILGUN_API_KEY=key 10 | SENTRY_DSN= -------------------------------------------------------------------------------- /server/forgot.mail: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyctf/librectf/ec8f23edde72547faf5390944a1491d113ba37de/server/forgot.mail -------------------------------------------------------------------------------- /server/import_problems.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export $(cat /var/easyctf/env | xargs) 4 | python3 manage.py import ~/problems 5 | -------------------------------------------------------------------------------- /server/manage.py: -------------------------------------------------------------------------------- 1 | from flask_migrate import Migrate, MigrateCommand 2 | from flask_script import Command, Manager, Server 3 | 4 | from easyctf import create_app 5 | from easyctf.models import Problem 6 | from easyctf.objects import db 7 | 8 | app = create_app() 9 | migrate = Migrate(app, db) 10 | 11 | manager = Manager(app) 12 | manager.add_command("db", MigrateCommand) 13 | 14 | ServerCommand = Server(host="0.0.0.0", port=8000, use_debugger=True, use_reloader=True) 15 | manager.add_command("runserver", ServerCommand) 16 | 17 | 18 | class ImportCommand(Command): 19 | "Import CTF challenges from local repository." 20 | 21 | def __init__(self): 22 | Command.__init__(self, func=Problem.import_repository) 23 | 24 | 25 | manager.add_command("import", ImportCommand) 26 | 27 | if __name__ == "__main__": 28 | manager.run() 29 | -------------------------------------------------------------------------------- /server/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /server/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 | -------------------------------------------------------------------------------- /server/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger("alembic.env") 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | 22 | config.set_main_option( 23 | "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") 24 | ) 25 | target_metadata = current_app.extensions["migrate"].db.metadata 26 | 27 | # other values from the config, defined by the needs of env.py, 28 | # can be acquired: 29 | # my_important_option = config.get_main_option("my_important_option") 30 | # ... etc. 31 | 32 | 33 | def run_migrations_offline(): 34 | """Run migrations in 'offline' mode. 35 | 36 | This configures the context with just a URL 37 | and not an Engine, though an Engine is acceptable 38 | here as well. By skipping the Engine creation 39 | we don't even need a DBAPI to be available. 40 | 41 | Calls to context.execute() here emit the given string to the 42 | script output. 43 | 44 | """ 45 | url = config.get_main_option("sqlalchemy.url") 46 | context.configure(url=url) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | def run_migrations_online(): 53 | """Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine 56 | and associate a connection with the context. 57 | 58 | """ 59 | 60 | # this callback is used to prevent an auto-migration from being generated 61 | # when there are no changes to the schema 62 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 63 | def process_revision_directives(context, revision, directives): 64 | if getattr(config.cmd_opts, "autogenerate", False): 65 | script = directives[0] 66 | if script.upgrade_ops.is_empty(): 67 | directives[:] = [] 68 | logger.info("No changes in schema detected.") 69 | 70 | engine = engine_from_config( 71 | config.get_section(config.config_ini_section), 72 | prefix="sqlalchemy.", 73 | poolclass=pool.NullPool, 74 | ) 75 | 76 | connection = engine.connect() 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | process_revision_directives=process_revision_directives, 81 | **current_app.extensions["migrate"].configure_args 82 | ) 83 | 84 | try: 85 | with context.begin_transaction(): 86 | context.run_migrations() 87 | finally: 88 | connection.close() 89 | 90 | 91 | if context.is_offline_mode(): 92 | run_migrations_offline() 93 | else: 94 | run_migrations_online() 95 | -------------------------------------------------------------------------------- /server/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 | -------------------------------------------------------------------------------- /server/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "easyctf-platform" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Michael Zhang "] 6 | license = "AGPL-3.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | bcrypt = "^4.0.1" 11 | celery = "^5.2.7" 12 | coverage = "^6.5.0" 13 | cryptography = "^38.0.4" 14 | Flask = "^2.2.2" 15 | Flask-Breadcrumbs = "^0.5.1" 16 | Flask-Caching = "^2.0.1" 17 | Flask-Celery-Helper = "^1.1.0" 18 | flask-csp = "^0.10" 19 | Flask-Login = "^0.6.2" 20 | Flask-Migrate = "^4.0.0" 21 | Flask-Script = "^2.0.6" 22 | flask-sqlalchemy = "^3.0.2" 23 | Flask-WTF = "^1.0.1" 24 | gitdb = "^4.0.10" 25 | GitPython = "^3.1.29" 26 | markdown2 = "^2.4.6" 27 | mysqlclient = "^2.1.1" 28 | onetimepass = "^1.0.1" 29 | paramiko = "^2.12.0" 30 | passlib = "^1.7.4" 31 | pathlib = "^1.0.1" 32 | Pillow = "^9.3.0" 33 | pycryptodome = "^3.16.0" 34 | PyQRCode = "^1.2.1" 35 | pytest = "^7.2.0" 36 | PyYAML = "^6.0" 37 | rauth = "^0.7.3" 38 | raven = {extras = ["flask"], version = "^6.10.0"} 39 | redis = "^4.3.5" 40 | requests = "^2.28.1" 41 | SQLAlchemy = "^1.4.44" 42 | WTForms-Components = "^0.10.5" 43 | Werkzeug = "^2.2.2" 44 | cachelib = "^0.9.0" 45 | boto3 = "^1.26.17" 46 | 47 | [tool.poetry.dev-dependencies] 48 | 49 | [build-system] 50 | requires = ["poetry-core>=1.0.0"] 51 | build-backend = "poetry.core.masonry.api" 52 | -------------------------------------------------------------------------------- /server/registration.mail: -------------------------------------------------------------------------------- 1 |

Hey ${username}!

2 | 3 |

You recently signed up for EasyCTF IV with this email. To confirm your 4 | email, please click on the following link (or copy-paste it into your browser 5 | your email client didn't render a link).

6 | 7 | ${link} 8 | 9 |

We hope you have fun!

10 | 11 |

__
12 | The EasyCTF Team

-------------------------------------------------------------------------------- /shell/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vagrant -------------------------------------------------------------------------------- /shell/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "ubuntu/trusty64" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | # config.vm.network "forwarded_port", guest: 80, host: 8080 26 | 27 | # Create a private network, which allows host-only access to the machine 28 | # using a specific IP. 29 | # config.vm.network "private_network", ip: "192.168.33.10" 30 | 31 | # Create a public network, which generally matched to bridged network. 32 | # Bridged networks make the machine appear as another physical device on 33 | # your network. 34 | # config.vm.network "public_network" 35 | 36 | # Share an additional folder to the guest VM. The first argument is 37 | # the path on the host to the actual folder. The second argument is 38 | # the path on the guest to mount the folder. And the optional third 39 | # argument is a set of non-required options. 40 | # config.vm.synced_folder "../data", "/vagrant_data" 41 | 42 | # Provider-specific configuration so you can fine-tune various 43 | # backing providers for Vagrant. These expose provider-specific options. 44 | # Example for VirtualBox: 45 | # 46 | # config.vm.provider "virtualbox" do |vb| 47 | # # Display the VirtualBox GUI when booting the machine 48 | # vb.gui = true 49 | # 50 | # # Customize the amount of memory on the VM: 51 | # vb.memory = "1024" 52 | # end 53 | # 54 | # View the documentation for the provider you are using for more 55 | # information on available options. 56 | 57 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 58 | # such as FTP and Heroku are also available. See the documentation at 59 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 60 | # config.push.define "atlas" do |push| 61 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 62 | # end 63 | 64 | # Enable provisioning with a shell script. Additional provisioners such as 65 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 66 | # documentation for more information about their specific syntax and use. 67 | # config.vm.provision "shell", inline: <<-SHELL 68 | # sudo apt-get update 69 | # sudo apt-get install -y apache2 70 | # SHELL 71 | config.vm.provision "shell", path: "setup.sh" 72 | end 73 | -------------------------------------------------------------------------------- /shell/include/bin/addctfuser: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function randomuser() { cat /dev/urandom | tr -dc '0-9' | fold -w 5 | head -n 1; } 4 | function randomsalt() { cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1; } 5 | function randompass() { cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1; } 6 | 7 | #======================================== 8 | echo "Creating CTF user." 9 | 10 | USERFOUND=0 11 | NEWUSER="" 12 | while [ $USERFOUND -lt 1 ]; do 13 | NEWUSER="user$(randomuser)" 14 | getent passwd $NEWUSER >/dev/null 2>&1 || USERFOUND=1 15 | done 16 | salt=$(randomsalt) 17 | RAWPASS=$(randompass) 18 | NEWPASS=$(openssl passwd -1 -salt $salt $RAWPASS) 19 | 20 | sudo useradd --gid ctfuser \ 21 | --password $NEWPASS \ 22 | --create-home \ 23 | --no-user-group \ 24 | --shell /bin/bash $NEWUSER 25 | echo "$NEWUSER:$RAWPASS" 26 | -------------------------------------------------------------------------------- /shell/include/etc/adduser.conf: -------------------------------------------------------------------------------- 1 | # /etc/adduser.conf: `adduser' configuration. 2 | # See adduser(8) and adduser.conf(5) for full documentation. 3 | 4 | # The DSHELL variable specifies the default login shell on your 5 | # system. 6 | DSHELL=/bin/bash 7 | 8 | # The DHOME variable specifies the directory containing users' home 9 | # directories. 10 | DHOME=/home 11 | 12 | # If GROUPHOMES is "yes", then the home directories will be created as 13 | # /home/groupname/user. 14 | GROUPHOMES=yes 15 | 16 | # If LETTERHOMES is "yes", then the created home directories will have 17 | # an extra directory - the first letter of the user name. For example: 18 | # /home/u/user. 19 | LETTERHOMES=no 20 | 21 | # The SKEL variable specifies the directory containing "skeletal" user 22 | # files; in other words, files such as a sample .profile that will be 23 | # copied to the new user's home directory when it is created. 24 | SKEL=/etc/skel 25 | 26 | # FIRST_SYSTEM_[GU]ID to LAST_SYSTEM_[GU]ID inclusive is the range for UIDs 27 | # for dynamically allocated administrative and system accounts/groups. 28 | # Please note that system software, such as the users allocated by the base-passwd 29 | # package, may assume that UIDs less than 100 are unallocated. 30 | FIRST_SYSTEM_UID=100 31 | LAST_SYSTEM_UID=999 32 | 33 | FIRST_SYSTEM_GID=100 34 | LAST_SYSTEM_GID=999 35 | 36 | # FIRST_[GU]ID to LAST_[GU]ID inclusive is the range of UIDs of dynamically 37 | # allocated user accounts/groups. 38 | FIRST_UID=1000 39 | LAST_UID=29999 40 | 41 | FIRST_GID=1000 42 | LAST_GID=29999 43 | 44 | # The USERGROUPS variable can be either "yes" or "no". If "yes" each 45 | # created user will be given their own group to use as a default. If 46 | # "no", each created user will be placed in the group whose gid is 47 | # USERS_GID (see below). 48 | USERGROUPS=no 49 | 50 | # If USERGROUPS is "no", then USERS_GID should be the GID of the group 51 | # `users' (or the equivalent group) on your system. 52 | USERS_GID=1337 53 | 54 | # If DIR_MODE is set, directories will be created with the specified 55 | # mode. Otherwise the default mode 0755 will be used. 56 | DIR_MODE=0700 57 | 58 | # If SETGID_HOME is "yes" home directories for users with their own 59 | # group the setgid bit will be set. This was the default for 60 | # versions << 3.13 of adduser. Because it has some bad side effects we 61 | # no longer do this per default. If you want it nevertheless you can 62 | # still set it here. 63 | SETGID_HOME=no 64 | 65 | # If QUOTAUSER is set, a default quota will be set from that user with 66 | # `edquota -p QUOTAUSER newuser' 67 | QUOTAUSER="loluser" 68 | 69 | # If SKEL_IGNORE_REGEX is set, adduser will ignore files matching this 70 | # regular expression when creating a new home directory 71 | SKEL_IGNORE_REGEX="dpkg-(old|new|dist|save)" 72 | 73 | # Set this if you want the --add_extra_groups option to adduser to add 74 | # new users to other groups. 75 | # This is the list of groups that new non-system users will be added to 76 | # Default: 77 | #EXTRA_GROUPS="dialout cdrom floppy audio video plugdev users" 78 | 79 | # If ADD_EXTRA_GROUPS is set to something non-zero, the EXTRA_GROUPS 80 | # option above will be default behavior for adding new, non-system users 81 | #ADD_EXTRA_GROUPS=1 82 | 83 | 84 | # check user and group names also against this regular expression. 85 | #NAME_REGEX="^[a-z][-a-z0-9_]*\$" 86 | 87 | # use extrausers by default 88 | #USE_EXTRAUSERS=1 89 | -------------------------------------------------------------------------------- /shell/include/etc/pam.d/login: -------------------------------------------------------------------------------- 1 | # 2 | # The PAM configuration file for the Shadow `login' service 3 | # 4 | 5 | # Enforce a minimal delay in case of failure (in microseconds). 6 | # (Replaces the `FAIL_DELAY' setting from login.defs) 7 | # Note that other modules may require another minimal delay. (for example, 8 | # to disable any delay, you should add the nodelay option to pam_unix) 9 | auth optional pam_faildelay.so delay=3000000 10 | 11 | # Outputs an issue file prior to each login prompt (Replaces the 12 | # ISSUE_FILE option from login.defs). Uncomment for use 13 | # auth required pam_issue.so issue=/etc/issue 14 | 15 | # Disallows root logins except on tty's listed in /etc/securetty 16 | # (Replaces the `CONSOLE' setting from login.defs) 17 | # 18 | # With the default control of this module: 19 | # [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] 20 | # root will not be prompted for a password on insecure lines. 21 | # if an invalid username is entered, a password is prompted (but login 22 | # will eventually be rejected) 23 | # 24 | # You can change it to a "requisite" module if you think root may mis-type 25 | # her login and should not be prompted for a password in that case. But 26 | # this will leave the system as vulnerable to user enumeration attacks. 27 | # 28 | # You can change it to a "required" module if you think it permits to 29 | # guess valid user names of your system (invalid user names are considered 30 | # as possibly being root on insecure lines), but root passwords may be 31 | # communicated over insecure lines. 32 | auth [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] pam_securetty.so 33 | 34 | # Disallows other than root logins when /etc/nologin exists 35 | # (Replaces the `NOLOGINS_FILE' option from login.defs) 36 | auth requisite pam_nologin.so 37 | 38 | # SELinux needs to be the first session rule. This ensures that any 39 | # lingering context has been cleared. Without out this it is possible 40 | # that a module could execute code in the wrong domain. 41 | # When the module is present, "required" would be sufficient (When SELinux 42 | # is disabled, this returns success.) 43 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close 44 | 45 | # This module parses environment configuration file(s) 46 | # and also allows you to use an extended config 47 | # file /etc/security/pam_env.conf. 48 | # 49 | # parsing /etc/environment needs "readenv=1" 50 | session required pam_env.so readenv=1 51 | # locale variables are also kept into /etc/default/locale in etch 52 | # reading this file *in addition to /etc/environment* does not hurt 53 | session required pam_env.so readenv=1 envfile=/etc/default/locale 54 | 55 | # Standard Un*x authentication. 56 | @include common-auth 57 | 58 | # This allows certain extra groups to be granted to a user 59 | # based on things like time of day, tty, service, and user. 60 | # Please edit /etc/security/group.conf to fit your needs 61 | # (Replaces the `CONSOLE_GROUPS' option in login.defs) 62 | auth optional pam_group.so 63 | 64 | # Uncomment and edit /etc/security/time.conf if you need to set 65 | # time restrainst on logins. 66 | # (Replaces the `PORTTIME_CHECKS_ENAB' option from login.defs 67 | # as well as /etc/porttime) 68 | # account requisite pam_time.so 69 | 70 | # Uncomment and edit /etc/security/access.conf if you need to 71 | # set access limits. 72 | # (Replaces /etc/login.access file) 73 | # account required pam_access.so 74 | 75 | # Sets up user limits according to /etc/security/limits.conf 76 | # (Replaces the use of /etc/limits in old login) 77 | session required pam_limits.so 78 | 79 | # Prints the last login info upon succesful login 80 | # (Replaces the `LASTLOG_ENAB' option from login.defs) 81 | session optional pam_lastlog.so 82 | 83 | # Prints the message of the day upon succesful login. 84 | # (Replaces the `MOTD_FILE' option in login.defs) 85 | # This includes a dynamically generated part from /run/motd.dynamic 86 | # and a static (admin-editable) part from /etc/motd. 87 | # session optional pam_motd.so motd=/run/motd.dynamic noupdate 88 | # session optional pam_motd.so 89 | 90 | # Prints the status of the user's mailbox upon succesful login 91 | # (Replaces the `MAIL_CHECK_ENAB' option from login.defs). 92 | # 93 | # This also defines the MAIL environment variable 94 | # However, userdel also needs MAIL_DIR and MAIL_FILE variables 95 | # in /etc/login.defs to make sure that removing a user 96 | # also removes the user's mail spool file. 97 | # See comments in /etc/login.defs 98 | session optional pam_mail.so standard 99 | 100 | # Standard Un*x account and session 101 | @include common-account 102 | @include common-session 103 | @include common-password 104 | 105 | # SELinux needs to intervene at login time to ensure that the process 106 | # starts in the proper default security context. Only sessions which are 107 | # intended to run in the user's context should be run after this. 108 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open 109 | # When the module is present, "required" would be sufficient (When SELinux 110 | # is disabled, this returns success.) 111 | -------------------------------------------------------------------------------- /shell/include/etc/security/limits.conf: -------------------------------------------------------------------------------- 1 | # /etc/security/limits.conf 2 | # 3 | #Each line describes a limit for a user in the form: 4 | # 5 | # 6 | # 7 | #Where: 8 | # can be: 9 | # - a user name 10 | # - a group name, with @group syntax 11 | # - the wildcard *, for default entry 12 | # - the wildcard %, can be also used with %group syntax, 13 | # for maxlogin limit 14 | # - NOTE: group and wildcard limits are not applied to root. 15 | # To apply a limit to the root user, must be 16 | # the literal username root. 17 | # 18 | # can have the two values: 19 | # - "soft" for enforcing the soft limits 20 | # - "hard" for enforcing hard limits 21 | # 22 | # can be one of the following: 23 | # - core - limits the core file size (KB) 24 | # - data - max data size (KB) 25 | # - fsize - maximum filesize (KB) 26 | # - memlock - max locked-in-memory address space (KB) 27 | # - nofile - max number of open files 28 | # - rss - max resident set size (KB) 29 | # - stack - max stack size (KB) 30 | # - cpu - max CPU time (MIN) 31 | # - nproc - max number of processes 32 | # - as - address space limit (KB) 33 | # - maxlogins - max number of logins for this user 34 | # - maxsyslogins - max number of logins on the system 35 | # - priority - the priority to run user process with 36 | # - locks - max number of file locks the user can hold 37 | # - sigpending - max number of pending signals 38 | # - msgqueue - max memory used by POSIX message queues (bytes) 39 | # - nice - max nice priority allowed to raise to values: [-20, 19] 40 | # - rtprio - max realtime priority 41 | # - chroot - change root to directory (Debian-specific) 42 | # 43 | # 44 | # 45 | 46 | #* soft core 0 47 | #root hard core 100000 48 | #* hard rss 10000 49 | #@student hard nproc 20 50 | #@faculty soft nproc 20 51 | #@faculty hard nproc 50 52 | #ftp hard nproc 0 53 | #ftp - chroot /ftp 54 | #@student - maxlogins 4 55 | 56 | # End of file 57 | 58 | @ctfuser hard as 512000 59 | @ctfuser hard nproc 25 60 | @ctfuser hard maxlogins 4 61 | -------------------------------------------------------------------------------- /shell/include/etc/sudoers: -------------------------------------------------------------------------------- 1 | # 2 | # This file MUST be edited with the 'visudo' command as root. 3 | # 4 | # Please consider adding local content in /etc/sudoers.d/ instead of 5 | # directly modifying this file. 6 | # 7 | # See the man page for details on how to write a sudoers file. 8 | # 9 | Defaults env_reset 10 | Defaults mail_badpass 11 | Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" 12 | 13 | # Host alias specification 14 | 15 | # User alias specification 16 | 17 | # Cmnd alias specification 18 | 19 | # User privilege specification 20 | root ALL=(ALL:ALL) ALL 21 | ctfadmin ALL=(ALL) NOPASSWD:ALL 22 | 23 | # Members of the admin group may gain root privileges 24 | # admin ALL=(ALL) ALL 25 | 26 | # Allow members of group sudo to execute any command 27 | # sudo ALL=(ALL) NOPASSWD:ALL 28 | 29 | # See sudoers(5) for more information on "#include" directives: 30 | 31 | #includedir /etc/sudoers.d -------------------------------------------------------------------------------- /shell/setup.sh: -------------------------------------------------------------------------------- 1 | FILE=`basename $0` 2 | PROJECT=$(realpath `dirname FILE`) 3 | INCLUDE=$PROJECT/include 4 | source $PROJECT/.env 5 | 6 | #======================================== 7 | echo "Copying configuration files..." 8 | cp $INCLUDE/etc/pam.d/login /etc/pam.d 9 | cp $INCLUDE/etc/sudoers /etc 10 | chmod 440 /etc/sudoers 11 | cp $INCLUDE/etc/adduser.conf /etc 12 | cp $INCLUDE/etc/security/limits.conf /etc/security 13 | cp $INCLUDE/bin/addctfuser /bin/addctfuser 14 | 15 | #======================================== 16 | echo "Creating administrator user..." 17 | groupadd ctfadmin 18 | useradd --gid ctfadmin \ 19 | --groups sudo \ 20 | --home-dir /home/ctfadmin \ 21 | --create-home \ 22 | --shell /bin/addctfuser ctfadmin 23 | echo "ctfadmin:$ADMIN_PASSWORD" | chpasswd 24 | 25 | chown ctfadmin:ctfadmin /bin/addctfuser 26 | chmod 0100 /bin/addctfuser 27 | 28 | #======================================== 29 | echo "Creating ctfuser group..." 30 | groupadd --gid 1337 ctfuser 31 | --------------------------------------------------------------------------------