Input, output, and flow control
Basics for input, output and flow control. Refer to tenlib.flow
's documentation for details.
Input
Receiving arguments using sys.argv
or argparse
can get tedious, especially
when you just want to run a quick program.
Program arguments
To avoid this, the arguments to the main function are mapped to command-line arguments automatically.
from ten import *
@entry
def main(a, b):
print('a:', a)
print('b:', b)
main()
You'd call the program like so:
$ ./program.py testA testB
a: testA
b: testB
Argument types
Another annoying thing when fetching arguments is typing: what if I want an argument to be an integer ? Well, type-hinting is supported.
@entry
def main(a: int, b: int):
c = a + b
print(c)
You can even use lists:
# Note: list[int] requires py3.10 -- For earlier versions, use typing.List[int]
@entry
def main(all_numbers: list[int]):
c = sum(all_numbers)
print(c)
$ ./program.py 1 2 3 4
10
Optional arguments
If your input argument has a default value, its expected type will be the same as the default value, and it will become an optional argument.
@entry
def main(some_arg, some_number=3, some_string='test'):
print('some_arg:', repr(some_arg))
print('some_number:', repr(some_number))
print('some_string', repr(some_string))
The two last arguments will be optional, as can be seen using the --help
command.
$ ./program.py -h
usage: program.py [-h] [-s SOME_INT] [-S SOME_STR] some_arg
positional arguments:
some_arg
optional arguments:
-h, --help show this help message and exit
-s SOME_INT, --some-int SOME_INT
-S SOME_STR, --some-str SOME_STR
Example call:
$ ./program.py some_value -s 123
some_arg: 'some_value'
some_number: 123
some_string: 'test'
Entry class
For more complex scripts, you might want to use an object instead of a single function. To do so, apply the @entry
decorator to the class instead. It'll get instanciated with the program arguments, and its run()
method will be called.
from ten import *
@entry
class Exploit:
def __init__(self, url: str, workers: int):
self.url = url
self.workers = workers
def run(self):
...
Exploit()
Generally, the __init__
function only sets values from the arguments. Therefore, @entry
synergizes well with @dataclass
:
from ten import *
from dataclasses import dataclass
@entry
@dataclass
class Exploit:
url: str
workers: int
def run(self):
session = ScopedSession(url, workers)
...
Exploit()
Documentation
Entrypoint arguments can be documented using the @arg
decorator.
@entry
@arg("url", "Target URL")
@arg("proxy", "Proxy URL")
@arg("max_connections", "Maximum number of connections")
def main(url, proxy=None, max_connections=10):
"""CLI documentation for the program.
"""
The --help
command will display the documentation:
/tmp/l1 --help
Usage: l1 [-h] [-p PROXY] [-m MAX_CONNECTIONS] url
CLI documentation for the program.
Positional Arguments:
url Target URL
Options:
-h, --help show this help message and exit
-p, --proxy PROXY Proxy URL
-m, --max-connections MAX_CONNECTIONS
Maximum number of connections
Output
Spamming print
s works, but it can get hard to read. Use the msg_*
functions to display easy to read, identifiable messages.
Basics
msg_info('This is an informational message')
msg_success('This message indicates a success')
...
msg_failure('This message indicates a failure.')
This displays the line with a prefix:
Ten's output is handled by rich
library, so you can use its built-in style edition features.
msg_info(f'Found this value: [blue]{value}[/]')
msg_failure(f'This value is not valid: [red b]{value}[/]'')
Changing the display style
A few other output styles were implemented. Use set_message_formatter()
to
set the style you prefer:
set_message_formatter("Oldschool")
# OR ...
set_message_formatter("Slick") # personal favourite
those two would yield:
Flow control
Pausing
You can pause execution at any time using pause()
.
Exiting early
Oftentimes in exploits, you want to display a message, then exit, because something didn't go as planned.
You can do it in one line using:
leave('Exploit done, exiting.')
If the reason for the early exit is a failure or an error, you can use the eponym functions:
# We want ints only ! a stack trace will be displayed.
if not instance(value, int):
error('Value should be of type int: {!r}', value)
# Cannot exploit with value < 0. Exits, but no stack trace displayed
if value < 0:
failure('Value is inferior to zero, unexploitable.')
Assumptions
The same as the assert
builtin, but the message gets displayed using msg_failure()
, and no stack trace is displayed.
assume(value >= 0, f'Value is inferior to zero: {value}')
inform: Status of your functions
Say you have a slow function in your code. You'd generally end up with something like this:
def slow_func():
print('Starting slow_func, might be slow...')
# Actually getting stuff done
...
if result:
print('Result OK !')
else:
print('Result not OK !')
return result
With inform
, you can let ten handle the output. go
gets displayed along with a spinner while the function is running. Then, a different message gets displayed in function of the result.
@inform(
go="Starting slow_func, might be slow...",
ok="Result OK !",
ko="Result not OK !",
)
def slow_func():
# Actually getting stuff done
...
Use the {result}
format to incorporate the result in the success/failure message:
@inform(
"Counting links in page...",
"Found {result} links in page",
"No links were found.",
)
def count_links(url):
r = session.get(url)
links = r.select("a")
return len(links)
For single-function programs, you can combine it with @entry
:
@entry
@inform("Downloading sources...", "Downloaded {result} source files.")
def main(url):
...
main()