Written: September 22, 2021
For BSides København CTF 2021, I was a part of the team developing a couple of defensive CTF challenges. In this article, I will outline the concept and our experience from the CTF.
The defensive CTF challenges at BSides København 2021.
My BSides København 2021: Talk about defensive CTF
Let’s start with the general idea of a defensive CTF challenge: The user is given access to a machine running a vulnerable service, their objective is to find the vulnerable code and/or configuration, and then to solve this vulnerability. Once the user feels like they have solved the vulnerability, then another machine will run hard-coded attacks and validations to test if the solution works. It’s important not only to check if the service is vulnerable, but instead also to check if the service is still working as expected. Otherwise, then the user would just be able to remove the functionality to obtain the flag.
Summarized the major steps in solving a defensive CTF challenge would, therefore, be:
Our approach to structuring a defensive challenge is to divide each challenge into two machines: One machine running a vulnerable service for whom the user will be granted root access, and another which will validate if the vulnerability has been solved and the expected functionality is kept intact. The vulnerable machine will have to be unique to each CTF team, but the validation machine does not need to be unique depending on how the infrastructure will be set up.
The two core machines making up a defensive CTF challenge.
Both machines are running as docker containers, making quick deployment of many instances easy to manage. Furthermore, this allows us to ensure that we have a similar environment regardless of how the competition will be hosted, which in our case would be running on the open-source CTF platform developed by Aalborg University (AAU) called HAAUKINS.
Read more about the platform who hosted the CTF: Haaukins
To be able to restart the running service without stopping the container, then we currently are using a “hack” of keeping the service running with “tail -f /dev/null”. This allows for the user to be able to restart the vulnerable service without causing the docker container to stop.
We would highly appreciate if anyone wish to help develop this idea further, so that we together can create fun learning experiences for blue teamers and developers. Regardless of if this means a future collaboration, or someone just getting inspired by our idea/approach. Therefore, we have made a template/proof-of-concept available on Github. However, please note that it is still "rough code" which needs further refinement in order to be more stable.
Our proof-of-concept/template code is available on: Github
An example of a challenge; Securing a Flask service that is vulnerable to server-side template injection (SSTI). In this challenge, the user will gain access to a machine running a vulnerable service displaying “x was not found” on any given URL path. The task is to somehow secure this application, without limiting the functionality. For example, then a rough solution to this problem could be to write a simple scrubber that removes all special characters, as displayed in the code example below.
When the solution has been written, and the service restarted, then the user would go to the website of the separate validator-machine associated with the challenge and tell it to run a validation against their solution. If the results from the attack attempts and checks for expected functionality are deemed to be sufficient, then the validation machine will grant the user the challenge flag.
Example of vulnerable code, made secure by the highlighted "scrubber code".
A nice write-up was written by one of the winning teams, Ponyblod, which goes through how they solved two of the defensive challenges. A “fun” thing to note was that the challenge “SuperCalcuRace” was easier to solve than I imagined while creating the race condition challenge, as I forgot about the fact that Python cannot be fully multithreaded due to its Global Interpreter Lock (Python GIL), which proves the importance on getting more beta-testing done for future challenges. Regardless, I’d like to thank the team for their nice write-up.
If you wish to get a participants perspective, then read: Ponyblod CTF write-up
Other feedback included that it might have been a good idea to be more explicit on which type of vulnerability should be solved. Giving the participants more clear hints could be solved by feedback from other participants about utilizing the ability to capture the attack and validation traffic for analysis. This could be considered as something to recommend future participants to utilize, as it’s tough to mitigate when the user has access to the application receiving the traffic. It “just” puts the challenge into the following scenario; You have to fix a vulnerability after the service already has been exploited, with knowledge of the network capture of the attack, which still covers the key learning outcome of finding vulnerable code and making it more secure.
If you have any questions about the concept or would like to share ideas, then feel free to reach out! Again, you are more than welcome to “steal” our idea and PoC challenge setup for your own CTF or training platform… we just want to see the space of fun blue team/developer security training being expanded.
Another link for our proof-of-concept/template code on: Github