CTF challenges: Dockerizing and Repository structure
In this article we’ll be exploring how we can dockerize common CTF challenges and and nice way to structure the repo containing all challenges.
Structuring the challenges repo
We had a team of CTFers, with different levels of experience working on questions with varying levels of difficulty and complexity. It was imperative for them to collaborate properly, testing out each other questions, giving their feedback, etc. Apart from dockerizing the challenges as explained above, we also had to consider CI/CD integration to deploy the dockerized challenges safely, quickly and in a consistent manner. All this meant, the GitHub repository containing our challenges needed to be structured in a well-defined and self explanatory way.
As seen in the above screenshot, the repo has folders for each category of questions. Each of these folders contain another folder which contain the files for the challenge. Apart from that, we have a k8s
folder which contains all our .yaml
files which defined our cluster configuration. Since we used TravisCI for CI/CD, we have a .travis.yml
file and a deploy.sh
script which Travis was instructed to execute for each job that it ran. I won’t be going into the details of our CI/CD setup, but if you’re interested take a look at this article which follows a very similar approach to the one we took.
Each question folder has a README.md
which is a small write-up about the question so that everyone has a clear idea about the question, and helps the participants who weren’t able to get the flag for that question. It also has a Dockerfile
which bundles up the challenge into a container making deployments easy. Continue reading to see how ;)
Containerisation and Docker
CTF challenges are usually not as simple as serving a simple Flask application, for example. You need to create owners and groups, edit permissions, etc. Furthermore, you need to make sure that it’s not vulnerable, or rather the only vulnerability it has, is the one the challenge necessitates it to have. You also need to make sure that all CTFers are playing the same exact challenge, so it’s important to isolate resources, and package it in a self sufficient environment.
Why do we need to worry about stuff like containers while hosting CTF challenges? To understand this, let’s try to imagine a scenario. You setup a challenge which involves RCE, and host it on a server. What happens if a CTFer succeeds in executing some malicious code on your server? That’s something we’d definitely like to avoid :)
This is the kind of problem that containers are the perfect solution to. Most container technology follow the secure by default ideology, i.e. they are isolated from the main system they’re running on. From Docker’s website:
A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another.
This means that we don’t need to worry about libraries, packages, system dependencies for each challenge. Containerising the challenge guarantees that the challenge will run on every machine.
We mainly had two kind of challenges that we needed to dockerize:
- Challenges reachable via TCP only connections using tools like
nc
orssh
- Challenges served as websites, i.e. reachable via HTTP(s).
Dockerizing TCP only challenges
For IEEECTF 2020, we had a question which involved two chroot jails. chroot is used to emulate a directory in the filesystem as the root of the filesystem, which enables us to make jails in an easy manner. One jail needed to be accessed via nc
and the other one was supposed to be accessed via ssh
.
We used a tool called nsjail for the first jail. It’s a process isolation tool built for Linux by the good devs over at Google. While it’s extremely powerful and can satisfy plenty of use cases, we were only interested in it’s network services isolation ability. It would expose a port which would be listening for any TCP connections, allowing for Remote Code Execution (RCE).
An alternative to nsjail is xinetd. It lets you define multi-threaded services which require forking a new server process for each new connection request. The new server then handles that connection. For such services, xinetd keeps listening for new requests so that it can spawn new servers.
The first step is to prepare the chroot jail itself. It’s a fairly simple albeit slightly time consuming process, although that depends on how extensive your jail is.
- Create a directory for your jail, and further subdirectories for dependencies.
mkdir jailed
cd jailed
mkdir bin/ lib64/ lib/
- Use
ldd
to list all dependencies of your desired binary. Copy the binary and the corresponding deps into the jail. We do this for thebash
executable below.
ldd /bin/bash
cp /lib/x86_64-linux-gnu/libtinfo.so.5 lib/
cp /lib/x86_64-linux-gnu/libdl.so.2 lib/
cp /lib/x86_64-linux-gnu/libc.so.6 lib/
cp /lib64/ld-linux-x86-64.so.2 lib64/
cp /bin/bash bin/
- You can repeat the above step for all the binaries (like
ls
,cat
, etc.) you want present in your jail.
Now that our jail is ready, we can go ahead with dockerizing it and then access it by exposing it via a nsjail server.
We can now, build an image, run the container and play around in the jail.
docker build -t jail:latest .
docker run -p 9002:9002 --privileged -it jail:latest
Now that we’ve our first jail up and running properly, lets move to the next one. Since this jail requires to be accessed via ssh
instead of a tool like nc
, we don’t need nsjail.
Assuming that you’ve prepared this jail as instructed above, go ahead and type out a Dockerfile like below
docker build -t jailed:latest
docker run -P jailed:latest
Run docker ps
, and grab the exposed port of the container. Now it’s time to ssh into the jail.
Dockerizing HTTP challenges
Dockerizing these are mostly straightforward, but sometimes you need to run your web server under some non-ordinary conditions. For example, we had a question built using node.js which involved the player entering commands, which would be executed in a V8 VM. We had to make sure that the player couldn’t run commands that could potentially affect the filesystem and harm the container running the challenge. We handled that by adding a user, a user group and using chmod
to edit permissions
Some web challenges sometimes require two running servers simultaneously. We had a question based on Server Side Request Forgery (SSRF), which required a node.js server and a simple server capable of serving HTML (for which we used the inbuilt http module in Python3) . To achieve this, we can write a simple bash script, like given below.
Now we can just instruct Docker to run this script whenever we start up a container.
Conclusion
We successfully dockerized multiple CTF questions each with their own dependencies and use cases. Now that we’ve packaged each question into one unit, we can move forward to actually hosting these for people to play. If you’re interested in that, do read the next article in the series, where we walk through deploying these dockerized challenges on a Kuberenetes cluster!
Thank you for reading this article! If you wish to go through the questions of our CTF or any config files, head over to the GitHub repo and feel free to leave a ⭐️!
If you enjoyed reading this article, do checkout the rest of the articles in our CTF series: