Introduction

Deploying resiliant Red Team infrastructure can be quite a time consuming process. This wiki maintained by Steve Borosh and Jeff Dimmock is probably the best public resource I’ve seen in regards to design considerations and hardening tips.

For someone like myself, who destroys and stands fresh infrastructure up for each engagement, building everything by hand is a long, laborious process. Anything that can be automated is a good thing.

Design

For the purpose of this post, this is what we’re going to build:

Network Access

Inbound

  • Inbound 221 on every VM & 50050 on the C2 servers, only from the attackers IP.
  • Inbound 53, 80 & 443 on the redirectors, from any source2.
  • Inbound 80 & 443 on the payload delivery server, only from the HTTP/S redirector IP.
  • Inbound 53 on the DNS C2 server, only from the redirector IPs.
  • Inbound 80 & 443 on the HTTP/S C2 server, from any source2.

1 I’m using the same SSH key across all instances for this post - separate them out as much as you like.
2 Again, I’m leaving things loosey-goosey. You may want to restrict these to something more sensible (e.g. the CIDR range of your victim).

Outbound

  • Outbound 53, 80, 443 on each VM to any destination for installing stuff3.

3 You can leave these open for initial installations, then close them afterwards.

Cloud Segregation

  • The redirectors will be hosted in AWS EC2.
  • The C2 & payload delivery servers will be hosted in Digital Ocean.

Domains

dontgethacked.site & rekt.site are already configured to use Cloudflare DNS, but are currently without records.

rekt.site

  • rekt.site will be used for DNS Beacons, for which we’ll need an A & NS record.
  • NS record -> DNS redirector IP.
  • A record for webdisk -> ns1.rekt.site.

dontgethacked.site

  • {support, cpanel}.dontgethacked.site -> HTTP/S redirector.
  • static.dontgethacked.site -> HTTP/S C2 server.
  • CloudFront Web Distribution for static.dontgethacked.site.

Terraform

To accomplish this, we’ll be using Terraform - an open source tool that codifies APIs into declarative configuration files. It supports many different providers, including AWS, Azure, Bitbucket, Cloudflare, DigitalOcean, Docker, GitHub, Google Cloud, OpenStack, OVH and vSphere to name a few.

Custom Variables

First, we define custom variables for the things we’ll need to refer to in the upcoming configurations. These include API tokens, IP address, SSH keys and so on.

variables.tf

variable "aws-akey" {}
variable "aws-skey" {}
variable "do-token" {}
variable "cf-email" {}
variable "cf-token" {}
variable "rasta-key" {}
variable "attacker-ip" {}
variable "dom1" {}
variable "sub1" {}
variable "sub2" {}
variable "sub3" {}
variable "dom2" {}
variable "sub4" {}

terraform.tfvars

aws-akey = "[removed]"
aws-skey = "[removed]"

do-token = "[removed]"

cf-email = "[removed]"
cf-token = "[removed]"

rasta-key = "rasta.pub"

attacker-ip = "2.31.13.109/32"

dom1 = "dontgethacked.site"
sub1 = "support"
sub2 = "cpanel"
sub3 = "static"

dom2 = "rekt.site"
sub4 = "webdisk"

Providers

Here we define the provider parameters that we’re going to use. Each provider is structured slightly differently - Digital Ocean, for instance, allows you to specify a region in the Droplet configuration, whereas AWS requires it here.

providers.tf

provider "aws" {
    access_key = "${var.aws-akey}"
    secret_key = "${var.aws-skey}"
    region = "eu-west-2"
}

provider "digitalocean" {
    token = "${var.do-token}"
}

provider "cloudflare" {
    email = "${var.cf-email}"
    token = "${var.cf-token}"
}

SSH Keys

ssh-keys.tf

resource "aws_key_pair" "rasta" {
    key_name   = "rasta"
    public_key = "${file("${var.rasta-key}")}"
}

resource "digitalocean_ssh_key" "rasta" {
    name = "rasta"
    public_key = "${file("${var.rasta-key}")}"
}

I’m storing rasta.pub on disk, but you could also place the entire key within the variable, e.g. rasta-key = "ssh-rsa blahblahblah".

AWS VPC

In AWS, we must create a Virtual Private Cloud, Subnet, Internet Gateway and Routing Table. We’re not using private networking, so the ranges are quite inconsequential.

aws-vpc.tf

resource "aws_vpc" "default" {
    cidr_block = "10.0.0.0/16"
    enable_dns_hostnames = true
}

resource "aws_subnet" "default" {
    vpc_id = "${aws_vpc.default.id}"
    cidr_block = "10.0.0.0/24"
}

resource "aws_internet_gateway" "default" {
    vpc_id = "${aws_vpc.default.id}"
}

resource "aws_route_table" "default" {
    vpc_id = "${aws_vpc.default.id}"

    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.default.id}"
    }
}

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

AWS Security Groups

Security Groups define the inbound/output firewall rules for AWS Instances. Notice how we can reference IP variables.

aws-security-groups.tf

resource "aws_security_group" "dns-rdir" {
    name = "dns-redirector"
    vpc_id = "${aws_vpc.default.id}"

    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["${var.attacker-ip}"]
    }
    ingress {
        from_port = 53
        to_port = 53
        protocol = "udp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    egress {
        from_port = 53
        to_port = 53
        protocol = "udp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
        from_port = 443
        to_port = 443
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

resource "aws_security_group" "http-rdir" {
    name = "http-redirector"
    vpc_id = "${aws_vpc.default.id}"

    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["${var.attacker-ip}"]
    }
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    ingress {
        from_port = 443
        to_port = 443
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    egress {
        from_port = 53
        to_port = 53
        protocol = "udp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
        from_port = 443
        to_port = 443
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

AWS Instances

Here we create the redirector instances.

aws-instances.tf

resource "aws_instance" "dns-rdir" {
    ami = "ami-489f8e2c" # Amazon Linux AMI 2017.03.1
    instance_type = "t2.micro"
    key_name = "${aws_key_pair.rasta.key_name}"
    vpc_security_group_ids = ["${aws_security_group.dns-rdir.id}"]
    subnet_id = "${aws_subnet.default.id}"
    associate_public_ip_address = true
}

resource "aws_instance" "http-rdir" {
    ami = "ami-489f8e2c" # Amazon Linux AMI 2017.03.1
    instance_type = "t2.micro"
    key_name = "${aws_key_pair.rasta.key_name}"
    vpc_security_group_ids = ["${aws_security_group.http-rdir.id}"]
    subnet_id = "${aws_subnet.default.id}"
    associate_public_ip_address = true
}

AWS CloudFront

I replicated most of the settings here from Raffi’s demo video.

aws-cloudfront.tf

resource "aws_cloudfront_distribution" "http-c2" {
    enabled = true
    is_ipv6_enabled = false

    origin {
        domain_name = "${var.sub3}.${var.dom1}"
        origin_id = "domain-front"

        custom_origin_config {
            http_port = 80
            https_port = 443
            origin_protocol_policy = "match-viewer"
            origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
        }
    }

    default_cache_behavior {
        target_origin_id = "domain-front"
        allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
        cached_methods = ["GET", "HEAD"]
        viewer_protocol_policy = "allow-all"
        min_ttl = 0
        default_ttl = 86400
        max_ttl = 31536000

        forwarded_values {
            query_string = true
            headers = ["*"]

            cookies {
                forward = "all"
            }
        }

    }

    restrictions {
        geo_restriction {
            restriction_type = "whitelist"
            locations = ["GB"]
        }
    }

    viewer_certificate {
        cloudfront_default_certificate = true
    }
}

Digital Ocean Droplets

do-droplets.tf

resource "digitalocean_droplet" "http-c2" {
    image = "ubuntu-14-04-x64"
    name = "http-c2"
    region = "lon1"
    size = "2gb"
    ssh_keys = ["${digitalocean_ssh_key.rasta.id}"]
}

resource "digitalocean_droplet" "dns-c2" {
    image = "ubuntu-14-04-x64"
    name = "dns-c2"
    region = "lon1"
    size = "2gb"
    ssh_keys = ["${digitalocean_ssh_key.rasta.id}"]
}

resource "digitalocean_droplet" "paydel" {
    image = "ubuntu-14-04-x64"
    name = "payload-delivery"
    region = "lon1"
    size = "512mb"
    ssh_keys = ["${digitalocean_ssh_key.rasta.id}"]
}

Digital Ocean Firewalls

Cross-provider configuration is one of my favourite aspects to Terraform. Notice that we refer to the public IP of our AWS redirector instances within this Digital Ocean configuration.

do-firewalls.tf

resource "digitalocean_firewall" "http-c2" {
    name = "http-c2"

    droplet_ids = ["${digitalocean_droplet.http-c2.id}"]

    inbound_rule = [
    {
        protocol = "tcp"
        port_range = "22"
        source_addresses = ["${var.attacker-ip}"]
    },
    {
        protocol = "tcp"
        port_range = "80"
        source_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "443"
        source_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "50050"
        source_addresses = ["${var.attacker-ip}"]
    }
    ]

    outbound_rule = [
    {
        protocol = "udp"
        port_range = "53"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "80"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "443"
        destination_addresses = ["0.0.0.0/0"]
    }
    ]
}

resource "digitalocean_firewall" "c2-dns" {
    name = "c2-dns"

    droplet_ids = ["${digitalocean_droplet.dns-c2.id}"]

    inbound_rule = [
    {
        protocol = "tcp"
        port_range = "22"
        source_addresses = ["${var.attacker-ip}"]
    },
    {
        protocol = "udp"
        port_range = "53"
        source_addresses = ["${aws_instance.dns-rdir.public_ip}"]
    },
    {
        protocol = "tcp"
        port_range = "50050"
        source_addresses = ["${var.attacker-ip}"]
    }
    ]

    outbound_rule = [
    {
        protocol = "udp"
        port_range = "53"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "80"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "443"
        destination_addresses = ["0.0.0.0/0"]
    }
    ]
}

resource "digitalocean_firewall" "paydel" {
    name = "paydel"

    droplet_ids = ["${digitalocean_droplet.paydel.id}"]

    inbound_rule = [
    {
        protocol = "tcp"
        port_range = "22"
        source_addresses = ["${var.attacker-ip}"]
    },
    {
        protocol = "tcp"
        port_range = "80"
        source_addresses = ["${aws_instance.http-rdir.public_ip}"]
    },
    {
        protocol = "tcp"
        port_range = "443"
        source_addresses = ["${aws_instance.http-rdir.public_ip}"]
    },
    {
        protocol = "tcp"
        port_range = "50050"
        source_addresses = ["${var.attacker-ip}"]
    }
    ]

    outbound_rule = [
    {
        protocol = "udp"
        port_range = "53"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "80"
        destination_addresses = ["0.0.0.0/0"]
    },
    {
        protocol = "tcp"
        port_range = "443"
        destination_addresses = ["0.0.0.0/0"]
    }
    ]
}

Cloudfront

cf-records.tf

resource "cloudflare_record" "http-rdir1" {
    domain = "${var.dom1}"
    name   = "${var.sub1}"
    value  = "${aws_instance.http-rdir.public_ip}"
    type   = "A"
    ttl    = 300
}

resource "cloudflare_record" "http-rdir2" {
    domain = "${var.dom1}"
    name   = "${var.sub2}"
    value  = "${aws_instance.http-rdir.public_ip}"
    type   = "A"
    ttl    = 300
}

resource "cloudflare_record" "http-df" {
    domain = "${var.dom1}"
    name   = "${var.sub3}"
    value  = "${digitalocean_droplet.http-c2.ipv4_address}"
    type   = "A"
    ttl    = 300
}

resource "cloudflare_record" "dns-c2-ns1" {
    domain = "${var.dom2}"
    name   = "ns1"
    value  = "${aws_instance.dns-rdir.public_ip}"
    type   = "A"
    ttl    = 300
}

resource "cloudflare_record" "dns-c2-a" {
    domain = "${var.dom2}"
    name   = "${var.sub4}"
    value  = "ns1.${var.dom2}"
    type   = "NS"
    ttl    = 300
}

Outputs

Outputs are printed at the end of deployment, so we can print all the IPs etc as they get assigned. You can also print them on-demand after deployment with > terraform.exe output dns-rdir-ip for example.

outputs.tf

output "dns-rdir-ip" {
    value = "${aws_instance.dns-rdir.public_ip}"
}

output "http-rdir-ip" {
    value = "${aws_instance.http-rdir.public_ip}"
}

output "paydel-ip" {
    value = "${digitalocean_droplet.paydel.ipv4_address}"
}

output "http-c2-ip" {
    value = "${digitalocean_droplet.http-c2.ipv4_address}"
}

output "dns-c2-ip" {
    value = "${digitalocean_droplet.dns-c2.ipv4_address}"
}

output "cf-domain" {
    value = "${aws_cloudfront_distribution.http-c2.domain_name}"
}

Deploy & Test

We can finally deploy our infrastrucutre and test it out.

> terraform.exe plan -out plan
> terraform.exe apply plan

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

Outputs:

cf-domain = d2x0m979j4p9ih.cloudfront.net
dns-c2-ip = 138.68.188.159
dns-rdir-ip = 35.177.246.178
http-c2-ip = 138.68.188.160
http-rdir-ip = 35.176.5.164
paydel-ip = 178.62.74.205

Payload Delivery

Verify that the DNS records were created and resolve to the expected IPs with nslookup.

Name:    cpanel.dontgethacked.site
Address:  35.176.5.164

Name:    support.dontgethacked.site
Address:  35.176.5.164

Write a test file into the web root of the payload delivery server and grab it with curl.

╭─[email protected] ~  
╰─➤  curl http://cpanel.dontgethacked.site/test
this is my payload delivery server

╭─[email protected] ~  
╰─➤  curl http://support.dontgethacked.site/test
this is my payload delivery server

DNS Beacon

Verify that the NS record was create and resolves to the expected IP.

Name:    ns1.rekt.site
Address:  35.177.246.178

Create a DNS Beacon listener on the DNS C2 server and test DNS responses from it.

Name:    webdisk.rekt.site
Address:  0.0.0.0

Name:    blahblah.webdisk.rekt.site
Address:  0.0.0.0

Domain Front

Again, verify the DNS record.

Name:    static.dontgethacked.site
Address:  138.68.188.160

Host a test file on the HTTP/S C2 server and verify that we can read it using the direct CloudFront URL.

╭─[email protected] ~  
╰─➤  curl -A 'notcurl' http://d2x0m979j4p9ih.cloudfront.net/test
this is my http/s c2 server

Finally, verify that we can also read it via a0.awsstatic.com and specifying the host header.

╭─[email protected] ~  
╰─➤  curl -A 'notcurl' http://a0.awsstatic.com/test -H 'Host: d2x0m979j4p9ih.cloudfront.net'
this is my http/s c2 server

Looks good to me.

In Part 2 we cover the automatic installation of software & tools such as Oracle Java, CS, Apache & Socat.