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.

Project Outcomes

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:

  1. Transparency - the framework, the Agent in particular, must not abstract anything (important) away from the visibility or control of the operator.

  2. 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…).

  3. 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.

  • HTTPAgent / TCPAgent - .NET Framework Console Applications
  • Team Server - an ASP.NET Core 3.1 Web Application
  • Agent Modules - .NET Standard Class Libraries
  • Agent / C2 Shared Projects

The Agents

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:

  • Controllers
  • Interfaces
  • Modules

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();

private void ListDirectory(string data, AgentController agent, ConfigurationController config)
    string results = // do some stuff

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.

Team Server

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.

External Agent Modules

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 -n 30

[+] 10/05/2020 15:50:40 : AgentCommandResponse

Pinging with 32 bytes of data:
Reply from bytes=32 time<1ms TTL=128
Reply from bytes=32 time<1ms TTL=128
Reply from bytes=32 time<1ms TTL=128
Reply from 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

[+] 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


[+] 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 80

[+] 11/05/2020 08:29:20 : AgentCommandResponse

Bind Address  Bind Port  Forward Address  Forward Port
------------  ---------  ---------------  ------------       8888   80
[email protected]:~$ curl
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<H1>301 Moved</H1>
The document has moved
<A HREF="">here</A>.


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