CTF Development Template
Description
This is a framework developed by Selfmade Ninja Labs to make CTF development process easy. The template provides a comprehensive structure for creating, managing, and deploying Capture The Flag (CTF) challenges with standardized configurations, automated deployment pipelines, and integrated scoring systems.
Overview
This template streamlines the CTF challenge creation workflow by providing:
- Standardized project structure
- Automated Docker containerization
- CI/CD pipeline integration
- Built-in scoring and achievement systems
- Challenge management utilities
- Resource allocation controls
Documentation sections below will be expanded as we go through each file and component...
Project Structure
ctf-template/
├── config.json # Main lab configuration
├── challenges.json # Challenge definitions and metadata
├── achievements.json # Achievement and scoring rules
├── .gitlab-ci.yml # CI/CD pipeline configuration
├── briefs/ # Challenge brief descriptions
├── hints/ # Hint files organized by challenge
├── solutions/ # Solution writeups and answers
├── data/ # Challenge assets and resources
├── plans/ # Lab deployment plans
├── mission_assets/ # Visual assets for challenges
│ └── projectname/ # Project-specific assets (avatars, covers)
└── README.md # This documentation file
Getting Started
(This section will be expanded with detailed setup instructions)
Configuration Files
config.json - Main Lab Configuration
The config.json file contains the core configuration for your CTF lab. This file defines how your challenge environment will be deployed, managed, and accessed.
Mandatory Fields to Update
| Field | Description | Example |
|---|---|---|
lab_name |
Must be same as git project name, lowercase with hyphens | "web-exploitation-lab" |
lab_id |
Same as lab_name | "web-exploitation-lab" |
hostname |
Format: {git-project-id}.selfmade.ninja |
"web-exploit.selfmade.ninja" |
Basic Information Fields
| Field | Description | Example |
|---|---|---|
author |
Challenge author name | "John Doe" |
author_email |
Author's email address | "john@example.com" |
story_name |
Story/theme name for the challenge | "Secret Mission" |
story_description |
Brief story description | "Infiltrate the enemy server" |
story_id |
Unique story identifier | "secret-mission-001" |
story_index |
Story sequence number | "1" |
description |
Lab description | "Web exploitation challenges" |
type |
Challenge type/category | "web" |
Visual Assets
| Field | Description |
|---|---|
avatar |
Cover image for the main challenge display |
avatar_small |
Avatar image (smaller thumbnail) |
Additional Optional Fields
| Field | Description | Example |
|---|---|---|
event |
Event name (must be pre-created) | "yukthi-finals-2024" |
mode |
Challenge mode | "team" |
Environment Configuration
| Field | Description | Options/Example |
|---|---|---|
version |
Lab version | 0.1 |
stability |
Lab stability level | "beta" (public) or "alpha" (admin only) |
shell |
Default shell | "/bin/bash" |
ports |
Open ports for Docker image | "80" (default if unsure) |
cpu |
CPU allocation | 1 |
mem |
Memory allocation | "256m" |
interface |
Network interface | "eth0" |
Time Management System
| Field | Description | Example |
|---|---|---|
expireable |
Enable time-limited challenge | true |
expiry |
Challenge duration | "50 mins" |
renewable |
Allow time renewal | true |
renewal_max |
Maximum renewals allowed | 20 |
renewal_period |
Time added per renewal | "2 mins" |
renewal_jolt |
Jolt points required for renewal | 2 |
System Commands
The cmds section defines system scripts for lab lifecycle management:
"cmds": {
"linkuser": "/var/labsdata/scripts/linkuser.sh",
"entry": "/var/labsdata/scripts/entry.sh",
"spawn": "/var/labsdata/scripts/spawn.sh",
"unregister": "/var/labsdata/scripts/unregister.sh"
}
Connection Information
The connection_info array defines how users access the lab environment:
- Device IP: Dynamic IP address placeholder
- Device Port: Dynamic port placeholder
- VS Code Forward: For development environments
- Local Forward: Using socat for local port forwarding
Example Configuration
{
"author": "Jane Smith",
"author_email": "jane@selfmade.ninja",
"lab_name": "web-security-basics",
"lab_id": "web-security-basics",
"hostname": "web-security-basics.selfmade.ninja",
"story_name": "Corporate Espionage",
"story_description": "Infiltrate the corporate network",
"version": 0.1,
"stability": "beta",
"ports": "80",
"expireable": true,
"expiry": "45 mins",
"renewable": true,
"renewal_max": 15,
"renewal_period": "3 mins",
"renewal_jolt": 3,
"event": "yukthi-finals-2024",
"mode": "team"
}
challenges.json - Challenge Definitions
The challenges.json file contains configuration for each individual challenge in your CTF lab. This file defines all the challenges, their properties, scoring, and relationships.
Structure
The file contains an array of challenge objects, where each challenge has the following properties:
Basic Challenge Information
| Field | Description | Example |
|---|---|---|
id |
Unique challenge identifier (integer) | 1 |
name |
Challenge name/title | "Friend's Hidden Identity" |
description |
Short description about the challenge | "Decode the accomplice's real identity" |
category |
Array of challenge categories | ["web", "osint"] |
Challenge Content
| Field | Description | Example |
|---|---|---|
brief |
Reference to brief file in briefs/ directory |
"0.md" |
solution |
Reference to solution file in solutions/ directory |
"0.md" |
Visual Assets
| Field | Description | Example |
|---|---|---|
avatar |
Challenge avatar image path | "projectname/2nd-avatar.png" |
cover |
Challenge cover image path | "projectname/2nd-back.jpg" |
Note: Images are stored in mission_assets/ directory
Scoring Configuration
| Field | Description | Example |
|---|---|---|
difficulty_percent |
Difficulty percentage (0-100)<br/>• Below 20: Easy<br/>• 20-50: Medium<br/>• Above 50: Hard | 75 |
value |
Base point value for the challenge | 200 |
bonus |
Additional bonus points | 0 |
value_multiplier |
Multiplier for final score calculation | 1.5 |
Challenge State
| Field | Description | Example |
|---|---|---|
locked |
Whether challenge is initially locked | false |
requires |
Array of challenge IDs that must be completed first | [1] |
Hints System
The hints array defines available hints for the challenge:
"hints": [{
"cost": 25,
"hint": "0.md"
}, {
"cost": 40,
"hint": "1.md"
}]
| Field | Description |
|---|---|
cost |
Jolt points required to unlock hint |
hint |
Reference to hint file in hints/{challenge_id}/ directory |
Example Challenge Configuration
{
"id": 2,
"name": "Friend's Hidden Identity",
"description": "Decode the accomplice's real identity and contact details.",
"category": ["web", "osint"],
"brief": "1.md",
"hints": [{
"cost": 25,
"hint": "0.md"
}, {
"cost": 40,
"hint": "1.md"
}],
"avatar": "projectname/2nd-avatar.png",
"cover": "projectname/2nd-back.jpg",
"difficulty_percent": 75,
"value": 200,
"bonus": 0,
"locked": true,
"value_multiplier": 1.5,
"solution": "0.md",
"requires": [1]
}
Challenge Dependencies
Use the requires field to create challenge sequences:
- Empty array
[]= No dependencies (can be attempted immediately) [1]= Must complete challenge ID 1 first[1, 2]= Must complete both challenges 1 and 2 first
achievements.json - Achievement and Scoring System
The achievements.json file defines special achievements that players can unlock based on their performance and completion criteria.
Achievement Generation Tool
For automated achievement generation, use the Labs Challenge Automation repository:
python achievements.py <num_challenges> <challenge_folder> --names <challenge_names>... --difficulty <difficulty>
Parameters:
<num_challenges>: Number of challenges in your lab<challenge_folder>: Path to your challenge directory--names: List of challenge names--difficulty: Overall difficulty level
Achievement Structure
Each achievement object contains the following properties:
| Field | Description | Example |
|---|---|---|
id |
Unique achievement identifier | "complete_challenge_all_in_20min" |
name |
Achievement name/title | "Complete all challenges within 20 minutes" |
avatar |
Achievement badge image path | "cmd/badge-8.png" |
Rewards System
Achievements can grant various rewards:
"rewards": {
"jolt": 25,
"zeal": 70
}
| Reward Type | Description |
|---|---|
jolt |
Jolt points awarded |
zeal |
Zeal points awarded |
Requirements Configuration
Define achievement unlock criteria:
"requirements": {
"challenges": "all",
"complete_within": "20 mins"
}
| Requirement | Description | Options |
|---|---|---|
challenges |
Which challenges must be completed | "all", specific challenge IDs |
complete_within |
Time limit for completion | "20 mins", "1 hour", etc. |
Example Achievement
{
"id": "complete_challenge_all_in_20min",
"name": "Complete all challenges within 20 minutes",
"avatar": "cmd/badge-8.png",
"rewards": {
"jolt": 25,
"zeal": 70
},
"requirements": {
"challenges": "all",
"complete_within": "20 mins"
}
}
Challenge Development
Dockerfile Requirements
When developing your CTF challenge, you must create a Dockerfile to containerize your environment.
Mandatory Lines (DO NOT MODIFY)
The following lines are required and must be included in your Dockerfile exactly as shown:
COPY ./data /var/labsdata
RUN chmod 700 /var/labsdata
RUN chmod +x /var/labsdata/scripts/*.sh
CMD "/var/labsdata/scripts/entry.sh"
⚠️ Warning: These lines are essential for the lab management system to function properly. Modifying or removing them will cause deployment failures.
Dockerfile Structure
Your Dockerfile should follow this general structure:
# Base image (choose appropriate base)
FROM ubuntu:20.04
# Install your application dependencies
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
# Copy your application files
COPY . /app
WORKDIR /app
# Configure your application
RUN your-setup-commands
# Expose required ports (must match config.json)
EXPOSE 80
# MANDATORY LINES - DO NOT MODIFY
COPY ./data /var/labsdata
RUN chmod 700 /var/labsdata
RUN chmod +x /var/labsdata/scripts/*.sh
CMD "/var/labsdata/scripts/entry.sh"
Important Notes
- Port Configuration: Ensure the
EXPOSEdirective matches theportsfield in yourconfig.json - Base Image: Choose an appropriate base image for your challenge type
- Dependencies: Install all required packages and dependencies
- File Permissions: Set appropriate permissions for your application files
- Working Directory: Set up your application's working directory before the mandatory lines
Example Dockerfile Templates
Web Application Example:
FROM nginx:alpine
COPY ./web-content /usr/share/nginx/html
EXPOSE 80
# MANDATORY LINES
COPY ./data /var/labsdata
RUN chmod 700 /var/labsdata
RUN chmod +x /var/labsdata/scripts/*.sh
CMD "/var/labsdata/scripts/entry.sh"
Python Application Example:
FROM python:3.9-slim
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ./app /app
WORKDIR /app
EXPOSE 8080
# MANDATORY LINES
COPY ./data /var/labsdata
RUN chmod 700 /var/labsdata
RUN chmod +x /var/labsdata/scripts/*.sh
CMD "/var/labsdata/scripts/entry.sh"
data/scripts/entry.sh - Container Startup Script
The entry.sh file is responsible for the startup of the Docker container. This script is executed when the container starts and can be used to initialize services.
Default Content (Required)
The file must contain these lines at minimum:
#!/bin/sh
tail -f /dev/null
⚠️ Important: The tail -f /dev/null command is mandatory and must be the last line to keep the container running.
Usage Guidelines
You can modify this file to start initial services, but follow these rules:
✅ Allowed:
- Start SSH services
- Initialize databases
- Set up system configurations
- Start background services
❌ Prohibited:
- DO NOT start web servers (Apache, Nginx, etc.)
- DO NOT start application servers that expose the challenge directly
Why no web servers? Starting web servers immediately may allow CTF players to access the website before the challenge is properly deployed and configured.
Example Configurations
Basic Setup (Default):
#!/bin/sh
tail -f /dev/null
With SSH Service:
#!/bin/sh
# Start SSH service
service ssh start
# Keep container running
tail -f /dev/null
With Database Setup:
#!/bin/sh
# Initialize database
mysql -u root -e "CREATE DATABASE ctf_db;"
# Start required services
service mysql start
# Keep container running
tail -f /dev/null
With System Configuration:
#!/bin/sh
# Set up users
useradd -m ctfuser
# Configure permissions
chmod 755 /app
# Keep container running
tail -f /dev/null
File Location
This file must be located at: data/scripts/entry.sh
Permissions
The file permissions are automatically set by the mandatory Dockerfile line:
RUN chmod +x /var/labsdata/scripts/*.sh
data/scripts/spawn.sh - Challenge-Specific Deployment Script
The spawn.sh script is the most powerful script in the CTF template. It executes when a user clicks "Start Mission" and allows challenge-specific configuration and dynamic flag generation.
How It Works
The script receives two parameters:
$1- Challenge ID (from challenges.json)$2- Dynamically generated flag for the user
Script Structure
challid=$1
flag=$2
case $challid in
"1")
<Your Spawn Scripts or commands goes here...>
echo "Challenge $challid spawned with $flag"
;;
"2")
<Your Spawn Scripts or commands goes here...>
echo "Challenge $challid spawned with $flag"
;;
esac
Key Features
🚀 Dynamic Flag Injection
- Each user gets a unique, randomly generated flag
- Flags are generated on-the-fly when mission starts
- Perfect for preventing flag sharing between users
🎯 Challenge-Specific Logic
- Different setup for each challenge ID
- Start services only for specific challenges
- Customize environment per challenge
⚡ Real-Time Deployment
- Executes when user clicks "Start Mission"
- Allows last-minute configuration
- Can modify files, start services, set permissions
Common Use Cases
1. Dynamic Flag Placement:
challid=$1
flag=$2
case $challid in
"1")
# Replace placeholder with actual flag
sed -i "s/FLAG_PLACEHOLDER/$flag/g" /var/www/html/config.php
echo "Web challenge spawned with flag: $flag"
;;
"2")
# Write flag to hidden file
echo "$flag" > /home/user/.secret_flag
chmod 600 /home/user/.secret_flag
echo "OSINT challenge spawned with flag: $flag"
;;
esac
2. Challenge-Specific Services:
challid=$1
flag=$2
case $challid in
"1")
# Only start web server for challenge 1
service apache2 start
echo "$flag" > /var/www/html/flag.txt
echo "Web server started for challenge 1"
;;
"2")
# Only start SSH for challenge 2
service ssh start
echo "$flag" > /root/flag.txt
echo "SSH service started for challenge 2"
;;
"3")
# Start database for challenge 3
service mysql start
mysql -u root -e "INSERT INTO flags (value) VALUES ('$flag');"
echo "Database challenge spawned"
;;
esac
3. File System Modifications:
challid=$1
flag=$2
case $challid in
"1")
# Create challenge-specific directory structure
mkdir -p /challenge1/hidden
echo "$flag" > /challenge1/hidden/secret.txt
# Set specific permissions
chmod 755 /challenge1
chmod 700 /challenge1/hidden
echo "File system challenge prepared"
;;
"2")
# Modify configuration files
sed -i "s/SECRET_KEY/$flag/g" /app/config.ini
# Start application
cd /app && python app.py &
echo "Application challenge started"
;;
esac
4. User Account Setup:
challid=$1
flag=$2
case $challid in
"1")
# Create user with flag as password
useradd -m ctfuser
echo "ctfuser:$flag" | chpasswd
service ssh start
echo "SSH brute force challenge ready"
;;
"2")
# Hide flag in user's history
echo "cat /root/secret.txt # $flag" >> /home/user/.bash_history
echo "Forensics challenge prepared"
;;
esac
Best Practices
✅ Do:
- Use the provided
$flagvariable for dynamic flags - Include echo statements for logging
- Handle different challenge IDs appropriately
- Start web servers here (not in entry.sh)
- Test each case thoroughly
❌ Don't:
- Hardcode flags in the script
- Forget to handle all challenge IDs from challenges.json
- Start conflicting services
- Ignore error handling
File Location
This file must be located at: data/scripts/spawn.sh
Integration with Other Components
- challenges.json: Challenge IDs must match case statements
- entry.sh: Keep entry.sh minimal, use spawn.sh for challenge logic
- Dockerfile: Permissions set automatically via mandatory lines
Other Script Files
The following script files are provided in the template and should NOT be modified:
| Script | Purpose | Action Required |
|---|---|---|
data/scripts/linkuser.sh |
User linking functionality | ❌ Do not modify |
data/scripts/unregister.sh |
User cleanup and unregistration | ❌ Do not modify |
These scripts are part of the core lab management system and are automatically configured by the framework.
data/ Directory - Challenge Files Storage
The data/ directory is your main workspace for all challenge-related files and assets.
Key Features
📁 Complete Container Copy
- The entire
data/folder is copied into the container via the mandatory Docker command:COPY ./data /var/labsdata
🎯 Store Everything Here
- Web application files
- Configuration files
- Source code
- Assets and resources
- Any files needed for your challenge
Directory Structure Examples
data/
├── scripts/ # System scripts (entry.sh, spawn.sh, etc.)
├── www/ # Web application files
│ ├── index.html
│ ├── css/
│ └── js/
├── configs/ # Configuration files
│ ├── nginx.conf
│ └── app.config
├── src/ # Source code
│ ├── vulnerable_app.py
│ └── database.sql
└── assets/ # Challenge assets
├── images/
└── documents/
Best Practices
✅ Organize by Purpose:
- Create subdirectories for different file types
- Keep scripts in
scripts/directory - Group related files together
✅ File Accessibility:
- All files in
data/become available in the container at/var/labsdata/ - Reference files using absolute paths:
/var/labsdata/www/index.html - Set appropriate permissions in your Dockerfile or spawn.sh
Example Usage in spawn.sh:
case $challid in
"1")
# Copy web files to Apache directory
cp -r /var/labsdata/www/* /var/www/html/
# Use configuration file
cp /var/labsdata/configs/nginx.conf /etc/nginx/nginx.conf
# Replace flag in source code
sed -i "s/FLAG_PLACEHOLDER/$flag/g" /var/labsdata/src/app.py
;;
esac
plans/ Directory - Configuration Override
The plans/ directory allows you to override settings from config.json for different deployment scenarios.
Default Behavior
By default, all challenges are free and accessible to all users.
Premium Challenge Configuration
To make your challenge premium (paid access), create configuration override files in the plans/ directory.
Example: plans/premium.json
{
"free": false
}
Override Options
You can override any config.json setting by creating corresponding files in plans/:
plans/free.json (Default)
{
"free": true
}
plans/free.json (Paid Access)
{
"free": false
}
plans/admin.json (Custom Settings)
{
"free": false,
"expiry": "2 hours",
"renewal_max": 5,
"cpu": 2,
"mem": "512m"
}
How It Works
- Base Configuration:
config.jsoncontains default settings - Plan Override: Files in
plans/override specific settings - Deployment: The system applies the appropriate plan during deployment
- Access Control:
"free": falserestricts access to premium users only
Use Cases
Free Challenge (Default):
- No plans/ files needed
- Or create
plans/free.jsonwith"free": true
Premium Challenge:
- Create
plans/premium.jsonwith"free": false - Challenge requires paid access
Custom Resource Allocation:
- Override CPU, memory, or time limits
- Create specialized deployment configurations
- Different settings for different user tiers
Best Practices
✅ Plan Organization:
- Use descriptive filenames (free.json, premium.json, enterprise.json)
- Keep override files minimal (only changed settings)
- Document which plan is intended for which use case
✅ Testing:
- Test both free and premium configurations
- Verify access controls work correctly
- Ensure resource allocations are appropriate
Development Commands
The following commands are used for building and testing your CTF challenges. These commands require labsctl to be installed, which is available by default on intern/employee server access.
Local Development Build
For local development and testing:
labsctl build -f <lab_id>:lab --no-cache
Usage:
- Replace
<lab_id>with your lab ID fromconfig.json - Use this command when developing locally
- The
--no-cacheflag ensures fresh builds - This builds the image from your local files
Example:
labsctl build -f web-security-basics:lab --no-cache
Install from Git Repository
For installing directly from your Git repository:
labsctl install -f <lab_id>:lab --no-cache
Usage:
- Use this after pushing your code to Git
- Installs and deploys the lab from the repository
- Automatically pulls latest changes from Git
- The lab will be available in the UI after successful installation
Example:
labsctl install -f web-security-basics:lab --no-cache
Deployment Logs
Monitor deployment progress and troubleshoot issues:
# View deployment logs
tail -f /var/log/DeployLog
# View specific deployment log
cat /var/log/DeployLog
The deployment logs contain detailed information about:
- Build process status
- Error messages and debugging info
- Container deployment progress
- Service startup confirmations
Development Workflow
- Develop Locally: Create your challenge files and configurations
- Local Build: Test with
labsctl buildcommand - Push to Git: Commit and push your changes
- Deploy: Use
labsctl installto deploy from Git - Monitor: Check
/var/log/DeployLogfor deployment status - Test: Access your lab through the UI
Prerequisites
- Server Access: Available for interns and employees
- labsctl: Pre-installed on provided servers
- Git Repository: Your challenge code must be in a Git repository
- Proper Configuration: All configuration files must be properly set up
.gitlab-ci.yml - CI/CD Pipeline
The .gitlab-ci.yml file contains the automated CI/CD pipeline configuration for your CTF lab.
⚠️ Important: DO NOT modify this file. It is pre-configured for the Selfmade Ninja Labs infrastructure and any modifications may break the automated deployment process.
Testing Your Challenge
Use the local build command to test your challenge before deployment:
labsctl build -f <lab_id>:lab --no-cache
This command will:
- Build your Docker container locally
- Validate your configuration files
- Test the challenge deployment process
- Show any errors or issues before actual deployment
Deployment
The deployment process is handled automatically through the labsctl commands and CI/CD pipeline integration.