Challenge specification¶
This document contains the full detailed technical specification for creating a challenge compliant to the OpenChallSpec version 0.0.1. For tips on how to create a reader, see writing_a_reader.
Wording and terminology¶
This document uses key words defined in RFC 2119.
The term “reader” in this document refers to any automated script that reads the challenge configuration file. It is used as an umbrella term for command line tools, CTF platforms, deployment backends or anything else that might interact with the OCS in its raw form.
Versioning¶
The OCS follows semantic versioning. Readers SHOULD support all versions of the OCS below the version they are written for but still at the same MAJOR version. Semantic versioning ensures that older OCS versions can be parsed without issue by readers designed for new versions, so in practice backwards compatibility should be automatic. Readers SHOULD refuse to parse versions of the OCS with a MINOR version higher than what they were designed for, at the same MAJOR version. Similarly, readers MAY also refuse to work with higher PATCH versions. This is because the Schema is not written in a forwards compatible manner, and new features will usually constitute schema violations when parsed with older OCS versions.
Challenge structure¶
A challenge is a directory containing a challenge.yml or challenge.yaml configuration file. Readers MUST support both .yml and .yaml file extensions. The configuration file MUST be written in YAML. Anything outside the challenge directory is not considered part of the challenge. All challenge files MUST be located inside the challenge directory, as this can otherwise cause packageability issues.
Schema¶
The OCS includes a JSON Schema for validation of the challenge configuration file structure, which can be found here. Yes, the OCS uses JSON schema to validate a YAML file, but it works the same way as both JSON and YAML files parse to the same “dictionary” representation, and JSON Schema validators in any programming language usually take this dictionary representation as input. Readers MAY validate the configuration against the schema before attempting to interpret the configuration. A configuration file that doesn’t comply with the schema is considered invalid. The schema includes default values for all non-required fields, which may be a convenient method of filling in defaults.
Implementation completeness¶
Readers are not required to implement functionality for all fields and sub-fields below. Functionality for any field MAY be implemented. If the challenge configuration contains fields the reader does not support the user SHOULD be notified of this, unless the incompatibility is obvious, like when a standalone deployment script that only deploys challenge containers does not do anything with the challenge description. The script MAY continue running ignoring these fields. No CTF platform (probably) supports all features in the OCS, so ignoring certain fields is a very typical thing to do.
Configuration fields¶
Below is a list of all valid fields in the challenge configuration. Unless otherwise specified, only the described keys are valid for any object. This means that any additional unexpected keys make the configuration invalid.
authors¶
| Required | true |
|---|---|
| Type | String, Array of strings |
The challenge’s authors. If the type is string, this is simplified syntax. It SHOULD be interpreted as a single element array of this one string. The first item in the array MAY be considered the main author.
categories¶
| Required | true |
|---|---|
| Type | String, Array of strings |
The challenge’s categories. If the type is string, this is simplified syntax. It SHOULD be interpreted as a single element array of this one string. The first item in the array MAY be considered the main category. If the reader does not support multiple categories, the first one MUST be used. There MUST be at least one category.
tags¶
| Required | false |
|---|---|
| Type | String, Array of strings |
| Default | [] |
The challenge’s tags. If the type is string, this is simplified syntax. It SHOULD be interpreted as a single element array of this one string. The first item in the array MAY be considered the main tag.
Tags are “private categories”. They MUST NOT be shown to players. They record high level concepts that spoil the challenge, like “SQL injection”.
hints¶
| Required | false |
|---|---|
| Type | Array of objects |
| Default | [] |
An array of the challenge’s hints. If supported, these MUST be shown to players. Some hints cost points, in which case the action of opening a hint should subtract the price from the opening teams point total.
The objects are composed of these two sub-fields:
content¶
Required true Type String The hint text that is shown when opened.
cost¶
Required false Type Number Default 0The hint price.
flag_format_prefix¶
| Required | true |
|---|---|
| Type | String, null |
The first part of the flag format that the challenge’s flag(s) start(s) with. May also be null instead of a string signifying no flag format present for the challenge. In that case, the values of both flag_format_prefix and flag_format_suffix MUST be ignored for flag validation.
To validate a player submitted flag, a validator SHOULD first check if the flag starts with the flag_format_prefix and ends with the flag_format_suffix. If so, the prefix and suffix is stripped from the flag and rest should be matched against the list of flags. If it didn’t, the flag’s flag format is invalid.
flag_format_suffix¶
| Required | false |
|---|---|
| Type | String |
| Default | } |
The last part of the flag format that the challenge’s flag(s) start(s) with. Defaults to } for convenience. For more info, see flag_format_prefix.
flags¶
| Required | true |
|---|---|
| Type | String, Array or objects |
An array of the challenge’s flags. If the type is string, this is simplified syntax. It SHOULD be interpreted as this array instead: [{"flag": "<initial string here>"}]
Every element in the array is a separate flag, meaning that for a flag submission to be valid it must match at least one of the listed flags. If a reader doesn’t support multiple flags, the first flag MUST be used.
The objects in the array are composed of these two sub-fields:
flag¶
Required true Type String The flag contents, without the flag format as that is defined separately.
type¶
Required false Type String Default textMUST be either
textorregex.If the type is
text, theflagfield is to be compared directly to the contents of the user submitted flag. If they are the same, the submission is considered correct.If the type is
regex, theflagfield is considered to be a regex and the user submitted flag is to be matched against the regex. If it matches, the submission is considered correct. When writing challenges, theflagSHOULD start with^and end with$, to prevent false positives for very short flags.
max_attempts¶
| Required | false |
|---|---|
| Type | Integer, null |
| Default | null |
A positive integer signifying how many times teams may attempt to submit a flag before they are stopped from submitting any more for the challenge. If null, the teams have an unlimited number of tries.
Use of this option is heavily discouraged, as it often leads to a bad player experience. If you want to prevent brute-force attacks, try rate limiting instead.
score¶
| Required | false |
|---|---|
| Type | Number, null |
| Default | null |
An integer signifying how many points a team receives in reward for solving the challenge, for static scoring. For dynamic scoring, set to null. The dynamic scoring formula is handled by the ctf platform.
downloadable_files¶
| Required | false |
|---|---|
| Type | String, Array of strings |
| Default | [] |
An array of files downloadable by players. If the type is string, this is simplified syntax. It SHOULD be interpreted as a single element array of this one string. The string MUST be one of three things:
- A relative path to a file in the challenge directory. Readers MUST check if the file exists, and if not, move on to the other two options.
- A relative path to a directory in the challenge directory. All files in the directory should be included with the challenge. Readers MUST check if the directory exists, and if not, assume the string is the last option.
- A URL to a file. When players attempt to download this file, they MUST be redirected to this URL. Therefore, it does not have to be a direct download and can be for example a google drive link.
custom_service_types¶
| Required | false |
|---|---|
| Type | Array of objects |
| Default | [] |
A list of custom service types. A service type is a concept that defines how services should be automatically shown to players. It is used in definitions of predefined_services, service and deployment. There are two built-in service types. they look like this:
- type: website
user_display: "{url}"
hyperlink: true
- type: tcp
user_display: "nc {host} {port}"
hyperlink: false
These built-ins MUST be treated as if they are always the first two items in the array. For example, if the custom_service_types array contains only a newly defined type foo, the reader MUST treat the list of defined types as containing website, tcp and foo. Duplicate types MUST NOT be allowed. Therefore, website and tcp cannot be redefined.
Each object in the custom_service_types array has the following 3 fields:
type¶
Required true Type String The name of the type that is being defined. Can be any string that is not an already defined type.
user_display¶
Required true Type String Defines how services with this type will be shown to players and solve scripts. Variables can be substituted in by typing the variable name immediately enclosed in curly brackets. Additional whitespace between the name and bracket MUST NOT be supported. For example, for the
tcpservice type, if the hostname of a service is192.0.2.69and the port is1337, the ctf platform will show the stringnc 192.0.2.69 1337in the challenge details.The variables in the substitution context depend on the environment. If the service is in predefined_services, all needed variables MUST be provided in that same object. Otherwise, if the service is automatically deployed with a service or deployment configuration, it is the job of the deployment script to provide a context with the required variables. Deployment scripts MUST attempt to deploy services of a custom type they don’t know of and format user_display by providing the
host,portandurlvariables in the substitution context.
hyperlink¶
Required false Type Boolean Default false If the resulting user_display string after substitution is reachable by a web browser. If this is
true, ctf platforms MAY encase the string in an<a>tag.
predefined_services¶
| Required | false |
|---|---|
| Type | Array of objects |
| Default | [] |
A list of services for the challenge that are not automatically deployed. For example. if you will be manually deploying a service for a challenge, the hostname and port/URL to the challenge should be entered here.
Each predefined service object consists of the following one mandatory field, and any number of additional fields:
type¶
Required true Type string The service type for this service. MUST be either
website,tcp, or one defined in custom_service_types. See custom_service_types for info on what a service type is.
All other fields are formatting context for formatting user_display. Therefore, if the service type is website, a url field must be passed. If the object is instead tcp, a hostname and ip field must be passed.
service¶
| Required | false |
|---|---|
| Type | Object, null |
| Default | null |
This field is a simplified syntax of the deployment field. It consists of 3 mandatory fields image, type and internal_port, and one optional field external_port. When this field is present, assume that the deployment field has the following contents where <field name> is replaced by the contents of this service field:
type: docker
containers:
default:
image: <image>
services:
- type: <type>
internal_port: <internal_port>
external_port: <external_port>
networks: {}
volumes: {}
If this field is present, the deployment field MUST NOT be present.
deployment¶
| Required | false |
|---|---|
| Type | Object, null |
| Default | null |
Defines in detail all services that are used by the challenge. At the top level, the object consists of the following fields:
type¶
Required true Type string Currently, only the
dockertype is supported, so this MUST be the value. In the future more backends may be supported, like LXC or some jails.
networks¶
Required false Type Object Default {} Defines networks between containers, for multiple containers. These behave the same way as regular docker networks. A container will be able to reach another container by its container name if they have a network in common.
Each key in the networks object is a network name. Its value is an array of strings of container names in this network. For example, the following will put the
fooandbarcontainers on the same network:networks: test-network: - foo - bar
volumes¶
Required false Type Object Default {} Defines persistent volumes for one or multiple containers. These behave the same way as regular docker volumes. A volume can be mounted into a container at a mountpoint, and the data in it will persist between container recreations. If the volume is mounted in two containers at the same time, it behaves like a shared folder.
Each key in the volumes object is a volume name. Its value is an array of objects representing a mountpoint. Each mountpoint object has exactly one key, being the container name, and its value is where to mount the volume inside the container. For example, the following will mount the same volume at
/shared_volumein both thefooandbarcontainers:volumes: test-volume: - foo: /shared_volume - bar: /shared_volume
containers¶
Required true Type Object This is the last field of the deployment object. Defines all docker containers for this challenge. Each key in the containers object is a container name. Its value is a container object. These objects contain the following fields:
image¶
Required true Type String Defines the docker image for this container. This can be defined in one of three ways:
- A path to a directory containing a
Dockerfile. In this case, the image will be built from said dockerfile. Readers MUST check if the directory exists, and if not, move on to the other two options.- A path to a file containing an exported docker image. This file is usually obtained using the
docker savecommand and results in a tarball. In this case, the exported image will be imported and used. Readers MUST check if the file exists, and if not, assume the string is the last option.- A docker image tag. This can be a from an image locally on the system, publically available on dockerhub, from a private container repository etc. In this case, the image will be pulled if required and used.
services¶
Required false Type Array of objects Default [] Defines the services exposed by this challenge. Each service is an object in the array. The Object has the following three fields:
type¶
Required true Type String The service type for this service. MUST be either
website,tcp, or one defined in custom_service_types. See custom_service_types for info on what a service type is.
internal_port¶
Required true Type Integer The port inside the container that is exposed. This is the port your service binds to when running in the container.
external_port¶
Required false Type String Default See below The port on the host machine that the service is exposed on. If ommited, The deployment script will pick some available port. This SHOULD NOT be set unless the service requires being exposed on a specific port because this can cause issues with port collisions if the service is run on a host that also runs multiple other services.
extra_exposed_ports¶
Required false Type Array of objects Default [] Defines other ports that need to be exposed from within the container. These can be thought of as “hidden services”. They are formatted the same way as services, however they do not have a
typeas they will never be shown to users or solve scripts, andexternal_portis mandatory because of this.
Here is an example of a fully utilized deployment configuration:
deployment:
type: docker
containers:
web:
image: ./container
services:
- type: website
internal_port: 80
external_port: 80
extra_exposed_ports:
- internal_port: 1337
external_port: 1337
db:
image: local_db_image:latest
networks:
network:
- web
- db
volumes:
volume:
- web: /shared_volume
- db: /shared_volume
While it is supported, it is highly RECOMMENDED that challenges are created without volumes, networks, or multiple containers and services, as these features are not expected to be widely supported and are only required in very few situations. The service field SHOULD be used instead unless absolutely necessary.
If this field is present, the service field MUST NOT be present.
solution_image¶
| Required | false |
|---|---|
| Type | String, null |
| Default | null |
A solution script that can be run to validate the challenge is functioning and solvable. This is meant mostly to test challenges with services, and could be run periodically during a CTF to validate that a challenge has not gone offline or broke in other ways. The solution is housed in a docker container so it can be run anywhere.
The string defines the docker image for this solution. This can be defined in the same ways as the image in a service container.
The solution container usually needs to know on which host and port a service runs on. This information is passed as a string in a command line argument when running a docker container. The string MUST be formatted using the user_display of service types, meaning that for example, for tcp services, a string like nc 192.0.2.69 1337 will be given.
If a challenge has multiple services, they MUST be passed as separate command line arguments in the following order: 1. All predefined_services, in the order they are defined 2. For all containers in the order they are defined: all services, in the order they are defined
When creating the container, be sure to use ENTRYPOINT in exec form as otherwise the command line arguments will not be passed to the entrypoint in the container. Using CMD instead will not work.
The solution container MUST be run with an environment variable FLAG, containing the first text-type flags entry enclosed in the flag format (a valid flag). If no such entry exists, the environment variable MUST be set to an empty string.
If the challenge is functioning as expected, the solution container MUST output nothing more than a valid flag and optionally a trailing newline. Scripts that run this solution container SHOULD strip the resulting flag from whitespace on both ends before validating, in order to prevent rouge whitespace from invalidating the flag. Any output that is not a valid flag should be treated as if the service is malfunctioning.
unlocked_by¶
| Required | false |
|---|---|
| Type | String, Array of strings |
| Default | [] |
If a challenge should only be accessible to players after a certain other challenge is solved, this should be defined here. If the type is string, this is simplified syntax. It SHOULD be interpreted as a single element array of this one string. Each entry in the array can be either the exact case sensitive challenge title of another challenge, or a different challenges challenge_id. Referencing a challenge by its challenge_id has the added benefit of the link not breaking if the challenge is renamed.
Specifying multiple requirement challenges is NOT RECOMMENDED, as support in CTF platforms is uncommon. If you do specify multiple challenges, see all_unlocked_by_required for exact behaviour.
all_unlocked_by_required¶
| Required | false |
|---|---|
| Type | Boolean |
| Default | false |
If unlocked_by contains multiple challenges, defines if one or all need to be solved for this challenge to unlock. If true, all challenges in the array MUST be solved for this challenge to be accessible. If false, any one of the challenges in the array MUST be solved for this challenge to be accessible.
release_delay¶
| Required | false |
|---|---|
| Type | Number |
| Default | 0 |
The amount of seconds after the CTF start when the challenge should be automatically released. If 0, the challenge is released when the CTF starts.
human_metadata¶
| Required | false |
|---|---|
| Type | Object |
| Default | {} |
Contains metadata that is designed to be read by humans, and not parsed by scripts. This can be useful for some data that you want to display in user interfaces.
This field is composed of the following two sub-fields:
challenge_version¶
Required false Type string, null Default null
Defines the version of the challenge. SHOULD be shown on user interfaces for deployment backends so admins can easily see which version of the challenge is deployed, if they specified a version. The format of the string is undefined and can be decided by the challenge author.
event_name¶
Required false Type string, null Default null
Defines the name of the event the challenge is for, for example exampleCTF 2020. For archival purposes.
challenge_id¶
| Required | false |
|---|---|
| Type | string, null |
| Default | null |
A unique identifier for this challenge. MUST be unique not only among the pool of challenges for the CTF this challenge belongs to, but among all challenges. It is therefore RECOMMENDED that this is set to a generated UUID.
This id can be used in unlocked_by instead of the challenge title. The advantage of this is that the link will not break if the challenge is renamed. This can also be used by readers to recognize if it is reading a challenge it already knows about, even if the title has changed.
custom¶
| Required | false |
|---|---|
| Type | Object |
| Default | {} |
An object with an undefined structure. Any custom data that is not supported by the OCS can be put here. This is useful if you have tooling that provides functionality not supported by the OCS itself, as you will be able to specify configuration values in this object in any format you like. For example, if you have implemented a feature in your CTF platform that plays an audio file when a player solves a specific challenge, you could specify which audio file to play in a custom solve_audio configuration field in this object.
spec¶
| Required | true |
|---|---|
| Type | string |
The version of the OCS the challenge is written in. The current version is 0.0.1, so this MUST be the value if the challenge follows the version described in this document.