external_c2 framework
Python framework for building and utilizing interfaces to transfer data between frameworks, with a specific focus on serving as an extension to Command and Control frameworks.
Currently, this is only intended as an implementation of Cobalt Strike's External C2 specification as described in this spec document, but is subject to change as the project matures.
Credits
Massive credit goes to xychix. This project would not have been possible at all without their valuable contributions. Basically this project is a rebuild and extension of Outflank's External C2 project
Architecture
This project consists of the following main parts:
- Builder
- Skeletons
- Frameworks
- Transports
- Encoders
- Manager (not yet implemented)
Builder
The builder reads in a config file, and uses configured options to generate a build by replacing markers
within skeletons
.
The builder can be used with build_files.py
. A sample builder configuration is provided as sample_builder_config.config.sample
.
Skeletons
skeletons
are the different "skeletons" of code that the builder
will dynamically populate to generate a completely usable build. skeletons
contain markers
that will be replaced with usable values by the builder
. There are three different 'types' of skeletons:
Skeleton Markers
A marker can be placed inside any file in a skeleton
, which will be replaced with a value specified in the builder config. In best practice, markers should never be used to directly write variables, and should only ever be used to set values. If a marker's value has to be reused, one should opt to store the value in a variable and reference it that way, instead of reusing the same marker.
The marker format is: ```[var:::identifier_for_the_marker]```
Strings will be written into a skeleton directly wrapped in single quotes, and numbers will be written as is.
In the event a string in the config is wrapped in double quotes, the string will be written directly to the file wrapped in double quotes instead, and the wrapping single quotes will be stripped.
This relationship can demonstrated as:
#################
# Skeleton Code #
#################
# Skeleton contains the following line of code:
foo = ```[var:::bar]```
##############
# End Result #
##############
# Stored in config as:
# foo = bar
# Written as:
foo = 'bar'
# Stored in config as:
# foo = "bar"
# Written as:
foo = "bar"
# Stored in config as:
# foo = 2
# Written as:
foo = 2
# Stored in config as:
# foo = "2"
# Written as:
foo = "2"
Frameworks
Frameworks are the base application that determines what data is being used by the transport
and encoder
, and how that data is used. What a specific framework
actually does doesn't really matter, so long as logic exists to import and use the encoder
and transport
. Most of the essential portions of a framework (primarily client
logic) will be stored as a skeleton
, with an interface to interact with the server portion of it being stored as a base framework
object.
Generally, a framework is contains a server
and client
, and makes use of encoders
and transports
to relay data between them.
There are few fundamentals to consider when building a framework
:
- The
framework
is responsible for ensuring that theencoder
is made available to thetransport
to be used. - If the
framework
uses a client-server relationship, they should be appropriately organized as such. - Understand that in a majority of cases, the end-user will never directly interact with a framework's
client
, so if you want things to be reconfigurable on theclient
, it needs to be able to do that during runtime with no direct interaction. - There should be little need for creating a
server
skeleton
because the end-user is going to be directly interacting with a framework'sserver
. Instead, opt to both read in options from a configuration, and give the end-user the ability to modify options (such as a block timer or verbosity) during runtime. - A
framework
skeleton will be processed by the builder, iterating through every file in it, so if a certain argument needs to be configurable at build time, it can be easily done. - A
framework
server
should be able to be interfaced by a commonframework_manager
.
Framework Server
The server is the application that brokers communication between the client
and the c2 server
. The server logic is primarily static. The logic for the server for the cobalt_strike
framework, referred to as third-party Client Controller
within the spec, is shown below:
- Parse the configuration
- Import the specified encoding module
- Import the specified transport module
- Establish a connection to the c2 server
- Request a stager from the c2 server
- Encode the stager with the
encoder
module - Transport the stager with the
transport
module - Await for a metadata response from the client received via the
transport
- Decode the metadata with the
encoder
module - Relay the metadata to the c2 server.
- Receive a new task from the c2 server.
- Encode the new task
- Relay the new task to the client via the
transport
- Receive for a response from the client received via the
transport
- Decode the response via the
encoder
module - Relay the response to the c2 server.
- Repeat steps 11-16
A server
should support the ability to handle multiple clients (not yet implemented), and be interfaced by a framework_manager
.
Framework Client
The client is essentially the payload that runs on the endpoint. The logic of the client for the cobalt_strike
framework is primarily static, and shown below:
- Run any preparations need to be utilizing the
transport
- Receive the stager
- Inject the stager and open the handle to the beacon
- Obtain metadata from the beacon
- Relay the metadata from the beacon to the C2 server via the
transport
- Watch the
transport
for new tasks - Relay new tasks to the beacon
- Relay responses from the beacon via the
transport
- Repeat steps 6-8.
The client makes use of the specified encoder
and transport
to relay data between itself and its respective server
.
Encoders
Encoders receive data, then modify that data to either prepare it for use to be sent via the transport, or decode data received via the transport back into its raw form to be interpreted by whatever framework
component is utilizing it.
Encoders should expect to be interfaced directly by the transport, and handle data in a framework and component agnostic manner.
Transports
Transports serve the role of sending and receiving data through a communication channel, and interfacing with the encoder to ensure that data is transformed to whatever format is necessary. Transports should expect to receive data from a framework component, or via the communication channel, and have the ability to relay data through the communication channel. Transports are responsible for calling the encoder
to encode or decode data as necessary.
Transports should expect to be interfaced directly by the framework
component, and handle data in a framework and component agnostic manner.
How to use this
-
First, determine which transport and encoding module you'd like to use. We'll use
transport_imgur
andencoder_lsbjpg
for the following example. -
Next, create a
builder_config.config
to suit your needs, refer to the provided sample config and template for direction on how to do this. -
Generate a build with
build_files.py
. As an example, one would generate a build in thebuilds
directory usingencoder_lsbjpg
, andtransport_imgur
for Cobalt Strike, verbosely, with the following command:
python build_files.py -b builds -f cobalt_strike -c sample_builder_config.config.sample -e encoder_lsbjpg -t transport_imgur -v
- Next, start running your built server and distribute your client.
Cobalt Strike
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
Next, execute the client on the targeted endpoint.
If everything worked, a new beacon will be registered within the Cobalt Strike console that you may interact with.
FAQ
Why would you write this?: There weren't very many released implementation of the Cobalt Strike spec, and of the ones that are released, they either are not in a language that I am familiar with or do not have the modularity and abstraction that I was seeking.
Why Python 2?: I'm lazy and it's easy to implement new transport and encoding channels in it.
Your code sucks: That's not a question.
Can I submit new transport and/or encoder modules?: Yes please! Submit a pull request and I would be happy to review.
How do I compile the client into an executable I can distribute?:
I've tested this successfully in Kali, to recreate my environment, just ensure you have veil-evasion installed and that you ran through its setup. It should have setup a wine environment with python installed that has all of the dependencies you need. You MAY have to install the pefile
module into this environment as well.
Then, you can go to the directory for the client you want to generate an executable for and run:
chmod +x compile_dll.sh
./compile_dll.sh
wine "C:\\Python27\\python.exe" /usr/share/veil/pyinstaller/pyinstaller.py -F -r c2file.dll -w --key "ayyyyyyylmao" client.py
Replace the value for key
with whatever you want. You should see the client executable in the dist/
directory. If you want to generate an executable that provides a console that you can use for debugging, compile the executable with wine "C:\\Python27\\python.exe" /usr/share/veil/pyinstaller/pyinstaller.py -F -r c2file.dll -c client.py