Skip to content

Commit 83e5b4a

Browse files
authored
Unify clean shutdown and update C2 channel with session tracking (#358)
1 parent 21d9b9f commit 83e5b4a

File tree

13 files changed

+742
-138
lines changed

13 files changed

+742
-138
lines changed

c2/channel/channel.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,107 @@
33
// components in order to support extracting components such as lhost and lport.
44
package channel
55

6+
import (
7+
"crypto/rand"
8+
"encoding/base32"
9+
"io"
10+
"net"
11+
"sync/atomic"
12+
"time"
13+
14+
"github.com/vulncheck-oss/go-exploit/output"
15+
)
16+
617
type Channel struct {
718
IPAddr string
819
HTTPAddr string
920
Port int
1021
HTTPPort int
1122
Timeout int
1223
IsClient bool
24+
Shutdown *atomic.Bool
25+
Sessions map[string]Session
26+
Input io.Reader
27+
Output io.Writer // Currently unused but figured we'd add it ahead of time
28+
}
29+
30+
type Session struct {
31+
RemoteAddr string
32+
ConnectionTime time.Time
33+
conn *net.Conn
34+
}
35+
36+
// HasSessions checks if a channel has any tracked sessions. This can be used to lookup if a C2
37+
// successfully received callbacks:
38+
//
39+
// c, ok := c2.GetInstance(conf.C2Type)
40+
// c.Channel().HasSessions()
41+
func (c *Channel) HasSessions() bool {
42+
return len(c.Sessions) > 0
43+
}
44+
45+
// AddSession adds a remote connection for session tracking. If a network connection is being
46+
// tracked it can be added here and will be cleaned up and closed automatically by the C2 on
47+
// shutdown.
48+
func (c *Channel) AddSession(conn *net.Conn, addr string) bool {
49+
if len(c.Sessions) == 0 {
50+
c.Sessions = make(map[string]Session)
51+
}
52+
// This is my session randomizing logic. The theory is that it keeps us dependency free while
53+
// also creating the same 16bit strength of UUIDs. If we only plan on using the random UUIDs
54+
// anyway this should meet the same goals while also being URL safe and no special characters.
55+
k := make([]byte, 16)
56+
_, err := rand.Read(k)
57+
if err != nil {
58+
output.PrintfFrameworkError("Could not add session: %s", err.Error())
59+
60+
return false
61+
}
62+
id := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(k)
63+
c.Sessions[id] = Session{
64+
// Add the time of now to the current connection time
65+
ConnectionTime: time.Now(),
66+
conn: conn,
67+
RemoteAddr: addr,
68+
}
69+
70+
return true
71+
}
72+
73+
// RemoveSession removes a specific session ID and if a connection exists, closes it.
74+
func (c *Channel) RemoveSession(id string) bool {
75+
if len(c.Sessions) == 0 {
76+
output.PrintFrameworkDebug("No sessions exist")
77+
78+
return false
79+
}
80+
_, ok := c.Sessions[id]
81+
if !ok {
82+
output.PrintFrameworkError("Session ID does not exist")
83+
84+
return false
85+
}
86+
if c.Sessions[id].conn != nil {
87+
(*c.Sessions[id].conn).Close()
88+
}
89+
delete(c.Sessions, id)
90+
91+
return true
92+
}
93+
94+
// RemoveSessions removes all tracked sessions and closes any open connections if applicable.
95+
func (c *Channel) RemoveSessions() bool {
96+
if len(c.Sessions) == 0 {
97+
output.PrintFrameworkDebug("No sessions exist")
98+
99+
return false
100+
}
101+
for k := range c.Sessions {
102+
if c.Sessions[k].conn != nil {
103+
(*c.Sessions[k].conn).Close()
104+
}
105+
delete(c.Sessions, k)
106+
}
107+
108+
return true
13109
}

c2/cli/basic.go

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,81 +4,115 @@ import (
44
"bufio"
55
"net"
66
"os"
7-
"strings"
87
"sync"
8+
"testing"
99
"time"
1010

11+
"github.com/vulncheck-oss/go-exploit/c2/channel"
1112
"github.com/vulncheck-oss/go-exploit/output"
1213
"github.com/vulncheck-oss/go-exploit/protocol"
1314
)
1415

16+
// backgroundResponse handles the network connection reading for response data and contains a
17+
// trigger to the shutdown of the channel to ensure cleanup happens on socket close.
18+
func backgroundResponse(ch *channel.Channel, wg *sync.WaitGroup, conn net.Conn, responseCh chan string) {
19+
defer wg.Done()
20+
defer func(channel *channel.Channel) {
21+
// Signals for both routines to stop, this should get triggered when socket is closed
22+
// and causes it to fail the read
23+
channel.Shutdown.Store(true)
24+
}(ch)
25+
responseBuffer := make([]byte, 1024)
26+
for {
27+
if ch.Shutdown.Load() {
28+
return
29+
}
30+
31+
err := conn.SetReadDeadline(time.Now().Add(1 * time.Second))
32+
if err != nil {
33+
output.PrintfFrameworkError("Error setting read deadline: %s, exiting.", err)
34+
35+
return
36+
}
37+
38+
bytesRead, err := conn.Read(responseBuffer)
39+
if err != nil && !os.IsTimeout(err) {
40+
// things have gone sideways, but the command line won't know that
41+
// until they attempt to execute a command and the socket fails.
42+
// i think that's largely okay.
43+
return
44+
}
45+
46+
if bytesRead > 0 {
47+
// I think there is technically a race condition here where the socket
48+
// could have move data to write, but the user has already called exit
49+
// below. I that that's tolerable for now.
50+
responseCh <- string(responseBuffer[:bytesRead])
51+
}
52+
}
53+
}
54+
1555
// A very basic reverse/bind shell handler.
16-
func Basic(conn net.Conn) {
56+
func Basic(conn net.Conn, ch *channel.Channel) {
1757
// Create channels for communication between goroutines.
1858
responseCh := make(chan string)
19-
quit := make(chan struct{})
2059

2160
// Use a WaitGroup to wait for goroutines to finish.
2261
var wg sync.WaitGroup
2362

2463
// Goroutine to read responses from the server.
2564
wg.Add(1)
26-
go func() {
27-
defer wg.Done()
28-
responseBuffer := make([]byte, 1024)
29-
for {
30-
select {
31-
case <-quit:
32-
return
33-
default:
34-
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
35-
bytesRead, err := conn.Read(responseBuffer)
36-
if err != nil && !strings.Contains(err.Error(), "i/o timeout") {
37-
// things have gone sideways, but the command line won't know that
38-
// until they attempt to execute a command and the socket fails.
39-
// i think that's largely okay.
40-
return
41-
}
42-
if bytesRead > 0 {
43-
// I think there is technically a race condition here where the socket
44-
// could have move data to write, but the user has already called exit
45-
// below. I that that's tolerable for now.
46-
responseCh <- string(responseBuffer[:bytesRead])
47-
}
48-
}
49-
}
50-
}()
65+
66+
// If running in the test context inherit the channel input setting, this will let us control the
67+
// input of the shell programmatically.
68+
if !testing.Testing() {
69+
ch.Input = os.Stdin
70+
}
71+
go backgroundResponse(ch, &wg, conn, responseCh)
5172

5273
// Goroutine to handle responses and print them.
5374
wg.Add(1)
54-
go func() {
75+
go func(channel *channel.Channel) {
5576
defer wg.Done()
56-
for response := range responseCh {
57-
select {
58-
case <-quit:
77+
for {
78+
if channel.Shutdown.Load() {
5979
return
60-
default:
80+
}
81+
select {
82+
case response := <-responseCh:
6183
output.PrintShell(response)
84+
default:
6285
}
6386
}
64-
}()
87+
}(ch)
6588

66-
for {
67-
// read user input until they type 'exit\n' or the socket breaks
68-
// note that ReadString is blocking, so they won't know the socket
69-
// is broken until they attempt to write something
70-
reader := bufio.NewReader(os.Stdin)
71-
command, _ := reader.ReadString('\n')
72-
ok := protocol.TCPWrite(conn, []byte(command))
73-
if !ok || command == "exit\n" {
74-
break
75-
}
76-
}
89+
go func(channel *channel.Channel) {
90+
// no waitgroup for this one because blocking IO, but this should not matter
91+
// since we are intentionally not trying to be a multi-implant C2 framework.
92+
// There still remains the issue that you would need to hit enter to find out
93+
// that the socket is dead but at least we can stop Basic() regardless of this fact.
94+
// This issue of unblocking stdin is discussed at length here https://github.com/golang/go/issues/24842
95+
for {
96+
reader := bufio.NewReader(ch.Input)
97+
command, _ := reader.ReadString('\n')
98+
if channel.Shutdown.Load() {
99+
break
100+
}
101+
if command == "exit\n" {
102+
channel.Shutdown.Store(true)
77103

78-
// signal for everyone to shutdown
79-
quit <- struct{}{}
80-
close(responseCh)
104+
break
105+
}
106+
ok := protocol.TCPWrite(conn, []byte(command))
107+
if !ok {
108+
channel.Shutdown.Store(true)
109+
110+
break
111+
}
112+
}
113+
}(ch)
81114

82115
// wait until the go routines are clean up
83116
wg.Wait()
117+
close(responseCh)
84118
}

c2/external/external.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,13 @@ import (
163163

164164
// The Server struct holds the declared external modules internal functions and channel data.
165165
type Server struct {
166-
flags func()
167-
init func()
168-
run func(int) bool
169-
meta func(*channel.Channel)
170-
Channel *channel.Channel
171-
name string
166+
flags func()
167+
init func()
168+
run func(int) bool
169+
shutdown func() bool
170+
meta func(*channel.Channel)
171+
channel *channel.Channel
172+
name string
172173
}
173174

174175
// The External interface defines which functions are required to be defined in an external C2
@@ -180,6 +181,7 @@ type External interface {
180181
SetFlags(func())
181182
SetInit(func())
182183
SetRun(func(int) bool)
184+
SetShutdown(func() bool)
183185
}
184186

185187
var serverSingletons map[string]*Server
@@ -245,14 +247,14 @@ func (externalServer *Server) SetChannel(f func(*channel.Channel)) {
245247
}
246248

247249
// Init triggers the set C2 initialization and passes the channel to the external module.
248-
func (externalServer *Server) Init(channel channel.Channel) bool {
250+
func (externalServer *Server) Init(channel *channel.Channel) bool {
249251
if channel.IsClient {
250252
output.PrintFrameworkError("Called ExternalServer as a client.")
251253

252254
return false
253255
}
254256
externalServer.init()
255-
externalServer.meta(&channel)
257+
externalServer.meta(channel)
256258

257259
return true
258260
}
@@ -268,3 +270,21 @@ func (externalServer *Server) SetRun(f func(int) bool) {
268270
func (externalServer *Server) Run(timeout int) {
269271
externalServer.run(timeout)
270272
}
273+
274+
// SetShutdown sets the function for server shutdown handling and session cleanup logic. This
275+
// function is what gets called when the framework receives a OS signal, a shell is closed, or
276+
// manually invoked.
277+
func (externalServer *Server) SetShutdown(f func() bool) {
278+
externalServer.shutdown = f
279+
}
280+
281+
// Shutdown triggers the set shutdown function.
282+
func (externalServer *Server) Shutdown() bool {
283+
return externalServer.shutdown()
284+
}
285+
286+
// Return the underlying C2 channel containing channel metadata and session tracking.
287+
func (externalServer *Server) Channel() *channel.Channel {
288+
// I'd much rather have just exposed a `Server.Channel`, but we are interface bound
289+
return externalServer.channel
290+
}

c2/factory.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import (
1515
// An interface used by both reverse shells, bind shells, and stagers.
1616
type Interface interface {
1717
CreateFlags()
18-
Init(channel channel.Channel) bool
18+
Init(channel *channel.Channel) bool
1919
Run(timeout int)
20+
Shutdown() bool
21+
Channel() *channel.Channel
2022
}
2123

2224
// Internal representation of a C2 implementation. Each C2 is managed by
@@ -65,7 +67,7 @@ var internalSupported = map[string]Impl{
6567
"HTTPServeFile": {Name: "HTTPServeFile", Category: HTTPServeFileCategory},
6668
"HTTPServeShell": {Name: "HTTPServeShell", Category: HTTPServeShellCategory},
6769
"HTTPShellServer": {Name: "HTTPShellServer", Category: HTTPShellServerCategory},
68-
// Insure the internal supported External module name is an error if used
70+
// Ensure the internal supported External module name is an error if used
6971
// directly.
7072
"External": {Name: "", Category: InvalidCategory},
7173
"ShellTunnel": {Name: "ShellTunnel", Category: ShellTunnelCategory},
@@ -150,6 +152,37 @@ func CreateFlags(implementation Impl) {
150152
}
151153
}
152154

155+
// HasSessions returns if the underlying channel has active sessions. This is useful for code that
156+
// needs to validate if callbacks have occurred and is a helper wrapper around the channel package
157+
// function of the same name.
158+
func HasSessions(implementation Impl) bool {
159+
switch implementation.Category {
160+
case SimpleShellServerCategory:
161+
return simpleshell.GetServerInstance().Channel().HasSessions()
162+
case SimpleShellClientCategory:
163+
return simpleshell.GetClientInstance().Channel().HasSessions()
164+
case SSLShellServerCategory:
165+
return sslshell.GetInstance().Channel().HasSessions()
166+
case HTTPServeFileCategory:
167+
return httpservefile.GetInstance().Channel().HasSessions()
168+
case HTTPServeShellCategory:
169+
return httpserveshell.GetInstance().Channel().HasSessions()
170+
case ExternalCategory:
171+
if implementation.Name != "" {
172+
return external.GetInstance(implementation.Name).Channel().HasSessions()
173+
}
174+
case HTTPShellServerCategory:
175+
return httpshellserver.GetInstance().Channel().HasSessions()
176+
case ShellTunnelCategory:
177+
return shelltunnel.GetInstance().Channel().HasSessions()
178+
case InvalidCategory:
179+
default:
180+
}
181+
output.PrintFrameworkError("Invalid C2 Server")
182+
183+
return false
184+
}
185+
153186
// Return the internal representation of a C2 from a string.
154187
func StringToImpl(c2Name string) (Impl, bool) {
155188
for _, value := range internalSupported {

0 commit comments

Comments
 (0)