If you want to look into all the new features available to you as a payload developer from an agent perspective, check out Agents 2.3 -> 3.0. The rest of this page is for higher-level updates and UI changes.
The main goal of this update was to re-do the entire back-end for Mythic from Python3 to GoLang. Mythic's Python back-end was written during a time when asyncio was starting to gain popularity and as such had a mix of naturally asyncio functionality and some faked asyncio functionality. This combination resulted in a database connectivity bug that would only sometimes show up in unreproducible ways and would make it appear as though all of the containers were offline, despite them all being online.
Since the work to change all of the database connectivity code would have touched almost every single function in conjunction with the core of how Mythic operated, it was the perfect time to just redo the back-end and solve a lot of technical debt as well.
What if you're automatically deploying Mythic 2.3 right now and want to know what changes need to happen on your end to automatically deploy Mythic 3.0? No worries, here are the deployment changes:
mythic-cli
isn't included as part of the repo anymore (2.5MB changing binary is gross). Instead, you need to run sudo make
in the Mythic folder first (that'll create a docker container to build it and then copy it out to the Mythic folder for you automatically).
Mythic no longer does logging/webhooks directly. Instead, if you want logging and webhooks, you need to install those from the C2Profiles GitHub.
A basic webhook container is: basic_webhook
A basic logger container is: basic_logger
If you're scripting anything with Mythic, make sure to update to the latest mythic
pypi package and double check your scripts still work.
Mythic v2.3 used PyPi packages (mythic_payloadtype_container
, mythic_c2_container
, and mythic_translation_container
) and very specific folder layouts to configure and run the various agents, c2 profiles, and translation containers. This became tedious to maintain and resulted in a lot of duplicated effort as the capabilities offered to containers expanded. To address this, now Mythic containers all use mythic_container
as their PyPi package if they're written in Python or github.com/MythicMeta/MythicContainer
package if they're written in GoLang.
This new format also allows a single "container" to have multiple payload types, multiple c2 profiles, translation containers, and even two new kinds of containers.
In an effort to embrace the microarchitecture more, Mythic split out the logging from the main server. Instead, when there is a logging-based event, Mythic will emit that message to a logging container (if one is installed). This allows operators/developers to configure logging to happen however they want. You can write to stdout, to certain files, adjust the format of the messages, you can even use the MythicRPC functionality to enrich the data and log even more. You can even ship the data directly to your SIEM. An example of this is available at https://github.com/MythicC2Profiles/basic_logger
.
Continuing the effort of the microarchitecture, Mythic split out webhooks. Right now there are three webhook-able events - new callbacks, mythic starting up, and "feedback". You can configure global settings for these in the Mythic UI with a single webhook URL and channel, but you can also configure the container with per-webhook type custom webhook URLs, channels, and the message itself. Similar to the logging, the webhook simply listens on a RabbitMQ queue for messages and then does something with them. What happens with them is entirely under your control, so you can adjust everything and post to entirely different services if you want. An example of the GoLang version of this logger is available at https://github.com/MythicC2Profiles/basic_webhook
.
The number and kinds of webhook events will expand over time.
The rewrite also included a few new agent features.
There's a decision that can be made about where "crypto" lives for an agent. Is the crypto part of the agent core, or is it specific to the c2 profile? Historically, Mythic only supported configuring crypto as part of a c2 profile. Now though, you can mark crypto_type=true
with a Payload Type's build parameters and supercede a c2 profile's cryptography. This gives the agent developer a higher degree of customizability than existed before.
A few new types of parameters were added for building payloads - Dictionaries, Arrays, Numbers, and Dates. Now, any type of parameter that exists for a C2 profile parameter also exists for a build parameter.
One of the tough things about agent development and usage is that the build process could potentially take a while. However, from the UI, the operator simply sees a spinning circle and has no indication of what's going on - did the container crash? is it still building? how far along in the build process is it?
To help with this, Payload Types can now declare "build steps" as part of their Payload Type definition and during the build process they can report back to Mythic that a step is done either successfully or with error and provide both stdout and stderr to go along with it. This makes it a lot easier to see where in the build process you're hitting an error.
Each one of those steps are clickable and provide detailed information about what's going on along with how long each step took. A Payload's detailed information also shows more detail about the step itself, like the description of what's going on.
Mythic now provides an additional services - Jupyter Notebooks. From the hamburger icon in the top left, if you click on "Services", you'll see the services available to you.
Jupyter Notebooks, /jupyter
, provide a handy, persistent way to allow scripting without requiring operators to get set up on their own environment. Right now it's a single, shared instance for all of Mythic (not per operation), but if it gains traction an people really like it, there's a beefier version that allows multi-user sign-ins that can be used instead.
This will slowly get a library of common scripting examples that you can draw from when creating your own scripts for your operations. This also provides a much nicer interface for testing and scripting than pulling code from a wiki page and hoping it's updated.
The password
is mythic
.
This will open a new tab to /console
where you can interact with Hasura - the provider for the GraphQL component of Mythic. All of the web UI and scripting goes through Hasura, so this provides a great way to test out custom GraphQL queries and see what all you can do.
When you go here you'll be prompted for a credential to log in - use sudo ./mythic-cli config get hasura_secret
to get the password to use.
If you generate an API token for yourself via your settings page in the Mythic UI, you can supply that token as shown above so you can see exactly what your account is able to do as if you logged in via scripting.
This provides a single page to look at all of the possible logging and webhook message types that can be emitted and allows you to send test messages. This is particularly handy if you're writing/modifying a logging/webhook container and want to make sure that your messages are going through properly.
As much as possible, operators should be operating. It's very annoying and potentially time consuming if you have to stop what you're doing and record somewhere that you ran into an issue, that you got caught, or even just that something is weird with a tool you're using. To try to help with this, Mythic now has a "feedback" button at the top of the screen (the thumbs down icon).
You can click this at any time and submit to a webhook information for a bug you encountered, record a deconfliction event, submit feedback about something confusing, or even recording a feature request. You must have a webhook container running, like the one from https://github.com/MythicC2Profiles/basic_webhook
and configure a webhook for your operation, but then it's super easy to record this sort of stuff and continue on with your operation. Then, when your op is done, you can go back and file github issues, feature requests, ask for UI tweaks about things that were confusing, or even having an easy timeline of when you got caught. Because this data is sent to the webhook containers, you can take the data and do whatever you want with it. You don't technically have to do a webhook - you could turn it directly into a github issue or just save it off as a note somewhere for you to visit later.
Tagging tasks with additional information has been around for a while in Mythic, but it got a bigger facelift this time around. By clicking the tag icon at the top of the screen, you can view all of your current tag types for the operation.
Tag types consist of a description, a short-hand display, and a color. The color you set will show both the light mode and dark mode so that you can be sure to pick a color that'll show properly regardless of what users prefer.
Once you have tag types created, you can use them in your operation to tag various things with more information. This is most useful in combination with scripting to "auto tag" things, but you can manually tag everything as well. Right now it just helps you see things as your're scrolling through the UI, but soon there will be a dashboard that gives overviews and more detailed information about all of the things that were tagged.
For Tasks, Files, Credentials, Keylogs, Processes, and the FileBrowser, you can click on the tag icon to edit/add tags. This allows you to provide more context to the generic tag type. For example - you can have a tag type of cred
and if you find credentials in a file, you can then tag that file with the cred
tag so it's easier for other people to know (and you to remember) that you pulled creds from that file.
When you create a new tag you select the type of tag, supply your own source of where the data is coming from (some operator, some automated script, etc). You can supply an external-linkable URL for more information or maybe even the target of where the data applies. The JSON data doesn't technically have to be valid JSON, but if it is, then when you click to view the tag it'll be automatically parsed into a nice table.
you can see the short-hand tag displayed next to the file to let you know that it's been tagged. When you click on the tag cred
now instead of the tag icon itself, you see the data that was supplied.
Sometimes, especially during development, you want to test out a new feature or expose a new function within Mythic for a payload type. Historically, that meant you either needed to already have a load
command created, or you'd have to create an entirely new payload, execute it, then test out your new function. That can be a headache and impractical, especially if it's a script_only
command that you want to make available to your callbacks now.
To facilitate this, Mythic now allows you to manually adjust which commands are available in your payloads and callbacks through the UI.
This does NOT actually adjust anything within the payload itself, nor does it adjust anything within a running callback. This simply adjusts Mythic's perception of which commands are available. As such, if you use this to add commandX, but it's not actually part of your payload or callback, then your agent won't know what to do with the information. In that case, you'd still need a proper load command to send the new command down to your agent.
For payloads, you can select the blue "info" icon next to your payload and scroll down to the "commands" area. You'll see a new button called "Add/Remove Commands":
Clicking on that will open up a new dialog box where you can select which commands to add and remove:
The same flow is available for Callbacks - click the blue down arrow next to an active callback and select the "View Metadata" entry, then scroll down to the loaded commands.
With Mythic 3.0, you have two options for your containers - GoLang or Python. This section will only go over the Python side since there are no 2.3 agents with GoLang containers.
When you git clone the new Mythic v3.0.0 you'll notice that there's no mythic-cli
binary. To reduce the size of the GitHub clones, this binary is now included as part of the main base docker image, so run sudo make
and the binary will be downloaded and copied into the normal spot.
The Apfell v3.0.0 (https://github.com/MythicAgents/apfell/tree/v3.0.0) branch and the Apollo v3.0.0 (https://github.com/MythicAgents/apollo/tree/v3.0.0) branch are good examples of two slightly different ways you can format things.
Make sure you update your Docker version to at least 20.10.22
or above (current is 23.0.1
. This is required for the latest docker containers to work properly. A simple sudo apt upgrade
and install should suffice. Also install docker-compose via sudo apt install docker-compose-plugin
vs the docker-compose
script as the script will soon be deprecated according to Docker.
The ExternalAgent
format is still the same. These next pieces are how you can test updates of your agent locally before copying your agent's folder back into your normal ExternalAgent format.
If you're doing local development, you need at least Python version 3.10 because of the new typing features it offers.
Make a directory, agent name
, in Mythic/InstalledServices
Copy your entire Payload type's agent name
directory into InstalledServices
(yes, the path will look like Mythic/InstalledServices/agentName/agentName
)
In Mythic/InstalledServices/agentName
create a main.py
and a Dockerfile
In your new Dockerfile
, copy the contents of your old Dockerfile
and change the FROM
line to FROM itsafeaturemythic/mythic_python_base:latest
In your new main.py
add:
import mythic_container
from [agent name].mythic import *
mythic_container.mythic_service.start_and_run_forever()
To make your new agent name
directory a PyPi package that can be imported, create a __init__.py
file in Mythic/InstalledServices/agentName/agentName
. In the Mythic/InstalledServices/agentName/agentName/Mythic
folder make a __init__.py
file with the following contents (this will loop through all of your command files and import them automatically):
import glob
import os.path
from pathlib import Path
from importlib import import_module, invalidate_caches
import sys
# Get file paths of all modules.
currentPath = Path(__file__)
searchPath = currentPath.parent / "agent_functions" / "*.py"
modules = glob.glob(f"{searchPath}")
invalidate_caches()
for x in modules:
if not x.endswith("__init__.py") and x[-3:] == ".py":
module = import_module(f"{__name__}.agent_functions." + Path(x).stem)
for el in dir(module):
if "__" not in el:
globals()[el] = getattr(module, el)
sys.path.append(os.path.abspath(currentPath.name))
In your Mythic/InstalledServices/agentName/agentName/mythic/agent_functions
files, we need to replace all mythic_payloadtype_container
with mythic_container
.
If you have an import like from agent_functions.execute_pe import PRINTSPOOFER_FILE_ID
which references another command file, update it to from .execute_pe import PRINTSPOOFER_FILE_ID
. If you include a local library at the same level as agent_functions
, you can import it like from [agent name].mythic.[package] import [thing]
If you're doing local development, you'll need a rabbitmq_config.json
file at the same level as your main.py
to tell your service where Mythic is located and the rabbitmq password. The configuration options you can supply can be found in the Local Development section.
There are some changes to the rabbitmq_config.json
file keys:
container_files_path
is no longer used and can be deleted.
username
is no longer used and can be deleted.
password
is now rabbitmq_password.
host
is now rabbitmq_host.
name
is no longer used and can be deleted.
virtual_host
is no longer used and can be deleted.
Now to actually update the content of your builder/command files. There's not much you need to do.
Because the new structure treats your entire agent directory as a Python package, the container no longer knows the paths for things. This gives you a lot more freedom in how you want to organize your code, but does require you to specify where things are located. In your builder.py
file where you define your Payload Type, you need to add the following:
agent_path = pathlib.Path(".") / "apollo" / "mythic"
agent_code_path = pathlib.Path(".") / "apollo" / "agent_code"
agent_icon_path = agent_path / "agent_functions" / "apollo.svg"
The agent_path
is the path to your general agent structure (typically with the agent_functions
as a sub-folder. The agent_code_path
points to your agent's actual code.
Something that's a little different is the agent icons - the agents will sync that over automatically with the rest of their definition (no more having to copy it over manually or get it from an install). What that means though is you either need to supply agent_icon_path
and provide the path to your agent's svg icon or specify agent_icon_bytes
and provide the raw bytes for your icon.
In your payload type's build function you can report back on build steps via the SendMythicRPCPayloadUpdateBuildStep
RPC call (based on your defined build steps). This will update the UI step-by-step for the operator so they know what's going on.
You can also set UpdatedFilename
(or updated_filename
for Python) in your build response and adjust the final filename of the payload. This can be helpful if your payload type allows you to build to various outputs (exe, dll, dylib, binary, etc). This allows you to adjust the filename based on that so that when the user clicks "download" in the UI, they get the right file and don't have to change the filename.
Browserscripts work just the same, but browserscripts will look for their code at agent_path / browser_scripts / filename.js
OR at the path specified by the name
parameter for the script. So, that means your can either specify the name as test.js
and have it located in your agent_path / browser_scripts / test.js
file or specify a full path as your name.
The browser_script attribute is a single BrowserScript value, not an array. This is because the entire Python back-end is gone, so there's no more need to supply a script for the old UI and the new UI.
When looping through c2 profile parameters - arrays are actually arrays, crypto types and dictionary types are dictionaries, so do better checking here for name of parameters. A bunch of agents simply check if the supplied value is a dictionary and then automatically try to pull out certain values, but that might not be the case anymore. For example, when looping through the http
profile, both the AESPSK
and the headers
parameters will be passed in as dictionaries.
for key, val in c2.get_parameters_dict().items():
if key == "AESPSK":
c2_code = c2_code.replace(key, val["enc_key"] if val["enc_key"] is not None else "")
elif not isinstance(val, str):
c2_code = c2_code.replace(key, json.dumps(val))
else:
c2_code = c2_code.replace(key, val)
The current create_tasking
functions still work just like normal; however, the newer create_go_tasking
function gives you more contextual data and mirrors the data structures from the new Golang container version.
async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse:
response = MythicCommandBase.PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
Success=True,
)
return response
This taskData
variable is defined here: https://github.com/MythicMeta/MythicContainerPyPi/blob/main/mythic_container/MythicCommandBase.py#L1068 and provides a lot more context in a well-defined class.
Tasks can specify for a certain function to execute when the task finishes executing. That hasn't changed. However, the format of how you define it has changed slightly. Before, you'd simply pass the name of a function and the container would loop through all known function definitions looking for one that matched. That's not super great, so now you define a dictionary of function name to function as part of your command definition.
completion_functions: dict[str, Callable[[PTTaskCompletionFunctionMessage], Awaitable[PTTaskCompletionFunctionMessageResponse]]] = {}
The PTTaskCompletionFunctionMessage and response classes can be found in the PyPi code and auto-completed via IDEs. This syntax is just the Python way of saying that the format is:
async def functionName(myArg: PTTaskCompletionFunctionMessage) -> PTTaskCompletionFunctionMessageResponse:
do something here
To leverage this new functionName
function as part of your tasking, in your create_tasking
function you need to set the name:
async def create_tasking(self, task: MythicTask) -> MythicTask:
task.completed_callback_function = "functionName"
return task
If you're using the new create_go_tasking
function, then you need to do somthing very similar:
async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse:
response = MythicCommandBase.PTTaskCreateTaskingMessageResponse(
TaskID=taskData.Task.ID,
CompletionFunctionName="functionName"
)
return response
Sending back data via the process_response
key within your responses
allows you to hook into the associated command's process_response
function within your Payload Type's container. The format of this function has changed slightly:
old:
async def process_response(self, response: AgentResponse):
resp = await MythicRPC().execute("update_callback", task_id=response.task.id, sleep_info=response.response)
new:
async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
await MythicRPC().execute("update_callback", task_id=task.Task.ID, sleep_info=response)
return resp
Similar to the completion functions, dynamic query functions look a little different, but are generally still the same:
dynamic_query_function: Callable[[PTRPCDynamicQueryFunctionMessage], Awaitable[PTRPCDynamicQueryFunctionMessageResponse]] = None,
which is to say that the function is pre-defined (one per command parameter) and looks like:
async def dynamic_query_function(myArg: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse:
do something
The opsec functionality has been removed from a special CommandOPSEC class and moved to the main command class itself. So, your command can have two additional functions:
async def opsec_pre(self, taskData: PTTaskMessageAllData) -> PTTTaskOPSECPreTaskMessageResponse:
response = PTTTaskOPSECPreTaskMessageResponse(
TaskID=taskData.Task.ID, Success=True, OpsecPreBlocked=False,
OpsecPreMessage="Not implemented, passing by default",
)
return response
async def opsec_post(self, taskData: PTTaskMessageAllData) -> PTTTaskOPSECPostTaskMessageResponse:
response = PTTTaskOPSECPostTaskMessageResponse(
TaskID=taskData.Task.ID, Success=True, OpsecPostBlocked=False,
OpsecPostMessage="Not implemented, passing by default",
)
return response
The RPC call to start SOCKS is no longer control_socks
. Instead, you'll use the SendMythicRPCProxyStart
and SendMythicRPCProxyStop
commands as detailed here.
C2 profiles also need to be updated for Mythic 3.0.0, in an extremely similar way to Payload Types.
Make a directory, c2 name
, in Mythic/InstalledServices
Copy your entire C2 Profile's c2 name
directory into InstalledServices
(yes, the path will look like Mythic/InstalledServices/c2Name/c2Name
)
Remove c2_service.sh
, mythic_service.py
, and rabbitmq_config.json
from your mythic
folder
Remove C2_RPC_Functions.py
In Mythic/InstalledServices/c2Name
create a main.py
and a Dockerfile
In your new Dockerfile
, copy the contents of your old Dockerfile
and change the FROM
line to FROM itsafeaturemythic/mythic_python_base:latest
In your mythic/c2_functions/
folder, your definition file should import mythic_container
instead of mythic_c2_container
(similar to what we did for agent updates).
In your new main.py
add:
import mythic_container
from [c2 name].mythic import *
mythic_container.mythic_service.start_and_run_forever()
In your c2 profile definition, add in two more attributes - server_folder_path
(path to the folder where your server binary and config.json files exist), and server_binary_path
(path to the binary to execute if you're doing an egress c2 profile and not a p2p profile).
To see what this looks like all together, look at the websocket
example here: https://github.com/MythicMeta/ExampleContainers/tree/main/Payload_Type/python_services. You'll notice that the websocket
is just one of multiple services that the single docker contianer is offering. If you want your container to only offer that one, then you can remove the other folders and adjust your main.py
accordingly.
Keys in the C2 Profile Parameter Type Dictionary
will be sorted alphabetically - they will NOT maintain the order they were specified in the UI. This is currently a limitation of the Golang Google JSON library.
Mythic provides a MYTHIC_ADDRESS
environment variable that points to http://mythic_server:17443/agent_message
for C2 Profiles to use for forwarding their messages. With Mythic 3.0+, there are going to be more options for connections outside of a static HTTP endpoint. Therefore, the MYTHIC_ADDRESS
field exists, but there's additional values for MYTHIC_SERVER_HOST
and MYTHIC_SERVER_PORT
so that we can dynamically use these later on.
Translation containers are no different than C2 Profiles and Payload Types for the new format of things. Look to translator
in the ExampleContainers (https://github.com/MythicMeta/ExampleContainers/tree/main/Payload_Type/python_services) repository for an example of how to format your new structure. Translation containers boil down to one class definition with a few functions.
One big change from Mythic 2.3 -> 3.0 for Translation Containers is that they now operate over gRPC instead of RabbitMQ. This means that they need to access the gRPC port on the Mythic Server if you intend on running a translation container on a separate host from Mythic itself. This port is configurable in the Mythic/.env
file, but by default it's 17443. This change to gRPC instead of RabbitMQ for the translation container messages speeds things up and reduces the burden on RabbitMQ for transmitting potentially large messages.
Some additional notes about Translation container message updates:
Although Mythic 3 will base64 decode a message before providing it to translate_from_c2_format, Mythic 3 will not base64 encode the result of translate_to_c2_format which you will still need to do like you would have for Mythic 2.3
Mythic 2.3 allowed UUID prefixes to custom agent messages to a little endian encoded 16 byte value. In Mythic 3 any 16 byte UUID prefix needs to be big endian encoded
Mythic 2.3 required the translation container to base64 encode/decode inputs and outputs for generate_keys. Mythic 2.3 would directly use that base64 data to populate enc_key or dec_key values for building and would provide that base64 data directly to any translate_to_c2_format and translate_from_c2_format functions.
Mythic 3 expects generate_keys to provide the keys as byte arrays. Mythic 3 will base64 encode/decode the byte arrays when populating any enc_key or dec_key value for an agent configuration, but will use the byte array when calling any translate_to_c2_format and translate_from_c2_format function
Mythic 2.3 would provide the entire message as input to translate_from_c2_format. Mythic 3 provides the message, minus any UUID prefix