DevOps is an essential aspect of any software engineering project. On this note, CTF challenge design is no different. Being able to continuously push updates in a CTF challenge, and at the same time deploy it temporarily for testing, not only allows for better quality assurance practices, but it also gives the author the satisfaction (and bragging rights) when sending it over to fellow hackers to try it out!
In this context, this post provides a tutorial on how to continuously deploy your challenge by (ab)using the Github Workflows environment. Github’s Workflows feature allows developers to define and run workflow pipelines in a controlled environment using Github Actions as building blocks. Essentially, GitHub provides an ephemeral VM free-of-charge to run your workflows on. Interestingly, Github (or should I say… Microsoft) has been generous enough provide a workflow invocation timeout of 6 hours! Now the majority of the more common workflows that are used for packaging, deployment, testing do not exceed running times of an hour. But what if …. we make great use of this workflow time to host our challenge for a couple of hours? However, the problem is, that the workflow environment does not allow any ingress traffic, so how can people access the challenge, even if we host it there? Well…. there might be is a solution for that… keep reading.
First things first, you got to create a challenge right? Hopefully, you have already been through the hassle of challenge design and implementation so you should already have a challenge ready to be deployed! But if not, there is nothing to worry about! You can follow along with using a dummy pwn challenge I created for testing purposes. This is a containerized pwn challenge which simply prints back “Hello friend…” on any successful TCP connection.
In any case, I would greatly encourage you to always containerize your challenges with Docker so that you can always have full control over the environment that your code runs in. Details on how to achieve that, are out of the scope of this post, therefore, I trust that you are comfortable doing that. Feel free to tweak the code of the challenge to embed your challenge in the dockerized environment.
TCP Tunneling with Ngrok
Remember the problem stated above? In order to facilitate external access to our workflow environment, we can use an extremely handy tool called ngrok! With Ngrok we can bypass Firewall/NAT rules and expose localhost to the internet as depicted in the illustration below:
If this sounds interesting, I would encourage you to explore further details by visiting the origin post of the above image given in the caption.
As Ngrok allows for TCP tunnelling only to signed up members for free (with some limitations of course) go ahead and sign up for a Ngrok account here https://ngrok.com/. After signing in, you should be able to find an authorization token in your Ngrok dashboard.
Note that down because it will be used in the next steps.
Now that we have a working containerized challenge, a Github workflow needs to be defined by creating a new YAML file in the
.github/workflows/ directory. Create a new file called
.github/workflows/ and paste the following workflow definition:
In the above workflow definition, we define 2 jobs to run in sequence whenever a new push is performed in the repository. The
cancel job is an optional step to ensure that any previous workflows that are still running are cancelled in order to avoid having more than 1 workflows running at the same time. More importantly, the
deploy job is where all the magic happens:
- The repository is checked out
- The challenge is deployed by running
docker-compose up -d. Note that any other alternative command to deploy your challenge can be used here, but I always try to have single command deployments as it makes the process a lot easier.
- A custom Github Action is run which runs ngrok and tunnels any TCP traffic from/to port 4000. This is a Github Action that I published recently to abstract away some of the implementation details but if you are interested you can see the full source code of the action here.
You may have noticed the parameters, provided in the ngrok-tunnelling-action so let’s spend some time to explain those:
timeout: Keep in mind that the tunnelling step of the
deploy job is blocking, that means that if not cancelled, the workflow will keep running until the timeout of 6 hours is hit. This parameter allows you to specify a shorter timeout when the full amount of 6 hours is not needed. This accepts numerous time units such as seconds or hours i.e.
port: This specified the port that the traffic will be tunnelled to/from. This should be the port that your challenge exposes through the docker container.
ngrok_authtoken: This is simply your Ngrok authorization token acquired from the previous steps. Of course, since the token should not be visible in plain text, make sure it is defined in your repository secrets by navigating to your repository Settings -> Secrets -> New repository secret
We are almost done! Now, every time a new push arrives in the repository this workflow should be triggered! If everything went as planned, you should be able to navigate to your Ngrok dashboard and find the URL of your tunnel which can be used to access your challenge!
Let’s go ahead and test this out!
Awesome! Works like charm! You can find the full code for this post in this repository:
A test pwn challenge deployed in Github Workflows environment GitHub is home to over 50 million developers working…
There is definitely a lot of room for improvements so feel free to tweak/extend anything you like. Remember that the workflow definition can be amended so that is triggered only when a new release is created if that works better for your use case.
Any feedback is always much appreciated!