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 fromsys.argv.This function can be used as entry point for the
halborn_ctfchallenges.The allowed flags are:
[METHOD]: The method to execute. Only ‘build’ and ‘run’ are allowed. Valids arebuild, 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
runfrom thechallenge.pyfile and the class namedChallengein debug mode:halborn_ctf run -vv
Executing method
buildfrom thefile.pyfile and the class namedChallengeCustom: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
everyparameter 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
cmdon as a subprocess on a shell.An exam
Example
Example of executing
lsand getting the output and error:process, stdout, stderr = run("ls -la", capture_output=True) print(stdout) print(stderr)
Example of installing
forgein the/usrdirectory:run('curl -L https://foundry.paradigm.xyz | bash', env={"FOUNDRY_DIR": '/usr'}) run('foundryup', env={"FOUNDRY_DIR": '/usr'})
Example of executing
anvilin 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
PATHenvironment variable for the shell to find it. The default executed shell corresponds to the/bin/shand there is not way to have awareness on.bashrcor any other.rcfile unless manually loaded. If enviroment variables are required they can be provided through the**kwargsparameter that accepts the same arguments assubprocess.Popenbackground (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
backgroundandcapture_outputare set this error is raised.AttributeError – If any argument that is internally used to spawn
subprocess.Popenis specified on thekwargs, this error is raised.
- Returns:
- process: (
subprocess.Popen) The process opened, you can use all methods of the
popen-objects.
- process: (
- stdout: (str or None)
A decoded string of the
stdoutof the executed process. Only ifcapture_outputis set toTrue. OtherwiseNoneis returned
- stderr: (str or None)
A decoded string of the
stderrof the executed process. Only ifcapture_outputis set toTrue. OtherwiseNoneis returned
- Return type:
halborn_ctf.state module
- class halborn_ctf.state.State(*args, **kw)[source]
Bases:
dictWrapper 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
sourcemust 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
ChallengeGenericand 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_ctfCLI (halborn_ctf.cli.run):halborn_ctf run -vv
Server can be accessed under
localhost:8080by 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
FLAGenvironment 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:
ABCGeneric CTF challenge template
Each created/deployed challenge does have two steps defined,
buildandrun. Thebuildstep is only executed once when the challenge is created and uploaded into the platform for playing and it is optional. Therunstep 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_MAPPINGattribute.An attribute named
statecan be used to store any sort of object that will persiste between thebuildandrunsteps. 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 asreadyandstate_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 byCHALLENGE_NAME.
Note
Only if
HAS_FILES==True.- FLAG_TYPE = 0
The type of flag. Set to
FlagType.NONEif 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
statevariable or any dynamic content (for example a file content).The string does support
Markdownsyntax and will be displayed on the platform UI and under the/inforouteExample:
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
/inforoute.- 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 thesolved. 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
/solvedroute.- 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
pathtoMappingInfodictionary details.Example
Have the challenge
/path expose the anvil service which is running internally on port8545: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
9999and 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_hostas 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
runinstead.
- 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'])
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
runfunction 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 anyhalborn_ctf.network.filters.
- property solved
Returns and allows to set if the challenge is solved or not. Only functional if
HAS_SOLVERis 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_SOLVERis 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
buildstep will pickle this variable for therunmethod 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)
- class halborn_ctf.templates.MappingInfo[source]
Bases:
TypedDictDictionary data type to store the details for a path mapping
- class halborn_ctf.templates.StrFile(filepath: str, content: str)[source]
Bases:
objectIt allows to create a container for temporary generated files from strings. It can be used on the
GenericChallenge.HAS_FILESto add on-the fly files:Example:
from halborn_ctf.templates import StrFile def files(self): return [ StrFile('folder/test.txt', 'THIS IS THE CONTENT') ]
- class halborn_ctf.templates.Web3Challenge[source]
Bases:
GenericChallengeClass extending the GenericChallenge with
GenericChallenge.HAS_SOLVERandGenericChallenge.HAS_FILESboth set toTrue.- FLAG_TYPE = 0
The type of flag. Set to
FlagType.NONEif 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 thesolved. 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
/solvedroute.- Type:
(bool)
- abstract run()[source]
All the dynamic funtionallity that should be executed during the creation of a challenge for each player.
The
runfunction 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 anyhalborn_ctf.network.filters.
- abstract solver()[source]
Refer to
HAS_SOLVER