A simple CTF platform in a single binary.
Scrap is designed to be as fast and lightweight as possible. It compiles into a single Rust binary and can handle many thousands of teams. Furthermore, Scrap's browser webpage is completely free of JavaScript. Due to various design decisions, there are a few constraints:
- Only dynamic scoring is supported.
- There is a maximum of 64 challenges.
- Teams lack email verification.
- Registration lacks captchas.
Registration can be rate limited through a reverse proxy if necessary.
Download the latest version from the releases page and run it:
./scrap --port 8000 --repo ./repository --static ./static --uri postgres://user:pass@host/dbScrap is centered around a central repository that specifies challenges and files. An example is available here.
repository
├── ctf.toml
├── first
│ └── challenge.toml
├── second
│ └── challenge.toml
└── third
│ └── challenge.toml
└── junk
└── trash
Scrap requires a single ctf.toml, as well as a challenge.toml for each challenge. All other files and directories are ignored.
ctf.toml must be in the base directory.
# HTML title element text
title = "MyCTF"
# Homepage Markdown/HTML
home = """
# Welcome to MyCTF!
Enjoy our many *challenges*."""
# CTF start time
# If removed, infinitely in the past
start = 2000-01-01T00:00:00Z
# CTF stop time
# If removed, infinitely in the future
stop = 2100-01-01T00:00:00ZChallenges, scoreboard, and flag submission remain unavailable until the time specified by start. Flag submission becomes unavailable once the time specified by stop is reached.
Each challenge.toml must be exactly two levels below the base directory. The name of the intermediate challenge directory is irrelevant.
# Unique identifier
slug = "caesar_cipher"
# Title text
title = "Caesar Cipher"
# Author text
author = "username"
# Description Markdown/HTML
description = """
I encrypted a [message](ciphertext.txt) \
with a [Caesar cipher](encrypt.py)!"""
# Tag text
tags = [ "crypto", "classical" ]
# Paths to files anywhere in the challenge directory
files = [ "path/to/ciphertext.txt", "encrypt.py" ]
# Challenge flag
flag = "flag{}"
# Challenge status
enabled = trueScrap will either update or add a challenge depending on whether slug exists in the database.
Paths in files can traverse directories, but must have unique filenames. These files can be referred to by filename in description for links.
Challenges with enabled set to true are displayed, open to flag submission, and used in calculating score. Challenges with enabled set to false are not, but maintain state for future toggling.
Scrap requires a PostgreSQL server with the pgcrypto extension enabled:
create extension pgcrypto;Scrap uses a static directory, which must be served separately at /static. This can be used for serving stylesheets and favicons. Scrap will also create a subdirectory named files to hold challenge files.
static
├── style.css
├── favicon.ico
Scrap requires four arguments at runtime:
portServer portrepoPath to repositorystaticPath to static directoryuriPostgreSQL database URI
./scrap --port 8000 --repo ./repository --static ./static --uri postgres://user:pass@host/dbScrap supports graceful reloading on SIGUSR1. Send the signal to reload the CTF and challenge configuration from the repository.
At the moment, Scrap requires nightly Rust.
cargo +nightly build --releaseScrap assumes that you have included style.css and favicon.png within the static directory. An example SCSS file is available here to function as documentation for the classes.
The dynamic scoring formula can be changed by modifying the value function in scrap.sql.