Scripting
How to use the Scripting API
What is Mythic scripting?
The main Mythic server Docker container is based around WebSockets and RESTful API endpoints. We can hit the same RESTful endpoints and listen to the same WebSocket endpoints that the main browser user interface uses as part of scripting, which means scripting can technically be done in any language.
Where is it?
In the /Mythic/Mythic_CLI
folder is a mythic.py
file that serves as the main mythic API we can use for scripting. This is an async, python3 file that we can import and leverage in scripts. The rest of this page goes into those functions and how to leverage them. The scripting has only a handful of required python3 packages that are located in the requirements.txt
file in the same folder.
Be sure to run pip3 install -r requirements.txt
.
Mythic CLI is now available as a PyPI package! Install via pip3 install mythic
If you're using the PyPI package, the RESTful interfaces that mythic currently uses are located under mythic_rest
, so do the following:
The PyPI package specifically refers to mythic_rest
because there are likely to be other interfaces in the future (such as graphql).
Scripting base
The scripting base is pretty simple:
Line 1 imports all of the functions from the mythic api into the script. From here, there's two functions we write:
main()
- this function calls the scripting function asynchronously (await scripting()
), then loops through all functions sitting on the current event loop until everything is done. This is what allows you to eventually hook into the WebSocket eventing and wait for notifications.scripting()
- this is the initial function where you write your code.
In the scripting function, we do a few things:
The first thing is to create an instance of the
Mythic
server. This includes the credentials we use to log in and information about the server itself. Theglobal_timeout
is an optional parameter to globally provide timeouts if no others are provided. If theglobal_timeout
is set to anything less than 0, then the program will wait indefinitely.Once this object is created, the next thing is to log into mythic with
await mythic.login()
. This sends the credentials over the connection and gets back the standard JWT access_token and refresh_tokens.The next standard thing to do is to do
await mythic.set_or_create_apitoken()
. Because dealing with JWT access tokens is annoying (they have short timestamps) and trying to make sure you properly deal with refresh tokens in scripting is error prone, there's a helper function to create a user level API token. These API tokens show up in yoursettings
page and can be deactivated or deleted. The advantage of these tokens is that while they're active, they don't expire. This is very useful for long running tasks. Doingset_or_create_apitoken()
is a helper function to get and potentially create a user-level API token and set it on your currentmythic
instance.
If you've already created an API token that you'd like to use instead of supplying your username and password, you can create the mythic
object with the following:
Because you already have the apitoken set, there's no need for additional calls to login
or set_or_create_apitoken
.
Scripting Principals
The Mythic scripting file tries to make everything into classes/objects so that it's easier to use with an IDE than just python dictionaries everywhere. This makes it closely resemble the Mythic database (although there's a lot more to the database than what's captured in the scripting so far).
Everything call to mythic that doesn't start with listen
will return a MythicResponse
class object. This allows you to properly inspect the .status
of the query ( success
or error
) as well as the .response_code
(the web response code from the query). The actual response object(s) is in the .response
component. For example:
the json_print
function allows you to easily print Mythic class objects as JSON. If you just want to access it as JSON data (i.e. not printing it), then each object has a .to_json()
function.
Listening for Notifications
So far, the components have been doing RESTful API calls directly. But one of the more interesting aspects of scripting is to listen for specific notification events and reacting to them. In Mythic, these are all indicated with function names starting with listen_for
. These functions all have the same general format:
If a callback_function
isn't supplied, then the resulting data is simply printed to the screen. The timeout can be used for indicating how long you want to listen. Let's see how this can be used:
This example waits for new callbacks, and when a new one is recognized by Mythic, the issue_shell_whoami
function defined on lines 14-25 is executed. These callback functions always have 2 parameters - the mythic instance associated with the data and a STRING
representation of the data. To get back to a python dictionary, run json.loads(data)
. At this point, you can either deal with data as a dictionary directly (as shown in the above code), or you can cast it back to the right object. In the above example, we could do callback = Callback(**json.loads(data))
which would result in callback
being a Callback
object instead of a python dictionary.
Troubleshooting
When scripting, you might run into issues, so it's important to know how to troubleshoot what's going on. Every function returns a MythicResponse object. All Mythic specific objects (Responses, Tasks, Operators, etc) can be printed with await json_print(object here)
. This provides a clean way to see the output that you actually got back. The reason everything returns a MythicResponse
instead of just the object is so that we can capture HTTP response codes, raw output, and the Object casted types. This allows you to more easily manipulate and see data you get back via scripting.
Last updated