Agent Customization in Mythic: Tailoring Tools for Red Team Needs
A tale as old as time
In offensive security, we often use tools in a wide range of situations and environments. Because of this, we need to customize tooling to work properly in a very specific scenario and often in a way that might not make sense to contribute back to the original project.
When it comes to command and control (C2) modifications, these can be very difficult and daunting to even conceptualize what you need to do. This blog will walk through how you, an operator, can make local tweaks to any part of Mythic, its agents, or its communications profiles.
Agent Modification
All publicly distributed agents for Mythic run in Docker. This means that when you install an agent, you have to locally build (or fetch) an image and then build a container from that image. These images can be pre-built and hosted online so that it’s faster to get a container running locally; the overview page has a column specifically for identifying the latest (if any) hosted image.
When you install an agent within Mythic and there’s a hosted image, then this information is tracked within Mythic’s .env
file. In this case, three new environment variables are created — the following is an example from the apfell agent:
APFELL_REMOTE_IMAGE="ghcr.io/mythicagents/apfell:v0.0.1.1-rc4"
APFELL_USE_BUILD_CONTEXT="false"
APFELL_USE_VOLUEME="true"
By default, Mythic will use that pre-built image and not use the local build context. Mythic also normally mounts the agent’s folder into the container under /Mythic
, but when [agent name]_USE_VOLUME
is true, then a local Docker Volume is created and mounted there instead. The main reason for this is that agents might want to cache data between builds or tasks, so having a persistent volume (or local mount) allows this to happen. If you’re wanting to make changes though, then you need to get rid of this so that the cached data doesn’t overwrite your changes. These two things together mean that any local changes are not used. We can use mythic-cli
to change these values though:
sudo ./mythic-cli config set apfell_use_build_context true
sudo ./mythic-cli config set apfell_use_volume false
Then, the next time the image is built, the local build context is used instead of the pre-built image, and our local code is mounted into the container.
sudo ./mythic-cli build apfell
At this point, we can get any changes we make to local code reflected as part of a build.
Dockerfiles
When we set the [name]_USE_BUILD_CONTEXT
to true
, what we’re really saying is that new builds should use the Mythic/InstalledServices/[name]/Dockerfile
file when creating the image.
There’s two main components we talk about with agents: the agent code itself and the helper code that defines agent commands/metadata that syncs up with Mythic. The agent code itself can be written in any language. The helper code can be written in Python or GoLang.
A common Dockerfile for a Python based container might look like the following. We create a builder
temporary image that gets extra stuff installed and installs all of our dependencies, then that’s copied over into our second stage container. The final pieces are are what’s important — all of the current code is copied over and the main.py
is executed when the container starts.
FROM python:3.11-slim-bookworm as builder
COPY [".docker/requirements.txt", "requirements.txt"]
RUN apt-get -y update && \
apt-get -y upgrade && \
apt-get install --no-install-recommends \
software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \
xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \
binutils-aarch64-linux-gnu libc-dev-arm64-cross -y
RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt
FROM python:3.11-slim-bookworm
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/*
WORKDIR /Mythic/
COPY [".", "."]
CMD ["python3", "main.py"]
A GoLang version would be similar. In the following example we can see a base container used, the code copied over, and then in this case a Makefile used to compile the command/meta code:
FROM itsafeaturemythic/mythic_go_macos:latest
WORKDIR /Mythic/
COPY [".", "."]
RUN make build
CMD make run
Agent Code Changes
At this point, you can make any modifications to the agent code, generate a new payload, and see your changes. Each time you make changes to agent code at this point, you need to generate a new payload. If you want to modify the metadata portion, then after each change you’ll need to rebuild the container with sudo ./mythic-cli build [agent name]
. For information on how to create or edit commands, check out Mythic’s documentation.
Each agent will be different in how it structures its code, how it processes tasks, and how it’s built. If you want to adjust how an agent is built (potentially adding/removing steps), then you’ll want to check out the build
function, likely within builder.py
and builder.go
.
Mythic Server Changes
Maybe there’s a new feature you want to add to the main Mythic server or you want to change how something works. The process for making that change to the Mythic server is extremely similar to making changes for an agent. First, we need to adjust a environment variable in the Mythic/.env
file to have a local image built instead of using the pre-built container:
sudo ./mythic-cli config set mythic_server_use_build_context true
Then, any changes to the code we make in Mythic/mythic-docker/src
will get pulled in, compiled, and used each time you run the build command:
sudo ./mythic-cli build mythic_server
Mythic UI Changes
Sometimes you might want to make modifications to Mythic’s UI. That could be to fix a bug you’re experiencing or to add a new feature. Mythic’s UI is also created, hosted, and built within Docker containers. By default, when you access Mythic’s UI, you’re accessing a compiled React UI hosted in an Nginx container. If you want to make changes, the first thing we need to do is switch to the development environment:
sudo ./mythic-cli config set mythic_react_debug true
sudo ./mythic-cli build mythic_react
This will take a few minutes, but it will tear down the current mythic_react
container and build a new one that uses nodejs
and downloads all of the needed modules.
At this point, the UI you’re seeing is a hot-reloaded dev environment. Any changes you make to the code in the Mythic/MythicReactUI/src
folder is automatically compiled and reflected in your UI.
When you’re done making changes, if you want to save it off, you can run the following command to build the code and save it in the mythic_react
folder:
sudo ./mythic-cli build_ui
Wrapping up
At this point you’ve seen how to make your own custom modifications to agents, the main mythic server, and even to the React UI. I’m sure you also started to notice a pattern — make sure the local build context is used by potentially changing an environment variable, then make your change, then run the build command.
This same pattern applies to C2 Profiles, Translation Containers, Webhook Containers, and Logging Containers. It can be daunting at first to see a bunch of Docker containers and trying to figure out how to make even the smallest of adjustments, but once you get used to tracking how the image is made and what is mounted locally, then it’ll quickly become a lot easier. If you’re ever curious about the local volume usage for any container, you can check that out with mythic-cli
as well:
sudo ./mythic-cli volume ls
You can also remove any volume via:
sudo ./mythic-cli volume rm [volume name]
If you’re ever curious what a configuration variable in the Mythic/.env
file is for or how it affects things, you can always use mythic-cli
to get help for it (it accepts regex too):
% ./mythic-cli config help apfell_.\*
[+] Getting configuration values:
Setting Value
––––––– –––––––
APFELL_REMOTE_IMAGE This setting configures the remote image to use if you have *_use_build_context set to false. This value should get automatically updated by the agent/c2 profile's repo as new releases are created. This value will also get updated each time you install an agent. So if you want to pull an agent's latest image, just re-install the agent (or manually update this value and restart the local container).
APFELL_USE_BUILD_CONTEXT This setting determines if you use the pre-built image hosted on GitHub/DockerHub or if you use your local InstalledService/[agent]/Dockerfile to build a new local image for your container. If you're wanting to make changes to an agent or c2 profile (adding commands, updating code, etc), then you need to set this to 'true' and update your Dockerfile to copy in your modified code. If your container code is Golang, then you'll also need to make sure your modified Dockerfile rebuilds that Go code based on your changes (probably with a 'go build' or 'make build' command depending on the agent). If your container code is Python, then simply copying in the changes should be sufficient. Also make sure you change [agent]_use_volume to 'false' so that your changes don't get overwritten by an old volume. Alternatively, you could remove the old volume with './mythic-cli volume rm [agent]_volume' and then build your new container with './mythic-cli build [agent]'.
APFELL_USE_VOLUME This creates a new volume in the format [agent]_volume that's mapped into an agent's '/Mythic' directory. The first time this is created with an empty volume, the contents of your pre-built agent/c2 gets copied to the volume. If you install a new version of the agent/c2 profile or rebuild the container (either via rebuild_on_start or directly calling ./mythic-cli build [agent]) then the volume is deleted and recreated. This will DELETE any changes you made in the container. For agents this could be temporary files created as part of builds. For C2 Profiles this could be profile updates. If this is set to 'false' the no new volume is created and instead the local InstalledServices/[agent] directory is mapped into the container, preserving all changes on rebuild and restart.