Executive summary
King of the Forest was my final-year honours project at Abertay University. I designed and built a competitive cyber security range that combined the offensive focus of capture-the-flag exercises with the service ownership, incident pressure and defensive decision-making found in cyber defence competitions.
The range gave two teams equivalent Windows and Linux environments. Each team controlled its own firewall, Linux entry host, two Windows clients and an Active Directory domain controller. Participants were expected to investigate and secure their own systems while attempting to work through the opposing team’s exposed Linux host towards its internal domain. The design deliberately prevented direct access to the opposing Windows network, so the network position and route into the environment mattered as much as the individual weaknesses placed on each host.
I built the platform across eight reused university machines. Six formed a Proxmox VE cluster, one ran OPNsense as the physical router and one ran TrueNAS Core for shared storage. Proxmox software-defined networking and VXLAN allowed the isolated competition networks to span all six virtualisation nodes. I then created the mirrored team domains, introduced controlled Linux and Active Directory weaknesses, developed a Python and Flask scoring service, containerised it with Docker and Caddy, and wrote C++ scoring agents for Windows and Linux.
The environment and the selected security conditions behaved as intended. The scoring application, flag handling, TLS transport and agent authentication also worked in testing. The main limitation was the reused hardware: mechanical disks and the lack of graphics acceleration made the Windows guests too slow to run the full scoring path reliably on the main cluster. I therefore kept the final evaluation precise. The range, segmentation and training conditions were validated on the eight-machine platform, while the complete Windows and Linux agent-to-score workflow was validated in a separate SSD-backed test environment.
The project gave me practical experience across physical infrastructure, virtualisation, software-defined networking, routing, Active Directory, Linux administration, native services, backend development, containers and security testing. More importantly, it forced me to treat the environment as one connected system. The range only worked when the hardware, storage, network boundaries, operating systems, deliberately weak configurations, scoring logic and participant experience all worked together.
Research context
This project was completed for CMP403 / Honours Project Dissertation:
King of the Forest: A Realistic ‘King of the Hill’ Approach to Competitive Cyber Security Training Environments
The aim was to build a realistic competitive range that placed offensive and defensive work inside the same exercise and used visible feedback to keep participants engaged.
Research background
Capture-the-flag and cyber defence competitions are both effective forms of practical cyber security training, but they normally develop different parts of the job.
Capture-the-flag events are usually centred on offensive problem-solving. Participants identify weaknesses, exploit systems and recover flags that represent completed challenges. Cyber defence competitions place participants in control of a network and ask them to keep services operating while responding to attacks, faults and operational requests.
I wanted to place both sides inside the same exercise. Rather than giving one group a fixed attacking role and another group a fixed defending role, each team would own an environment and have both responsibilities at the same time. They had to understand the weaknesses in the opposing environment while also recognising that the same weaknesses existed in their own.
The project was therefore not simply a collection of vulnerable virtual machines. It had to support:
- equivalent environments for fair competition;
- a controlled route from an exposed system into a private domain;
- Linux and Active Directory security problems at different difficulty levels;
- freedom for teams to investigate, patch and reconfigure their own systems;
- visible scoring and hidden flags to provide feedback;
- enough isolation to protect the underlying university lab and management network;
- repeatable testing so I could show which parts worked and where the design reached its limits.
My dissertation evaluated the project against three questions:
- Did the deliberately introduced vulnerabilities behave as intended?
- Did the scoring system and host agents record points as intended?
- Did the environment implement the competitive and motivational features identified in research into capture-the-flag events, cyber defence competitions and gamification?
Design overview
I divided the system into four dependent layers:
physical router, switch and reused hosts
|
v
Proxmox cluster and shared TrueNAS storage
|
v
VXLAN networks, virtual firewalls and routing
|
v
mirrored Linux and Active Directory environments
|
v
Flask scoring service, web interface and host agents
The ordering mattered. Shared storage and inter-node networking had to work before I could distribute virtual machines across the cluster. The virtual firewalls and routes had to preserve isolation before I populated the Windows domains. The scoring service then depended on stable addressing, local DNS, TLS, machine identity and communication paths into both team networks.
I used a develop-and-test approach throughout the project. I built each layer, tested it in isolation and then used it as the foundation for the next stage. This reduced the number of problems I had to diagnose at once and made it easier to separate an infrastructure problem from an operating-system or application problem.
At a high level, the finished architecture was:
University Hacklab network
|
v
Physical OPNsense router
|
+---- ZeroTier management access
|
v
172.16.1.0/24 development network
|
+---- six-node Proxmox VE cluster
|
+---- TrueNAS NFS storage
|
v
Neutral virtual OPNsense gateway
|
v
10.10.1.0/24 - No Man's Land
| |
v v
Team 1 firewall Team 2 firewall
| |
v v
10.10.2.0/24 10.10.3.0/24
Team 1 private network Team 2 private network
The address ranges shown here belonged to the isolated university lab and are no longer in use.

Physical platform
The university supplied eight decommissioned Hacklab machines, a managed Netgear switch and the required network cabling. I assigned the machines as follows:
| Role | Quantity | Operating system | Main specification |
|---|---|---|---|
| Virtualisation node | 6 | Proxmox VE 8.1.3 | Intel Core i7-6700, 32 GB DDR4, 1 TB HDD, 1 Gb/s NIC |
| Physical router | 1 | OPNsense | Same base hardware with an additional Realtek RTL8125B 2.5 Gb/s NIC |
| Shared storage | 1 | TrueNAS Core | Same base hardware with a second 1 TB HDD for the storage pool |
This gave the project a large amount of aggregate memory and enough CPU capacity to host the range, but it did not give the cluster fast storage. Every virtualisation node used a mechanical hard drive, and the shared TrueNAS dataset was also built from a single mechanical disk. That became the most important technical constraint later in the project.
Physical routing and management
The physical OPNsense host separated the university-facing connection from the development network used by Proxmox and TrueNAS. The onboard interface connected towards the Hacklab network, while the additional 2.5 Gb/s interface connected to the managed switch on the cluster side.
The Realtek interface was not supported by the default installation, so I installed the required OPNsense driver package before the interface could be assigned. Once both interfaces were operational, I configured the cluster-side network as 172.16.1.0/24, with OPNsense at 172.16.1.1.
I recorded the MAC address of each Proxmox and TrueNAS host and created DHCP reservations. This kept management addressing consistent without relying on each machine being manually configured with a static address.
I also added ZeroTier to the physical router. Authorised management devices joined a separate overlay network and reached the development subnet through static routes and OPNsense firewall rules. This allowed me to manage the cluster remotely without exposing the Proxmox management interfaces directly to the wider network.
The management path was deliberately separate from participant traffic:
management device
|
v
ZeroTier overlay
|
v
physical OPNsense router
|
v
Proxmox and TrueNAS management network
That separation reduced the chance of a mistake inside the competition environment exposing the cluster control plane.
Shared storage
The TrueNAS machine used its second hard disk to create a ZFS pool and dataset. Because only one data disk was available, the pool was a single-disk stripe rather than a redundant layout. I exported the dataset over NFS and restricted the share to the 172.16.1.0/24 development subnet.
I then registered the NFS export in the Proxmox datacentre configuration. Once mounted at cluster level, every node could use the same location for:
- installation media;
- virtual-machine backups;
- snapshots;
- shared templates and other project material.
The storage design improved consistency and recovery compared with leaving everything on individual nodes, but it was not high availability. The NFS service depended on one TrueNAS host and one data disk. A production range would require redundant storage, tested recovery procedures and better separation between active virtual-machine storage and backup storage.
Proxmox cluster
I installed Proxmox VE 8.1.3 on the six virtualisation machines and joined them into one cluster. This gave me a single management plane for the nodes, storage and virtual machines.
The cluster allowed workloads to be placed across multiple hosts while retaining one view of the environment. It also made it easier to copy, back up and rebuild the two team networks consistently. The main benefit was operational rather than automatic resilience: the reused hardware, single NFS server and local mechanical disks did not provide the redundancy expected from a production cluster.
Software-defined networking
The participant networks had to span six Proxmox nodes without being tied to one physical switch port or one local Linux bridge. I used Proxmox software-defined networking and created a VXLAN zone named Enviro.
Each Proxmox node was added to the peer list. I set the MTU to 1450 to account for VXLAN encapsulation overhead. Without that adjustment, the logical topology could appear correct while larger packets were fragmented or dropped when they crossed the overlay.
Three virtual networks were created inside the VXLAN zone:
| Network | Address range | Purpose |
|---|---|---|
No Man’s Land (NML) |
10.10.1.0/24 |
Neutral network joining the internet gateway and both team firewalls |
| Team 1 | 10.10.2.0/24 |
Private systems owned and defended by Team 1 |
| Team 2 | 10.10.3.0/24 |
Private systems owned and defended by Team 2 |
The VXLAN design meant a virtual machine could run on any of the six nodes and still remain attached to the correct competition network. This was important because the Windows workloads had to be spread across the cluster rather than concentrated on one host.
Virtual firewalls
The virtual network used three additional OPNsense instances.
The neutral firewall connected No Man’s Land to the external lab connection. Its policy allowed the competition environment to reach services it required while denying access back into the underlying Proxmox management subnet.
Each team then received its own OPNsense firewall between No Man’s Land and its private network. I used static routes and hybrid NAT so the team networks could exchange the traffic required by the exercise without translating every internal address.
The firewall responsibilities were deliberately separate:
| Boundary | Responsibility |
|---|---|
| Physical OPNsense router | Protect the development network and provide consistent management addressing |
| Neutral virtual firewall | Connect No Man’s Land to required external services while blocking the Proxmox management network |
| Team 1 firewall | Route between No Man’s Land and Team 1, exposing only the intended Linux entry host |
| Team 2 firewall | Mirror the Team 1 policy for the second environment |
Intended route into a team network
The Windows clients and domain controllers were not directly reachable from the opposing network. Only the Linux systems were exposed across the team boundary.
The intended route was:
opposing team
|
v
No Man's Land
|
v
exposed Linux host
|
v
team-controlled firewall and internal route
|
v
private Windows domain
This forced participants to consider network position. Discovering an Active Directory weakness was not enough if the participant had no route to the domain. Defenders also had a clear boundary to inspect and could change their firewall policy, host configuration and internal controls as the exercise progressed.
Mirrored team environments
Each team received the same base environment:
| System | Quantity per team | Role |
|---|---|---|
| Windows Server 2019 evaluation | 1 | Domain controller and DNS |
| Windows 10 Pro evaluation | 2 | Domain-joined client systems |
| Ubuntu Server 18.04 | 1 | Exposed Linux entry host and domain-connected file service |
| OPNsense | 1 | Team-controlled routing and firewall boundary |
I created two equivalent Active Directory domains and joined both Windows clients to their respective domains. Each directory contained 200 accounts so participants had enough identity data to investigate rather than a tiny lab domain where every account was immediately obvious.
Group Policy was used to keep the environments consistent. I enabled RDP through policy and mounted a shared directory from the Linux server as a network drive on the Windows systems. The Linux hosts were joined to their corresponding domains through Winbind.
This created a small but connected enterprise-style environment. The Linux system was not an isolated challenge machine. It had a relationship with the Windows domain, provided a shared resource and sat at the network boundary participants were expected to work through.
Why the environments were mirrored
A competitive range needs both teams to begin from a comparable position. I therefore kept the system roles, network layout, account population and deliberately weak configurations as close as possible between Team 1 and Team 2.
Mirroring also supported the main training idea. When a team discovered a condition in the opponent’s domain, it could reasonably assume the same class of problem existed in its own environment. Offensive discovery could therefore lead directly into defensive review.
The environments were still intended to change after the exercise began. Teams owned their firewalls and hosts, so they could patch systems, change credentials, remove weak configurations, alter routes or make other defensive decisions. The starting point was mirrored; the final state did not have to be.
Deliberately weak configurations
I selected weaknesses across several layers rather than building one linear exploitation path. The environment included exposed services, an operating-system privilege condition, weak credentials, directory permissions, Kerberos configuration issues and protocol-level weaknesses.
| Layer | Examples represented in the range | Training problem |
|---|---|---|
| Exposed service | SSH and Webmin on the Linux hosts | Find and defend the first reachable system |
| Operating system | Ubuntu 18.04 using a kernel affected by Dirty Pipe | Separate initial access from local privilege escalation |
| Authentication | Guessable passwords and supplied candidate lists | Review credential policy and account exposure |
| Kerberos | Kerberoastable service accounts and accounts without pre-authentication | Understand how account configuration changes offline attack opportunities |
| Directory permissions | Delegated replication rights | Identify privilege embedded in Active Directory rather than on one endpoint |
| Directory data | Passwords stored in account descriptions | Find sensitive information exposed through ordinary directory enumeration |
| Protocol configuration | SMB signing disabled | Examine authentication traffic and the risks created by weak protocol policy |
| Network placement | Windows systems protected behind team firewalls | Gain and defend the route into the internal domain |
The public portfolio describes the design and test results without reproducing step-by-step exploitation instructions.
Linux entry systems
The Ubuntu hosts ran OpenSSH 7.6p1 and Webmin 1.996, with ports for SSH, SMB and Webmin exposed as part of the intended attack surface.
Participants received candidate usernames and a password list. The design allowed account discovery and credential attacks without making the first stage entirely automatic. Once access was obtained, the host still required local investigation and privilege escalation.
The Linux systems included:
- SSH user-enumeration behaviour associated with the deployed OpenSSH version;
- weak credentials drawn from the supplied password material;
- an authenticated command-execution path through the deployed Webmin service;
- a Linux 5.8 kernel affected by Dirty Pipe, CVE-2022-0847;
- domain integration through Winbind;
- a shared directory used by the Windows clients.
The Linux stage was deliberately more approachable than the internal domain. It provided multiple routes into the exercise for participants who were less familiar with Active Directory, while still requiring them to establish access, gain privilege and understand how the host connected to the rest of the environment.
Active Directory conditions
The Windows domains contained several common identity and configuration problems:
- service accounts configured in a way that exposed them to Kerberoasting;
- accounts with Kerberos pre-authentication disabled;
- delegated directory replication rights that enabled DCSync-style behaviour;
- passwords placed in Active Directory object descriptions;
- disabled SMB signing;
- weak or guessable account passwords.
The conditions varied in difficulty. Finding a password in an object description required basic directory enumeration. Understanding replication rights or Kerberos service-account exposure required more knowledge of Active Directory permissions and authentication.
This range of difficulty was intentional. A team could make progress without already being an Active Directory specialist, while more experienced participants still had deeper configuration and privilege relationships to investigate.
The same conditions also created defensive work. Teams had to decide which accounts were legitimate, which permissions could be removed, how to rotate credentials without breaking services, where authentication traffic was exposed and which changes would reduce the attack surface without destroying the environment they were meant to maintain.
Scoring requirements
The scoring system had to represent both parts of the exercise.
Cyber defence competitions normally reward service availability and continued control. Capture-the-flag exercises reward the discovery of challenge material. I implemented both:
- host agents submitted periodic updates associated with a team and machine;
- hidden flags awarded one-off points when recovered from the opposing environment;
- a public scoreboard refreshed regularly so both teams could see the competition changing.
The initial point values were:
Periodic host updates
| Machine type | Points per accepted update |
|---|---|
| Windows client | 20 |
| Domain controller | 50 |
| Linux host | 10 |
Hidden flags
| Flag location | One-off points |
|---|---|
| Windows client | 150 |
| Domain controller | 200 |
| Linux host | 100 |
The values placed more weight on the domain controllers and made flags worth more than one ordinary agent update. The model was a prototype, not a fully balanced competition economy. A real event would need testing around update frequency, service criticality, score inflation and recovery after an incorrect award.
Scoring server placement
The scoring service ran on Ubuntu Server 22.04 and used three virtual network interfaces:
- one interface on the physical development network;
- one interface inside Team 1;
- one interface inside Team 2.
The team-facing addresses were added to the DNS configuration of each domain controller. Clients could therefore resolve the local pointserv hostname to the interface inside their own environment.
This kept agent traffic local to each team network while allowing one central service to maintain the scoreboard. The opposing team did not need direct access to the other domain in order for both sides to submit score updates.
Flask application and data model
I wrote the scoring backend in Python using Flask, Flask-SQLAlchemy and SQLite.
The two main database models were Team and Flag:
class Team(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
points = db.Column(db.Integer, default=0)
class Flag(db.Model):
id = db.Column(db.Integer, primary_key=True)
flag = db.Column(db.String(100), unique=True, nullable=False)
submitted = db.Column(db.Boolean, default=False)
machine_id = db.Column(db.String(100), nullable=False)
A team record held the registered name and current score. A flag record held the challenge value, whether it had already been submitted and the machine associated with it.
The application exposed a small set of routes:
| Route | Purpose |
|---|---|
/ |
Render the live scoreboard |
/register |
Display teams and accept a new team name |
/teams |
Return team names and points as JSON |
/flags |
Render the flag-submission interface |
/api/add_team |
Create a team record |
/api/submit_flag |
Validate a flag and award one-off points |
/api/post_data |
Accept an authenticated machine update |
Live scoreboard
The browser requested /teams every five seconds and rebuilt the displayed team elements from the returned JSON:
function updateTeams() {
fetch('/teams')
.then((response) => response.json())
.then((teams) => {
const container = document.getElementById('teams-container');
container.innerHTML = '';
teams.forEach((team) => {
const element = document.createElement('div');
element.classList.add('team');
element.innerHTML = `<h2>${team.name}</h2><p>Points: ${team.points}</p>`;
container.appendChild(element);
});
});
}
setInterval(updateTeams, 5000);
updateTeams();
The browser did not calculate or retain the authoritative score. It only displayed the values committed by the server.
Host-update flow
Each machine had a fixed identifier and an associated API key. The agent submitted JSON containing the machine and team identifiers and placed the key in the X-API-Key header.
The server validated the machine-to-key relationship before looking up the team and applying the mapped score:
machine_id = data['MachineID']
team_id = data['TeamID']
api_key = request.headers.get('X-API-Key')
if machine_id not in valid_machines:
return jsonify({'error': 'Unauthorized'}), 401
if api_key != valid_machines[machine_id]:
return jsonify({'error': 'Unauthorized'}), 401
team = Team.query.filter_by(name=team_id).first()
if not team:
return jsonify({'error': 'TeamID not found'}), 404
team.points += points_mapping.get(machine_id, 0)
db.session.commit()
The real prototype stored the machine identifiers and keys in the application configuration. I have omitted the original lab values here.
I implemented separate responses for:
- missing machine or team identifiers;
- an unknown machine;
- a machine and key mismatch;
- an unknown team;
- a valid score update.
These error paths were important because the scoring system could not be evaluated only by sending correct requests. I also needed to show that obvious attempts to mix one machine identifier with another machine’s key were rejected.
Flag-submission flow
Flags followed a different path:
receive submitted value
|
v
look up flag record
|
+----> unknown value: reject
|
v
check submitted state
|
+----> already used: reject
|
v
identify machine and opposing team
|
v
apply one-off machine value
|
v
mark flag as submitted and commit
A flag associated with a Team 1 machine awarded points to Team 2, and a flag associated with a Team 2 machine awarded points to Team 1. Repeated submissions were blocked by the submitted field.
This model worked for one two-team event, but the submitted state was global. It did not support several competitions, per-team submissions or resetting one event without rebuilding data. That became one of the design limitations identified during evaluation.
Container deployment
I containerised the Flask service and placed Caddy in front of it using Docker Compose.
The deployment separated the host-facing reverse proxy from the application network:
networks:
public-points:
internal: false
private-points:
internal: true
services:
caddy:
image: caddy/caddy:latest
ports:
- "80:80"
- "443:443"
networks:
- public-points
- private-points
pointserv:
image: pointserv:latest
volumes:
- "./instance:/app/instance"
networks:
- private-points
Caddy accepted the HTTP and HTTPS connections and reverse-proxied them to Flask over the private Docker network. The Flask container was not connected directly to the public-facing network.
The Caddy configuration used an internal certificate for the lab hostname:
pointserv.local {
tls internal
reverse_proxy http://pointserv:5000
}
The SQLite database lived in a bind-mounted instance directory so it survived replacement of the application container. Caddy retained its certificate state in a named volume. The submitted Compose file also declared a separate database volume that was not attached to either service; the application was using the bind mount instead.
The design reduced direct exposure of the Flask service, but it remained a university prototype. It did not use a production WSGI server, an external transactional database, central secret management or a managed certificate authority.
Windows scoring service
The host updates had to run without a participant manually executing a command. I wrote a C++ Windows service named PointService and installed it on the Windows systems.
The service integrated with the Windows Service Control Manager. Its lifecycle included:
- registering the service entry point;
- registering a control handler;
- reporting
START_PENDINGandRUNNINGstates; - creating a stop event;
- launching a worker thread;
- signalling the worker during service shutdown;
- reporting the final stopped state.
The worker ran under the local system context. Every five seconds it:
- read the current team value from
C:\King.txt; - combined that value with the build-specific machine identifier;
- created a JSON request body;
- added the machine’s
X-API-Keyheader; - sent the request to the scoring API through libcurl;
- checked the libcurl result;
- released the request resources;
- waited for the next interval.
A simplified version of the request construction was:
std::string teamId = readFileContents("C:\\King.txt");
std::string body =
"{\"MachineID\": \"" + machineId +
"\", \"TeamID\": \"" + teamId + "\"}";
curl_easy_setopt(curl, CURLOPT_URL, scoreUrl.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
headers = curl_slist_append(headers, apiHeader.c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
The service package included the executable and the libcurl and compression-library dependencies it required. It was installed from a dedicated directory using the Windows service-management tools.
Linux scoring daemon
I created a corresponding C++ daemon for the Ubuntu hosts. It used libcurl and followed the same API contract as the Windows service:
read local team-control file
|
v
build MachineID and TeamID JSON
|
v
add machine-specific API header
|
v
POST to scoring service
|
v
wait five seconds and repeat
The daemon was built with CMake, ran with root privileges and was installed through systemd. Keeping the request format consistent meant the backend did not need separate scoring logic for Windows and Linux. Only the host integration changed.
The first implementation performed network initialisation, request execution and cleanup inside the repeated loop. It worked, but a stronger daemon would retain reusable resources, use structured logging, implement back-off after failures and queue updates rather than discarding them when the server was unavailable.
Meaning of a host update
The prototype treated a running agent and the value in the local team-control file as evidence that a team controlled the machine.
That was a simple signal, but it combined several separate questions:
- Is the host online?
- Is the agent process running?
- Are the services on the host healthy?
- Which team currently controls the system?
- Is the request coming from the enrolled agent?
- Has the request been copied or replayed?
A running agent did not prove that the domain service, file share or other required function was healthy. It also did not prove that the binary was untampered. A participant with administrative access could inspect the program, recover the request format and reproduce the update.
This distinction is important in an attack-and-defence range. Availability scoring should come from independent service checks. Ownership scoring needs a trust mechanism that remains meaningful after participants gain administrative access to the host. The prototype showed the end-to-end workflow, but it did not solve that larger trust problem.
Validation approach
I evaluated the project at four boundaries:
- Infrastructure: cluster membership, storage availability and inter-node networking.
- Segmentation: protection of the management subnet and the intended cross-team route.
- Training conditions: whether the selected Linux and Active Directory configurations produced the intended lab behaviour.
- Scoring: registration, score retrieval, host updates, flag state, error handling and TLS transport.
This prevented one successful component from being used as evidence that the entire system worked. For example, a successful Flask request did not prove that the Windows guests could run the agent reliably on the main cluster.
Infrastructure and segmentation testing
All six Proxmox hosts joined the cluster and could access the NFS storage registered at datacentre level. The VXLAN networks carried traffic between virtual machines placed on different physical nodes.
I confirmed that:
- the physical OPNsense router separated the development network from the wider lab;
- ZeroTier management clients could reach the intended management interfaces;
- the neutral virtual firewall blocked the competition networks from reaching the Proxmox management subnet;
- each team firewall routed between No Man’s Land and its private network;
- the exposed Linux hosts were reachable across the team boundary;
- the opposing Windows clients and domain controllers were not directly reachable;
- the reduced MTU was applied consistently to the overlay interfaces.
The result was the network behaviour I intended: participants had a path into the opposing environment, but not a flat route directly to every system.
Linux validation
The Linux tests confirmed the intended sequence from discovery to privilege:
- the deployed services and ports were visible from the permitted network position;
- account-enumeration behaviour could identify valid usernames;
- the supplied credential material supported successful authentication testing;
- the Webmin configuration allowed the intended authenticated command-execution behaviour;
- the Ubuntu 18.04 host and Linux 5.8 kernel reproduced the Dirty Pipe local privilege condition;
- the host remained connected to the relevant domain and file-sharing configuration.
The value of the Linux environment was not one specific exploit. It was the combination of an accessible entry surface, local privilege work and a route towards the private domain.
Active Directory validation
I tested the deliberately weak domain configurations before treating them as usable competition conditions.
The validation confirmed:
- disabled SMB signing produced the expected weak authentication environment;
- the accounts with delegated replication rights could perform the intended directory-replication behaviour;
- service accounts produced Kerberos service tickets suitable for the Kerberoasting exercise;
- accounts without pre-authentication produced the expected AS-REP material;
- passwords placed in directory descriptions were visible through ordinary directory enumeration;
- weak account credentials could be identified using the supplied candidate material.
These tests showed that the conditions were present and repeatable in the controlled environment. They did not establish that every team would follow the same route or that the weaknesses were equally difficult for every participant.
Scoring validation
The registration page accepted two team names and stored them in SQLite. Returning to the main page displayed both teams, and the browser refreshed their point totals every five seconds.
The flag interface produced the expected outcomes:
- an unknown value returned a flag-not-found error;
- a valid value awarded the correct points to the opposing team;
- a repeated value returned an already-submitted error;
- an invalid machine association was rejected.
The agent API also produced distinct responses for valid and invalid requests. A request using the machine identifier from one host with the key assigned to another was rejected. Requests with missing fields or unknown teams returned the corresponding error rather than silently changing a score.
I inspected the traffic between the Windows service and scoring server and confirmed that the request content was protected by TLS in transit. The service ran under the expected Windows authority and read the team value from the local control file before updating the correct score.
These tests validated the controls implemented in the prototype. They did not make the embedded API keys secure against a participant who could reverse engineer the agent.
Hardware boundary
The main cluster could host the range, but the Windows workloads were severely affected by storage latency and the lack of graphics acceleration.
The six Proxmox nodes used mechanical disks for their local operating systems and virtual machines. When several Windows guests were active, the systems stuttered and took a long time to complete basic operations. The graphical workload also remained on the CPUs because the nodes had no suitable GPU allocation for the guests.
This affected development time and prevented the complete scoring workflow from being tested reliably inside the full range. The scoring application and agents were therefore tested together in a separate SSD-backed environment, where the Windows service completed its request loop as intended.
I kept that boundary explicit:
| Component | Main eight-machine range | Separate SSD-backed test environment |
|---|---|---|
| Proxmox cluster | Validated | Not applicable |
| TrueNAS NFS storage | Validated | Not applicable |
| VXLAN and virtual firewalls | Validated | Not applicable |
| Mirrored Linux and Windows environments | Validated, with significant performance constraints | Reduced test deployment |
| Linux and Active Directory weaknesses | Validated | Supporting tests where required |
| Flask application and scoreboard | Implemented | End-to-end validated |
| Linux daemon | Implemented | End-to-end validated |
| Windows service | Could not run reliably at full range load | End-to-end validated |
This was not the same as deploying the complete scoring system successfully on the main range. The result was a working range and a working scoring prototype whose full integration was limited by the storage platform.
Gamification design
The scoring system was tied to the research rather than added after the technical build.
My dissertation used Fogg’s Behaviour Model to consider motivation, ability and the trigger for a desired action. In this project:
- ability was addressed by placing conditions at different levels of difficulty;
- motivation came from team competition, visible points, hidden flags and ownership of the environment;
- triggers were provided by score changes, discoveries, attacks against the team’s systems and the need to respond.
The Linux entry systems reduced the initial barrier. Participants could begin with service discovery, credentials and local privilege before reaching the more demanding identity relationships inside Active Directory.
The Windows environment then provided a wider range of problems. A password in an account description was relatively easy to identify, while Kerberos configuration and replication permissions required a deeper understanding of the domain.
The environment also gave teams freedom to change their own systems. They were not following a fixed set of challenge instructions. A team could patch one weakness, alter its firewall, rotate an account, remove a permission or introduce another defensive control. That freedom supported creative problem-solving but also made the exercise harder to balance and score.
The game elements implemented were:
- small teams as a social and collaborative unit;
- mirrored starting environments for direct competition;
- a continuously refreshed leaderboard;
- recurring host points;
- one-off hidden flags;
- different point values for different systems;
- ownership of the firewall and internal network;
- freedom to change the environment after the exercise began.
I did not conduct a participant study measuring learning outcomes, engagement or retention. The evaluation could show that the project implemented the mechanisms described in the research and that they behaved technically as designed. It could not show that the finished range improved learning for a measured participant group.
Findings
The project answered the three research questions with an important qualification around the scoring deployment.
Deliberately introduced weaknesses
The selected Linux and Active Directory conditions behaved as intended in the controlled range. They provided several routes through the environment and covered different levels of technical difficulty.
Scoring system
The Flask service, database, registration flow, scoreboard, flags, error handling, TLS communication and both host-agent implementations worked in testing. The complete agent-to-score path was validated in the separate SSD-backed environment rather than under the full load of the main cluster.
Competitive and motivational features
The environment implemented the planned team structure, direct competition, visible score feedback, point values and hidden flags. It also combined offensive and defensive responsibilities in the same context, which was the main objective of the project.
The project did not prove long-term educational effectiveness because it did not include a participant trial. It proved that the technical platform and the selected gamification mechanisms could be assembled into one functioning prototype.
Engineering limitations
Mechanical storage and graphical workload
Mechanical disks were the largest performance bottleneck. Moving active virtual-machine storage to SSD or NVMe would be the first infrastructure change. Better graphical support, or reducing the number of full desktop guests, would also improve responsiveness.
Single points of failure
The TrueNAS design used one host and one data disk. The physical router, switch and management path also had no redundancy. This was acceptable for an honours prototype but not for a dependable training platform.
Embedded agent credentials
The API keys were compiled into the agent binaries. A participant with sufficient access could recover a key through static or runtime analysis and reconstruct the request format. The machine identifier and service location were also build-time values, which meant maintaining separate configurations or binaries for individual hosts.
A stronger model would use enrolled machine identities, short-lived credentials, rotation, revocation and server-side telemetry for suspicious updates.
No replay protection
The API checked the machine and key combination but did not include a nonce, timestamp, sequence number or signed request body. A captured valid request could therefore be replayed while the credential remained accepted.
Self-signed trust model
Caddy’s internal certificate encrypted traffic, but the design did not provide a competition-managed trust chain with protected client trust stores. A participant controlling a host could potentially alter its trust configuration or attempt to intercept traffic. TLS protected the tested connection; it did not create a complete hostile-participant trust model.
Agent health was not service health
The recurring update represented the agent and control file, not the actual availability of domain services, SMB, Webmin, SSH or other required functions. Independent service checks would be required for dependable availability scoring.
Flask and SQLite prototype scale
The application committed score changes directly inside request handlers and used SQLite as its only database. It did not provide:
- an audit trail of every score event;
- score correction and rollback;
- transaction handling for a high rate of concurrent updates;
- role-based administration;
- event separation;
- team enrolment controls;
- recovery from partial failures;
- monitoring of suspicious scoring activity.
Global flag state
Each flag had one global submitted value. This worked for one two-team exercise but not for repeated events, more teams or per-team completion tracking.
Agent reliability
The agents logged immediate network errors but did not queue unsent updates, use exponential back-off or persist delivery state. They also lacked an automated configuration, update and revocation mechanism.
Evaluation scope
The evaluation proved component behaviour in a controlled university lab. It did not measure performance under a full competition, adversarial load against the scoring service, participant learning outcomes or long-running operational stability.
Future development
The next version would begin with the infrastructure rather than adding more game features to the existing platform.
Storage and deployment
I would move active workloads to SSD or NVMe storage, separate backups from active virtual-machine disks and add redundancy to the storage layer. I would also define the range through reusable templates or infrastructure automation so both team environments could be rebuilt from a known state.
Scoring architecture
I would separate the scoring concerns into independent services:
external service health checks
|
v
availability events
machine enrolment and signed ownership claims
|
v
ownership events
flag submission service
|
v
challenge events
all validated events
|
v
auditable scoring ledger
|
v
scoreboard projection
This would make it possible to distinguish a healthy service from a running agent and to rebuild the scoreboard from an event history rather than only storing the latest total.
Agent identity
Agents should enrol with the scoring service and receive short-lived credentials. Requests should include a timestamp, unique identifier and signature, with server-side replay detection. Secrets should not be compiled directly into every binary.
No software running on a participant-controlled host can be treated as completely trusted, so the design should minimise what the agent is allowed to assert and compare it with independent observations.
Observability
A later range should collect:
- firewall logs;
- Windows event logs;
- Linux authentication and service logs;
- endpoint telemetry;
- score-event audit records;
- agent status and delivery errors;
- service-check results.
That would make the platform more useful for threat hunting, incident response and post-exercise review rather than only producing a final score.
Simulated user activity
I considered using Carnegie Mellon University’s GHOSTS framework to generate routine browsing, email and endpoint activity. Background behaviour would make the environment more realistic and give defenders ordinary traffic to separate from competition activity.
I left it out because the existing Windows guests were already constrained by storage and graphical performance. Adding automated browsing and other user simulation would have made the bottleneck worse. It remains a useful extension once the base platform can support it.
Competition design
Additional badges, themes and a narrative could strengthen the motivational side of the exercise. I would only add them after the scoring and infrastructure were dependable. A more complex theme would not compensate for unreliable machines, unclear scoring or a trust model participants could trivially bypass.
Wider value
The main value of King of the Forest was the shared context it created.
An offensive participant could not treat the target as a disposable challenge machine because the same type of environment also had to be defended. A defensive participant could not treat every weakness as an abstract configuration issue because another team was actively trying to use it.
The range could support collaboration between different security roles. A penetration tester, SOC analyst, incident responder, system administrator and threat hunter would each approach the same environment differently. Putting those roles in one team creates opportunities to share how an attack path is found, what evidence it generates, how the system is contained and how the underlying weakness is removed.
The project also showed the engineering work behind a realistic training range. The visible challenge is only one layer. The platform also needs repeatable networks, storage, routing, host configuration, trust boundaries, scoring logic, recovery and a clear evaluation of what the system actually proves.
Final outcome
King of the Forest brought together:
- an eight-machine physical lab;
- a six-node Proxmox VE cluster;
- TrueNAS shared storage;
- software-defined VXLAN networks;
- physical and virtual OPNsense firewalls;
- two mirrored Active Directory and Linux environments;
- controlled weaknesses across services, operating systems, identity and protocol configuration;
- a Python and Flask scoring API;
- SQLite data storage;
- a live web scoreboard and flag interface;
- Docker and Caddy deployment;
- a C++ Windows service;
- a C++ Linux daemon;
- infrastructure, security and application testing.
The finished project was not a production competition platform, and the hardware prevented full deployment of the scoring agents under the load of the main range. It was a functioning technical prototype with a clearly defined result boundary. I could show which parts ran on the physical platform, which parts were validated separately and what would need to change before the environment could support a dependable live event.
