From 07d38bb454dc8680ad1e131722ef02e4bc95507a Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 10 Apr 2025 12:45:22 -0600 Subject: [PATCH 01/10] Unify clean shutdown and update C2 channel with session tracking logic --- c2/channel/channel.go | 82 ++++++++++++++++++++ c2/cli/basic.go | 112 +++++++++++++++++----------- c2/external/external.go | 30 ++++++-- c2/factory.go | 6 +- c2/factory_test.go | 4 +- c2/httpservefile/httpservefile.go | 48 +++++++++++- c2/httpserveshell/httpserveshell.go | 17 ++++- c2/shelltunnel/shelltunnel.go | 46 +++++++++--- c2/simpleshell/simpleshellclient.go | 34 +++++++-- c2/simpleshell/simpleshellserver.go | 56 ++++++++++---- c2/sslshell/sslshellserver.go | 55 ++++++++++---- framework.go | 24 +++++- 12 files changed, 405 insertions(+), 109 deletions(-) diff --git a/c2/channel/channel.go b/c2/channel/channel.go index 574b5f4..3025631 100644 --- a/c2/channel/channel.go +++ b/c2/channel/channel.go @@ -3,6 +3,17 @@ // components in order to support extracting components such as lhost and lport. package channel +import ( + "crypto/rand" + "encoding/base32" + "io" + "net" + "sync/atomic" + "time" + + "github.com/vulncheck-oss/go-exploit/output" +) + type Channel struct { IPAddr string HTTPAddr string @@ -10,4 +21,75 @@ type Channel struct { HTTPPort int Timeout int IsClient bool + Shutdown *atomic.Bool + Sessions map[string]Session + Input io.Reader + // Output io.Writer +} + +type Session struct { + RemoteAddr string + ConnectionTime time.Time + conn *net.Conn +} + +func (c *Channel) HasSessions() bool { + return len(c.Sessions) > 0 +} + +func (c *Channel) AddSession(conn *net.Conn, addr string) bool { + if len(c.Sessions) == 0 { + c.Sessions = make(map[string]Session) + } + k := make([]byte, 16) + _, err := rand.Read(k) + if err != nil { + output.PrintfFrameworkError("Could not add session: %s", err.Error()) + + return false + } + id := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(k) + c.Sessions[id] = Session{ + ConnectionTime: time.Now(), + conn: conn, + RemoteAddr: addr, + } + + return true +} + +func (c *Channel) RemoveSession(id string) bool { + if len(c.Sessions) == 0 { + output.PrintFrameworkError("No sessions exist") + + return false + } + _, ok := c.Sessions[id] + if !ok { + output.PrintFrameworkError("Session ID is not available") + + return false + } + if c.Sessions[id].conn != nil { + (*c.Sessions[id].conn).Close() + } + delete(c.Sessions, id) + + return true +} + +func (c *Channel) RemoveSessions() bool { + if len(c.Sessions) == 0 { + output.PrintFrameworkError("No sessions exist") + + return false + } + for k := range c.Sessions { + if c.Sessions[k].conn != nil { + (*c.Sessions[k].conn).Close() + } + delete(c.Sessions, k) + } + + return true } diff --git a/c2/cli/basic.go b/c2/cli/basic.go index db441cf..a29e7d2 100644 --- a/c2/cli/basic.go +++ b/c2/cli/basic.go @@ -4,81 +4,109 @@ import ( "bufio" "net" "os" - "strings" "sync" + "testing" "time" + "github.com/vulncheck-oss/go-exploit/c2/channel" "github.com/vulncheck-oss/go-exploit/output" "github.com/vulncheck-oss/go-exploit/protocol" ) // A very basic reverse/bind shell handler. -func Basic(conn net.Conn) { +func Basic(conn net.Conn, ch *channel.Channel) { // Create channels for communication between goroutines. responseCh := make(chan string) - quit := make(chan struct{}) // Use a WaitGroup to wait for goroutines to finish. var wg sync.WaitGroup // Goroutine to read responses from the server. wg.Add(1) - go func() { + + if !testing.Testing() { + ch.Input = os.Stdin + } + go func(ch *channel.Channel) { defer wg.Done() + defer func(channel *channel.Channel) { + // Signals for both routines to stop, this should get triggered when socket is closed + // and causes it to fail the read + channel.Shutdown.Store(true) + }(ch) responseBuffer := make([]byte, 1024) for { - select { - case <-quit: + if ch.Shutdown.Load() { + return + } + + err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + if err != nil { + output.PrintfFrameworkError("Error setting read deadline: %s, exiting.", err) + return - default: - _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) - bytesRead, err := conn.Read(responseBuffer) - if err != nil && !strings.Contains(err.Error(), "i/o timeout") { - // things have gone sideways, but the command line won't know that - // until they attempt to execute a command and the socket fails. - // i think that's largely okay. - return - } - if bytesRead > 0 { - // I think there is technically a race condition here where the socket - // could have move data to write, but the user has already called exit - // below. I that that's tolerable for now. - responseCh <- string(responseBuffer[:bytesRead]) - } + } + + bytesRead, err := conn.Read(responseBuffer) + if err != nil && !os.IsTimeout(err) { + // things have gone sideways, but the command line won't know that + // until they attempt to execute a command and the socket fails. + // i think that's largely okay. + return + } + + if bytesRead > 0 { + // I think there is technically a race condition here where the socket + // could have move data to write, but the user has already called exit + // below. I that that's tolerable for now. + responseCh <- string(responseBuffer[:bytesRead]) } } - }() + }(ch) // Goroutine to handle responses and print them. wg.Add(1) - go func() { + go func(channel *channel.Channel) { defer wg.Done() - for response := range responseCh { - select { - case <-quit: + for { + if channel.Shutdown.Load() { return - default: + } + select { + case response := <-responseCh: output.PrintShell(response) + default: } } - }() + }(ch) - for { - // read user input until they type 'exit\n' or the socket breaks - // note that ReadString is blocking, so they won't know the socket - // is broken until they attempt to write something - reader := bufio.NewReader(os.Stdin) - command, _ := reader.ReadString('\n') - ok := protocol.TCPWrite(conn, []byte(command)) - if !ok || command == "exit\n" { - break - } - } + go func(channel *channel.Channel) { + // no waitgroup for this one because blocking IO, but this should not matter + // since we are intentionally not trying to be a multi-implant C2 framework. + // There still remains the issue that you would need to hit enter to find out + // that the socket is dead but at least we can stop Basic() regardless of this fact. + // This issue of unblocking stdin is discussed at length here https://github.com/golang/go/issues/24842 + for { + reader := bufio.NewReader(ch.Input) + command, _ := reader.ReadString('\n') + if channel.Shutdown.Load() { + break + } + if command == "exit\n" { + channel.Shutdown.Store(true) - // signal for everyone to shutdown - quit <- struct{}{} - close(responseCh) + break + } + ok := protocol.TCPWrite(conn, []byte(command)) + if !ok { + channel.Shutdown.Store(true) + + break + } + } + }(ch) // wait until the go routines are clean up wg.Wait() + close(responseCh) } diff --git a/c2/external/external.go b/c2/external/external.go index 03b5e8b..753eb89 100644 --- a/c2/external/external.go +++ b/c2/external/external.go @@ -163,12 +163,13 @@ import ( // The Server struct holds the declared external modules internal functions and channel data. type Server struct { - flags func() - init func() - run func(int) bool - meta func(*channel.Channel) - Channel *channel.Channel - name string + flags func() + init func() + run func(int) bool + shutdown func() bool + meta func(*channel.Channel) + channel *channel.Channel + name string } // The External interface defines which functions are required to be defined in an external C2 @@ -180,6 +181,7 @@ type External interface { SetFlags(func()) SetInit(func()) SetRun(func(int) bool) + SetShutdown(func() bool) } var serverSingletons map[string]*Server @@ -245,14 +247,14 @@ func (externalServer *Server) SetChannel(f func(*channel.Channel)) { } // Init triggers the set C2 initialization and passes the channel to the external module. -func (externalServer *Server) Init(channel channel.Channel) bool { +func (externalServer *Server) Init(channel *channel.Channel) bool { if channel.IsClient { output.PrintFrameworkError("Called ExternalServer as a client.") return false } externalServer.init() - externalServer.meta(&channel) + externalServer.meta(channel) return true } @@ -268,3 +270,15 @@ func (externalServer *Server) SetRun(f func(int) bool) { func (externalServer *Server) Run(timeout int) { externalServer.run(timeout) } + +func (externalServer *Server) SetShutdown(f func() bool) { + externalServer.shutdown = f +} + +func (externalServer *Server) Shutdown() bool { + return externalServer.shutdown() +} + +func (externalServer *Server) Channel() *channel.Channel { + return externalServer.channel +} diff --git a/c2/factory.go b/c2/factory.go index ae05083..f22f9a4 100644 --- a/c2/factory.go +++ b/c2/factory.go @@ -14,8 +14,10 @@ import ( // An interface used by both reverse shells, bind shells, and stagers. type Interface interface { CreateFlags() - Init(channel channel.Channel) bool + Init(channel *channel.Channel) bool Run(timeout int) + Shutdown() bool + Channel() *channel.Channel } // Internal representation of a C2 implementation. Each C2 is managed by @@ -61,7 +63,7 @@ var internalSupported = map[string]Impl{ "SSLShellServer": {Name: "SSLShellServer", Category: SSLShellServerCategory}, "HTTPServeFile": {Name: "HTTPServeFile", Category: HTTPServeFileCategory}, "HTTPServeShell": {Name: "HTTPServeShell", Category: HTTPServeShellCategory}, - // Insure the internal supported External module name is an error if used + // Ensure the internal supported External module name is an error if used // directly. "External": {Name: "", Category: InvalidCategory}, "ShellTunnel": {Name: "ShellTunnel", Category: ShellTunnelCategory}, diff --git a/c2/factory_test.go b/c2/factory_test.go index 7c379e8..d5cc4de 100644 --- a/c2/factory_test.go +++ b/c2/factory_test.go @@ -22,12 +22,12 @@ func TestHTTPServeFileInit(t *testing.T) { } httpservefile.GetInstance().FilesToServe = "factory.go" - success = impl.Init(channel.Channel{IPAddr: "127.0.0.2", Port: 1271, HTTPAddr: "127.0.0.1", HTTPPort: 1270, IsClient: true}) + success = impl.Init(&channel.Channel{IPAddr: "127.0.0.2", Port: 1271, HTTPAddr: "127.0.0.1", HTTPPort: 1270, IsClient: true}) if success { t.Fatal("Failed to check if it was invoked as a client") } - success = impl.Init(channel.Channel{IPAddr: "127.0.0.2", Port: 1271, HTTPAddr: "127.0.0.1", HTTPPort: 1270, IsClient: false}) + success = impl.Init(&channel.Channel{IPAddr: "127.0.0.2", Port: 1271, HTTPAddr: "127.0.0.1", HTTPPort: 1270, IsClient: false}) if !success { t.Fatal("Failed to successfully process well-formed init call") } diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index a0b16c5..f2615f1 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -66,6 +66,7 @@ type Server struct { HostedFiles map[string]HostedFile // RealName -> struct // A comma delimited list of all the files to serve FilesToServe string + channel *channel.Channel } var singleton *Server @@ -94,8 +95,24 @@ func (httpServer *Server) CreateFlags() { } } +func (httpServer *Server) Channel() *channel.Channel { + return httpServer.channel +} + +func (httpServer *Server) Shutdown() bool { + output.PrintFrameworkStatus("Shutting down the HTTP Server") + if len(httpServer.Channel().Sessions) > 0 { + for k := range httpServer.Channel().Sessions { + httpServer.Channel().RemoveSession(k) + } + } + + return true +} + // load the provided files into memory, stored in a map, and loads the tls cert if needed. -func (httpServer *Server) Init(channel channel.Channel) bool { +func (httpServer *Server) Init(channel *channel.Channel) bool { + httpServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called C2HTTPServer as a client. Use lhost and lport.") @@ -192,6 +209,7 @@ func (httpServer *Server) Run(timeout int) { for _, hosted := range httpServer.HostedFiles { http.HandleFunc("/"+hosted.RandomName, func(writer http.ResponseWriter, req *http.Request) { output.PrintfFrameworkStatus("Connection from %s requested %s", req.RemoteAddr, req.URL.Path) + httpServer.Channel().AddSession(nil, req.RemoteAddr) writer.Header().Set("Server", httpServer.ServerField) @@ -223,18 +241,40 @@ func (httpServer *Server) Run(timeout int) { TLSConfig: tlsConfig, } defer server.Close() + go func() { + for { + if httpServer.Channel().Shutdown.Load() { + server.Close() + httpServer.Shutdown() + + break + } + } + }() _ = server.ListenAndServeTLS("", "") } else { output.PrintfFrameworkStatus("Starting an HTTP server on %s", connectionString) + server := http.Server{ + Addr: connectionString, + } + defer server.Close() + go func() { + for { + if httpServer.Channel().Shutdown.Load() { + server.Close() + httpServer.Shutdown() + + break + } + } + }() _ = http.ListenAndServe(connectionString, nil) } }() // let the server run for timeout seconds time.Sleep(time.Duration(timeout) * time.Second) - - // We don't actually clean up anything, but exiting c2 will eventually terminate the program - output.PrintFrameworkStatus("Shutting down the HTTP Server") + httpServer.Channel().Shutdown.Store(true) } // Returns the random name of the provided filename. If filename is empty, return the first entry. diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index 3ee7565..5336a23 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -48,6 +48,8 @@ type Server struct { HTTPAddr string // The HTTP port to bind to HTTPPort int + + channel *channel.Channel } var singleton *Server @@ -74,7 +76,8 @@ func (serveShell *Server) CreateFlags() { } // load the provided file into memory. Generate the random filename. -func (serveShell *Server) Init(channel channel.Channel) bool { +func (serveShell *Server) Init(channel *channel.Channel) bool { + serveShell.channel = channel if len(serveShell.HTTPAddr) == 0 { output.PrintFrameworkError("User must specify -httpServeFile.BindAddr") @@ -94,6 +97,18 @@ func (serveShell *Server) Init(channel channel.Channel) bool { return simpleshell.GetServerInstance().Init(channel) } +func (serveShell *Server) Shutdown() bool { + // Since no logic actually handles client interactions here, this is stub. + // + // Each of the shell sessions and httpservefile sessions should be handled by the channel Shutdown + // atomic. + return true +} + +func (serveShell *Server) Channel() *channel.Channel { + return serveShell.channel +} + // start the http server and shell and wait for them to exit. func (serveShell *Server) Run(timeout int) { var wg sync.WaitGroup diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index 8fe55be..7977048 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -84,6 +84,8 @@ type Server struct { // The file path to the user provided certificate (if provided) CertificateFile string + + channel *channel.Channel } var ( @@ -111,7 +113,8 @@ func (shellTunnel *Server) CreateFlags() { flag.StringVar(&shellTunnel.CertificateFile, "shellTunnel.CertificateFile", "", "The certificate to use when being an SSL server") } -func (shellTunnel *Server) Init(channel channel.Channel) bool { +func (shellTunnel *Server) Init(channel *channel.Channel) bool { + shellTunnel.channel = channel if channel.IsClient { output.PrintFrameworkError("Called ShellTunnel as a client. Use lhost and lport.") @@ -146,16 +149,39 @@ func (shellTunnel *Server) Init(channel channel.Channel) bool { return true } -func (shellTunnel *Server) Run(timeout int) { - // track if we got a shell or not - success := false +func (shellTunnel *Server) Shutdown() bool { + output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + if len(shellTunnel.Channel().Sessions) > 0 { + for k, session := range shellTunnel.Channel().Sessions { + output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) + shellTunnel.Channel().RemoveSession(k) + } + } + shellTunnel.Listener.Close() + return true +} + +func (shellTunnel *Server) Channel() *channel.Channel { + return shellTunnel.channel +} + +func (shellTunnel *Server) Run(timeout int) { // terminate the server if no shells come in within timeout seconds go func() { time.Sleep(time.Duration(timeout) * time.Second) - if !success { + if !shellTunnel.Channel().HasSessions() { output.PrintFrameworkError("Timeout met. Shutting down shell listener.") - shellTunnel.Listener.Close() + shellTunnel.Channel().Shutdown.Store(true) + } + }() + go func() { + for { + if shellTunnel.Channel().Shutdown.Load() { + shellTunnel.Shutdown() + + break + } } }() @@ -170,13 +196,12 @@ func (shellTunnel *Server) Run(timeout int) { return } - success = true output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) - go handleTunnelConn(client, shellTunnel.ConnectBackHost, shellTunnel.ConnectBackPort, shellTunnel.ConnectBackSSL) + go handleTunnelConn(client, shellTunnel.ConnectBackHost, shellTunnel.ConnectBackPort, shellTunnel.ConnectBackSSL, shellTunnel.channel) } } -func (shellTunnel *Server) createTLSListener(channel channel.Channel) (net.Listener, error) { +func (shellTunnel *Server) createTLSListener(channel *channel.Channel) (net.Listener, error) { var ok bool var err error var certificate tls.Certificate @@ -207,7 +232,7 @@ func (shellTunnel *Server) createTLSListener(channel channel.Channel) (net.Liste return listener, nil } -func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool) { +func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool, ch *channel.Channel) { defer clientConn.Close() // attempt to connect back to the serve. MixedConnect is both proxy aware and can @@ -218,6 +243,7 @@ func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool) { return } + ch.AddSession(&clientConn, clientConn.RemoteAddr().String()) output.PrintfFrameworkSuccess("Connect back to %s:%d success!", host, port) defer serverConn.Close() diff --git a/c2/simpleshell/simpleshellclient.go b/c2/simpleshell/simpleshellclient.go index df2a96b..d78fd28 100644 --- a/c2/simpleshell/simpleshellclient.go +++ b/c2/simpleshell/simpleshellclient.go @@ -13,6 +13,7 @@ import ( type Client struct { ConnectAddr string ConnectPort int + channel *channel.Channel } var clientSingleton *Client @@ -28,9 +29,23 @@ func GetClientInstance() *Client { func (shellClient *Client) CreateFlags() { } -func (shellClient *Client) Init(channel channel.Channel) bool { +func (shellClient *Client) Shutdown() bool { + ok := shellClient.Channel().RemoveSessions() + + // we done here + output.PrintfFrameworkStatus("Connection closed") + + return ok +} + +func (shellClient *Client) Channel() *channel.Channel { + return shellClient.channel +} + +func (shellClient *Client) Init(channel *channel.Channel) bool { shellClient.ConnectAddr = channel.IPAddr shellClient.ConnectPort = channel.Port + shellClient.channel = channel if !channel.IsClient { output.PrintFrameworkError("Called SimpleShellClient as a server. Use bport.") @@ -52,15 +67,22 @@ func (shellClient *Client) Run(timeout int) { if !ok { return } - // close the connection when the shell is complete - defer conn.Close() + go func() { + for { + if shellClient.Channel().Shutdown.Load() { + shellClient.Shutdown() + + break + } + } + }() output.PrintfFrameworkStatus("Active shell on %s:%d", shellClient.ConnectAddr, shellClient.ConnectPort) + shellClient.Channel().AddSession(&conn, conn.RemoteAddr().String()) - cli.Basic(conn) + cli.Basic(conn, shellClient.channel) - // we done here - output.PrintfFrameworkStatus("Connection closed") + shellClient.Channel().Shutdown.Store(true) } func connect(host string, port int, timeout int) (net.Conn, bool) { diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index 165bda4..eaadfb3 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -19,6 +19,7 @@ import ( // the terminate the connection. type Server struct { Listener net.Listener + channel *channel.Channel } var serverSingleton *Server @@ -36,8 +37,26 @@ func GetServerInstance() *Server { func (shellServer *Server) CreateFlags() { } +func (shellServer *Server) Channel() *channel.Channel { + return shellServer.channel +} + +func (shellServer *Server) Shutdown() bool { + output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + if len(shellServer.Channel().Sessions) > 0 { + for k, session := range shellServer.Channel().Sessions { + output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) + shellServer.Channel().RemoveSession(k) + } + } + shellServer.Listener.Close() + + return true +} + // Validate configuration and create the listening socket. -func (shellServer *Server) Init(channel channel.Channel) bool { +func (shellServer *Server) Init(channel *channel.Channel) bool { + shellServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called SimpleShellServer as a client. Use lhost and lport.") @@ -62,18 +81,23 @@ func (shellServer *Server) Run(timeout int) { // mutex for user input var cliLock sync.Mutex - // track if we got a shell or not - success := false - // terminate the server if no shells come in within timeout seconds go func() { time.Sleep(time.Duration(timeout) * time.Second) - if !success { + if !shellServer.Channel().HasSessions() { output.PrintFrameworkError("Timeout met. Shutting down shell listener.") - shellServer.Listener.Close() + shellServer.Channel().Shutdown.Store(true) } }() + go func() { + for { + if shellServer.Channel().Shutdown.Load() { + shellServer.Shutdown() + break + } + } + }() // Accept arbitrary connections. In the future we need something for the // user to select which connection to make active for { @@ -83,15 +107,14 @@ func (shellServer *Server) Run(timeout int) { output.PrintFrameworkError(err.Error()) } - return + break } - success = true output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) - go handleSimpleConn(client, &cliLock, client.RemoteAddr()) + go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) } } -func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr) { +func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr, ch *channel.Channel) { // connections will stack up here. Currently that will mean a race // to the next connection but we can add in attacker handling of // connections latter @@ -101,10 +124,11 @@ func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr) { // close the connection when the shell is complete defer conn.Close() - output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) - - cli.Basic(conn) - - // we done here - output.PrintfFrameworkStatus("Connection closed %v", remoteAddr) + // Add the session for tracking first, so it can be cleaned up. + ch.AddSession(&conn, conn.RemoteAddr().String()) + // Only complete the full session handshake once + if !ch.Shutdown.Load() { + output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) + cli.Basic(conn, ch) + } } diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index 2f47c76..ca3e965 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -38,6 +38,8 @@ type Server struct { PrivateKeyFile string // The file path to the user provided certificate (if provided) CertificateFile string + // Upstream c2 channel, used for signaling for shutdowns and status reporting + channel *channel.Channel } var singleton *Server @@ -61,9 +63,27 @@ func (shellServer *Server) CreateFlags() { } } +func (shellServer *Server) Shutdown() bool { + output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + if len(shellServer.channel.Sessions) > 0 { + for k, session := range shellServer.channel.Sessions { + output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) + shellServer.channel.RemoveSession(k) + } + } + shellServer.Listener.Close() + + return true +} + +func (shellServer *Server) Channel() *channel.Channel { + return shellServer.channel +} + // Parses the user provided files or generates the certificate files and starts // the TLS listener on the user provided IP/port. -func (shellServer *Server) Init(channel channel.Channel) bool { +func (shellServer *Server) Init(channel *channel.Channel) bool { + shellServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called SSLShellServer as a client. Use lhost and lport.") @@ -109,18 +129,23 @@ func (shellServer *Server) Run(timeout int) { // mutex for user input var cliLock sync.Mutex - // track if we got a shell or not - success := false - // terminate the server if no shells come in within timeout seconds go func() { time.Sleep(time.Duration(timeout) * time.Second) - if !success { + if !shellServer.channel.HasSessions() { output.PrintFrameworkError("Timeout met. Shutting down shell listener.") - shellServer.Listener.Close() + shellServer.channel.Shutdown.Store(true) } }() + go func() { + for { + if shellServer.channel.Shutdown.Load() { + shellServer.Shutdown() + break + } + } + }() // Accept arbitrary connections. In the future we need something for the // user to select which connection to make active for { @@ -132,13 +157,12 @@ func (shellServer *Server) Run(timeout int) { return } - success = true output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) - go handleSimpleConn(client, &cliLock, client.RemoteAddr()) + go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) } } -func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr) { +func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr, channel *channel.Channel) { // connections will stack up here. Currently that will mean a race // to the next connection but we can add in attacker handling of // connections latter @@ -148,10 +172,11 @@ func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr) { // close the connection when the shell is complete defer conn.Close() - output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) - - cli.Basic(conn) - - // we done here - output.PrintfFrameworkStatus("Connection closed %v", remoteAddr) + // Add the session for tracking first, so it can be cleaned up. + channel.AddSession(&conn, conn.RemoteAddr().String()) + // Only complete the full session handshake once + if !channel.Shutdown.Load() { + output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) + cli.Basic(conn, channel) + } } diff --git a/framework.go b/framework.go index 3651ca3..9377cd2 100644 --- a/framework.go +++ b/framework.go @@ -65,8 +65,10 @@ import ( "crypto/tls" "fmt" "os" + "os/signal" "strings" "sync" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2" @@ -293,11 +295,27 @@ func startC2Server(conf *config.Config) bool { return false } - success = c2Impl.Init(channel.Channel{ + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + + var shutdown atomic.Bool + shutdown.Store(false) + c2channel := &channel.Channel{ IPAddr: conf.Lhost, Port: conf.Lport, IsClient: false, - }) + Shutdown: &shutdown, + } + // Handle the signal interrupt channel. If the signal is triggered, then trigger the done + // channel which will clean up the server and close cleanly. + go func(sigint <-chan os.Signal, channel *channel.Channel) { + <-sigint + output.PrintfFrameworkStatus("Interrupt signal received") + channel.Shutdown.Store(true) + // os.Exit(1) + }(sigint, c2channel) + + success = c2Impl.Init(c2channel) if !success { return false } @@ -357,7 +375,7 @@ func doScan(sploit Exploit, conf *config.Config) bool { return false } - success = c2Impl.Init(channel.Channel{ + success = c2Impl.Init(&channel.Channel{ IPAddr: conf.Rhost, Port: conf.Bport, IsClient: true, From 8dd18fed94bf614d07546ffae1d191e95b621b20 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 10 Apr 2025 20:49:56 -0600 Subject: [PATCH 02/10] Clean shutdown & session tracking - complete testing plan - Ordering of session catching to kill off all `net.Conn` instances - Fixed doScan and doVerify holding C2 open until timeout, this appears to be a regression and will now instantly close the C2 if these fail. - Ensured that `-o` was supported - Clarified phrasing of shutdown belongs to C2 - `HTTPServeShell` emits 2 shutdown messages right now since it starts up 2 instances. Might be worth adding context to the channel for C2Type and printing that. Just juggled waitgroup properly and moved the Drop to inside the core check. - `ShellTunnel` required some coercing of Session logic in order to track connections for client and server upstream. There's a few hacks to deal with shutdown logic and currently can't handle a queue of sessions if we ever add support for that, it'll need to revisit some of that logic if we do (but that applies to everything). --- c2/channel/channel.go | 4 +-- c2/cli/basic.go | 2 ++ c2/httpservefile/httpservefile.go | 14 +++++++++-- c2/httpserveshell/httpserveshell.go | 39 +++++++++++++++++++++++++++-- c2/shelltunnel/shelltunnel.go | 20 +++++++++++++-- c2/simpleshell/simpleshellserver.go | 7 +++--- c2/sslshell/sslshellserver.go | 8 +++--- framework.go | 18 +++++++++++++ 8 files changed, 98 insertions(+), 14 deletions(-) diff --git a/c2/channel/channel.go b/c2/channel/channel.go index 3025631..f4520c1 100644 --- a/c2/channel/channel.go +++ b/c2/channel/channel.go @@ -60,7 +60,7 @@ func (c *Channel) AddSession(conn *net.Conn, addr string) bool { func (c *Channel) RemoveSession(id string) bool { if len(c.Sessions) == 0 { - output.PrintFrameworkError("No sessions exist") + output.PrintFrameworkDebug("No sessions exist") return false } @@ -80,7 +80,7 @@ func (c *Channel) RemoveSession(id string) bool { func (c *Channel) RemoveSessions() bool { if len(c.Sessions) == 0 { - output.PrintFrameworkError("No sessions exist") + output.PrintFrameworkDebug("No sessions exist") return false } diff --git a/c2/cli/basic.go b/c2/cli/basic.go index a29e7d2..9cc6de5 100644 --- a/c2/cli/basic.go +++ b/c2/cli/basic.go @@ -14,6 +14,8 @@ import ( ) // A very basic reverse/bind shell handler. +// +//nolint:gocognit func Basic(conn net.Conn, ch *channel.Channel) { // Create channels for communication between goroutines. responseCh := make(chan string) diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index f2615f1..3d5c228 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -30,6 +30,7 @@ import ( "os" "path" "strings" + "sync" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -227,7 +228,9 @@ func (httpServer *Server) Run(timeout int) { }) } + var wg sync.WaitGroup connectionString := fmt.Sprintf("%s:%d", httpServer.HTTPAddr, httpServer.HTTPPort) + wg.Add(1) go func() { if httpServer.TLS { output.PrintfFrameworkStatus("Starting an HTTPS server on %s", connectionString) @@ -246,6 +249,7 @@ func (httpServer *Server) Run(timeout int) { if httpServer.Channel().Shutdown.Load() { server.Close() httpServer.Shutdown() + wg.Done() break } @@ -263,17 +267,23 @@ func (httpServer *Server) Run(timeout int) { if httpServer.Channel().Shutdown.Load() { server.Close() httpServer.Shutdown() + wg.Done() break } } }() + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + output.PrintFrameworkError("Timeout met. Shutting down shell listener.") + // We do not care about sessions with file + httpServer.channel.Shutdown.Store(true) + }() _ = http.ListenAndServe(connectionString, nil) } }() - // let the server run for timeout seconds - time.Sleep(time.Duration(timeout) * time.Second) + wg.Wait() httpServer.Channel().Shutdown.Store(true) } diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index 5336a23..c57edf0 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -102,6 +102,13 @@ func (serveShell *Server) Shutdown() bool { // // Each of the shell sessions and httpservefile sessions should be handled by the channel Shutdown // atomic. + if serveShell.SSLShell { + sslshell.GetInstance().Channel().Shutdown.Store(true) + } else { + simpleshell.GetServerInstance().Channel().Shutdown.Store(true) + } + httpservefile.GetInstance().Channel().Shutdown.Store(true) + return true } @@ -113,21 +120,49 @@ func (serveShell *Server) Channel() *channel.Channel { func (serveShell *Server) Run(timeout int) { var wg sync.WaitGroup + go func() { + for { + if serveShell.channel.Shutdown.Load() { + serveShell.Shutdown() + wg.Done() + + break + } + } + }() // Spin up the shell wg.Add(1) go func() { - defer wg.Done() if serveShell.SSLShell { sslshell.GetInstance().Run(timeout) + go func() { + for { + if sslshell.GetInstance().Channel().Shutdown.Load() { + sslshell.GetInstance().Shutdown() + wg.Done() + + break + } + } + }() } else { simpleshell.GetServerInstance().Run(timeout) + go func() { + for { + if simpleshell.GetServerInstance().Channel().Shutdown.Load() { + simpleshell.GetServerInstance().Shutdown() + wg.Done() + + break + } + } + }() } }() // Spin up the http server wg.Add(1) go func() { - defer wg.Done() httpservefile.GetInstance().Run(timeout) }() diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index 7977048..95aa946 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -150,7 +150,7 @@ func (shellTunnel *Server) Init(channel *channel.Channel) bool { } func (shellTunnel *Server) Shutdown() bool { - output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + output.PrintFrameworkStatus("C2 received shutdown, killing server and client sockets for shell tunnel") if len(shellTunnel.Channel().Sessions) > 0 { for k, session := range shellTunnel.Channel().Sessions { output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) @@ -196,7 +196,11 @@ func (shellTunnel *Server) Run(timeout int) { return } + if shellTunnel.Channel().Shutdown.Load() { + break + } output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) + shellTunnel.Channel().AddSession(&client, client.RemoteAddr().String()) go handleTunnelConn(client, shellTunnel.ConnectBackHost, shellTunnel.ConnectBackPort, shellTunnel.ConnectBackSSL, shellTunnel.channel) } } @@ -239,11 +243,20 @@ func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool, ch * // produce an ssl or unencrypted connection so works pretty nice here serverConn, ok := protocol.MixedConnect(host, port, ssl) if !ok { + // This is a bit of a hack as the type of C2 callbacks is not tracked and we will have 1 + // in the sessions from the client call. This checks if it's 1 or less and if it is then it + // will drop future conns. + if len(ch.Sessions) <= 1 { + output.PrintfFrameworkError("Failed to connect back to %s:%d closing server", host, port) + ch.Shutdown.Store(true) + + return + } output.PrintfFrameworkError("Failed to connect back to %s:%d", host, port) return } - ch.AddSession(&clientConn, clientConn.RemoteAddr().String()) + ch.AddSession(&serverConn, serverConn.RemoteAddr().String()) output.PrintfFrameworkSuccess("Connect back to %s:%d success!", host, port) defer serverConn.Close() @@ -262,4 +275,7 @@ func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool, ch * }() <-done + // Trigger shutdown after the first connection is dropped. in a future where multiple are handled + // this might not be ideal. Revist this when that time comes. + ch.Shutdown.Store(true) } diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index eaadfb3..85a11e0 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -42,7 +42,7 @@ func (shellServer *Server) Channel() *channel.Channel { } func (shellServer *Server) Shutdown() bool { - output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + output.PrintFrameworkStatus("C2 received shutdown, killing server and client sockets for shell server") if len(shellServer.Channel().Sessions) > 0 { for k, session := range shellServer.Channel().Sessions { output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) @@ -110,6 +110,9 @@ func (shellServer *Server) Run(timeout int) { break } output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) + // Add the session for tracking first, so it can be cleaned up. + shellServer.Channel().AddSession(&client, client.RemoteAddr().String()) + go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) } } @@ -124,8 +127,6 @@ func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr, c // close the connection when the shell is complete defer conn.Close() - // Add the session for tracking first, so it can be cleaned up. - ch.AddSession(&conn, conn.RemoteAddr().String()) // Only complete the full session handshake once if !ch.Shutdown.Load() { output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index ca3e965..162182c 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -64,7 +64,7 @@ func (shellServer *Server) CreateFlags() { } func (shellServer *Server) Shutdown() bool { - output.PrintFrameworkStatus("Received shutdown, killing server and client sockets") + output.PrintFrameworkStatus("C2 received shutdown, killing server and client sockets for SSL shell server") if len(shellServer.channel.Sessions) > 0 { for k, session := range shellServer.channel.Sessions { output.PrintfFrameworkStatus("Connection closed: %s", session.RemoteAddr) @@ -158,6 +158,10 @@ func (shellServer *Server) Run(timeout int) { return } output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) + + // Add the session for tracking first, so it can be cleaned up. + shellServer.Channel().AddSession(&client, client.RemoteAddr().String()) + go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) } } @@ -172,8 +176,6 @@ func handleSimpleConn(conn net.Conn, cliLock *sync.Mutex, remoteAddr net.Addr, c // close the connection when the shell is complete defer conn.Close() - // Add the session for tracking first, so it can be cleaned up. - channel.AddSession(&conn, conn.RemoteAddr().String()) // Only complete the full session handshake once if !channel.Shutdown.Load() { output.PrintfFrameworkStatus("Active shell from %v", remoteAddr) diff --git a/framework.go b/framework.go index 9377cd2..b602d7a 100644 --- a/framework.go +++ b/framework.go @@ -332,6 +332,8 @@ func startC2Server(conf *config.Config) bool { } // execute verify, version check, and exploit. Return false if an unrecoverable error occurred. +// +//nolint:gocognit func doScan(sploit Exploit, conf *config.Config) bool { // autodetect if the target is using SSL or not if conf.DetermineSSL { @@ -344,12 +346,28 @@ func doScan(sploit Exploit, conf *config.Config) bool { if conf.DoVerify { if !doVerify(sploit, conf) { + if !conf.ThirdPartyC2Server { + c, ok := c2.GetInstance(conf.C2Type) + if !ok { + output.PrintFrameworkError("Could not get C2 configuration") + } + c.Shutdown() + } + return true } } if conf.DoVersionCheck { if !doVersionCheck(sploit, conf) { + if !conf.ThirdPartyC2Server { + c, ok := c2.GetInstance(conf.C2Type) + if !ok { + output.PrintFrameworkError("Could not get C2 configuration") + } + c.Shutdown() + } + return true } } From 1dd48277134bc9a0bfafcdb63b332dd068752149 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Fri, 11 Apr 2025 10:56:31 -0600 Subject: [PATCH 03/10] Add documentation and comments for clean shutdown. --- c2/channel/channel.go | 16 +++++++++++++++- c2/cli/basic.go | 2 ++ c2/external/external.go | 6 ++++++ c2/httpservefile/httpservefile.go | 8 +++++++- c2/httpserveshell/httpserveshell.go | 7 ++++++- c2/shelltunnel/shelltunnel.go | 4 ++++ c2/simpleshell/simpleshellclient.go | 1 + c2/simpleshell/simpleshellserver.go | 3 +++ c2/sslshell/sslshellserver.go | 2 ++ framework.go | 9 ++++++++- 10 files changed, 54 insertions(+), 4 deletions(-) diff --git a/c2/channel/channel.go b/c2/channel/channel.go index f4520c1..adba897 100644 --- a/c2/channel/channel.go +++ b/c2/channel/channel.go @@ -24,7 +24,7 @@ type Channel struct { Shutdown *atomic.Bool Sessions map[string]Session Input io.Reader - // Output io.Writer + Output io.Writer // Currently unused but figured we'd add it ahead of time } type Session struct { @@ -33,14 +33,25 @@ type Session struct { conn *net.Conn } +// HasSessions checks if a channel has any tracked sessions. This can be used to lookup if a C2 +// successfully received callbacks: +// +// c, ok := c2.GetInstance(conf.C2Type) +// c.Channel().HasSessions() func (c *Channel) HasSessions() bool { return len(c.Sessions) > 0 } +// AddSession adds a remote connection for session tracking. If a network connection is being +// tracked it can be added here and will be cleaned up and closed automatically by the C2 on +// shutdown. func (c *Channel) AddSession(conn *net.Conn, addr string) bool { if len(c.Sessions) == 0 { c.Sessions = make(map[string]Session) } + // This is my session randomizing logic. The theory is that it keeps us dependency free while + // also creating the same 16bit strength of UUIDs. If we only plan on using the random UUIDs + // anyway this should meet the same goals while also being URL safe and no special characters. k := make([]byte, 16) _, err := rand.Read(k) if err != nil { @@ -50,6 +61,7 @@ func (c *Channel) AddSession(conn *net.Conn, addr string) bool { } id := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(k) c.Sessions[id] = Session{ + // Add the time of now to the current connection time ConnectionTime: time.Now(), conn: conn, RemoteAddr: addr, @@ -58,6 +70,7 @@ func (c *Channel) AddSession(conn *net.Conn, addr string) bool { return true } +// RemoveSession removes a specific session ID and if a connection exists, closes it. func (c *Channel) RemoveSession(id string) bool { if len(c.Sessions) == 0 { output.PrintFrameworkDebug("No sessions exist") @@ -78,6 +91,7 @@ func (c *Channel) RemoveSession(id string) bool { return true } +// RemoveSessions removes all tracked sessions and closes any open connections if applicable. func (c *Channel) RemoveSessions() bool { if len(c.Sessions) == 0 { output.PrintFrameworkDebug("No sessions exist") diff --git a/c2/cli/basic.go b/c2/cli/basic.go index 9cc6de5..7e13856 100644 --- a/c2/cli/basic.go +++ b/c2/cli/basic.go @@ -26,6 +26,8 @@ func Basic(conn net.Conn, ch *channel.Channel) { // Goroutine to read responses from the server. wg.Add(1) + // If running in the test context inherit the channel input setting, this will let us control the + // input of the shell programmatically. if !testing.Testing() { ch.Input = os.Stdin } diff --git a/c2/external/external.go b/c2/external/external.go index 753eb89..eb8de15 100644 --- a/c2/external/external.go +++ b/c2/external/external.go @@ -271,14 +271,20 @@ func (externalServer *Server) Run(timeout int) { externalServer.run(timeout) } +// SetShutdown sets the function for server shutdown handling and session cleanup logic. This +// function is what gets called when the framework receives a OS signal, a shell is closed, or +// manually invoked. func (externalServer *Server) SetShutdown(f func() bool) { externalServer.shutdown = f } +// Shutdown triggers the set shutdown function. func (externalServer *Server) Shutdown() bool { return externalServer.shutdown() } +// Return the underlying C2 channel containing channel metadata and session tracking. func (externalServer *Server) Channel() *channel.Channel { + // I'd much rather have just exposed a `Server.Channel`, but we are interface bound return externalServer.channel } diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index 3d5c228..e74d201 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -67,7 +67,8 @@ type Server struct { HostedFiles map[string]HostedFile // RealName -> struct // A comma delimited list of all the files to serve FilesToServe string - channel *channel.Channel + // C2 channel and session metadata + channel *channel.Channel } var singleton *Server @@ -96,10 +97,12 @@ func (httpServer *Server) CreateFlags() { } } +// Return the C2 specific channel. func (httpServer *Server) Channel() *channel.Channel { return httpServer.channel } +// Shutdown the C2 server and cleanup all the sessions. func (httpServer *Server) Shutdown() bool { output.PrintFrameworkStatus("Shutting down the HTTP Server") if len(httpServer.Channel().Sessions) > 0 { @@ -244,6 +247,7 @@ func (httpServer *Server) Run(timeout int) { TLSConfig: tlsConfig, } defer server.Close() + // Track if the server has signaled for shutdown and if so mark the waitgroup and trigger shutdown go func() { for { if httpServer.Channel().Shutdown.Load() { @@ -262,6 +266,7 @@ func (httpServer *Server) Run(timeout int) { Addr: connectionString, } defer server.Close() + // Track if the server has signaled for shutdown and if so mark the waitgroup and trigger shutdown go func() { for { if httpServer.Channel().Shutdown.Load() { @@ -273,6 +278,7 @@ func (httpServer *Server) Run(timeout int) { } } }() + // Handle timeouts go func() { time.Sleep(time.Duration(timeout) * time.Second) output.PrintFrameworkError("Timeout met. Shutting down shell listener.") diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index c57edf0..d509c2a 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -48,7 +48,7 @@ type Server struct { HTTPAddr string // The HTTP port to bind to HTTPPort int - + // The underlying C2 channel with metadata and session information channel *channel.Channel } @@ -97,6 +97,7 @@ func (serveShell *Server) Init(channel *channel.Channel) bool { return simpleshell.GetServerInstance().Init(channel) } +// Shutdown triggers the shutdown for all running C2s. func (serveShell *Server) Shutdown() bool { // Since no logic actually handles client interactions here, this is stub. // @@ -112,6 +113,7 @@ func (serveShell *Server) Shutdown() bool { return true } +// Return the underlying C2 channel. func (serveShell *Server) Channel() *channel.Channel { return serveShell.channel } @@ -120,6 +122,7 @@ func (serveShell *Server) Channel() *channel.Channel { func (serveShell *Server) Run(timeout int) { var wg sync.WaitGroup + // Check if the channel has signaled shutdown and trigger cleanup no matter where it comes from. go func() { for { if serveShell.channel.Shutdown.Load() { @@ -135,6 +138,7 @@ func (serveShell *Server) Run(timeout int) { go func() { if serveShell.SSLShell { sslshell.GetInstance().Run(timeout) + // Handle shutdown for OS signaling or timeout from underlying instance go func() { for { if sslshell.GetInstance().Channel().Shutdown.Load() { @@ -147,6 +151,7 @@ func (serveShell *Server) Run(timeout int) { }() } else { simpleshell.GetServerInstance().Run(timeout) + // Handle shutdown for OS signaling or timeout from underlying instance go func() { for { if simpleshell.GetServerInstance().Channel().Shutdown.Load() { diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index 95aa946..60442f5 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -85,6 +85,7 @@ type Server struct { // The file path to the user provided certificate (if provided) CertificateFile string + // Underlying C2 channel with metadata and session tracking channel *channel.Channel } @@ -200,6 +201,9 @@ func (shellTunnel *Server) Run(timeout int) { break } output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr()) + // ShellTunnel is a bit of an outliar as we need to track the incoming connections and also the + // tunneled connections. This will allow for cleanup of connections on both ends of the pipe, + // but may not be immediately clear. shellTunnel.Channel().AddSession(&client, client.RemoteAddr().String()) go handleTunnelConn(client, shellTunnel.ConnectBackHost, shellTunnel.ConnectBackPort, shellTunnel.ConnectBackSSL, shellTunnel.channel) } diff --git a/c2/simpleshell/simpleshellclient.go b/c2/simpleshell/simpleshellclient.go index d78fd28..904f043 100644 --- a/c2/simpleshell/simpleshellclient.go +++ b/c2/simpleshell/simpleshellclient.go @@ -67,6 +67,7 @@ func (shellClient *Client) Run(timeout int) { if !ok { return } + // Track if the C2 is indicated to shutdown for any reason. go func() { for { if shellClient.Channel().Shutdown.Load() { diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index 85a11e0..0399c91 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -37,10 +37,12 @@ func GetServerInstance() *Server { func (shellServer *Server) CreateFlags() { } +// Get the underlying C2 channel with metadata and session information. func (shellServer *Server) Channel() *channel.Channel { return shellServer.channel } +// Shutdown the C2 and cleanup any active connections. func (shellServer *Server) Shutdown() bool { output.PrintFrameworkStatus("C2 received shutdown, killing server and client sockets for shell server") if len(shellServer.Channel().Sessions) > 0 { @@ -89,6 +91,7 @@ func (shellServer *Server) Run(timeout int) { shellServer.Channel().Shutdown.Store(true) } }() + // Track if the shutdown is signaled for any reason. go func() { for { if shellServer.Channel().Shutdown.Load() { diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index 162182c..65d485b 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -63,6 +63,7 @@ func (shellServer *Server) CreateFlags() { } } +// Shutdown the C2 and close server and client connections when applicable. func (shellServer *Server) Shutdown() bool { output.PrintFrameworkStatus("C2 received shutdown, killing server and client sockets for SSL shell server") if len(shellServer.channel.Sessions) > 0 { @@ -76,6 +77,7 @@ func (shellServer *Server) Shutdown() bool { return true } +// Return the underlying C2 channel with metadata and session information. func (shellServer *Server) Channel() *channel.Channel { return shellServer.channel } diff --git a/framework.go b/framework.go index b602d7a..ea02fc3 100644 --- a/framework.go +++ b/framework.go @@ -312,7 +312,6 @@ func startC2Server(conf *config.Config) bool { <-sigint output.PrintfFrameworkStatus("Interrupt signal received") channel.Shutdown.Store(true) - // os.Exit(1) }(sigint, c2channel) success = c2Impl.Init(c2channel) @@ -346,10 +345,14 @@ func doScan(sploit Exploit, conf *config.Config) bool { if conf.DoVerify { if !doVerify(sploit, conf) { + // C2 cleanup is meaningless with third party C2s if !conf.ThirdPartyC2Server { + // Shuts down the C2 if verification fails c, ok := c2.GetInstance(conf.C2Type) if !ok { output.PrintFrameworkError("Could not get C2 configuration") + + return false } c.Shutdown() } @@ -360,10 +363,14 @@ func doScan(sploit Exploit, conf *config.Config) bool { if conf.DoVersionCheck { if !doVersionCheck(sploit, conf) { + // C2 cleanup is meaningless with third party C2s if !conf.ThirdPartyC2Server { + // Shuts down the C2 if version check fails c, ok := c2.GetInstance(conf.C2Type) if !ok { output.PrintFrameworkError("Could not get C2 configuration") + + return false } c.Shutdown() } From 6fb5629c879246691aac15cfacc2f3181c9e70c1 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 24 Apr 2025 13:26:49 -0600 Subject: [PATCH 04/10] Add clean shutdown to HTTPShellServer and add shutdown timeout for TLS on ServeFile --- c2/httpservefile/httpservefile.go | 8 ++ c2/httpshellserver/httpshellserver.go | 109 +++++++++++++++++++++----- 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index e74d201..10c87c0 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -259,6 +259,14 @@ func (httpServer *Server) Run(timeout int) { } } }() + // Handle timeouts + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + output.PrintFrameworkError("Timeout met. Shutting down shell listener.") + // We do not care about sessions with file + httpServer.channel.Shutdown.Store(true) + }() + _ = server.ListenAndServeTLS("", "") } else { output.PrintfFrameworkStatus("Starting an HTTP server on %s", connectionString) diff --git a/c2/httpshellserver/httpshellserver.go b/c2/httpshellserver/httpshellserver.go index 616be60..bfb51c1 100644 --- a/c2/httpshellserver/httpshellserver.go +++ b/c2/httpshellserver/httpshellserver.go @@ -10,12 +10,13 @@ import ( "os" "strings" "sync" + "testing" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" "github.com/vulncheck-oss/go-exploit/encryption" - "github.com/vulncheck-oss/go-exploit/random" "github.com/vulncheck-oss/go-exploit/output" + "github.com/vulncheck-oss/go-exploit/random" ) var ( @@ -45,6 +46,7 @@ type Server struct { // Randomly generated during init, gives some sense of security where there is otherwise none. // This should appear in a header with the name VC-Auth AuthHeader string + channel *channel.Channel } // A basic singleton interface for the c2. @@ -56,8 +58,13 @@ func GetInstance() *Server { return singleton } -func (httpServer *Server) Init(channel channel.Channel) bool { - httpServer.AuthHeader = random.RandLetters(20) +func (httpServer *Server) Init(channel *channel.Channel) bool { + httpServer.channel = channel + if testing.Testing() { + httpServer.AuthHeader = "testing-auth-header" + } else { + httpServer.AuthHeader = random.RandLetters(20) + } if channel.IsClient { output.PrintFrameworkError("Called C2HTTPServer as a client. Use lhost and lport.") @@ -104,29 +111,48 @@ func (httpServer *Server) CreateFlags() { flag.StringVar(&httpServer.CertificateFile, "httpShellServer.CertificateFile", "", "The certificate to use with the HTTPS server") } +// Get the underlying C2 channel with metadata and session information. +func (httpServer *Server) Channel() *channel.Channel { + return httpServer.channel +} + +// Shutdown the C2 server and cleanup all the sessions. +func (httpServer *Server) Shutdown() bool { + output.PrintFrameworkStatus("Shutting down the HTTP Server") + if len(httpServer.Channel().Sessions) > 0 { + for k := range httpServer.Channel().Sessions { + httpServer.Channel().RemoveSession(k) + } + } + + return true +} + // start the HTTP server and listen for incoming requests for `httpServer.FileName`. +// //nolint:gocognit func (httpServer *Server) Run(timeout int) { http.HandleFunc("/rx", func(writer http.ResponseWriter, req *http.Request) { - authHeader := req.Header.Get("VC-Auth") + authHeader := req.Header.Get("Vc-Auth") if authHeader != httpServer.AuthHeader { writer.WriteHeader(http.StatusForbidden) - output.PrintfFrameworkDebug("Auth header mismatch from %s: %s, should be %s", req.RemoteAddr, req.Header.Get("VC-Auth"), httpServer.AuthHeader) - + output.PrintfFrameworkDebug("Auth header mismatch from %s: %s, should be %s", req.RemoteAddr, req.Header.Get("Vc-Auth"), httpServer.AuthHeader) + return } + body, _ := io.ReadAll(req.Body) if strings.TrimSpace(string(body)) != "" { - fmt.Printf("\n%s: %s\n", req.RemoteAddr, string(body)) + output.PrintfSuccess("%s: %s", req.RemoteAddr, string(body)) } }) http.HandleFunc("/", func(writer http.ResponseWriter, req *http.Request) { - authHeader := req.Header.Get("VC-Auth") + authHeader := req.Header.Get("Vc-Auth") if authHeader != httpServer.AuthHeader { writer.WriteHeader(http.StatusForbidden) - output.PrintfFrameworkDebug("Auth header mismatch from %s: %s, should be %s", req.RemoteAddr, req.Header.Get("VC-Auth"), httpServer.AuthHeader) - + output.PrintfFrameworkDebug("Auth header mismatch from %s: %s, should be %s", req.RemoteAddr, req.Header.Get("Vc-Auth"), httpServer.AuthHeader) + return } lastSeen = time.Now() @@ -135,6 +161,7 @@ func (httpServer *Server) Run(timeout int) { if !httpServer.Success { go func() { httpServer.Success = true + httpServer.Channel().AddSession(nil, req.RemoteAddr) output.PrintfSuccess("Received initial connection from %s, entering shell", req.RemoteAddr) cliLock.Lock() defer cliLock.Unlock() @@ -155,8 +182,9 @@ func (httpServer *Server) Run(timeout int) { } if trimmedCommand == "exit" { output.PrintStatus("Exit received, shutting down") + httpServer.Channel().Shutdown.Store(true) - os.Exit(0) + return } if strings.TrimSpace(command) != "" { commandChan <- strings.TrimSpace(command) @@ -174,7 +202,9 @@ func (httpServer *Server) Run(timeout int) { } }) + var wg sync.WaitGroup connectionString := fmt.Sprintf("%s:%d", httpServer.HTTPAddr, httpServer.HTTPPort) + wg.Add(1) go func() { if httpServer.TLS { output.PrintfFrameworkStatus("Starting an HTTPS server on %s...", connectionString) @@ -184,23 +214,64 @@ func (httpServer *Server) Run(timeout int) { //nolint MinVersion: tls.VersionSSL30, } - server := http.Server { + server := http.Server{ Addr: connectionString, TLSConfig: tlsConfig, // required to disable HTTP/2 according to https://pkg.go.dev/net/http#hdr-HTTP_2 - TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler),1), + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 1), } defer server.Close() + // Track if the server has signaled for shutdown and if so mark the waitgroup and trigger shutdown + go func() { + for { + if httpServer.Channel().Shutdown.Load() { + httpServer.Shutdown() + server.Close() + wg.Done() + + break + } + } + }() + // Handle timeouts + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + if !httpServer.Channel().HasSessions() { + output.PrintFrameworkError("Timeout met. Shutting down shell listener.") + httpServer.channel.Shutdown.Store(true) + } + }() _ = server.ListenAndServeTLS("", "") } else { output.PrintfFrameworkStatus("Starting an HTTP server on %s", connectionString) - _ = http.ListenAndServe(connectionString, nil) + server := http.Server{ + Addr: connectionString, + } + defer server.Close() + // Track if the server has signaled for shutdown and if so mark the waitgroup and trigger shutdown + go func() { + for { + if httpServer.Channel().Shutdown.Load() { + server.Close() + httpServer.Shutdown() + wg.Done() + + break + } + } + }() + // Handle timeouts + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + if !httpServer.Channel().HasSessions() { + output.PrintFrameworkError("Timeout met. Shutting down shell listener.") + httpServer.channel.Shutdown.Store(true) + } + }() + _ = server.ListenAndServe() } }() - // let the server run for timeout seconds - time.Sleep(time.Duration(timeout) * time.Second) - - // We don't actually clean up anything, but exiting c2 will eventually terminate the program - output.PrintFrameworkStatus("Shutting down the HTTP Shell Server") + wg.Wait() + httpServer.Channel().Shutdown.Store(true) } From a7587c633ea49cf1828d8c2b833ad7037f69ee4b Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Fri, 25 Apr 2025 13:04:16 -0600 Subject: [PATCH 05/10] Add nil c2 channel checks --- c2/httpservefile/httpservefile.go | 5 +++++ c2/httpserveshell/httpserveshell.go | 5 +++++ c2/httpshellserver/httpshellserver.go | 5 +++++ c2/shelltunnel/shelltunnel.go | 5 +++++ c2/simpleshell/simpleshellclient.go | 5 +++++ c2/simpleshell/simpleshellserver.go | 5 +++++ c2/sslshell/sslshellserver.go | 5 +++++ 7 files changed, 35 insertions(+) diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index 10c87c0..3d8102a 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -116,6 +116,11 @@ func (httpServer *Server) Shutdown() bool { // load the provided files into memory, stored in a map, and loads the tls cert if needed. func (httpServer *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } httpServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called C2HTTPServer as a client. Use lhost and lport.") diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index d509c2a..54d2c79 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -77,6 +77,11 @@ func (serveShell *Server) CreateFlags() { // load the provided file into memory. Generate the random filename. func (serveShell *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } serveShell.channel = channel if len(serveShell.HTTPAddr) == 0 { output.PrintFrameworkError("User must specify -httpServeFile.BindAddr") diff --git a/c2/httpshellserver/httpshellserver.go b/c2/httpshellserver/httpshellserver.go index bfb51c1..fa6c1f1 100644 --- a/c2/httpshellserver/httpshellserver.go +++ b/c2/httpshellserver/httpshellserver.go @@ -59,6 +59,11 @@ func GetInstance() *Server { } func (httpServer *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } httpServer.channel = channel if testing.Testing() { httpServer.AuthHeader = "testing-auth-header" diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index 60442f5..1b788f0 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -115,6 +115,11 @@ func (shellTunnel *Server) CreateFlags() { } func (shellTunnel *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } shellTunnel.channel = channel if channel.IsClient { output.PrintFrameworkError("Called ShellTunnel as a client. Use lhost and lport.") diff --git a/c2/simpleshell/simpleshellclient.go b/c2/simpleshell/simpleshellclient.go index 904f043..9ffc919 100644 --- a/c2/simpleshell/simpleshellclient.go +++ b/c2/simpleshell/simpleshellclient.go @@ -43,6 +43,11 @@ func (shellClient *Client) Channel() *channel.Channel { } func (shellClient *Client) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } shellClient.ConnectAddr = channel.IPAddr shellClient.ConnectPort = channel.Port shellClient.channel = channel diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index 0399c91..01059fc 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -58,6 +58,11 @@ func (shellServer *Server) Shutdown() bool { // Validate configuration and create the listening socket. func (shellServer *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } shellServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called SimpleShellServer as a client. Use lhost and lport.") diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index 65d485b..6dc44c8 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -85,6 +85,11 @@ func (shellServer *Server) Channel() *channel.Channel { // Parses the user provided files or generates the certificate files and starts // the TLS listener on the user provided IP/port. func (shellServer *Server) Init(channel *channel.Channel) bool { + if channel == nil { + output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") + + return false + } shellServer.channel = channel if channel.IsClient { output.PrintFrameworkError("Called SSLShellServer as a client. Use lhost and lport.") From 03f3f9080b11e2ef838bc18e1e0a6c48b3f2d3a1 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Fri, 25 Apr 2025 13:15:12 -0600 Subject: [PATCH 06/10] Fix the channel check to be an atomic check and switch it to set a default vs error. --- c2/httpservefile/httpservefile.go | 6 ++---- c2/httpserveshell/httpserveshell.go | 6 ++---- c2/httpshellserver/httpshellserver.go | 3 +++ c2/shelltunnel/shelltunnel.go | 6 ++---- c2/simpleshell/simpleshellclient.go | 6 ++---- c2/simpleshell/simpleshellserver.go | 6 ++---- c2/sslshell/sslshellserver.go | 6 ++---- 7 files changed, 15 insertions(+), 24 deletions(-) diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index 3d8102a..308baab 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -116,10 +116,8 @@ func (httpServer *Server) Shutdown() bool { // load the provided files into memory, stored in a map, and loads the tls cert if needed. func (httpServer *Server) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } httpServer.channel = channel if channel.IsClient { diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index 54d2c79..83a6940 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -77,10 +77,8 @@ func (serveShell *Server) CreateFlags() { // load the provided file into memory. Generate the random filename. func (serveShell *Server) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } serveShell.channel = channel if len(serveShell.HTTPAddr) == 0 { diff --git a/c2/httpshellserver/httpshellserver.go b/c2/httpshellserver/httpshellserver.go index fa6c1f1..d5a5ebf 100644 --- a/c2/httpshellserver/httpshellserver.go +++ b/c2/httpshellserver/httpshellserver.go @@ -59,6 +59,9 @@ func GetInstance() *Server { } func (httpServer *Server) Init(channel *channel.Channel) bool { + if channel.Shutdown == nil { + channel.Shutdown.Store(false) + } if channel == nil { output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index 1b788f0..c02c7e3 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -115,10 +115,8 @@ func (shellTunnel *Server) CreateFlags() { } func (shellTunnel *Server) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } shellTunnel.channel = channel if channel.IsClient { diff --git a/c2/simpleshell/simpleshellclient.go b/c2/simpleshell/simpleshellclient.go index 9ffc919..e27aadc 100644 --- a/c2/simpleshell/simpleshellclient.go +++ b/c2/simpleshell/simpleshellclient.go @@ -43,10 +43,8 @@ func (shellClient *Client) Channel() *channel.Channel { } func (shellClient *Client) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } shellClient.ConnectAddr = channel.IPAddr shellClient.ConnectPort = channel.Port diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index 01059fc..58d62b2 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -58,10 +58,8 @@ func (shellServer *Server) Shutdown() bool { // Validate configuration and create the listening socket. func (shellServer *Server) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } shellServer.channel = channel if channel.IsClient { diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index 6dc44c8..9e6d5bd 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -85,10 +85,8 @@ func (shellServer *Server) Channel() *channel.Channel { // Parses the user provided files or generates the certificate files and starts // the TLS listener on the user provided IP/port. func (shellServer *Server) Init(channel *channel.Channel) bool { - if channel == nil { - output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") - - return false + if channel.Shutdown == nil { + channel.Shutdown.Store(false) } shellServer.channel = channel if channel.IsClient { From 45d6bd8611ebb7070ceda05aa67c848656265244 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Fri, 25 Apr 2025 13:29:49 -0600 Subject: [PATCH 07/10] Initialize null shutdown channels and bools vs erroring --- c2/httpservefile/httpservefile.go | 7 ++++++- c2/httpserveshell/httpserveshell.go | 7 ++++++- c2/httpshellserver/httpshellserver.go | 7 ++++++- c2/shelltunnel/shelltunnel.go | 7 ++++++- c2/simpleshell/simpleshellclient.go | 8 +++++++- c2/simpleshell/simpleshellserver.go | 7 ++++++- c2/sslshell/sslshellserver.go | 7 ++++++- 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/c2/httpservefile/httpservefile.go b/c2/httpservefile/httpservefile.go index 308baab..0ed6d1e 100644 --- a/c2/httpservefile/httpservefile.go +++ b/c2/httpservefile/httpservefile.go @@ -31,6 +31,7 @@ import ( "path" "strings" "sync" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -117,7 +118,11 @@ func (httpServer *Server) Shutdown() bool { // load the provided files into memory, stored in a map, and loads the tls cert if needed. func (httpServer *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } httpServer.channel = channel if channel.IsClient { diff --git a/c2/httpserveshell/httpserveshell.go b/c2/httpserveshell/httpserveshell.go index 83a6940..49746e2 100644 --- a/c2/httpserveshell/httpserveshell.go +++ b/c2/httpserveshell/httpserveshell.go @@ -33,6 +33,7 @@ package httpserveshell import ( "flag" "sync" + "sync/atomic" "github.com/vulncheck-oss/go-exploit/c2/channel" "github.com/vulncheck-oss/go-exploit/c2/httpservefile" @@ -78,7 +79,11 @@ func (serveShell *Server) CreateFlags() { // load the provided file into memory. Generate the random filename. func (serveShell *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } serveShell.channel = channel if len(serveShell.HTTPAddr) == 0 { diff --git a/c2/httpshellserver/httpshellserver.go b/c2/httpshellserver/httpshellserver.go index d5a5ebf..04ec01e 100644 --- a/c2/httpshellserver/httpshellserver.go +++ b/c2/httpshellserver/httpshellserver.go @@ -10,6 +10,7 @@ import ( "os" "strings" "sync" + "sync/atomic" "testing" "time" @@ -60,7 +61,11 @@ func GetInstance() *Server { func (httpServer *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } if channel == nil { output.PrintFrameworkError("Channel passed to C2 init was nil, ensure that channel is assigned and the shutdown atomic is set to false") diff --git a/c2/shelltunnel/shelltunnel.go b/c2/shelltunnel/shelltunnel.go index c02c7e3..f116d16 100644 --- a/c2/shelltunnel/shelltunnel.go +++ b/c2/shelltunnel/shelltunnel.go @@ -55,6 +55,7 @@ import ( "net" "strconv" "strings" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -116,7 +117,11 @@ func (shellTunnel *Server) CreateFlags() { func (shellTunnel *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } shellTunnel.channel = channel if channel.IsClient { diff --git a/c2/simpleshell/simpleshellclient.go b/c2/simpleshell/simpleshellclient.go index e27aadc..8e27f60 100644 --- a/c2/simpleshell/simpleshellclient.go +++ b/c2/simpleshell/simpleshellclient.go @@ -2,6 +2,7 @@ package simpleshell import ( "net" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -44,8 +45,13 @@ func (shellClient *Client) Channel() *channel.Channel { func (shellClient *Client) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } + shellClient.ConnectAddr = channel.IPAddr shellClient.ConnectPort = channel.Port shellClient.channel = channel diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index 58d62b2..608b156 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -59,7 +60,11 @@ func (shellServer *Server) Shutdown() bool { // Validate configuration and create the listening socket. func (shellServer *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } shellServer.channel = channel if channel.IsClient { diff --git a/c2/sslshell/sslshellserver.go b/c2/sslshell/sslshellserver.go index 9e6d5bd..2342585 100644 --- a/c2/sslshell/sslshellserver.go +++ b/c2/sslshell/sslshellserver.go @@ -23,6 +23,7 @@ import ( "net" "strings" "sync" + "sync/atomic" "time" "github.com/vulncheck-oss/go-exploit/c2/channel" @@ -86,7 +87,11 @@ func (shellServer *Server) Channel() *channel.Channel { // the TLS listener on the user provided IP/port. func (shellServer *Server) Init(channel *channel.Channel) bool { if channel.Shutdown == nil { - channel.Shutdown.Store(false) + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + channel.Shutdown = &shutdown } shellServer.channel = channel if channel.IsClient { From 96c7008fa4d3a141fcc4456f4aac787b466548f6 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Fri, 25 Apr 2025 13:47:38 -0600 Subject: [PATCH 08/10] Replace the framework success print for a shell print on HTTPShellServer --- c2/httpshellserver/httpshellserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c2/httpshellserver/httpshellserver.go b/c2/httpshellserver/httpshellserver.go index 04ec01e..2e9cf8b 100644 --- a/c2/httpshellserver/httpshellserver.go +++ b/c2/httpshellserver/httpshellserver.go @@ -156,7 +156,7 @@ func (httpServer *Server) Run(timeout int) { body, _ := io.ReadAll(req.Body) if strings.TrimSpace(string(body)) != "" { - output.PrintfSuccess("%s: %s", req.RemoteAddr, string(body)) + output.PrintShell(fmt.Sprintf("%s: %s", req.RemoteAddr, string(body))) } }) From cdf8997d767017766695ad342be0f59029dd1b2b Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 1 May 2025 13:44:17 -0600 Subject: [PATCH 09/10] C2 Unification add `c2.HashSessions()` helper function. --- c2/factory.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/c2/factory.go b/c2/factory.go index 416dd6f..d5ee50c 100644 --- a/c2/factory.go +++ b/c2/factory.go @@ -152,6 +152,39 @@ func CreateFlags(implementation Impl) { } } +// HasSessions returns if the underlying channel has active sessions. This is useful for code that +// needs to validate if callbacks have occurred and is a helper wrapper around the channel package +// function of the same name. +func HasSessions(implementation Impl) bool { + switch implementation.Category { + case SimpleShellServerCategory: + return simpleshell.GetServerInstance().Channel().HasSessions() + case SimpleShellClientCategory: + return simpleshell.GetClientInstance().Channel().HasSessions() + case SSLShellServerCategory: + return sslshell.GetInstance().Channel().HasSessions() + case HTTPServeFileCategory: + return httpservefile.GetInstance().Channel().HasSessions() + case HTTPServeShellCategory: + return httpserveshell.GetInstance().Channel().HasSessions() + case ExternalCategory: + if implementation.Name != "" { + return external.GetInstance(implementation.Name).Channel().HasSessions() + } + case HTTPShellServerCategory: + return httpshellserver.GetInstance().Channel().HasSessions() + case ShellTunnelCategory: + return shelltunnel.GetInstance().Channel().HasSessions() + case InvalidCategory: + // Calling your external C2 as explicitly invalid is odd. + output.PrintFrameworkError("Invalid C2 Server") + default: + output.PrintFrameworkError("Invalid C2 Server") + } + + return false +} + // Return the internal representation of a C2 from a string. func StringToImpl(c2Name string) (Impl, bool) { for _, value := range internalSupported { From 4b55c4b61b0c391e9467ee5d2773b64d4f730c47 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 1 May 2025 16:14:05 -0600 Subject: [PATCH 10/10] Simplify the cli basic handler logic --- c2/channel/channel.go | 2 +- c2/cli/basic.go | 78 ++++++++++++++++++++++--------------------- c2/factory.go | 4 +-- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/c2/channel/channel.go b/c2/channel/channel.go index adba897..3e51a33 100644 --- a/c2/channel/channel.go +++ b/c2/channel/channel.go @@ -79,7 +79,7 @@ func (c *Channel) RemoveSession(id string) bool { } _, ok := c.Sessions[id] if !ok { - output.PrintFrameworkError("Session ID is not available") + output.PrintFrameworkError("Session ID does not exist") return false } diff --git a/c2/cli/basic.go b/c2/cli/basic.go index 7e13856..ccfc927 100644 --- a/c2/cli/basic.go +++ b/c2/cli/basic.go @@ -13,9 +13,46 @@ import ( "github.com/vulncheck-oss/go-exploit/protocol" ) +// backgroundResponse handles the network connection reading for response data and contains a +// trigger to the shutdown of the channel to ensure cleanup happens on socket close. +func backgroundResponse(ch *channel.Channel, wg *sync.WaitGroup, conn net.Conn, responseCh chan string) { + defer wg.Done() + defer func(channel *channel.Channel) { + // Signals for both routines to stop, this should get triggered when socket is closed + // and causes it to fail the read + channel.Shutdown.Store(true) + }(ch) + responseBuffer := make([]byte, 1024) + for { + if ch.Shutdown.Load() { + return + } + + err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + if err != nil { + output.PrintfFrameworkError("Error setting read deadline: %s, exiting.", err) + + return + } + + bytesRead, err := conn.Read(responseBuffer) + if err != nil && !os.IsTimeout(err) { + // things have gone sideways, but the command line won't know that + // until they attempt to execute a command and the socket fails. + // i think that's largely okay. + return + } + + if bytesRead > 0 { + // I think there is technically a race condition here where the socket + // could have move data to write, but the user has already called exit + // below. I that that's tolerable for now. + responseCh <- string(responseBuffer[:bytesRead]) + } + } +} + // A very basic reverse/bind shell handler. -// -//nolint:gocognit func Basic(conn net.Conn, ch *channel.Channel) { // Create channels for communication between goroutines. responseCh := make(chan string) @@ -31,42 +68,7 @@ func Basic(conn net.Conn, ch *channel.Channel) { if !testing.Testing() { ch.Input = os.Stdin } - go func(ch *channel.Channel) { - defer wg.Done() - defer func(channel *channel.Channel) { - // Signals for both routines to stop, this should get triggered when socket is closed - // and causes it to fail the read - channel.Shutdown.Store(true) - }(ch) - responseBuffer := make([]byte, 1024) - for { - if ch.Shutdown.Load() { - return - } - - err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)) - if err != nil { - output.PrintfFrameworkError("Error setting read deadline: %s, exiting.", err) - - return - } - - bytesRead, err := conn.Read(responseBuffer) - if err != nil && !os.IsTimeout(err) { - // things have gone sideways, but the command line won't know that - // until they attempt to execute a command and the socket fails. - // i think that's largely okay. - return - } - - if bytesRead > 0 { - // I think there is technically a race condition here where the socket - // could have move data to write, but the user has already called exit - // below. I that that's tolerable for now. - responseCh <- string(responseBuffer[:bytesRead]) - } - } - }(ch) + go backgroundResponse(ch, &wg, conn, responseCh) // Goroutine to handle responses and print them. wg.Add(1) diff --git a/c2/factory.go b/c2/factory.go index d5ee50c..a89c0c6 100644 --- a/c2/factory.go +++ b/c2/factory.go @@ -176,11 +176,9 @@ func HasSessions(implementation Impl) bool { case ShellTunnelCategory: return shelltunnel.GetInstance().Channel().HasSessions() case InvalidCategory: - // Calling your external C2 as explicitly invalid is odd. - output.PrintFrameworkError("Invalid C2 Server") default: - output.PrintFrameworkError("Invalid C2 Server") } + output.PrintFrameworkError("Invalid C2 Server") return false }