Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
This page describes the format for getting new tasking
The contents of the JSON message from the agent to Mythic when requesting tasking is as follows:
There are two things to note here:
tasking_size
- This parameter defaults to one, but allows an agent to request how many tasks it wants to get back at once. If the agent specifies -1
as this value, then Mythic will return all of the tasking it has for that callback.
delegates
- This parameter is not required, but allows for an agent to forward on messages from other callbacks. This is the peer-to-peer scenario where inner messages are passed externally by the egress point. Each of these agentMessage is a self-contained "Agent Message" and the c2_profile
indicates the name of the C2 Profile used to connect the two agents. This allows Mythic to properly decode/translate the messages even for nested messages.
get_delegate_tasks
- This is an optional parameter. If you don't include it, it's assumed to be True
. This indicates whether or not this get_tasking
request should also check for tasks that belong to callbacks that are reachable from this callback. So, if agentA has a route to agentB, agentB has a task in the submitted
state, and agentA issues a get_tasking
, agentA can decide if it wants just its own tasking or if it also wants to pick up agentB's task as well.
Why does this matter? This is helpful if your linked agents issue their own periodic get_tasking
messages rather than simply waiting for tasking to come to them. This way the parent callback (agentA in this case) doesn't accidentally consume and toss aside the task for agentB; instead, agentB's own periodic get_tasking
message has to make its way up to Mythic for the task to be fetched.
Mythic responds with the following message format for get_tasking requests:
There are a few things to note here:
tasks
- This parameter is always a list, but contains between 0 and tasking_size
number of entries.
parameters
- this encapsulates the parameters for the task. If a command has parameters like: {"remote_path": "/users/desktop/test.png", "file_id": "uuid_here"}
, then the params
field will have that JSON blob as a STRING value (i.e. the command is responsible to parse that out).
delegates
- This parameter contains any responses for the messages that came through in the first message.
This get_tasking
request CAN also include a responses
field, socks
, rpfwd
, edges
, alerts
, and interactive
fields. This means you can technically only do checkin
and get_tasking
messages since you can forward responses in this message. The reason for this is you might not want to have to send TWO messages per sleep interval - ex: you don't want to post the response from an output and make a get_tasking
request back-to-back, but you also don't want to not do get_tasking
requests while you're periodically sending task responses back.
The main difference between submitting a response with a post_response
and submitting responses with get_tasking
is that in a get_tasking
message with a responses
key, you'll also get back additional tasking that's available. With a post_response
message and a responses
key, you won't get back additional tasking that's ready for your agent. You can still get socks
, rpfwd
, interact
, and delegates
messages as part of your message back from Mythic, but you won't have a tasks
key.
The contents of the JSON message from the agent to Mythic when posting tasking responses is as follows:
There are two things to note here:
responses
- This parameter is a list of all the responses for each tasking.
For each element in the responses array, we have a dictionary of information about the response. We also have a task_id
field to indicate which task this response is for. After that though, comes the actual response output from the task.
If you don't want to hook a certain feature (like sending keystrokes, downloading files, creating artifacts, etc), but just want to return output to the user, the response section can be as simple as:
{"task_id": "uuid of task", "user_output": "output of task here"}
Each response style is described in Hooking Features. The format described in each of the Hooking features sections replaces the ... response message
piece above
To continue adding to that JSON response, you can indicate that a command is finished by adding "completed": true
or indicate that there was an error with "status": "error"
.
delegates
- This parameter is not required, but allows for an agent to forward on messages from other callbacks. This is the peer-to-peer scenario where inner messages are passed externally by the egress point. Each of these messages is a self-contained "Agent Message".
Anything you put in user_output
will go directly to the user to see. There's no additional processing that happens. If you want to perform additional processing on the response, then instead of user_output
use the process_response
key. This will allow you to perform additional processing on whatever is passed through the process_response
key - from here, if you want to register something for the user to see, you'll need to use MythicRPCCreateResponse (you can use any MythicRPC at this point to register files, create credentials, etc).
Mythic responds with the following message format for post_response requests:
If your initial responses
array to Mythic has something improperly formatted and Mythic can't deserialize it into GoLang structs, then Mythic will simply set the responses
array going back as empty. So, you can't always check for a matching response array entry for each response you send to Mythic. In this case, Mythic can't respond back with task_id
in this response array because it failed to deserialize it completely.
There are two things to note here:
responses
- This parameter is always a list and contains a success or error + error message for each task that was responded to.
delegates
- This parameter contains any responses for the messages that came through in the first message
This message format also can take in socks
, rpfwd
, interact
, alerts
, edges
, and delegates
keys with their data as well. Just like with the get_tasking
message, you can send all of that data along with each message.
This page describes how an agent message is formatted
All messages go to the /agent_message
endpoint via the associated C2 Profile docker container. These messages can be:
POST request
message content in body
GET request
message content in FIRST header value
message content in FIRST cookie value
message content in FIRST query parameter
For query parameters, the Base64 content must be URL Safe Encoded - this has different meaning in different languages, but means that for the "unsafe" characters of +
and /
, they need to be swapped out with -
and _
instead of %encoded. Many languages have a special Base64 Encode/Decode function for this. If you're curious, this is an easy site to check your encoding: https://www.base64url.com/
message content in body
All agent messages have the same general structure, but it's the message inside the structure that varies.
Each message has the following general format shown below. The message is a JSON string, which is then typically encrypted (doesn't have to be though), with a UUID prepended, and then the entire thing base64 encoded:
There are a couple of components to note here in what's called an agentMessage
:
UUID
- This UUID varies based on the phase of the agent (initial checkin, staging, fully staged). This is a 36 character long of the format b50a5fe8-099d-4611-a2ac-96d93e6ec77b
. Optionally, if your agent is dealing with more of a binary-level specification rather than strings, you can use a 16 byte big-endian value here for the binary representation of the UUID4 string.
EncBlob
- This section is encrypted, typically by an AES256 key, but when agents are staging, this could be encrypted with RSA keys or as part of some other custom crypto/staging you're doing as part of your payload type container. .
JSON
- This is the actual message that's being sent by the agent to Mythic or from Mythic to an agent. If you're doing your own custom message format and leveraging a translation container, this this format will obviously be different and will match up with your custom version; however, in your translation container you will need to convert back to this format so that Mythic can process the message.
action
- This specifies what the rest of the message means. This can be one of the following:
staging_rsa
checkin
get_tasking
post_response
translation_staging (you're doing your own staging)
...
- This section varies based on the action that's being performed. The different variations here can be found in Hooking Features , Initial Checkin, and Agent Responses
delegates
- This section contains messages from other agents that are being passed along. This is how messages from nested peer-to-peer agents can be forwarded out through and egress callback. If your agent isn't forwarding messages on from others (such as in a p2p mesh or as an egress point), then you don't need this section. More info can be found here: Delegates (p2p)
+
- when you see something like UUID + EncBlob
, that's referring to byte concatenation of the two values. You don't need to do any specific processing or whatnot, just right after the first elements bytes put the second elements bytes
Let's look at a few concrete examples without encryption and already base64 decoded:
If you want to have a completely custom agent message format (different format for JSON, different field names/formatting, a binary or otherwise formatted protocol, etc), then there's only two things you have to do for it to work with Mythic.
Base64 encode the message
The first bytes of the message must be the associated UUID (payload, staging, callback).
Mythic uses these first few bytes to do a lookup in its database to find out everything about the message. Specifically for this case, it looks up if the associated payload type has a translation container, and if so, ships the message off to it first before trying to process it.
This section talks about the different components for creating messages from the agent to a C2 docker container and how those can be structured within a C2 profile. Specifically, this goes into the following components:
How agent messages are formatted
How to perform initial checkins and do encrypted key exchanges
How to Get Tasking
How to Post Responses
Uploading Files
Another major component of the agent side coding is the actual C2 communications piece within your agent. This piece is how your agent actually implements the C2 components to do its magic.
Every C2 profile has zero or more C2 Parameters that go with it. These describe things like callback intervals, API keys to use, how to format web requests, encryption keys, etc. These parameters are specific to that C2 profile, so any agent that "speaks" that c2 profile's language will leverage these parameters. If you look at the parameters in the UI, you'll see:
Name
- When creating payloads or issuing tasking, you will get a dictionary of name
-> user supplied value
for you to leverage. This is a unique key per C2 profile (ex: callback_host
)
description
- This is what's presented to the user for the parameter (ex: Callback host or redirector in URL format
)
default_value
- If the user doesn't supply a value, this is the default one that will be used
verifier_regex
- This is a regex applied to the user input in the UI for a visual cue that the parameter is correct. An example would be ^(http|https):\/\/[a-zA-Z0-9]+
for the callback_host
to make sure that it starts with http:// or https:// and contains at least one letter/number.
required
- Indicate if this is a required field or not.
randomized
- This is a boolean indicating if the parameter should be randomized each time. This comes into play each time a payload is generated with this c2 profile included. This allows you to have a random value in the c2 profile that's randomized for each payload (like a named pipe name).
format_string
- If randomized
is true
, then this is the regex format string used to generate that random value. For example, [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}
will generate a UUID4 each time.
How does SOCKS work within Mythic
Socks provides a way to negotiate and transmit TCP connections through a proxy (https://en.wikipedia.org/wiki/SOCKS). This allows operators to proxy network tools through the Mythic server and out through supported agents. SOCKS5 allows a lot more options for authentication compared to SOCKS4; however, Mythic currently doesn't leverage the authenticated components, so it's important that if you open up this port on your Mythic server that you lock it down.
Opened SOCKS5 ports in Mythic do not leverage additional authentication, so MAKE SURE YOU LOCK DOWN YOUR PORTS.
Without going into all the details of the SOCKS5 protocol, agents transmit dictionary messages that look like the following:
These messages contain three components:
exit
- boolean True or False. This indicates to either Mythic or your Agent that the connection has been terminated from one end and should be closed on the other end (after sending data
). Because Mythic and 2 HTTP connections sit between the actual tool you're trying to proxy and the agent that makes those requests on your tool's behalf, we need this sort of flag to indicate that a TCP connection has closed on one side.
server_id
- uint32. This number is how Mythic and the agent can track individual connections. Every new connection from a proxied tool (like through proxychains) will generate a new server_id
that Mythic will send with data to the Agent.
data
- base64 string. This is the actual bytes that the proxied tool is trying to send.
In Python translation containers, if exit
is True, then data
can be None
These SOCKS messages are passed around as an array of dictionaries in get_tasking
and post_response
messages via a (added if needed) socks
key:
or in the post_response
messages:
Notice that they're at the same level of "action" in these dictionaries - that's because they're not tied to any specific task, the same goes for delegate messages.
This means that if you send a get_tasking
request OR a post_response
request, you could get back socks
data. The same goes for rpfwd
, interactive
, and delegates
.
For the most part, the message processing is pretty straight forward:
Get a new SOCKS array
Get the first element from the list
If we know the server_id
, then we can forward the message off to the appropriate thread or channel to continue processing. If we've never seen the server_id before, then it's likely a new connection that opened up from an operator starting a new tool through proxychains, so we need to handle that appropriately.
For new connections, the first message is always a SOCKS Request message with encoded data for IP:PORT to connect to. This means that SOCKS authenticaion is already done. There's also a very specific message that gets sent back as a response to this. This small negotiation piece isn't something that Mythic created, it's just part of the SOCKS protocol to ensure that a tool like proxychains gets confirmation the agent was able to reach the desired IP:PORT
For existing connections, the agent looks at if exit
is True or not. If exit
is True, then the agent should close its corresponding TCP connection and clean up those resources. If it's not exit, then the agent should base64 decode the data
field and forward those bytes through the existing TCP connection.
The agent should also be streaming data back from its open TCP connections to Mythic in its get_tasking
and post_response
messages.
That's it really. The hard part is making sure that you don't exhaust all of the system resources by creating too many threads, running into deadlocks, or any number of other potential issues.
While not perfect, the poseidon agent have a generally working implementation for Mythic: https://github.com/MythicAgents/poseidon/blob/master/Payload_Type/poseidon/poseidon/agent_code/socks/socks.go
This page has the various different ways the initial checkin can happen and the encryption schemes used.
You will see a bunch of UUIDs mentioned throughout this section. All UUIDs are UUIDv4 formatted UUIDs (36 characters in length) and formatted like:
In general, the UUID concatenated with the encrypted message provides a way to give context to the encrypted message without requiring a lot of extra pieces and without having to do a bunch of nested base64 encodings. 99% of the time, your messages will use your callbackUUID in the outer message. The outer UUID gives Mythic information about how to decrypt or interpret the following encrypted blob. In general:
payloadUUID as the outer UUID tells Mythic to look up that payload UUID, then look up the C2 profile associated with it, find a parameter called AESPSK
, and use that as the key to decrypt the message
tempUUID as the outer UUID tells Mythic that this is a staging process. So, look up the UUID in the staging database to see information about the blob, such as if it's an RSA encrypted blob or is part of a Diffie-Hellman key exchange
callbackUUID as the outerUUID tells Mythic that this is a full callback with an established encryption key or in plaintext.
However, when your payload first executes, it doesn't have a callbackUUID, it's just a payloadUUID. This is why you'll see clarifiers as to which UUID we're referring to when doing specific messages. The whole goal of the checkin
process is to go from a payload (and thus payloadUUID) to a full callback (and thus callbackUUID), so at the end of staging and everything you'll end up with a new UUID that you'll use as the outer UUID.
If your already existing callback sends a checkin message more than once, Mythic simply uses that information to update information about the callback rather than trying to register a new callback.
In egress agent messages, you can opt for a 16 Byte big endian format for the UUID. If Mythic gets a message from an agent with this format of UUID, then it will respond with the same format for the UUID. However, currently for P2P messages Mythic doesn't track the format for the UUID of the agent, so these will get the standard 36 character long UUID String.
The plaintext checkin is useful for testing or when creating an agent for the first time. When creating payloads, you can generate encryption keys per c2 profile. To do so, the C2 Profile will have a parameter that has an attribute called crypto_type=True
. This will then signal to Mythic to either generate a new per-payload AES256_HMAC key or (if your agent is using a translation container) tell your agent's translation container to generate a new key. In the http
profile for example, this is a ChooseOne
option between aes256_hmac
or none
. If you're doing plaintext comms, then you need to set this value to none
when creating your payload. Mythic looks at that outer PayloadUUID
and checks if there's an associated encryption key with it in the database. If there is, Mythic will automatically try to decrypt the rest of the message, which will fail. This checkin has the following format:
integrity_level
is an integer from 1-4 that indicates the integrity level of the callback. On Windows, these levels correspond to low integrity (1) , medium integrity (2), high integrity (3), or SYSTEM integrity (4). On Linux, these don't have a great mapping, but you can think of (2) as a standard user, (3) as a user that's in the sudoers file or is able to run sudo, and (4) as the root user.
The JSON section is not encrypted in any way, it's all plaintext.
Here's an example checkin message message:
The checkin has the following response:
From here on, the agent messages use the new UUID instead of the payload UUID. This allows Mythic to track a payload trying to make a new callback vs a callback based on a payload.
This method uses a static AES256 key for all communications. This will be different for each payload that's created. When creating payloads, you can generate encryption keys per c2 profile. To do so, the C2 Profile will have a parameter that has an attribute called crypto_type=True
. This will then signal to Mythic to either generate a new per-payload AES256_HMAC key or (if your agent is using a translation container) tell your agent's translation container to generate a new key. In the http
profile for example, this is a ChooseOne
option between aes256_hmac
or none
. The key passed down to your agent during build time will be the base64 encoded version of the 32Byte key.
The message sent will be of the form:
Here's an example message with encryption key of hfN9Nk29S8LsjrE9ffbT9KONue4uozk+/TVMyrxDvvM=
and message:
The message response will be of the form:
Here's that sample message's response:
From here on, the agent messages use the new UUID instead of the payload UUID.
This first message from Agent -> Mythic has the Payload UUID as the outer UUID and the Payload UUID inside the checkin JSON message. Once the agent gets the reply with a callbackUUID, all future messages will have this callbackUUID as the outer UUID.
With that same example from above, the agent gets back a response of success with a new callback UUID. From there on, since it's a static encryption, we'll see a get tasking message like the following:
Notice how the outer UUID is different, but the encryption key is still the same.
Padding: PKCS7, block size of 16
Mode: CBC
IV is 16 random bytes
Final message: IV + Ciphertext + HMAC
where HMAC is SHA256 with the same AES key over (IV + Ciphertext)
There are two currently supported options for doing an encrypted key exchange in Mythic:
Client-side generated RSA keys
leveraged by the apfell-jxa and poseidon agents
Agent specific custom EKE
The agent starts running and generates a new 4096 bit Pub/Priv RSA key pair in memory. The agent then sends the following message to Mythic:
where the AES key initially used is defined as the initial encryption value when generating the payload. When creating payloads, you can generate encryption keys per c2 profile. To do so, the C2 Profile will have a parameter that has an attribute called crypto_type=True
. This will then signal to Mythic to either generate a new per-payload AES256_HMAC key or (if your agent is using a translation container) tell your agent's translation container to generate a new key. In the http
profile for example, this is a ChooseOne
option between aes256_hmac
or none
.
When it says "base64 of public RSA key" you can do one of two things:
Base64 encode the entire PEM exported key (including the ---BEGIN and ---END blocks)
Use the already base64 encoded data that's inbetween the ---BEGIN and ---END blocks
Here is an example of the first message using encryption key hfN9Nk29S8LsjrE9ffbT9KONue4uozk+/TVMyrxDvvM=
:
This message causes the following response:
Here's that sample response from our above sample message:
The response is encrypted with the same initial AESPSK value as before. However, the session_key
value is encrypted with the public RSA key that was in the initial message and base64 encoded. The response also includes a new staging UUID for the agent to use. This is not the final UUID for the new callback, this is a temporary UUID to indicate that the next message will be encrypted with the new AES key.
The next message from the agent to Mythic is as follows:
With our new temp UUID, the agent sends the following:
This checkin data is the same as all the other methods of checking in, the key things here are that the tempUUID is the temp UUID specified in the other message, the inner uuid is the payload UUID, and the AES key used is the negotiated one. It's with this information that Mythic is able to track the new messages as belonging to the same staging sequence and confirm that all of the information was transmitted properly. The final response is as follows:
With our example, the agent gets back the following:
From here on, the agent messages use the new UUID instead of the payload UUID or temp UUID and continues to use the new negotiated AES key.
Lastly, here's an example after that exchange with the new callback UUID doing a get tasking request:
Padding: PKCS7, block size of 16
Mode: CBC
IV is 16 random bytes
Final message: IV + Ciphertext + HMAC
where HMAC is SHA256 with the same AES key over (IV + Ciphertext)
PKCS1_OAEP
This is specifically OAEP with SHA1
4096Bits in size
This section requires you to have a #translation-containers associated with your payload type. The agent sends your own custom message to Mythic:
Mythic looks up the information for the payloadUUID and calls your translation container's translate_from_c2_format
function. That function gets a dictionary of information like the following:
To get the enc_key
, dec_key
, and type
, Mythic uses the payloadUUID to then look up information about the payload. It uses the profile
associated with the message to look up the C2 Profile parameters and look for any parameter with a crypto_type
set to true
. Mythic pulls this information and forwards it all to your translate_from_c2_format
function.
Ok, so that message gets your payloadUUID/crypto information and forwards it to your translation container, but then what?
Normally, when the translate_to_c2_format
function is called, you just translate from your own custom format to the standard JSON dictionary format that Mythic uses. No big deal. However, we're doing EKE here, so we need to do something a little different. Instead of sending back an action of checkin
, get_tasking
, post_response
, etc, we're going to generate an action of staging_translation
.
Mythic is able to do staging and EKE because it can save temporary pieces of information between agent messages. Mythic allows you to do this too if you generate a response like the following:
Let's break down these pieces a bit:
action
- this must be "staging_translation". This is what indicates to Mythic once the message comes back from the translate_from_c2_format
function that this message is part of staging.
session_id
- this is some random character string you generate so that we can differentiate between multiple instances of the same payload trying to go through the EKE process at the same time.
enc_key
/ dec_key
- this is the raw bytes of the encryption/decryption keys you want for the next message. The next time you get the translate_from_c2_format
message for this instance of the payload going through staging, THESE are the keys you'll be provided.
crypto_type
- this is more for you than anything, but gives you insight into what the enc_key
and dec_key
are. For example, with the http
profile and the staging_rsa
, the crypto type is set to aes256_hmac
so that I know exactly what it is. If you're handling multiple kinds of encryption or staging, this is a helpful way to make sure you're able to keep track of everything.
next_uuid
- this is the next UUID that appears in front of your message (instead of the payloadUUID). This is how Mythic will be able to look up this staging information and provide it to you as part of the next translate_from_c2_format
function call.
message
- this is the actual raw bytes of the message you want to send back to your agent.
This process just repeats as many times as you want until you finally return from translate_from_c2_format
an actual checkin
message.
What if there's other information you need/want to store though? There are three RPC endpoints you can hit that allow you to store arbitrary data as part of your build process, translation process, or custom c2 process:
create_agentstorage
- this take a unique_id string value and the raw bytes data value. The unique_id
is something that you need to generate, but since you're in control of it, you can make sure it's what you need. This returns a dictionary:
{"unique_id": "your unique id", "data": "base64 of the data you supplied"}
get_agentstorage
- this takes the unique_id string value and returns a dictionary of the stored item:
{"unique_id": "your unique id", "data": "base64 of the data you supplied"}
delete_agentstorage
- this takes the unique_id string value and removes the entry from the database
How does Reverse Port Forward work within Mythic
Reverse port forwards provide a way to tunnel incoming connections on one port out to another IP:Port somewhere else. It normally provides a way to expose an internal service to a network that would otherwise not be able to directly access it.
Agents transmit dictionary messages that look like the following:
These messages contain three components:
exit
- boolean True or False. This indicates to either Mythic or your Agent that the connection has been terminated from one end and should be closed on the other end (after sending data
). Because Mythic and 2 HTTP connections sit between the actual tool you're trying to proxy and the agent that makes those requests on your tool's behalf, we need this sort of flag to indicate that a TCP connection has closed on one side.
server_id
- unsigned int32. This number is how Mythic and the agent can track individual connections. Every new connection will generate a new server_id
. Unlike SOCKS where Mythic is getting the initial connection, the agent is getting the initial connection in a reverse port forward. In this case, the agent needs to generate this random uint32 value to track connections.
data
- base64 string. This is the actual bytes that the proxied tool is trying to send.
port
- an optional uint32 value that specifies the port you're listening on within your agent. If your agent allows for multiple rpfwd commands within a single callback, then you need to specify this port
so that Mythic knows which rpfwd command this data is associated with and can redirect it out to the appropriate remote IP:Port combination. This port
value is specifically the local port your agent is listening on, not the port for the remote connection.
In Python translation containers, if exit
is True, then data
can be None
These RPFWD messages are passed around as an array of dictionaries in get_tasking
and post_response
messages via a (added if needed) rpfwd
key:
or in the post_response
messages:
Notice that they're at the same level of "action" in these dictionaries - that's because they're not tied to any specific task, the same goes for delegate messages.
This means that if you send a get_tasking
request OR a post_response
request, you could get back rpfwd
data. The same goes for socks
, interactive
, and delegates
.
For the most part, the message processing is pretty straight forward:
Agent opens port X on the target host where it's running
ServerA makes a connection to PortX
Agent accepts the connection, generates a new uint32 server_id, and sends any data received to Mythic via rpfwd
key. If the agent is tracking multiple ports, then it should also send the port the connection was received on with the message.
Mythic looks up the server_id
(and optionally port) for that Callback if Mythic has seen this server_id, then it can pass it off to the appropriate thread or channel to continue processing. If we've never seen the server_id before, then it's likely a new connection that opened up, so we need to handle that appropriately. Mythic makes a new connection out to the RemoteIP:RemotePort specified when starting the rpfwd
session. Mythic forwards the data along and waits for data back. Any data received is sent back via the rpfwd
key the next time the agent checks in.
For existing connections, the agent looks at if exit
is True or not. If exit
is True, then the agent should close its corresponding TCP connection and clean up those resources. If it's not exit, then the agent should base64 decode the data
field and forward those bytes through the existing TCP connection.
The agent should also be streaming data back from its open TCP connections to Mythic in its get_tasking
and post_response
messages.
That's it really. The hard part is making sure that you don't exhaust all of the system resources by creating too many threads, running into deadlocks, or any number of other potential issues.
Delegate messages are messages that an agent is forwarding on behalf of another agent. The use case here is an agent forwarding peer-to-peer messages for a linked agent. Mythic supports this by having an optional delegates
array in messages. An example of what this looks like is in the next section, but this delegates
array can be part of any message from an agent to mythic.
When sending delegate messages, there's a simple standard format:
Within a delegates array are a series of JSON dictionaries:
UUID
- This field is some UUID identifier used by the agent to track where a message came from and where it should go back to. Ideally this is the same as the UUID for the callback on the other end of the connection, but can be any value. If the agent uses a value that does not match up with the UUID of the agent on the other end, Mythic will indicate that in the response. This allows the middle-man agent to generate some UUID identifier as needed upon first connection and then learn of and use the agent's real UUID once the messages start flowing.
message
- this is the actual message that the agent is transmitting on behalf of the other agent
c2_profile
- This field indicates the name of the C2 Profile associated with the connection between this agent and the delegated agent. This allows Mythic to know how these two agents are talking to each other when generating and tracking connections.
The new_uuid
field indicates that the uuid
field the agent sent doesn't match up with the UUID in the associated message. If the agent uses the right UUID with the agentMessage then the response would be:
Why do you care and why is this important? This allows an agent to randomly generate its own UUID for tracking connections with other agents and provides a mechanism for Mythic to reveal the right UUID for the callback on the other end. This implicitly gives the agent the right UUID to use if it needs to announce that it lost the route to the callback on the other end. If Mythic didn't correct the agent's use of UUID, then when the agent loses connection to the P2P agent, it wouldn't be able to properly indicate it to Mythic.
This means that if you send a get_tasking
request OR a post_response
request, you could get back delegates
data. The same goes for rpfwd
, interactive
, and socks
.
Ok, so let's walk through an example:
agentA is an egress agent speaking HTTP to Mythic. agentA sends messages directly to Mythic, such as the {"action": "get_tasking", "tasking_size": 1}
. All is well.
somehow agentB gets deployed and executed, this agent (for sake of example) opens a port on its host (same host as agentA or another one, doesn't matter)
agentA connects to agentB (or agentB connects to agentA if agentA opened the port and agentB did a connection to it) over this new P2P protocol (smb, tcp, etc)
agentB sends to agentA a staging message if it's doing EKE, a checkin message if it's already an established callback (like the example of re-linking to a callback), or a checkin message if it's doing like a static PSK or plaintext. The format of this message is exactly the same as if it wasn't going through agentA
agentA gets this message, and is like "new connection, who dis?", so it makes a random UUID to identify whomever is on the other end of the line and forwards that message off to Mythic with the next message agentA would be sending anyway. So, if the next message that agentA would send to Mythic is another get tasking, then it would look like: {"action": "get_tasking", "tasking_size": 1, "delegates": [ {"message": agentB's message, "c2_profile": "Name of the profile we're using to communicate", "uuid": "myRandomUUID"} ] }
. That's the message agentA sends to Mythic.
Mythic gets the message, processes the get_tasking for agentA, then sees it has delegate
messages (i.e. messages that it's passing along on behalf of other agents). So Mythic recursively processes each of the messages in this array. Because that message
value is the same as if agentB was talking directly to Mythic, Mythic can parse out the right UUIDs and information. The c2_profile
piece allows Mythic to look up any c2-specific encryption information to pass along for the message. Once Mythic is done processing the message, it sends a response back to agentA like: {"action": "get_tasking", "tasks": [ normal array of tasks ], "delegates": [ {"message": "response back to what agentB sent", "uuid": "myRandomUUID that agentA generated", "new_uuid": "the actual UUID that Mythic uses for agentB"} ] }
. If this is the first time that Mythic has seen a delegate from agentB through agentA, then Mythic knows that there's a route between the two and via which C2 profile, so it can automatically display that in the UI
agentA gets the response back, processes its get_tasking like normal, sees the delegates
array and loops through those messages. It sees "oh, it's myRandomUUID, i know that guy, let me forward it along" and also sees that it's been calling agentB by the wrong name, it now knows agentB's real name according to Mythic. This is important because if agentA and agentB ever lose connection, agentA can report back to Mythic that it can no longer to speak to agentB with the right UUID that Mythic knows.
This same process repeats and just keeps nesting for agentC that would send a message to agentB that would send the message to agentA that sends it to Mythic. agentA can't actually decrypt the messages between agentB and Mythic, but it doesn't need to. It just has to track that connection and shuttle messages around.
Now that there's a "route" between the two agents that Mythic is aware of, a few things happen:
when agentA now does a get_tasking
message (with or without a delegate message from agentB), if mythic sees a tasking for agentB, Mythic will automatically add in the same delegates
message that we saw before and send it back with agentA so that agentA can forward it to agentB. That's important - agentB never had to ask for tasking, Mythic automatically gave it to agentA because it knew there was a route between the two agents.
if you DON"T want that to happen though - if you want agentB to keep issuing get_tasking requests through agentA with periodic beaconing, then in agentA's get_tasking you can add get_delegate_tasks
to False. i.e ({"action": "get_tasking", "tasking_size": 1, "get_delegate_tasks": false}
) then even if there are tasks for agentB, Mythic WILL NOT send them along with agentA. agentB will have to ask for them directly
If this wasn't part of some task, then there would be no task_id to use. In this case, we can add the same edges
structure at a higher point in the message:
How is an agent supposed to work with a Peer-to-peer (P2P) profile? It's pretty simple and largely the same as working with a Push C2 egress connection:
If a payload is executed (it's not a callback yet), then make a connection to your designated P2P method (named pipes, tcp ports, etc). Once a connection is established, start your normal encrypted key exchange or checkin process.
If an existing callback loses connection for some reason, then make a connection to your designated P2P method (named pipes, tcp ports, etc). Once a connection is established, send your checkin message again to inform Mythic of your existence
At this point, just wait for messages to come to you (no need to do a get_tasking poll) and as you get any data (socks, edges, alerts, responses, etc) just send them out through your p2p connection.
Messages for interactive tasking have three pieces:
If you have a command called pty
and issue it, then when that task gets sent to your agent, you have your normal tasking structure. That tasking structure includes an id for the task that's a UUID. All follow-on interactive input for that task uses the same UUID (task_id
in the above message).
The data
is pretty straight forward - it's the base64 of the raw data you're trying to send to/from this interactive task. The message_type
field is an enum of int
. It might see complicated at first, but really it boils down to providing a way to support sending control codes through the web UI, scripting, and through an opened port.
When something is coming from Mythic -> Agent, you'll typically see Input
, Exit
, or Escape
-> CtrlZ
. When sending data back from Agent -> Mythic, you'll set either Output
or Error
. This enum example also includes what the user typically sees in a terminal (ex: ^C
when you type CtrlC) along with the hex value that's normally sent. Having data split out this way can be helpful depending on what you're trying to do. Consider the case of trying to do a tab-complete
. You want to send down data and the tab character (in that order). For other things though, like escape
, you might want to send down escape
and then data (in that order for things like control sequences).
You'll probably notice that some letters are missing from the control codes above. There's no need to send along a special control code for \n
or \r
because we can send those down as part of our input. Similarly, clearing the screen isn't useful through the web UI because it doesn't quite match up as a full TTY.
This data is located in a similar way to SOCKS and RPFWD:
the interactive
keyword takes an array of these sorts of messages to/from the agent. This keyword is at the same level in the JSON structure as action
, socks
, responses
, etc.
This means that if you send a get_tasking
request OR a post_response
request, you could get back interactive
data. The same goes for rpfwd
, socks
, and delegates
.
When sending responses back for interactive tasking, you send back an array in the interactive
keyword just like you got the data in the first place.
While not perfect, the poseidon agent have a generally working implementation for Mythic:
What happens when agentA and agentB can no longer communicate though? agentA needs to send a message back to Mythic to indicate that the connection is lost. This can be done with the key. Using all of the information agentA has about the connection, it can announce that Mythic should remove a edge between the two callbacks. This can either happen as part of a response to a tasking (such as an explicit task to unlink two agents) or just something that gets noticed (like a computer rebooted and now the connection is lost). In the first case, we see the example below as part of a normal post_response message: