DigitalOcean Red Team Automation Part 2

Introduction

In this post, I will focus on setting up and automating the Cloudflare DNS record creation process.  I will also add some DNS propagation logic to a new bootstrap shell script called certificate.sh.

Cloudflare Configuration

The next step I am going to take in this process is setting up the DNS records within Cloudflare by using Terraform.

Domain

For this simulation, I will be using the domain maxcdn-images.com.

Sub-Domains

  • cf.maxcdn-images.com – Used for the CloudFront Redirector
  • api.maxcdn-images.com – Used for the HTTP/S Redirector
  • ns1.maxcdn-images.com – Used for the DNS Redirector
  • cache.maxcdn-images.com – Used for DNS C2 Communications

Terraform Files

For the Cloudflare creation, I will be creating or modifying the following files to be used with Terraform.

  • variables.tf
  • providers.tf
  • cf-resources.tf
  • terraform.tfvars

As you can see, some of these files already exist within the project folder, those files will simply have more content added to them.

variables.tf

I will be adding some cloudflare variables to the variables.tf file, additions have been highlighted below.

# variables.tf

variable "do-token" {
  type = string
  description = "DigitalOcean Access Token"
}

variable "do-teamserver-pubkey" {
  type = string
  description = "Teamserver SSH Public Key"
}

variable "do-cf-redirector-pubkey" {
  type = string
  description = "CloudFront Redirector SSH Public Key"
}

variable "do-dns-redirector-pubkey" {
  type = string
  description = "DNS Redirector SSH Public Key"
}

variable "do-http-redirector-pubkey" {
  type = string
  description = "HTTP/S Redirector SSH Public Key"
}

variable "cf-api_key" {
  type = string
  description = "Cloudflare API Key"
}

variable "cf-email" {
  type = string
  description = "Cloudflare Email Account"
}

variable "cf-account" {
  type = string
  description = "Cloudflare Account ID"
}

variable "cf-zone_id" {
  type = string
  description = "Cloudflare Zone ID for Domain"
}

variable "cf-domain" {
  type = string
  description = "C2 Domain for Redirectors"
}

variable "cf-subdomain-api" {
  type = string
  description = "C2 Subdomain for HTTP/S Redirector"
}

variable "cf-subdomain-dns" {
  type = string
  description = "C2 Subdomains for DNS Redirector"
}

variable "cf-subdomain-cf" {
  type = string
  description = "C2 Subdomains for CloudFront Redirector"
}

variable "cf-subdomain-ns" {
  type = string
  description = "C2 DNS Communication Channel"
}

variable "operator_ip" {
  type = string
  description = "Operators attacking IP Range"
}

providers.tf

The providers.tf file tells Terraform which provider is responsible for understanding API interactions and exposing resources, additions have been highlighted below.

# providers.tf

provider "digitalocean" {
  version = "~> 1.12.0"
  token = var.do-token
}

provider "cloudflare" {
  email  = var.cf-email
  api_key  = var.cf-api_key
  account_id = var.cf-account
}

cf-resources.tf

The cf-resources.tf file contains the configuration for the Cloudflare DNS records.  For this simulation, I will be using only the ‘A‘ and ‘NS‘ records.

# cf-resources.tf

resource "cloudflare_record" "api" {
  zone_id = var.cf-zone_id
  name = var.cf-subdomain-api
  type = "A"
  value = digitalocean_droplet.https-redirector.ipv4_address
  ttl = 300
  proxied = false
}

resource "cloudflare_record" "dns" {
  zone_id = var.cf-zone_id
  name = var.cf-subdomain-dns
  type = "A"
  value = digitalocean_droplet.dns-redirector.ipv4_address
  ttl = 300
  proxied = false
}

resource "cloudflare_record" "cf" {
  zone_id = var.cf-zone_id
  name = var.cf-subdomain-cf
  type = "A"
  value = digitalocean_droplet.cf-redirector.ipv4_address
  ttl = 300
  proxied = false
}

resource "cloudflare_record" "cache" {
  zone_id = var.cf-zone_id
  name = var.cf-subdomain-cf
  type = "NS"
  value = "${var.cf-subdomain-dns}.${var.cf-domain}"
  ttl = 300
  proxied = false
}

terraform.tfvars

The terraform.tfvars file tells Terraform which variables to set at runtime.

# terraform.tfvars

do-token = "<your Access Token here>"
do-teamserver-pubkey = "/ssh-keys/teamserver.pub"
do-cf-redirector-pubkey = "/ssh-keys/cf-server.pub"
do-dns-redirector-pubkey = "/ssh-keys/dns-server.pub"
do-http-redirector-pubkey = "/ssh-keys/https-server.pub"

cf-api_key = "<your API KEY here>"
cf-email = "<your EMAIL here>"
cf-account = "<your CF ACCOUNT id>"
cf-zone_id = "<your CF ZONE id>"

cf-domain = "maxcdn-images.com"
cf-subdomain-cf = "cf"
cf-subdomain-api = "api"
cf-subdomain-dns = "ns1"
cf-subdomain-ns = "cache"

# Operators Attacking IP/Range and SSH Public Key
operator_ip = "64.114.212.201/32"

Shell Scripts

In order generate an SSL certificate with a service like LetsEncrypt, your DNS records need to resolve to the droplets (or machines) that were just created moments before. To get around this issue, I wrote a small bash script that will run in the background with the nohup prefix  so that it will complete as soon as the DNS record propagation is complete.

certificate.sh

This file can be used for both the api and cf subdomain.

#!/usr/bin/env bash
# shellcheck disable=SC2046

DOMAIN=$(cat /root/domain.txt)
SECONDS=30
TLD=$(echo "${DOMAIN}" | cut -d '.' -f2,3)

until [[ $(dig "${DOMAIN}" A +short) == "${PUBLIC_IP}" ]]
do
  echo "[*] Sleeping for ${SECONDS} seconds while waiting for DNS propagation."
  sleep $SECONDS
done

sleep $SECONDS
echo "[*] Generating a LetsEncrypt certificate for the domain: ${DOMAIN}"
certbot certonly -n --standalone --email [email protected]"${TLD}" -d "${DOMAIN}" --agree-tos --no-eff-email --staging
sleep 3
sed -i 's/SSLEngine on/SSLEngine on\n\t\tSSLProxyVerify none\n\t\tSSLProxyCheckPeerCN off\n\t\tSSLProxyCheckPeerName off/g' /etc/apache2/sites-enabled/default-ssl.conf
sed -i 's/SSLCertificateFile.*/SSLCertificateFile \/etc\/letsencrypt\/live\/'"${DOMAIN}"'\/cert.pem/g' /etc/apache2/sites-enabled/default-ssl.conf
sed -i 's/SSLCertificateKeyFile.*/SSLCertificateKeyFile \/etc\/letsencrypt\/live\/'"${DOMAIN}"'\/privkey.pem/g' /etc/apache2/sites-enabled/default-ssl.conf
sed -i 's/^Mutex/#Mutex/g' /etc/apache2/apache2.conf
echo "Domain: ${DOMAIN}" > /var/www/html/index.html
systemctl restart apache2
sleep 1 # Possible fix to terraform remote-exec bug

Now that I have created and stored the certificate.sh file with the others, I have to modify the do-resources.tf file to include the new shell script, as seen in the highlighted lines below.

do-resources.tf

# do-resources.tf

# DigitalOcean SSH Key Configuration
#
resource "digitalocean_ssh_key" "teamserver" {
  name       = "C2 Team Server SSH Key"
  public_key = file("${path.module}${var.do-teamserver-pubkey}")
}
resource "digitalocean_ssh_key" "cf-redirector" {
  name       = "CloudFront Redirector SSH Key"
  public_key = file("${path.module}${var.do-cf-redirector-pubkey}")
}
resource "digitalocean_ssh_key" "https-redirector" {
  name       = "HTTP/S Redirector SSH Key"
  public_key = file("${path.module}${var.do-http-redirector-pubkey}")
}
resource "digitalocean_ssh_key" "dns-redirector" {
  name       = "DNS Redirector SSH Key"
  public_key = file("${path.module}${var.do-dns-redirector-pubkey}")
}


# DigitalOcean Droplet Creation
#
resource "digitalocean_droplet" "teamserver" {
  image  = "ubuntu-18-04-x64"
  name   = "teamserver"
  region = "nyc3"
  size   = "s-2vcpu-4gb"
  ssh_keys = [digitalocean_ssh_key.teamserver.fingerprint]
  connection {
    host = digitalocean_droplet.teamserver.ipv4_address
    type = "ssh"
    user = "root"
    private_key = file("~/.ssh/c2-teamserver_rsa")
  }
  provisioner "file" {
    source      = "bootstrap/scripts/teamserver-setup.sh"
    destination = "/tmp/setup.sh"
  }
  provisioner "file" {
    source      = "bootstrap/files/cobaltstrike.tar.gz"
    destination = "/opt/cobaltstrike.tar.gz"
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/setup.sh && bash /tmp/setup.sh"
    ]
  }
}

resource "digitalocean_droplet" "dns-redirector" {
  image  = "ubuntu-16-04-x64"
  name   = "dns-redirector"
  region = "fra1"
  size   = "s-1vcpu-1gb"
  ssh_keys = [digitalocean_ssh_key.dns-redirector.fingerprint]
  depends_on = [digitalocean_droplet.teamserver]
  connection {
    host = digitalocean_droplet.dns-redirector.ipv4_address
    type = "ssh"
    user = "root"
    private_key = file("~/.ssh/c2-dns-redirector_rsa")
  }
  provisioner "file" {
    source      = "bootstrap/scripts/dns-server-setup.sh"
    destination = "/tmp/setup.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "export TEAMSERVER=${digitalocean_droplet.teamserver.ipv4_address}",
      "chmod +x /tmp/setup.sh && bash /tmp/setup.sh"
    ]
  }
}

resource "digitalocean_droplet" "https-redirector" {
  image  = "ubuntu-16-04-x64"
  name   = "https-redirector"
  region = "lon1"
  size   = "s-1vcpu-1gb"
  ssh_keys = [digitalocean_ssh_key.https-redirector.fingerprint]
  depends_on = [digitalocean_droplet.teamserver]
  connection {
    host = digitalocean_droplet.https-redirector.ipv4_address
    type = "ssh"
    user = "root"
    private_key = file("~/.ssh/c2-https-redirector_rsa")
  }
  provisioner "file" {
    source      = "bootstrap/scripts/https-server-setup.sh"
    destination = "/tmp/setup.sh"
  }
  provisioner "file" {
    source = "bootstrap/scripts/certificate.sh"
    destination = "/tmp/cert.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "echo ${digitalocean_droplet.https-redirector.ipv4_address} > /root/ip_address.txt",
      "echo ${digitalocean_droplet.teamserver.ipv4_address} > /root/teamserver_ip.txt",
      "echo ${var.cf-subdomain-api}.${var.cf-domain} > /root/domain.txt",
      "chmod +x /tmp/setup.sh && bash /tmp/setup.sh",
      "chmod +x /tmp/cert.sh && nohup bash /tmp/cert.sh &",
      "sleep 3"
    ]
  }
}

resource "digitalocean_droplet" "cf-redirector" {
  image = "ubuntu-16-04-x64"
  name = "cf-redirector"
  region = "tor1"
  size = "s-1vcpu-1gb"
  ssh_keys = [digitalocean_ssh_key.cf-redirector.fingerprint]
  depends_on = [digitalocean_droplet.teamserver]
  connection {
    host = digitalocean_droplet.cf-redirector.ipv4_address
    type = "ssh"
    user = "root"
    private_key = file("~/.ssh/c2-cf-redirector_rsa")
  }
  provisioner "file" {
    source = "bootstrap/scripts/cf-server-setup.sh"
    destination = "/tmp/setup.sh"
  }
  provisioner "file" {
    source = "bootstrap/scripts/certificate.sh"
    destination = "/tmp/cert.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "echo ${digitalocean_droplet.cf-redirector.ipv4_address} > /root/ip_address.txt",
      "echo ${digitalocean_droplet.teamserver.ipv4_address} > /root/teamserver_ip.txt",
      "echo ${var.cf-subdomain-cf}.${var.cf-domain} > /root/domain.txt",
      "chmod +x /tmp/setup.sh && bash /tmp/setup.sh",
      "chmod +x /tmp/cert.sh && nohup bash /tmp/cert.sh &",
      "sleep 3"
    ]
  }
}

# DigitalOcean Project
#
resource "digitalocean_project" "C2-Automation" {
  name        = "C2 Automation"
  description = "C2 Infrastructure Automation"
  purpose     = "C2 Infrastructure"
  environment = "Development"
  depends_on = [
    digitalocean_droplet.teamserver,
    digitalocean_droplet.cf-redirector,
    digitalocean_droplet.dns-redirector,
    digitalocean_droplet.https-redirector
  ]
  resources   = [
    digitalocean_droplet.teamserver.urn,
    digitalocean_droplet.cf-redirector.urn,
    digitalocean_droplet.dns-redirector.urn,
    digitalocean_droplet.https-redirector.urn
  ]
}


# Output variables
#
output "teamserver-ip" {
  value = digitalocean_droplet.teamserver.ipv4_address
}
output "cf-redirector-ip" {
  value = digitalocean_droplet.cf-redirector.ipv4_address
}
output "dns-redirector-ip" {
  value = digitalocean_droplet.dns-redirector.ipv4_address
}
output "https-redirector-ip" {
  value = digitalocean_droplet.https-redirector.ipv4_address
}

After the above modifications were complete, I was now ready to test the infrastructure using the terraform apply command.   When prompted to continue, type ‘yes’ as seen below.

Plan: 16 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

After pressing enter, you will see all of the resources (16) being created at their respective providers.  The order in which these operations take place, is dictated by the depends_on directive, to ensure something doesnt try to use a resource that isnt avaialble yet.

After about 5 minutes, you should see the output below.

Apply complete! Resources: 16 added, 0 changed, 0 destroyed.

Outputs:

cf-redirector-ip = 178.128.231.165
dns-redirector-ip = 134.209.251.94
https-redirector-ip = 134.209.184.216
teamserver-ip = 64.225.4.103

Awesome! All resources were created without issue. I should now be able to test by using a browser to access the HTTPS version of both api.maxcdn-images.com and cf.maxcdn-images.com.

  • api.maxcdn-images.com
(venv) [[email protected] red_team]$ dig api.maxcdn-images.com A +short
134.209.184.216
(venv) [[email protected] red_team]$ curl https://api.maxcdn-images.com
Domain: api.maxcdn-images.com
  • cf.maxcdn-images.com
(venv) [[email protected] red_team]$ dig cf.maxcdn-images.com A +short
178.128.231.165
(venv) [[email protected] red_team]$ curl https://cf.maxcdn-images.com
Domain: cf.maxcdn-images.com

Both tests worked without issue!

Coming Up

Now that I know the bootstrap shell scripts were able to run and setup SSL certificates, I am ready to move forward to the next part.  In Part 3, I am going to setup the automation of the Cobalt Strike listeners, and create a Domain Fronted C2 channel using AWS CloudFront (as seen in the network diagram on Part 1.)