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.
Creating the challenge file
Over an existing or empty folder with the files that are required by the challenge, create a challenge.py file with the following content:
$ cat challenge.py
from halborn_ctf.templates import GenericChallenge
class Challenge(GenericChallenge):
HAS_SOLVER = True
CHALLENGE_NAME = 'MY CHALLENGE'
# optional
def build(self):
# Do build....
pass
def run(self):
# Do deployment
pass
def solver(self):
self.solved = True
You can now test that the challenge does build with (halborn_ctf.templates.GenericChallenge.build):
$ halborn_ctf build -vv
Check that the challenge can run (halborn_ctf.templates.GenericChallenge.run):
$ halborn_ctf run -vv
2023-05-02 16:26:48 | root | __enter__ | INFO | pid=41948 pgid=41948
2023-05-02 16:26:48 | werkzeug | _log | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1: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":{}}(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.
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'
# 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']
}
}
# optional
def build(self):
# Do build....
pass
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'
# optional
def build(self):
# Do build....
pass
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
}
# optional
def build(self):
# Do build....
pass
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()
# optional
def build(self):
# Do build....
pass
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.