This is the multi-page printable view of this section. Click here to print.
Design
1 - Architecture
Concept
As explained in the chall-manager documentation, we avoid exposing its API to prevent the risk of direct resource manipulation by players.
CTFd inherently provides functionalities like authentication, team management, scoring, and flag handling. By adding our plugin, CTFd can serve as both a challenge management platform for administrators and a request manager that acts as a proxy with user authentication, mana limitations, and more.
Overview
The basic architecture is straightforward: we have created new API endpoints for both administrators and users. These endpoints mainly handle CRUD operations on challenge instances.
API
AdminInstance
This endpoint allows administrators to perform CRUD operations on challengeId for a specified sourceId. Essentially, this endpoint forwards requests to the chall-manager for processing.
UserInstance
Unlike the AdminInstance endpoint, this one does not accept sourceId as a parameter. Instead, it automatically identifies the source issuing the request and checks mana availability before forwarding the request to the chall-manager.
UserMana
This endpoint handles GET requests to display the remaining mana of the source issuing the request.
Detailed Overview
The following diagram provides a more detailed view of how your browser interacts with the API endpoints and how these endpoints are mapped to the corresponding chall-manager endpoints.
2 - Challenge
Overview
The plugin introduces a new challenge type called dynamic_iac
, allowing the deployment of instances per user or team (referred to as a source). It works seamlessly with both user modes defined by CTFd (individual users or teams).
What’s new about this challenge type?
To get started, navigate to the challenge creation panel in CTFd and select the dynamic_iac
challenge type.
Here’s what the plugin adds:
Global Scope
The global scope is a boolean setting that allows a single instance to be shared among all players.
For instance, in the following setup, challenges 1, 2, and 3 have the global scope enabled, while challenge 4 does not:
In this scenario, player X (yellow) and player Y (blue) will each have their own instances for challenge 4, but will share the same instance for challenges 1, 2, and 3. We recommend enabling this feature for static, stateless applications (e.g., websites).
Mana Cost
The mana cost is an integer representing the price users must pay in mana to deploy their own instance. Mana is refunded when the instance is destroyed. This system helps control the impact users have on the platform. For more details, see how mana works.
Until
The until setting allows you to specify a date and time at which instances will be automatically destroyed by the Janitor.
Example:
As a CTF administrator running a week-long event, you want a challenge available only on the first day. Set the Janitoring Strategy to Until, and configure the end date to DD/MM/YYYY 23:59.
As a player, you can start your instance anytime before this date and destroy it whenever you like before the deadline.
Timeout
The timeout is an integer that specifies, in seconds, how long after starting an instance the Janitor will automatically destroy it.
Example:
As a CTF administrator or challenge creator, you estimate that your challenge takes about 30 minutes to solve. Set the Janitoring Strategy to Timeout and set the value to 1800 seconds (30 minutes).
As a player, once you start your instance, it will be destroyed after 30 minutes unless renewed. You can also manually destroy the instance at any time to reclaim your mana.
Tips & Tricks
You can either combine the Until and Timeout values or leave both undefined. Find more info here.Scenario Archive
The scenario is a zip archive defining the challenge deployment as a Golang Pulumi project. You can use examples or create your own using the SDK and Pulumi.
3 - Flag
Concept
Cheating is a part of competitions, especially cash-prize is involved. The player experience is particularly frustrating, so we try to minimize some of the common cases of cheating.
We’ve concentrated on the two most common cases, namely ShareFlag and FlagHolding:
- ShareFlag: get or share flags with an another team.
- FlagHolding: store flags to submit them on the very last moment.
How this work
We redefine the submit
method of the dynamic_iac
challenge to verify the generated flag against the current source.
Here’s the algorithm:
flowchart LR Submission --> A{Instance On ?} A -->|True| B{Instance I flag ?} A -->|False| Expired B -->|True| C{submission == I.flag} B -->|False| D{CTFd C flag ?} C -->|True| Correct C -->|False| D D -->|True| E{submission == C.flag} D -->|False| Incorrect E -->|True| Correct E -->|False| Incorrect %% I/O Submission Expired Correct Incorrect
On the submit
method, we get all informations of the instance on chall-manager.
- We want the instance ON to prevent FlagHolding and make sure the submitted flag is the one in the instance.
- We check if the instance define a flag (if you don’t use the flag variation on the sdk).
- If the instance define a flag, we check if the submission is correct.
- If the instance does not define a flag or if the submission is incorrect, we check if a flag is defined on CTFd (also use as fallback).
- Now, it is the classic behavior of CTFd.
Conclusion
Addressing the shareflag issue is crucial to maintaining fairness in CTF competitions. By redefining the submission process with methods like the dynamic_iac challenge, we ensure that only legitimate efforts are rewarded, preserving the true spirit and integrity of the competition.
FAQ
Why don’t we use the CTFd Flag system for generated flag ?
That’s simple: if we generate the flag on CTFd, each team can submit any generated flag, which doesn’t address the shareflag issue.
Why must the instance be ON to submit?
There are two main reasons for this requirement:
- To prevent ShareFlag: the instance must be up and running to retrieve the generated flag from chall-manager, ensuring flags are unique and not shared between participants.
- To prevent FlagHolding: keeping the instance active forces players to consume their mana when submitting flags, encouraging continuous participation in the competition. Additionnaly, the generated flag is valid for a running instance and will be regenerated in case of recreation.
Why the CTFd Flag is consider as “fallback” ?
As we said before, the CTFd Flag system allows users to submit the same flag. We use this system to prevent connection error or latency with chall-manager or if the generated flag is invalid for synthax error (we choose the extended ASCII so it should not happen).
4 - Source
Concept
As explained here, the Source refers to the team or user that initiates a request. This abstraction allows compatibility with CTFd operating in either “users” or “teams” mode, making the plugin versatile and usable in both modes.
To enable sharing across all users, the sourceId
is set to 0
. The table below summarizes the different scenarios:
user_mode | global scope | sourceId |
---|---|---|
users | FALSE | current_user.id |
teams | FALSE | current_user.team_id |
users | TRUE | 0 |
teams | TRUE | 0 |
Example workflow
Instance creation
Here an example of sourceId usage in the instance creation process:
flowchart LR A[Launch] --> B{CTFd mode} B -->|users| C[sourceId = user.id] B -->|teams| D[sourceId = user.team_id] D --> E{challenge.global_scope} C --> E E -->|True| F[sourceId = 0] E -->|False| End1 F --> End1 End1(((1)))
The rest of the workflows is detailed in mana section.
5 - Mana
Concept
As a CTF administrator, it’s important to manage infrastructure resources such as VMs, containers, or Kubernetes Pods effectively. To avoid overloading the infrastructure by deploying excessive instances, we introduced a “mana” system that assigns a cost to each challenge.
For instance, consider these three challenges:
- challenge1: a static web server in the web category;
- challenge2: a Unix shell challenge in the pwn category;
- challenge3: a Windows server VM in the forensic category.
Challenge3 demands more resources than Challenge2, and Challenge2 requires more than Challenge1.
Suppose each team has a mana limit of 5, with the following costs assigned:
Name | Cost |
---|---|
challenge1 | 1 |
challenge2 | 2 |
challenge3 | 5 |
In this scenario, teams can deploy instances for Challenge1 and Challenge2 simultaneously but must remove both to deploy Challenge3.
Example:
- Team 1: Focused on pwn challenges, will likely prioritize deploying Challenge2.
- Team 2: Specializes in Windows forensics, making Challenge3 their main priority.
This system helps administrators control the use of resources on CTF infrastructures.
Combined with the flag variation engine feature of chall-manager, mana also helps minimize Flag Holding. To progress, players must submit flags and destroy instances to reclaim mana. If players choose to hoard flags until the last moment, mana limits their ability to continue, adding a strategic element to the competition.
How it works
Mana Total
To set the total mana available to users, go to the Settings section of the plugin and adjust the “Mana Total” field. By default, this feature is disabled with a value of 0. The total mana can be modified during the event without any impact on existing configurations.
Mana Cost
Each dynamic_iac
challenge is assigned a mana cost, which can be set to 0 if no cost is desired. Changes to mana costs can be made during the event, but previously issued coupons will retain the cost at the time of their creation.
Coupons
When a user requests an instance, a coupon is generated as long as they have enough mana. Administrators can bypass restrictions and deploy instances directly through the admin panel, creating coupons without limits. Each coupon stores the sourceId, challengeId, and challengeManaCost at the time of creation.
Example workflow
Instance creation
Here an example of the usage of mana and the coupon creation while the instance create process:
flowchart LR End1(((1))) End1 --> G{Mana is enabled ?} G -->|True|H{Source can afford ?} G -->|False|I[Deploy instance on CM] H -->|True|J[Create coupon] H -->|False|End J --> I I --> K{Instance is deployed ?} K -->|True|M[End] K -->|False|N{Mana is enabled?} N -->|True|L[Destroy coupon] N -->|False|M L --> M
Detailed process:
- We check that Mana Total is greater than 0.
- If mana is enable, we check that the Source can afford the current Mana Cost.
- If Source cannot afford the instance, the process end.
- If Source can afford, we create a coupon.
- While the coupon is created, or the mana feature is disable, we create the instance on chall-manager.
- We check that the instance is running on Chall-Manager.
- If it’s running correctly, we end the process.
- If not, we destroy the coupon if exists, then end process with an error.
Synchronicity
Due to the vital role of mana, we have to ensure its consistency: elseway it could be possible to brute-force the use of Chall-Manager to either run all possible challenges thus bypassing restrictions.
To provide this strict consistency, we implemented a Writer-preference Reader-Writer problem based upon Courtois et al. (1971) work. We model the problem such as administration tasks are Writers and players Readers, hence administration has priority over players. For scalability, we needed a distributed lock system, and due to CTFd’s use of Redis, we choosed to reuse it.
FAQ
Why aren’t previous coupon prices updated?
We modeled the mana mechanism on a pricing strategy similar to retail: prices can fluctuate during promotions, and when an item is returned, the customer receives the price paid at purchase. This approach allows CTF administrators to adjust challenge costs dynamically while maintaining consistency.
Why can administrators bypass the mana check?
During CTF events, administrators might need to deploy instances for users who have exhausted their mana. While coupons will still be generated and mana consumed, administrators can set the cost to 0 at the challenge level to waive the charge if needed.
6 - Testing
Purpose
Developing in an Open Source ecosystem is good, being active in responding to problems is better, detecting problems before they affect the end-user is the ideal goal!
Following this philosophy, we put a lot of effort into CI testing, mainly using Cypress, and analyze code security via CodeQL.
GitHub Actions
From a technology point of view, we use GitHub Actions whenever possible to automate our tests.
Security
To ensure ongoing security, we enable advanced security analysis on the repository and conduct periodic scans with CodeQL on each pull request.
Testing
Our tests are systematically executed on each push and pull request, following a two-stage process.
The first stage involves Cypress for plugin integration, where we validate correct test cases to ensure comprehensive coverage. The second stage is executed to check for potential error with invalid cases. This stage runs after the Cypress tests to save time and due to technical constraints, such as the creation of challenges (preconditions).
To detect potential divergences with CTFd mode, we execute the tests in mode: users
and mode: teams
.