halborn_ctf package

Subpackages

Submodules

halborn_ctf.cli module

This is the CLI module exposing a system command named halborn_ctf that helps running template extended challenges (halborn_ctf.templates.GenericChallenge).

The entry point for the CLI command is the run function.

halborn_ctf.cli.main(args)[source]

Wrapper allowing any method to be called on a given module/class provided via arguments in a CLI fashion

Parameters:

args (List[str]) – command line parameters as list of strings.

halborn_ctf.cli.run()[source]

Calls main() passing the CLI arguments extracted from sys.argv.

This function can be used as entry point for the halborn_ctf challenges.

The allowed flags are:

  • [METHOD]: The method to execute. Only ‘build’ and ‘run’ are allowed. Valids are build, run.

  • -f/--file: The file where the class/function is present. Defaults to "./challenge.py".

  • -c/--class: The class where the method is found. Defaults to "Challenge".

  • -v/--verbose: Verbose (INFO).

  • -vv/--debug: Verbose (DEBUG).

Example

Executing method run from the challenge.py file and the class named Challenge in debug mode:

halborn_ctf run -vv

Executing method build from the file.py file and the class named ChallengeCustom:

halborn_ctf build -f file.py -c ChallengeCustom

halborn_ctf.functions module

halborn_ctf.functions.periodic(*, every: int)[source]

It allows executing a function as a periodic function in a thread on the background.

Parameters:

every (int) – The amount of seconds to wait to execute the function again. It should be bigger than 0.

Example

@periodic(every=1)
def function():
    print("HI")

function() # Does start printing "HI" every 1 second

function.stop() # Does stop the function execution
Raises:

ValueError – If the every parameter is set to 0.

halborn_ctf.shell module

halborn_ctf.shell.run(cmd: str, *, background=False, capture_output=False, **kwargs)[source]

This is a function used to execute a given cmd on as a subprocess on a shell.

An exam

Example

Example of executing ls and getting the output and error:

process, stdout, stderr = run("ls -la", capture_output=True)

print(stdout)
print(stderr)

Example of installing forge in the /usr directory:

run('curl -L https://foundry.paradigm.xyz | bash', env={"FOUNDRY_DIR": '/usr'})
run('foundryup', env={"FOUNDRY_DIR": '/usr'})

Example of executing anvil in the background as a none blocking process:

run("anvil -p 8545", background_process=True)
Parameters:
  • cmd (str) – The command to be executed. Keep in mind that the program being executed must be present on the PATH environment variable for the shell to find it. The default executed shell corresponds to the /bin/sh and there is not way to have awareness on .bashrc or any other .rc file unless manually loaded. If enviroment variables are required they can be provided through the **kwargs parameter that accepts the same arguments as subprocess.Popen

  • background (bool, optional) – If set it will run the program on the background and will not wait for its execution to finish. Defaults to False.

  • capture_output (bool, optional) – If set it will capture the command output and return it from the function as a decoded string. This flag can not be set at the same time as background. Defaults to False.

Raises:
  • ValueError – If both background and capture_output are set this error is raised.

  • AttributeError – If any argument that is internally used to spawn subprocess.Popen is specified on the kwargs, this error is raised.

Returns:

  • process: (subprocess.Popen)

    The process opened, you can use all methods of the popen-objects.

  • stdout: (str or None)

    A decoded string of the stdout of the executed process. Only if capture_output is set to True. Otherwise None is returned

  • stderr: (str or None)

    A decoded string of the stderr of the executed process. Only if capture_output is set to True. Otherwise None is returned

Return type:

tuple

halborn_ctf.state module

class halborn_ctf.state.State(*args, **kw)[source]

Bases: dict

Wrapper around dictionary for easier access and storage

This class is not intended to be used directly but from templates members

Example

The class allows to create dictionaries that can be accessed like this:

state = State({
    'value': 0,
    'extra': {
        'more': 'initial'
    }
})

state.value = 1337
state.extra.more = 'data'

print(state.value) # 1337
print(state.extra.more) # data


state.newvalue = 0
# ValueError: Key "newvalue" not found
Raises:

ValueError – If the key is not declared during initialization and a value is stored to it.

Parameters:

dict (dict) – Extending from dictionary

udpate(source)[source]

Does allow updating an state recursively with another dictionary

Note

All keys from the source must exist on the state.

Raises:

ValueError – If the key is not found on the state

Example

It allows updating the current state with another dictionary:

state = State({
    'test': 'hi',
    'public': {
        'more': {
            'test': 'hi'
            }
        }
})

print(state)
# {'test': 'hi', 'public': {'more': {'test': 'hi'}}}

state.update({
    'test': 'change',
    'public': {
        'more': {
            'test': 'bye'
        }
    }
})

print(state)
# {'test': 'change', 'public': {'more': {'test': 'bye'}}}
Parameters:

source (dict) – The dictionary to update with

halborn_ctf.templates module

Templates to create CTF challenges.

Creating a challenge consists on the following steps:

  • Pick a template from this module to use. If undecided use the ChallengeGeneric and configure it by reading the documentation.

  • Create a class extending the template and implement the abstract methods:

    # challenge.py
    class Challenge(ChallengeGeneric):
    
        # Optional
        def build(self):
            pass
    
        def run(self):
            pass
    
  • Test the template by using the halborn_ctf CLI (halborn_ctf.cli.run):

    halborn_ctf run -vv
    
  • Server can be accessed under localhost:8080 by default.

class halborn_ctf.templates.FlagType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: Enum

DYNAMIC = 2

When the challenge is deployed a FLAG environment variable will be set.

NONE = 0

If no flag is present

STATIC = 1

If the flag is statically defined or embedded into the challenge somewhere.

class halborn_ctf.templates.GenericChallenge[source]

Bases: ABC

Generic CTF challenge template

Each created/deployed challenge does have two steps defined, build and run. The build step is only executed once when the challenge is created and uploaded into the platform for playing and it is optional. The run step is always executed for each player request to deploy a new challenge.

This template does also expose the challenge by using an HTTP server. The server does allow registering routes to it by using the PATH_MAPPING attribute.

An attribute named state can be used to store any sort of object that will persiste between the build and run steps. Furthermore, this attribute can be used to store anything that would be used across the different functions. The state_public property will be exposed under the /info path on the challenge domain.

The following routes will be exposed under localhost:8080:

  • /info: Does contain general info of the challenge such as ready and state_public.

  • /solved: Does execute the “solver” function and display if the challenge was solved together with a solved message or hint to the player.

Note

Only if HAS_SOLVER == True.

  • /files: Does download the files listed under the “files” function as a zip file named by CHALLENGE_NAME.

Note

Only if HAS_FILES == True.

CHALLENGE_NAME = 'challenge'

The name of the challenge.

Type:

(str)

FLAG_TYPE = 0

The type of flag. Set to FlagType.NONE if using a solver unless using manual flag input on the CTF platform.

Type:

(FlagType)

HAS_DETAILS = False

If the challenge has dynamic or specific implementation details. The required function “details” should be present. This function must return a string. You can format the string using any state variable or any dynamic content (for example a file content).

The string does support Markdown syntax and will be displayed on the platform UI and under the /info route

Example:

DETAILS_TEMPLATE = '''
This is a detailed description of the challenge:

You will require:

- This
- That

You can also check: {custom_value}

Don't forget: {extra}

Helper:

``` python
def helper():
    pass
```
'''

...

def __init__(self):
    super().__init__()

    self.state = {
        'custom_value': 0x1337
    }


def details(self):
    return DETAILS_TEMPLATE.format(
        **self.state,
        **{
            'extra': "Extra info"
        }
    )

Note

This function will be executed each time the user requests the /info route.

Type:

(bool)

HAS_FILES = False

If the challenge has downloadable files. A “files” function must be defined returning a list of files that will be downloable from the challenge container. Glob patterns can be used (glob):

def files(self):
    return [
        "filter.py",
        "folder/test.sol",
        "folder2/**",
        "folder3/*.sol",
    ]

Tip

You can also create virtual files from strings using StrFile.

Note

Each time the user request the /files route a zip archive with all of the listed files will be downloaded.

Type:

(bool)

HAS_SOLVER = False

If the challenge has a solver. The required function “solver” should be present. Although it is possible to have periodic functions (periodic) that set the solved. You can keep the method defined like this if the latter is being used:

def solver(self):
    pass

Note

This function will be executed each time the user requests the /solved route.

Type:

(bool)

PATH_MAPPING: dict[str, halborn_ctf.templates.MappingInfo] = {}

Mapping used internally to register the challenge URL’s paths. It does contain a mapping of path to MappingInfo dictionary details.

Example

Have the challenge / path expose the anvil service which is running internally on port 8545:

PATH_MAPPING = {
    '/': {
            'host': '127.0.0.1', # optional. Defaults to '127.0.0.1'
            'port': 8545,
            'path': '/', # optional. Defaults to '/'
            'methods': ['POST'] # optional. Defaults to ['GET']
    }
}

A request to http://challenge/ will be proxied to http://127.0.0.1:8545/

Redirect all request to the service running on port 9999 and under /service:

PATH_MAPPING = {
    '/<path:path>': {
            'port': 9999,
            'path': '/service',
            'methods': ['GET', 'POST', 'HEAD']
    }
}

A request to http://challenge/my_path/file will be proxied to http://127.0.0.1:9999/service/my_path/file. But not http://challenge/.

It allows to use filters:

PATH_MAPPING = {
    '/': {
            'port': 8545,
            'path': '/',
            'methods': ['POST'],
            'filter': network.filters.json_rpc.whitelist_methods(['evm_.*']),
    }
}

Note

There is no need to specify any of the required field for the filter such as listen_port, to_port, to_host as those will be extracted from the mapping itself and a random listening port used and remapped.

Type:

(dict[str, MappingInfo])

build()[source]

All the static funtionality that should be executed during the build phase of the challenge container. The running container will have everything executed here pre-bundled as this funcionality is only executed once for all running instances.

Note

At the end of the execution of this function all processes will be killed. Any dynamic funcionality or any code that should be depended to each deployment, dynamic keys, dynamic accounts… should be inserted into run instead.

register_path(path, handler, methods=['GET'])[source]

It does allow to define a custom flask endpoint for your challenge without a service to redirect to using the standard PATH_MAPPING.

Example:

def __init__(self) -> None:
    super().__init__()

    self.state = {
        'name': 'The name'
    }

def custom_handler(self):
    return "HELLO {}".format(self.state.name)

def run(self):
    self.register_path('/', self.custom_handler, methods=['GET'])

Note

You can even provide REST parameters to the path and access them on the handler:

Example:

def custom_handler(self, id):
    return "HELLO {}".format(id)

def run(self):
    self.register_path('/<id>', self.custom_handler, methods=['GET'])

Reference: https://pythonbasics.org/flask-tutorial-routes/

Tip

You can access the request body by importing from flask import request.

abstract run()[source]

All the dynamic funtionallity that should be executed during the creation of a challenge for each player.

The run function should be used to start the actual challenge for the player. Such as running the chain, deploying the contracts (if they have to be done dynamically), starting the services and execute any halborn_ctf.network.filters.

property solved

Returns and allows to set if the challenge is solved or not. Only functional if HAS_SOLVER is set.

Example:

def solver(self):
    ...
    self.solved = True
Type:

(bool)

property solved_msg

Returns and allows to set a message or hint for the player. Only functional if HAS_SOLVER is set. Example:

def solver(self):
    ...
    if self.solved:
        self.solved_msg = "You are the best hacker!"
    else:
        self.solved_msg = "Keep trying :("
Type:

(str)

property state

Extended dictionary to store variables that can be accessed during challenge execution.

The challenge build step will pickle this variable for the run method to have the same state.

Example

Initializing the state:

self.state = {
    'custom': 'Initial value'
}

Updating the state value:

self.state.custom = 'Changed value'

Reading an state value:

print(self.state.custom)
# Changed value
Type:

(State)

property state_public

It will expose the state content into the challenge /info route. Refer to state.

Type:

(State)

class halborn_ctf.templates.MappingInfo[source]

Bases: TypedDict

Dictionary data type to store the details for a path mapping

filter: Callable

One of the valid built-in filters or a generic_filter function.

Type:

(Callable, optional)

host: str

The host to redirect to. Defaults to '127.0.0.1'.

Type:

(str, optional)

methods: list[str]

The allowed methods. Defaults to ["GET"].

Type:

(list[str], optional)

path: str

The path to redirect to. Defaults to '/'.

Type:

(str, optional)

port: int

The port to redirect to.

Type:

(int)

class halborn_ctf.templates.StrFile(filepath: str, content: str)[source]

Bases: object

It allows to create a container for temporary generated files from strings. It can be used on the GenericChallenge.HAS_FILES to add on-the fly files:

Example:

from halborn_ctf.templates import StrFile

def files(self):
    return [
        StrFile('folder/test.txt', 'THIS IS THE CONTENT')
    ]
content: str
filepath: str
class halborn_ctf.templates.Web3Challenge[source]

Bases: GenericChallenge

Class extending the GenericChallenge with GenericChallenge.HAS_SOLVER and GenericChallenge.HAS_FILES both set to True.

FLAG_TYPE = 0

The type of flag. Set to FlagType.NONE if using a solver unless using manual flag input on the CTF platform.

Type:

(FlagType)

HAS_FILES = True

If the challenge has downloadable files. A “files” function must be defined returning a list of files that will be downloable from the challenge container. Glob patterns can be used (glob):

def files(self):
    return [
        "filter.py",
        "folder/test.sol",
        "folder2/**",
        "folder3/*.sol",
    ]

Tip

You can also create virtual files from strings using StrFile.

Note

Each time the user request the /files route a zip archive with all of the listed files will be downloaded.

Type:

(bool)

HAS_SOLVER = True

If the challenge has a solver. The required function “solver” should be present. Although it is possible to have periodic functions (periodic) that set the solved. You can keep the method defined like this if the latter is being used:

def solver(self):
    pass

Note

This function will be executed each time the user requests the /solved route.

Type:

(bool)

abstract files()[source]

Refer to HAS_FILES

abstract run()[source]

All the dynamic funtionallity that should be executed during the creation of a challenge for each player.

The run function should be used to start the actual challenge for the player. Such as running the chain, deploying the contracts (if they have to be done dynamically), starting the services and execute any halborn_ctf.network.filters.

abstract solver()[source]

Refer to HAS_SOLVER

Module contents