Thanks for opening this document! Understanding how to write a challenge is the first step to writing one for CSC (or for TJCTF), so it's great that you're getting a start on that now!
rCDS is a challenge deployment tool created by redpwn. We use it to make challenge deployment as simple as a GitHub Actions job. This is integrated with rCTF, also created by redpwn, which is the actual website that players access in order to get CTF details as well as challenge details — in the case of our club CTF, it is ctf.tjcsec.club.
For challenge authors, it is mostly unnecessary to know how rCDS works internally; however, I describe it here in order for this not to seem like a "black box."
First, the GitHub Actions workflow is triggered. In our club CTF repository, this is triggered on every commit to the main branch, which includes merges from other branches. You can also manually trigger it in the "Workflows" tab on GitHub. This workflow runs rCDS.
rCDS checks every challenge to ensure that it is synced with the various "backends." That is, if any change has been made to a challenge or its associated files, it is updated appropriately. rCDS syncs all challenges in four stages:
{{ tags }}
are replaced with actual text), and relevant metadata (i.e. author, flag, description) is provided to rCTF to make the challenge available on the main CTF site.Docker is a technology that lets us easily replicate the environment that we want a piece of code to run in.
Note how the description of the Docker stage of rCDS is, by far, the longest. This is not a coincidence! Docker is pretty tricky to get a full grasp of, but, luckily, you don't really need a full grasp of it to write a challenge.
To get some lingo out of the way, a Docker image is a template for what environment your app should run in. A container is an actual running instance of that image. Think of an image as a blueprint and a container as a building.
The most important file that you need to work with Docker is the Dockerfile
. This should be in the outermost directory of any files that you may want to include in the Docker image.
A minimal Dockerfile that runs a Flask server is specified below with comments to explain what each line does. The necessary files to run that Flask server (e.g. app.py
, templates/
, static/
) should be in the same directory as the Dockerfile. The comments do not go into too much detail, so if you have questions about what a specific line or command does, check the Dockerfile reference or ask me!
To manually build the Docker image, in your terminal, run:
<tag>
can be replaced with any "friendly" name that you want to call the image. <directory>
is the directory of the Dockerfile
(relative to your current working directory).
For example, to build the Flask server above, located in the current directory, and tag it with my-app
, run:
To run that image, use:
The -p 5000:5000
exposes port 5000
to your host computer (i.e. your computer, outside of the Docker container). This means that you can access the server at http://localhost:5000
whereas, without the -p
, the server would not be accessible to you at all.
pwn.red/jail
If you have seen nc challenge.tjcsec.club XXXXX
before, the container has probably been using pwn.red/jail
under the hood. This is a Docker image that redpwn, yet again, has written for our convenience for running sandboxed nc
servers. Because of this, they can explain how to use it much better than I can. Their Competitor FAQ is extremely useful for understanding what exactly pwn.red/jail
does and how to run it. Additionally, their Challenge Author Guide has some very helpful tips for using pwn.red/jail
. They also provide some minimal examples of its use here.
A minimal Dockerfile
using pwn.red/jail
is shown below:
hello.py
is shown below. For it to run properly, it should be marked executable (with chmod +x hello.py
) and have a shebang, the funny comment on the first line that specifies the interpreter.
You can build the image like normal and run it with the command below:
Now, how does rCDS know exactly what it needs to do for each challenge? Well, despite all of its strengths, it is not all-knowing, so, when you write a challenge, you need to specify a challenge.yaml
in the root (or outermost) directory of said challenge.
This requires you to specify the details of your challenge in YAML format. YAML is fairly intuitive, so you probably don't need to read that article in its entirety. Instead, read this minimal challenge.yaml
file below:
Most of these fields should seem pretty intuitive!
value
specifies the point value. For TJCTF, do not specify a point value (by omitting value
). Without an explicitly-stated point value, a challenge defaults to being dynamically scored, wherein the number of points a challenge is worth is scaled from 100 to 500 based off of the number of solves it has.
Additionally, in this specification, file
is specified under flag
. You can also specify the flag as a normal string by writing: flag: flag{my_flag}
. However, you often have the flag used somewhere in your code. Instead of hardcoding it directly in your code, you can store the flag in a separate file and read from that file when needed. This flag.file
lets you specify a file path to your flag file, thereby letting you specify your flag in one place instead of multiple.
The provide
is a list of files. Files can be specified either by only their file path (relative to the challenge's root directory) or by a specification like the one above. This zips the files (or directories) specified in files
into a zip file and provides it to players. Additionally, it has an exclude
key which, predictably, excludes a file from being included in the zip file. The additional
key specifies extra "fake" files that do not actually exist in the repository but you want to include inside the zip file. You may want a specification like this when you provide entire servers to players.
Let's try a less minimal specification. For example, the challenge.yaml
of TJCTF 2022's Analects is specified below.
This challenge, unlike the previous one, has a remote server. The containers
section is where you specify any challenge servers that you want to be deployed. Each container is its own key under containers
; all the properties of that container are specified like so.
build
specifies the path of the directory that is built and deployed; in that directory, there must be a Dockerfile
.replicas
specifies how many different times the image is run and deployed; for club CTF, one deployment is almost always sufficient, but, for TJCTF, you may want to increase the number.ports
specifies the port(s) that you want to be accessible to other containers in this deployment. In this case, mysql:3306
is accessible in the app
deployment (and app:80
is accessible in the mysql
deployment, though it is not directly used).resources
limits the resources that a container can use. Use your best judgment when setting these limits (or ask me)!expose
ensures that a port of a specific deployment is publicly accessible (i.e. accessible to the player). These port(s) must also be specified in that deployment's ports
specification in order to be publicly accessible. The target
is the port that you want to mark accessible. Additionally, you must specify either http
(and provide an arbitrary subdomain for the deployment) or tcp
(and provide an arbitrary port number for the deployment). Websites should generally specify http
whereas nc
servers should specify tcp
. Note that all http
and tcp
keys should be unique for all challenges.
rCDS also provides shortcuts for specifying exposed ports in the description. If you only expose one HTTP port, using {{ link }}
will result in a friendly link being formatted nicely. Likewise, if only one TCP port is specified, {{ nc }}
results in a friendly nc site.com XXXXX
being rendered.
I did not exhaustively go over all the keys available. For example, you can deploy a pre-existing image using image
or mark a challenge invisible with visible: false
. You can find the full schema of challenge.yaml
at the rCDS documentation.
pwn.red/jail
pwn.red/jail
also requires certain security options to be specified in challenge.yaml
. These can mostly be copied and pasted for all servers that use pwn.red/jail
.
"Good" challenges are difficult to write! Here, I provide some general guidelines as to how to write challenges.
Writing interesting challenges is not our main priority for the club CTF. Club CTF is not the place to show off how good you are. Instead, we want challenges that inform our audience. Club challenges can be designed to challenge others, but, by writing challenges that no one can solve, we are driving away the people we are trying to teach. Try to not make people think outside the box; what we are trying to do is to help others understand what is inside the box, not to imagine outside of it.
For TJCTF, however, try new topics! While one of our goals is to inform, we also want to push people to want to further pursue computer security by providing interesting problems. There is not one set way to write challenges, but I will share some tips that have helped me.
I personally prefer thinking of a vulnerability and basing a challenge off of said vulnerability. However, others prefer writing something that works and altering it a little to create a vulnerability. Each method has its own pros and cons, so I would suggest trying to figure out what works best for you.
If you are stuck trying to think of challenge ideas, I have found that playing in CTFs has helped me to think of interesting ideas. It is oftentimes in the things that don't work for a specific challenge that you find a stroke of genius for your own challenge.
Enjoyable challenges tend to require more thought than implementation. The general rule is that if you would not enjoy the challenge, it is likely that other people would not enjoy the challenge.
For more verbose guidelines, feel free to look at https://bit.ly/ctf-design. Good luck!