Hosting CTF: A Comprehensive Guide
If you want to jump to the technical details, scroll to the bottom.
Backstory
I hosted and coordinated a Capture The Flag (CTF) competition for around 300 people in my workplace. This was a great learning experience for me. This article may help you decide to host one for your team yourself, and it's pretty easy, too!
Initial Planning
Budget considerations
Well, like how it always is the cheaper, the better, one thing that played a crucial role is that I work in an infrastructure team, so having access to VMs was a no-cost affair. But keeping this aside, a small 4vCPU 2GB RAM machine can handle the load pretty well and is free if you have a student pack for Azure/AWS/GCP.
Timeline development
The most time-consuming part of CTF is forming the questions. Since this was the first time many people were playing, we decided to use medium-level questions. We also included a few non-technical questions so players do not lose interest in the game and drop off.
It is a good idea to survey how many people are interested in playing the CTF. This will help you estimate the traffic on the actual day and the level of the participants, so you can frame the questions accordingly!
Having a team that will form the questions is immensely important, this is the backbone on which your CTF depends, always vet the questions, make sure it is of different categories and most importantly not easily solved by ChatGPT :P
Technical Architecture
We selected to host this on VMs in the OpenStack environment but this really shouldn’t matter much, once you provision the VM it's the same process.
The VM was hosted on the company’s direct network, which is accessible only via VPN, so that isolated the VM from actors outside the company.
Challenge Types
- Web exploitation
- Reverse engineering
- Cryptography
- Forensics / Packet Inspection
- Miscellaneous
Challenge Design Principles
- Realistic scenarios
- Educational Value
- Progressive difficulty
- Clear, unambiguous instructions
Challenge Verification
Always check your questions, create a hidden account on the platform and test your questions from start to end, there is most likely a team that is framing all the questions make sure you test each other’s questions out so that you get the feel from the participant’s side too!
Testing
I wrote a script to test the concurrent connections and average response times and monitored the VM stats using htop
As a failover scenario, I had one more VM in a different DC and network as a spare (I only did this because I had access to free VMs) to continue the contest if something bad happened!
I was also taking exports of the current state of the CTF so that I could easily import it to the new VM and continue it after making a DNS update.
Pre-Event Communication
We used a Webex bot to send communications for example, when new questions dropped, leaderboard updates, and fun stats to keep the enthusiasm going.
We tried to be as clear as possible with the rules in the questions themselves. If a frequent question popped up, we notified everyone to clear it out!
Participant Management
We did not want users to use their IDs and passwords and did not have enough time to understand how we could integrate our company's OAuth into this, so we used the CTFd APIs to create local accounts for the users and send them via the bot to everyone.
Spin up CTFd
I used a VM with Ubuntu OS, you can follow these steps or modify them according to the package manager your OS uses.
apt install python3-pip -y -q && apt install python3.10-venv -y && apt install python-is-python3 -y -q && apt install docker -y && apt install docker-compose -y
git clone https://github.com/CTFd/CTFd.git
cd CTFd/
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
docker-compose up
docker ps
to check if the containers are up and running!
You should now be able to access CTFd at http://localhost:8000
This will spin up the CTFd with wsgi WORKERS=1
To have the better performance it is better to have more than one worker, to set up more than one worker you also need to include SECRET_KEY
in the docker-compose.yml
So bring down your setup by docker-compose down
and then change the docker-compose.yml
file as below
Your docker-compose
file is going to look like this once you add that
services:
ctfd:
build: .
user: root
restart: always
ports:
- "8000:8000"
environment:
- SECRET_KEY=<SECRET_HERE>
- UPLOAD_FOLDER=/var/uploads
- DATABASE_URL=mysql+pymysql://ctfd:ctfd@db/ctfd
- REDIS_URL=redis://cache:6379
- WORKERS=3
- LOG_FOLDER=/var/log/CTFd
- ACCESS_LOG=/var/log/CTFd/access.log
- ERROR_LOG=/var/log/CTFd/error.log
- REVERSE_PROXY=true
volumes:
- .data/CTFd/logs:/var/log/CTFd
- .data/CTFd/uploads:/var/uploads
- .:/opt/CTFd:ro
depends_on:
- db
networks:
default:
internal:
To enable HTTPS, you need to generate a certificate and then mount it as a volume on the nginx
container
We generated the certificate and the private key and stored it on the VM as ./conf/nginx/fullchain.pem
and ./conf/nginx/privkey.pem
You can either use Certbot to generate a certificate or follow your organisation's guidelines to generate the certificate and private key.
We referred to this article to set up the certificates
The nginx
part of the docker-compose.yml
now looks like this
nginx:
image: nginx:stable
restart: always
volumes:
- ./conf/nginx/http.conf:/etc/nginx/nginx.conf
- ./conf/nginx/fullchain.pem:/certificates/fullchain.pem:ro
- ./conf/nginx/privkey.pem:/certificates/privkey.pem:ro
ports:
- 80:80
- 443:443
depends_on:
- ctfd
Script to load test your CTFd deployment
import requests
import time
from concurrent.futures import ThreadPoolExecutor
import logging
from datetime import datetime
def setup_logging():
"""Configure logging to track performance metrics"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(message)s',
filename=f'load_test_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
)
def single_request(url, session):
"""Make a single request and measure response time"""
try:
start_time = time.time()
response = session.get(url)
duration = time.time() - start_time
return {
'status_code': response.status_code,
'duration': duration,
}
except Exception as e:
logging.error(f"Request failed: {str(e)}")
return None
def load_test(url, num_requests=100, max_workers=10):
"""
Perform load testing on a website
Parameters:
url: The URL to test
num_requests: Total number of requests to make
max_workers: Maximum concurrent requests
"""
setup_logging()
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
with requests.Session() as session:
futures = [
executor.submit(single_request, url, session)
for _ in range(num_requests)
]
for future in futures:
result = future.result()
if result:
results.append(result)
logging.info(
f"Status: {result['status_code']}, "
f"Duration: {result['duration']:.2f}s"
)
# Analyze results
successful_requests = len([r for r in results if r['status_code'] == 200])
avg_duration = sum(r['duration'] for r in results) / len(results)
print(f"\nLoad Test Results:")
print(f"Total Requests: {len(results)}")
print(f"Successful Requests: {successful_requests}")
print(f"Average Response Time: {avg_duration:.2f}s")
test_url = "https://yourdomain/users" # Use your test environment
load_test(
url=test_url,
num_requests=3000,
max_workers=30
)
A snippet of the VM utilisation during the load test
If this guide helped you, please consider giving it a like.
Top comments (0)