Quick Start
Following sections provide a - little talk, much code - introduction to halborn-ctf.
Everything should be copy-pastable and work out of the box, given your
Installation was successful.
Requirements
halborn_ctfdocker
Initializing the challenge
Over an empty folder type the following command:
$ halborn_ctf init
$ cat Dockefile
FROM python:3.11.2
RUN pip install halborn_ctf==0.1.11
# Your dependencies
COPY Dockerfile .
COPY challenge.py .
# Your build commands
ENTRYPOINT ["halborn_ctf"]
CMD ["run", "--local"]
$ cat challenge.py
from halborn_ctf.templates import GenericChallenge
class Challenge(GenericChallenge):
HAS_SOLVER = True
CHALLENGE_NAME = 'MY CHALLENGE'
def run(self):
# Do deployment
pass
def solver(self):
self.solved = True
Check that the challenge can build, this will compile the Dockerfile into a local image:
$ halborn_ctf build
Check that the challenge can run (halborn_ctf.templates.GenericChallenge.run). This will run the build
image and expose the required ports:
$ halborn_ctf run -vv
2023-06-17 16:05:58 | halborn_ctf.cli | main | WARNING | ============================
2023-06-17 16:05:58 | halborn_ctf.cli | main | WARNING | Logging level: DEBUG
2023-06-17 16:05:58 | halborn_ctf.cli | main | WARNING | ============================
2023-06-17 16:05:58 | root | __enter__ | INFO | pid=1 pgid=1
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8080
* Running on http://172.17.0.3:8080
You should see that a server has been spawned locally on port 8080.
By default the challenge will expose the following routes:
/info: Does return challenge public state:{"ready":true,"state":{},"config":{}}(http://127.0.0.1:8080/info)
Note
Since we have set HAS_SOLVER (halborn_ctf.templates.GenericChallenge.HAS_SOLVER). The solver function must exist.
The /solved route will also be exposed (http://127.0.0.1:8080/solved). Each time the route is accessed this function will be executed before responding the HTTP request.
The route is now returning that the challenge is solved as we set self.solved = True:
{
"msg": "Solved",
"solved": true
}
Tip
If the function does take a lot to execute or does require background processing take a look at Periodic solver.
Tip
If you have all the dependencies on your local system and want to play the challenge locally you can always use (this allows you to develop the challenge without having to build the container):
$ halborn_ctf run --local -vv
Creating the challenge
You can now modify the template files to meat your challenge requirements. Keep in mind that everything inside
the (halborn_ctf.templates.GenericChallenge.run) function will be executed for every new challenge instance. This means that long process
actions should be included on the Dockerfile instead. The build phase will be caching all layers and speedup development.
Service mapping
The previous code does have the minimum required functions to run a challenge. However, it does not have any functionallity and there is no way to access services. To register
internal services and expose them on the challenge server you must define a path mapping attribute (halborn_ctf.templates.GenericChallenge.PATH_MAPPING):
from halborn_ctf.templates import GenericChallenge
import halborn_ctf.shell as shell
import halborn_ctf.network as network
import requests
class Challenge(GenericChallenge):
HAS_SOLVER = True
CHALLENGE_NAME = 'MY CHALLENGE'
# To catch all paths and redirect to the service you need to specify both, the `/` and `/<path:path>` rules:
# rule1: A request to http://challenge/ will be proxied to http://127.0.0.1:9999/.
# rule2: A request to http://challenge/my_path/file will be proxied to http://127.0.0.1:9999/my_path/file.
PATH_MAPPING = {
'/': {
'port': 9999,
'path': '/',
'methods': ['GET']
},
'/<path:path>': {
'port': 9999,
'path': '/',
'methods': ['GET']
}
}
def run(self):
# Do deployment
shell.run('python -m http.server 9999', background=True)
network.wait_for_port(9999)
def solver(self):
response = requests.get('http://127.0.0.1:9999')
if "halborn_ctf.txt" in response:
self.solved = True
The previous challenge does use functions from this framework to run a shell command in the background with an http server on the current directory. It then waits for the port to be listening.
If you now try to access http://127.0.0.1:8080 you will be able to see the current directory listing. This is achieved by the PATH_MAPPING attribute which proxies
any request on the / path to the server listening on port 9999. It also proxies any subpath request /<path:path> to the same server from the / path.
If you now try to request http://127.0.0.1:8080/solved you will see that the challenge does report as not being solved.
Tip
To solve the challenge create a file named halborn_ctf.txt under the challenge directory:
$ touch halborn_ctf.txt
Downloadable files
Some challenges require the players to have some files to be used. For that the HAS_FILES (halborn_ctf.templates.GenericChallenge.HAS_FILES) flag can be set to True. Doing so, a function named files should be declared.
We can create a file as a test to be exposed with the challenge:
$ echo "Test content" > test.txt
from halborn_ctf.templates import GenericChallenge
class Challenge(GenericChallenge):
HAS_SOLVER = True
HAS_FILES = True
CHALLENGE_NAME = 'MY CHALLENGE'
def run(self):
# Do deployment....
pass
def solver(self):
self.solved = True
def files(self):
return [
'test.txt'
]
If we now try to access the server at /files (http://127.0.0.1:8080/files) a MY_CHALLENGE.zip file will be downloaded. The name is taken from CHALLENGE_NAME. The content of the file should include the test.txt and the challenge.py file itself.
Working with the state
If you want to persist variables across build and run and all periodic functions
you can use the halborn_ctf.templates.GenericChallenge.state and halborn_ctf.templates.GenericChallenge.state_public
properties. This property can be accessed anywhere but must be declared on the __init__ function with the initial values.
from halborn_ctf.templates import GenericChallenge
class Challenge(GenericChallenge):
HAS_SOLVER = True
HAS_FILES = True
CHALLENGE_NAME = 'MY CHALLENGE'
def __init__(self):
super().__init__()
self.state = {
'solved_attempts': 0
}
def run(self):
# Do deployment
pass
def solver(self):
self.state.solved_attempts += 1
if self.state.solved_attempts == 2:
self.solved = True
def files(self):
return [
'test.txt'
]
Note
The state_public can be accessed and seen on the /info challenge route. (http://127.0.0.1:8080/info)
Periodic solver
If the function does take a lot to execute or does require background processing you can always define a periodic function and start it before setting the challenge to ready. Take a look on how to use the decorator under halborn_ctf.functions.periodic.
from halborn_ctf.templates import GenericChallenge
from halborn_ctf.functions import periodic
class Challenge(GenericChallenge):
HAS_SOLVER = True
CHALLENGE_NAME = 'MY CHALLENGE'
@periodic(every=1)
def my_checker(self):
self.log.info('Checking...')
# Do some long computation
# ...
# self.solved = True
if self.solved:
########### Stop the periodic function ##########
self.my_checker.stop()
def run(self):
# Do deployment
########### Start the periodic function ##########
self.my_checker()
def solver(self):
# The solve is done on the `my_checker` function
pass
The previous challenge will be logging the Checking... string on the console every 1 second.
Warning
Although an external or periodic function is setting the self.solved the solver function must exist.