go-exploit
go-exploit copied to clipboard
Support external C2s
We are now at the point that simple reverse shells are not cutting all the use cases. This lays the framework to move to a channel object containing data that can be sent to a channel and payload builder and also exposes an interface for the C2 channel creation.
Very cool. The code looks good at a glance, but I want to spend more time with it tomorrow. This change will be a big deal for the framework, so I want to double check that I totally get it.
This is definitely one I don't want to casually merge for the same reason. I've done one push to a private repo for how to make this work. There's also a few other things that I should formally justify vs just assume, such as the Read and Write interfaces as those exist to handle special use cases and components. I'll attempt to write some usage cases and such tomorrow here so we can have a public discussion instead of mocks in super secret repos.
The clear parts - Simple interfaces & channel type
The core part of this is to expose a "channel" type to replace the host port juggling and allow for non-simple TCP/UDP style connections. It keeps all the old options for compatibility and easy of use for simple things, but also adds 2 any types (we will get to that in the next part).
Then the external module creates a server type that turns our C2 components into a set of functions that get called by the framework at execution time. Now we can create a module not tied into the framework and even hosted in outside repositories.
The expected use case revolves around:
Configure(*Server)SetFlags(func())SetInit(func())SetRun(func(int))SetChannel(func(*channel.Channel))
In the external C2 it's expected that the author will create a set of functions to set up any channel settings and internal C2 struct, initialization needed (databases, ssh keys, anything really), flag setting, and finally the function to handle the running of the C2. An example of this can be seen here:
func Configure(externalServer *external.Server) {
sshc2 := New()
externalServer.SetChannel(sshc2.SSHServerChannel)
externalServer.SetFlags(sshc2.SSHServerFlags)
externalServer.SetInit(sshc2.SSHServerInit)
externalServer.SetRun(sshc2.SSHServerRun)
}
And an example of how the flags are used in one of my active demo components (thanks to flag parsing happening post-Config call in the framework these can get inherited, which is neat):
func (c2 *SSHC2Meta) SSHServerFlags() {
flag.StringVar(&flagCommand, "command", "", "Run a single command and exit the payload.")
flag.BoolVar(&flagInteractive, "interactive", true, "Run the commands in an interactive shell.")
flag.BoolVar(&flagHeartbeat, "heartbeat", false, "Print heartbeat checkins from the c2")
flag.BoolVar(&flagServerMessages, "server-messages", false, "Print server messages to the client")
}
This will now allow for any type of C2 setup, including ones that are more client -> middle server <- client that are common in the RED TEAM world. After all starting listeners is about as stealthy as a chainsaw and we all have to deal with DNS intercepting proxies.
The less clear part - Read, Write any
Initially when mocking this I couldn't help but think of a C2 channel generically as just Read and Write functions, so I was shuffling the idea around of an alternative to the above would be just to define those functions. In fact, I have a little demo version of this that used HTTP requests trivially with these read/write interfaces: https://github.com/terrorbyte/go-exploit/blob/fc789cf9b4247f737b917ec9f52ef604f6179898/c2/external/http.go
It made it very easy to write by just setting these functions and washing my hands of anything else. In my head these represent kind of the 2 styles.
After actually writing an implementation I do think that this option might be something that we want to remove from the actual merged version as I don't think there's a clean way to handle configuration without being directly in-framework.
Other Notes
- I don't see a clear way to pass multiple External modules with the current structure and we might only be able to pass one at a time at the moment, which might become a pain if we ever allow for dynamic selection.
- Payload generation is a nightmare. I don't mind doing a compile loop for config changes on a payload, but it can feel incredibly un-ergonomic and pointing to an os file read feels meh.
- Do we consider external C2s part of Framework for logging reasons? Do we want to add an explicit C2 log level to try and clarify/decouple?
I also am going to make a larger change to support multiple of the same Impl types before we merge this. PR link coming soon
I'm pretty happy with where this is at now and have some demos in the works. I believe I am prepared for a review.
Looks good to me! :ship: