Post

Attack Lab Automation - UPDATED

banner

Introduction

If you recall in this post, I (briefly) went over how I was leveraging technologies such as Terraform and Ansible to deploy and provision my attack lab infrastructure. Some folks reached out looking for all the relevant code and more information so they could try this out for themselves and learn more. My apologies as this update and code publishing is way overdue.

I’ll continue to point out that there are way more robust labs similar to mine. One of which I will never fail to mention is @Centurion’s Detection Lab. Insanely complex but in a good way! AttackLab-Lite is more suited to my needs and is a way to continuously improve upon my Terraform, Ansible, Jenkins, and “DevOps” skill sets. I just like to do projects that help me in my day-to-day and more narrowly fit my particular use cases as well. This is also an excuse to keep on AttackLab-Lite and let it eventually evolve into a more robust lab environment with user simulation. In general, there isn’t much that’s special about AttackLab-Lite at the moment, it’s really just a means to get some VMs up and running with an Elastic Stack so I can do some testing of offensive tooling.

Hopefully this will help some folks out there learn more about Terraform, Packer, Ansible, and the like.

If you’re interested in the Jenkins and GitLab CI/CD approach to this, I’m dedicating a post to it as to not detract from the core content. I swear, this will be posted soon!

So, What Changed?

I’ve since made a few enhancements by adding in Packer and Jenkins to automagically build CentOS 8 Stream, Windows Server 2019, and Windows 10 virtual machine images to further automate the deployment of the AttackLab-Lite infrastructure to my local ESXi/vSphere environment.

Compared to the previous post, this will be a lot more technical and be a deep dive into the new process I’ve developed. Remember, this project is a work in progress and I would like to incorporate some interesting finds that I find over time to give the lab more “real world” telemetry to work with, as far as logging and environment behaviors are concerned.

TL;DR

  • Published Terraform, Packer, Ansible code to my GitHub
  • Enlisted the help of Packer to build baseline images for Windows and Linux
  • Cleaned up Ansible code and structure to be more cohesive and clean
  • Moved to CentOS 8 Stream for Elastic Stack since technically not EOL
  • Added Jenkins and Jenkins Pipelines for building/deploying/provisioning
  • Experimented with GitLab’s CI/CD for fun and profit

What does it look like now?

Similar to the diagram in the last post, but I’ll toss in a newer one that includes what the (logical) flow of Packer and Terraform looks like for context.

infrastructure Infrastructure

What makes AttackLab-Lite an attack lab of sorts? At the moment, not too much. But the two key components are:

  • Vulnerable-AD script is used to create some AS-REP/Kerberoastable users
  • LDAP has been configured to allow NULL bind via PowerShell (Gist)

There’s more to come in the way of making the lab environment more vulnerable to common attacks through simulation, see the Retrospective section - it’s all in progress!

How is this Blog Post Structured?

There’s quite a bit of content in here covering the core of AttackLab-Lite and some optional areas, like Jenkins and GitLab CI/CD which will be covered in a separate blog post.

  • Requirements & Assumptions - Requirements specific to AttackLab-Lite and some things to have in place before you get started
  • Building Images with Packer - A quick review of Packer and how it’s being used to build baseline virtual machine images
  • Windows Answer Files - Review of building answer files (Autounattend XML) from scratch and how they’re used to build images with Packer
  • Terraform Code Overview - A quick review of how the Terraform code is structured and use in the lab
  • Ansible Playbook Overview - A quick review of how Ansible playbooks are structured and used in the lab
  • Installing Terraform, Packer, Ansible - How to install Terraform, Packer, and Ansible on Linux and Windows
  • Deploying AttackLab-Lite - Building Packer images, deploying infrastructure with Terraform, and provisioning infrastructure with Ansible
  • Post-Deployment Activities - Some tasks to do after deploying the lab, specifically around enrolling Windows 10/Windows 2019 servers into the Elastic Stack
  • Retrospective - Things that are on the TODO or are in progress to make this a living project that can and will evolve over time

NOTE: This post is quite extensive and covers the what, why, and how of everything. If you’re interested in getting up and running with the lab as-is, feel free to consult the Wiki on the AttackLab-Lite GitHub page.

ESXi/vSphere Requirements & Assumptions

Attack-Lab Lite was built around ESXi deployment. This post assumes you are using VMware ESXi and vSphere as your base infrastructure for deployment.

Additionally, within vSphere, it is assumed you have configured:

  • A Datacenter
  • A Cluster
  • Have relevant ISO files uploaded to your ESXi host datastore (OS and Windows VMWare Tools)

On that note, you’ll need to extract the Windows VMware Tools from your ESXi host and add them to your datastore so Packer can find them.

  • Enable SSH on your ESXi host
  • Use an application such as WinSCP to download the windows.iso VMware Tools ISO from /vmimages/tools-isoimages
  • Upload the Windows VMware Tools ISO to your ESXi host datastore

Other aspects of Attack-Lab Lite and content in the post don’t strictly apply to ESXi/vSphere, such as the building of Windows Answer files and Ansible playbooks. This can be applied anywhere and to a lot of different use cases.

Packer and Terraform may be specific to ESXi/vSphere in the context of this post, but the code can serve as a primer to use in different applications, like using Packer with VirtualBox and Terraform with DigitalOcean or AWS, for example.

Building Images with Packer

Packer is a tool that allows you, in an automated and unattended fashion, to build virtual machine images of different operating systems for different virtualization hypervisors based on defined requirements. Packer allows you to create VM images for a variety of platforms and environments such as vSphere, AWS, Azure, VirtualBox, Proxmox and more.

During this stage you could install software, but I save that for Ansible. In the event I want to add or remove software down the road, it’s much easier and time effective to change the Ansible code than to build new images and go through the build, deploy, and provision pipelines all over again.

While building images for the Windows systems of the lab are relatively easy, as is also the case with CentOS 7/8 (or anything RHEL, really), it’s a little more tricky with Ubuntu. Ultimately, until I have the patience to figure out the Packer process for Ubuntu deployments, CentOS 8 Stream will be my go to for the Linux-based aspects of the attack lab.

Windows 10 / Windows Server 2019

For both the Windows 10 and Windows Server 2019 Packer build processes, there are a few components that make it possible:

  • Packer Template File
  • Packer Variables File
  • vSphere Variables File
  • Windows Answer Files (autounattend.xml)
  • Different PowerShell and Batch Scripts

Let’s break down the Windows 10 Packer template (win10.pkr.hcl); the structure is generally the same for the Windows Server 2019 and CentOS 8 Packer builds as well, but we’ll use the Windows 10 template as an example.

First, we’re defining the required Packer plugins for when we do our packer init later on, which tells Packer “Hey, these are the plugins I need to build out this image, go download them!” In this instance, we only have one which is for the vSphere Builder so Packer can interact with vSphere via API to build a virtual machine that will eventually become a template within vSphere itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#################
# PACKER PLUGINS
#################

packer {
    required_version = ">= 1.8.3"

    required_plugins {
        vmware = {
            version = ">= 1.0.8"
            source  = "github.com/hashicorp/vsphere"
        }
    }
}

Next, we define some variables for both the virtual machine itself as well as vSphere. These are intentionally blank as the real values are stored in win10.pkrvars.hcl and vsphere.pkrvars.hcl that Packer will use later to populate the relevant values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
####################
# VSPHERE VARIABLES
####################

variable "vsphere_ip" {
    type = string
    description = "vSphere Server IP Address or Hostname"
    default = ""
}

variable "vsphere_username" {
    type = string
    description = "vSphere Username"
    default = ""
}

variable "vsphere_password" {
    type = string
    description = "vSphere Password"
    default = ""
    sensitive = true
}

variable "vsphere_dc" {
    type = string
    description = "Datacenter in vSphere Instance"
    default = ""
}

variable "vsphere_datastore" {
    type = string
    description = "Datastore to Reference"
    default = ""
}

variable "vsphere_cluster" {
    type = string
    description = "Cluster in vSphere Instance"
    default = ""
}

variable "vsphere_folder" {
    type = string
    description = "Destination Folder to Store Template"
    default = ""
}

variable "vcenter_host" {
    type = string
    description = "ESXi Host where VM will be created"
    default = ""
}

###############
# VM VARIABLES
###############

variable "vm_name" {
    type = string
    description = "Name of VM Template"
    default = ""
}

variable "vm_guestos" {
    type = string
    description = "VM Guest OS Type"
    default = ""
}

variable "vm_firmware" {
    type = string
    description = "Firmware type for the VM; i.e.: 'efi' or 'bios'"
    default = ""
}

variable "vm_cpus" {
    type = number
    description = "Number of vCPU's for VM"
}

variable "vm_cpucores" {
    type = number
    description = "Number of vCPU Cores for VM"
}

variable "vm_ram" {
    type = number
    description = "Amount of RAM for VM"
}

variable "vm_cdrom_type" {
    type = string
    description = "CDROM Type for VM"
    default = ""
}

variable "vm_disk_controller" {
    type = list(string)
    description = "VM Disk Controller"
}

variable "vm_disk_size" {
    type = number
    description = "Desired Disk Size for VM"
}

variable "vm_network" {
    type = string
    description = "Desired Virtual Network to Connect VM To"
    default = ""
}

variable "vm_nic" {
    type = string
    description = "VM Network Interface Card Type"
    default = ""
}

variable "builder_username" {
    type = string
    description = "VM Guest Username to Build With"
    default = ""
}

variable "builder_password" {
    type = string
    description = "VM Guest User's Password to Authenticate With"
    default = ""
}

variable "os_iso_path" {
    type = string
    description = "Path to ISO on Host's Datastore"
    default = ""
}

variable "os_iso_url" {
    type = string
    description = "URL to ISO File for Packer to Download"
    default = ""
}

variable "os_iso_checksum" {
    type = string
    description = "SHA256 Checksum of the Target ISO File"
    default = ""
}

variable "vmtools_iso_path" {
    type = string
    description = "Path to ISO of Windows VMTools on Host's Datastore"
    default = ""
}

variable "vm_notes" {
    type = string
    description = "Notes for VM Template"
    default = ""
}

Next, we’re instructing Packer on how to build out the virtual machine within vSphere based on our defined variables. We also call upon local files in the repository to be injected by Packer which include the all-important Answer File (autounattend.xml) and some Batch/PowerShell scripts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#########
# SOURCE
#########

source "vsphere-iso" "win10-pro" {
    vcenter_server       = var.vsphere_ip
    username             = var.vsphere_user
    password             = var.vsphere_pass
    insecure_connection  = true

    datacenter           = var.vsphere_dc
    datastore            = var.vsphere_datastore
    cluster              = var.vsphere_cluster
    folder               = var.vsphere_folder
    host                 = var.vcenter_host

    vm_name              = var.vm_name
    guest_os_type        = var.vm_guestos
    firmware		     = var.vm_firmware
    CPUs                 = var.vm_cpus
    cpu_cores            = var.vm_cpucores
    RAM                  = var.vm_ram
    floppy_files         = [
        "autounattend.xml",
        "../scripts/Install-VMTools.bat",
        "../scripts/WinRM-Config.ps1"
    ]
    cdrom_type		     = var.vm_cdrom_type
    disk_controller_type = var.vm_disk_controller
    
    storage {
        disk_size             = var.vm_disk_size
        disk_thin_provisioned = true
    }

    network_adapters {
        network      = var.vm_network
        network_card = var.vm_nic
    }

    notes               = var.vm_notes
    convert_to_template = true

    communicator      = "winrm"
    winrm_username    = var.builder_username
    winrm_password    = var.builder_password
    winrm_timeout     = "3h"

    iso_paths         = [
        var.os_iso_path,
        var.vmtools_iso_path
    ]

    #iso_url	       = var.os_iso_url
    #iso_checksum      = var.os_iso_checksum

    remove_cdrom      = true
}

Finally, we go into the build process which puts it all together and eventually communicates over WinRM, or SSH if a Linux build, to gracefully shutdown the VM and convert it to a template as defined in the Source section.

1
2
3
4
5
6
7
8
9
10
11
########
# BUILD
########

build {
    sources = ["source.vsphere-iso.win10-pro"]

    provisioner "windows-shell" {
        inline = ["shutdown /s /t 5 /f /d p:4:1 /c \"Packer Shutdown\""]
    }
}

For the virtual machine settings itself, these come via variables that are defined in a separate file, win10.pkrvars.hcl, which is shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#Virtual Machine Settings
vm_name                     = "Win10Pro-Template"
vm_guestos                  = "windows9_64Guest"
vm_cpus                     = "1"
vm_cpucores                 = "4"
vm_ram                      = "4096"
vm_disk_controller          = ["lsilogic-sas"]
vm_disk_size                = "61440"
vm_network                  = "VM Network"
vm_nic                      = "e1000e"
vm_notes                    = "AttackLab-Lite - Windows 10 Pro Template - "
vm_cdrom_type               = "sata"

#WinRM Communicator Authentication; based on Autounattend.xml file
builder_username            = "Administrator"
builder_password            = "P@ssW0rd1!"

#ISO URL (https://gist.github.com/mndambuki/35172b6485e40a42eea44cb2bd89a214) - Windows 10 Enterprise 1909
#os_iso_url                  = "https://software-download.microsoft.com/download/pr/18362.30.190401-1528.19h1_release_svc_refresh_CLIENTENTERPRISEEVAL_OEMRET_x64FRE_en-us.iso"
#os_iso_checksum             = "ab4862ba7d1644c27f27516d24cb21e6b39234eb3301e5f1fb365a78b22f79b3"

#Path to Windows 10 ISO on VM Host
os_iso_path                 = "[Datastore] ISO/Win10_21H1_English_x64.iso"
os_iso_checksum             = "6911e839448fa999b07c321fc70e7408fe122214f5c4e80a9ccc64d22d0d85ea"

#Enable SSH on ESXi host and use WinSCP to browse to "/vmimages/tools-isoimages" and download "windows.iso" from there, then upload to datastore
vmtools_iso_path            = "[Datastore] vmtools/windows.iso"

There are Batch scripts and PowerShell scripts that are pretty basic for both Windows 10 and Server 2019. They simply enable WinRM for Packer to be able to communicate with the VM and to install VMware Tools.

CentOS 8 Stream

For CentOS 8 Stream image building with Packer, the components are nearly the same as Windows, with the exception of the instructions that tell the operating system how to set itself up:

  • Packer Template File
  • Packer Variables File
  • vSphere Variables File
  • Kickstart Template

Below is the Kickstart file used for building the base CentOS 8 Stream image. The Kickstart file is hosted via a HTTP server, provided by Packer, and is passed as a boot parameter when the CentOS 8 VM is initially created and started. This acts as a set of instructions that tells CentOS how and what to install and configure automagically - kind of like a Windows answer file!

Before reviewing the Kickstart file, let’s take a look at a portion of the centos8-stream.pkrvars.hcl file to see how it differs slightly from the Windows approach. Specifically, specifying the boot parameters via boot_command to pull the Kickstart configuration via Packer’s HTTP server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
...
...
    http_port_min   = 8601
    http_port_max   = 8610
    http_directory  = "http"
    boot_order      = "disk,cdrom"
    boot_wait       = "5s"

    boot_command = ["<tab><bs><bs><bs><bs><bs><bs> text inst.ks=http://:/ks.cfg<enter><wait>"]

    shutdown_command = "echo '${var.builder_password}' | sudo -S -E shutdown -P now"
    remove_cdrom     = true
...
...
...

The boot_command parameter modifies the initial boot option when booting from the CentOS 8 ISO to invoke inst.ks which tells CentOS that a Kickstart file will be used to drive the installation in an unattended manner. This points to the Kickstart template that is hosted via Packer’s built-in HTTP server.

centos8boot Boot option being modified by Packer

With the Kickstart file, I learned the hard way that it’s important to ensure perl is installed as part of this process within the post-installation tasks section. Otherwise, Terraform will fail to do some post-deployment provisioning tasks later on, such as setting the hostname and IP addresses of the VM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#CentOS 8 Stream - AttackLab-Lite (10/2022)
#Kickstarter Configuration File

#Accept EULA
eula --agreed

#Install from CDROM
cdrom

#Text-based Installation
text

#Use Network Mirror for Packages
url --url="http://mirror.centos.org/centos/8-stream/BaseOS/x86_64/os/"

#Ignore installation of Xorg
skipx

#Disable Initial Setup on First Boot
firstboot --disable

#Set Timezone
timezone America/New_York --isUtc

#Set Language
lang en_US.UTF-8

#Set Keyboard Layout
keyboard --vckeymap=us --xlayouts='us'

#DHCP Network Configuration
network --onboot=yes --device=ens192 --bootproto=dhcp --noipv6 --activate

#Set Hostname
network --hostname=centos8-template

#Disable Firewall
firewall --disabled

#Disable SELinux
selinux --disabled

#Set bootloader to MBR
bootloader --location=mbr

#Partition Scheme (auto, LVM)
autopart --type=lvm

#Initialize Disk
clearpart --linux --initlabel

#Root Password: P@ssW0rd1!
rootpw --iscrypted $6$S.3DjrjF.OEF$6xfMp7j.iVatWKlLQQiSQecOST8tJl8kSju0X2IwDZVTTDGqOULdioDVONKAwJuB.z/fhuH9sd5ocYfrI22N30

#Core CentOS 8 Packages to Install
%packages
@^server-product-environment
%end

#Post-installation - Additional Steps
%post
yum update -y
yum install -y open-vm-tools perl
systemctl enable --now cockpit.socket
systemctl enable --now sshd.service
%end

#Reboot after installation is complete
reboot --eject

I’ve found that building a Kickstart file was much easier and there’s an excellent resource you can reference to help you build your own Kickstart file, like this guide over here.

The root password SHA-512 digest is generated using the mkpasswd command, which is a part of the whois package… not sure why. But, if you install whois in Ubuntu or CentOS, or whatever flavor of Linux, you can create a SHA-512 digest of the root password by issuing the following command:

mkpasswd -m sha-512 password -s "<PASSWORD_HERE>"

With that, you can replace the password in the Kickstart file as needed, but I’d suggest leaving it as-is since this is just a lab environment. If you do change it however, you’ll need to reflect the change in the terraform.tfvars file, the centos8.pkrvars.hcl file, and within the Ansible inventory file as to not break anything.

What about Windows Answer Files?

Glad you asked! Building the autounattend.xml file, or answer file, for Packer to leverage when building out the base Windows 10 and Windows Server 2019 images is essential to ensuring a clean working image that Ansible will take over later, just as the Kickstart template does for CentOS. I had to dust off the old Sysadmin skills for this part as well as review others answer files for inspiration and clarification.

To develop the answer files, we’ll need to enlist the help of some software from Microsoft, which is the Microsoft Assessment and Deployment Kit (ADK) I stood up a Windows 10 virtual machine to install ADK on to generate the answer files for both Windows 10 and Windows Server 2019.

You don’t have to manually create your own answer file, feel free to use the ones available in the GitHub repository or read on if you’re curious. Additionally, there are some answer file generators available online, such as this one. I haven’t tested them, but I don’t see why it wouldn’t work.

Prerequisite Items

Along with the files outlined below, you’ll also need a Windows 10 host to install the ADK software on and extract the relevant file from the ISOs.

Snag the following software to get starting building answer files:

I suggest using a Windows 10 21H1 ISO as there may be some compatibility issues with the most recent version of Windows 10 and ADK/Windows System Imager Tool.

Extracting the WIM Image (Windows 10, Server 2019)

First, you will extract the relevant WIM image from the ISO. The WIM image is the base installation image and contains all necessary files for an installation of the Windows operating system version (i.e.: Pro vs Home) and allows you to customize the installation using tools in the ADK suite in order to generate those answer files. I could be wrong, but this is my approach to generating those files.

I mount the Windows 10 ISO natively and copy the install.wim file from the ISO, which is found under the D:\sources\ directory. This is assuming the ISO is mounted to D:\ In my case, I copy the install.wim file to C:\Temp\

Next, open up PowerShell or Command Prompt as an Administrator and run the following command:

1
dism /Get-WimInfo /WimFile:C:\Temp\install.wim

This will show you the index position for the edition of Windows 10 you want to base your answer file around. In my case, Windows 10 Pro will do just fine, and typically is index number 6. Below is an example of using dism to check the instal.wim file on the Windows 10 ISO that I mounted to my virtual machine.

dism

We do this process for two reasons:

  • We use dism to get the Windows 10 Pro index position (6) from the WIM file, which is used later in the Answer File (amd_64-Microsoft-Windows-Setup_neutral)
  • We’re making a copy of the WIM image for later use/import into Windows System Image Manager (WISM)

Generating an Answer File for Windows 10

Generating a Windows Answer file is a somewhat complex task, but there are a lot of resources that help break down the process on a deeper level, such as this documentation from Microsoft. I won’t be going into detail on how to build an answer file end-to-end, but will walk through the high-level steps to get started.

With your Windows 10 Pro WIM image extracted from the ISO, you can now use the Windows System Image Manager you installed earlier through the ADK suite.

Launch Windows System Image Manager and import your freshly extracted WIM image by right-clicking “Select a Windows image or catalog file” from the bottom-left section titled “Windows Image” and browse to the location of the Windows 10 Pro WIM we extracted earlier.

wism1

Upon selecting the WIM image, you may receive a message about a missing catalog file and be requested to have it be created. Accept this by clicking “Yes” and allow WISM to generate the catalog file - this can take several minutes.

wism2

After the catalog file has been generated, click “File” and select “New Answer File…“ This will populate content under the “Answer File” section of WISM.

wism3

The answer file Components we’ll be focusing on are:

  • windowsPE - Windows Preinstallation Environment (Windows PE) is the section used to define settings specific to the Windows installation, such as disk partitioning.
  • offlineServicing - During installation, this section allows you to install additional items such as drivers or set different settings to the image to be installed
  • specialize - Majority of settings go here as these settings are acknowledged at the beginning of the Out-Of-Box Experience (OOBE) such as executing commands and setting timezone or locale settings
  • oobeSystem - Settings here will run after the OOBE process is completed, such as executing additional scripts and commands

Each of the following components above link to Microsoft’s documentation for additional information and reading.

answerfile

Once complete, click “File” and then “Save Answer File As…“ to save your Answer File.

There are a lot of answer files available on GitHub for Packer and Windows, which you’re free to use. I based mine off of several available examples from GitHub and added a few changes to mine.

One of the changes would be enabling RDP and setting the firewall rule for RDP in the answer file itself within the specialize section of the answer file; see below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <fDenyTSConnections>false</fDenyTSConnections>
</component>
<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <FirewallGroups>
        <FirewallGroup wcm:action="add" wcm:keyValue="RemoteDesktop">
            <Active>true</Active>
            <Group>Remote Desktop</Group>
            <Profile>all</Profile>
        </FirewallGroup>
    </FirewallGroups>
</component>
<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <SecurityLayer>2</SecurityLayer>
        <UserAuthentication>1</UserAuthentication>
</component>

This gets added via the following components in WISM:

  • amd64_Microsoft-Windows-TerminalServices-LocalSessionManager_neutral
  • amd64_Microsoft-Windows-TerminalServices-RDP-WinStationExtensions_neutral
  • amd64_Networking-MPSSVC-Svc_neutral

A lot of folks will enable and configure RDP using a PowerShell script or Batch file, but I took the approach of doing so using the answer file. Whether it’s the right or wrong approach, I’m not sure, but it’s giving the results I’m going after.

Feel free to compare and contrast the Answer File in my GitHub repository for both Windows 10 and Server 2019 against WISM so you can see how it’s built out and structured.

Generating an Answer File for Windows Server 2019

Obtain the install.wim from the Windows Server 2019 ISO just like you did with Windows 10 and use the Windows System Image Manager (WISM) tool provided by the ADK suite to create an answer file for Windows Server 2019. Just note, when importing the WIM image from the Windows Server 2019 ISO into WISM, you should select “Windows Server 2019 SERVERSTANDARD” from the selection.

wism4

The process is essentially identical to that of Windows 10 Pro and the same answer file is structured and built the same way the Windows 10 Pro answer file is in my environment.

Deploying Infrastructure with Terraform

Terraform is Infrastructure-as-Code (IaC) and allows you to deploy and configure different resources across various platforms, such as deploying virtual machines in cloud environments like AWS or DigitalOcean. I’ve covered Terraform in the previous post in more detail and don’t want to repeat myself.

From the lab perspective, nothing has really changed in the way of Terraform except that virtual machine’s are now deployed in my ESXi/vSphere environment and derive from the Packer images built as described in the previous section. I use Terraform to deploy the following infrastructure:

  • 2x Windows Server 2019 VMs (Domain Controller, General Purpose Server)
  • 2x Windows 10 Pro VMs (Clients)
  • 1x CentOS 8 Stream VM (Elastic Stack)

Kali Linux is also in the lab, but it is deployed manually. I just attach the network interface to my Lab vSwitch so it can interact with the rest of the virtual machines in the lab.

Below is a layout of the Terraform files used for the lab. I keep the numbering of each file as I was used to load order in previous versions of Terraform, but with version 0.12+ that’s no longer the case as Terraform treats all .tf files as a single document - old habits.

1
2
3
4
5
6
7
terraform
├── 01-Win2019.tf
├── 02-Win10.tf
├── 03-CentOS8-Stream.tf
├── provider.tf
├── terraform.tfvars
└── variables.tf

Using the terraform.tfvars file, I define the number of instances of each virtual machine that I’d like to deploy by placing them in a list. By commenting out, adding, or removing, I can control how many to deploy - in the event I only want a Domain Controller for testing, I can opt to exclude the general purpose Windows Server 2019 virtual machine from the deployment. It makes the Ansible aspect later on a bit messy once you start omitting infrastructure, so I tend to keep it as is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Windows Server 2019 (DC, General Purpose)
windows-2019 = [
   {
		vm_name = "LABDC01"
		folder = "AD Attack Lab"
		vcpu = "2"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.200"
		dns_server_list = ["127.0.0.1","192.168.50.1"]
    },
    {
		vm_name = "LABSVR01"
		folder = "AD Attack Lab"
		vcpu = "2"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.100"
		dns_server_list = ["192.168.50.200","192.168.50.1"]
    }
]

This is used in conjunction with the 01-Win2019.tf file which will iterate through the terraform.tfvars file to enumerate how many Windows Server 2019 virtual machines are being requested based on the windows-2019 = [] list as shown above. This will be shown in more detail later in the post.

Below is an example of building out the Windows Server 2019 virtual machine resource in the 01-Win2019.tf file. Pay close attention to the repeating value throughout the file…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
resource "vsphere_virtual_machine" "windows-2019" {
  count		         = length(var.windows-2019)
  
  name             = lookup(var.windows-2019[count.index], "vm_name")
  datastore_id     = data.vsphere_datastore.datastore.id
  resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
  
  num_cpus         = lookup(var.windows-2019[count.index], "vcpu")
  memory           = lookup(var.windows-2019[count.index], "memory")

  guest_id         = data.vsphere_virtual_machine.Win2019-Template.guest_id
  scsi_type        = data.vsphere_virtual_machine.Win2019-Template.scsi_type
  firmware         = data.vsphere_virtual_machine.Win2019-Template.firmware
  
  folder           = lookup(var.windows-2019[count.index], "folder")

  network_interface {
    network_id   = data.vsphere_network.network.id
    adapter_type = data.vsphere_virtual_machine.Win2019-Template.network_interface_types[0]
  }

  disk {
    label            = "disk0"
    size             = data.vsphere_virtual_machine.Win2019-Template.disks.0.size
    eagerly_scrub    = data.vsphere_virtual_machine.Win2019-Template.disks.0.eagerly_scrub
    thin_provisioned = data.vsphere_virtual_machine.Win2019-Template.disks.0.thin_provisioned
  }

  clone {
    template_uuid = data.vsphere_virtual_machine.Win2019-Template.id
    
    customize {
      timeout = 20
      windows_options {
        computer_name    = lookup(var.windows-2019[count.index], "vm_name")
        admin_password   = lookup(var.windows-2019[count.index], "admin_password")
        auto_logon       = true
        auto_logon_count = 1
        time_zone        = 035

        #Download and execute ConfigureRemotingforAnsible (WinRM)
        run_once_command_list = [
          "powershell.exe [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 -Outfile C:\\WinRM_Ansible.ps1",
          "powershell.exe -ExecutionPolicy Bypass -File C:\\WinRM_Ansible.ps1"
        ]
      }
      
      network_interface {
        ipv4_address = lookup(var.windows-2019[count.index], "ipv4_address")
        ipv4_netmask = 24
      }

      ipv4_gateway    = lookup(var.windows-2019[count.index], "ipv4_gateway")
      dns_server_list = lookup(var.windows-2019[count.index], "dns_server_list")
    }
  }
}

Notice the many instances of lookup(var.windows-2019[count.index] littered throughout the file. This is referencing the total index count of the windows-2019 = [] list from the terraform.tfvars file. For each resource declared in the list, it will build it out to specification based on the number of vCPUs, vRAM, and so on. The same rules apply for the CentOS 8 and Windows 10 virtual machines that get deployed via Terraform - they’re declared as well in the terraform.tfvars file and have their own respective Terraform file to deploy from.

I love Terraform for infrastructure automation, whether for Red Team infrastructure, CTF environments, or my home lab. You can also use Terraform to provision your infrastructure post-deployment. In my case, however, I’ll be using Ansible for post-deployment tasks.

Post-Deployment Activities with Ansible

After Terraform has deployed the base infrastructure, I leverage Ansible to perform post-deployment tasks. This stage includes configuring the systems in the deployment, installing software, and executing additional scripts.

Using Ansible, I created playbooks to install software via Chocolatey on the Windows hosts and other software such as the components required for the Elastic Stack on the CentOS 8 virtual machine using native yum package management, as well as copy over core files and make other configuration changes to the systems.

I use Ansible in the environment because as I mentioned before, it’s easier to change a few lines of code in the playbooks than to bake software installations in during the build process with Packer. Again, that’s because in the event of a need to add or remove software, we need to start from square one again and I’d rather have the process as linear as possible. Even post-deployment and post-provisioning, I can always re-run a (modified) playbook later if I need to.

The layout of the Ansible playbooks looks like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ansible
├── ansible.cfg
├── attacklab.yml
├── inventory.yml
└── roles
    ├── active-directory
    │   ├── tasks
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    ├── elasticstack
    │   ├── files
    │   │   ├── elasticsearch.repo
    │   │   ├── input-beat.conf
    │   │   └── output-elasticsearch.conf
    │   ├── tasks
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    ├── join-domain
    │   ├── tasks
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    ├── vulnerable-ad
    │   ├── files
    │   │   ├── LDAP_NULL_Bind.ps1
    │   │   └── Vulnerable-AD.ps1
    │   ├── tasks
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    └── windows-init
        ├── files
        │   ├── bginfo.bat
        │   ├── lab_bginfo.bgi
        │   └── sysmon.xml
        ├── tasks
        │   └── main.yml
        └── vars
            └── main.yml

Roles and Playbooks

There are five (5) different roles that break out into core playbooks for the lab environment:

  • windows-init - This is used on all Windows-based system. It installs software via Chocolatey and copies over some files, such as the Sysmon configuration and Bginfo configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
---
- name: Disable IPv6
  win_shell: reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters /v DisabledComponents /t REG_DWORD /d 255 /f

- name: Create LabFiles folder in C:\
  win_file:
    path: C:\LabFiles\
    state: directory

- name: Install Chocolatey for Package Management
  win_chocolatey:
    name:
      - chocolatey
      - chocolatey-core.extension
    state: present

- name: Install bginfo
  win_chocolatey:
    name: bginfo
    state: present

- name: Copy bginfo Configuration File
  win_copy:
    src: "files/lab_bginfo.bgi"
    dest: C:\LabFiles\lab_bginfo.bgi

- name: Copy bginfo Startup Script
  win_copy:
    src: "files/bginfo.bat"
    dest: "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\bginfo.bat"

- name: Start bginfo with Configuration
  win_shell: C:\ProgramData\chocolatey\bin\Bginfo64.exe C:\LabFiles\lab_bginfo.bgi /SILENT /NOLICPROMPT /TIMER:0

- name: Install Sysmon
  win_chocolatey:
    name: sysmon
    state: present

- name: Copy Sysmon Configuration File (@SwiftOnSecurity)
  win_copy:
    src: "files/sysmon.xml"
    dest: C:\LabFiles\sysmon.xml

- name: Install, Configure, and Enable Sysmon64
  win_shell: C:\ProgramData\chocolatey\bin\Sysmon64.exe -accepteula -i C:\LabFiles\sysmon.xml
  ignore_errors: yes

- name: Install Sysinternals Suite
  win_chocolatey:
    name: sysinternals
    state: present

- name: Create Sysinternals Shortcut
  win_shortcut:
    src: C:\ProgramData\chocolatey\lib\sysinternals\tools
    dest: C:\LabFiles\Sysinternals.lnk

- name: Download Elastic Agent ()
  win_get_url:
    url: https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.zip
    dest: C:\LabFiles\elastic-agent--windows-x86_64.zip

- name: Extract Elastic Agent ZIP Archive
  win_shell: Expand-Archive -LiteralPath 'C:\LabFiles\elastic-agent--windows-x86_64.zip' -DestinationPath C:\LabFiles\

- name: Delete Elastic Agent ZIP File
  win_file:
    path: C:\LabFiles\elastic-agent--windows-x86_64.zip
    state: absent
  • active-directory - This role is dedicated to the Domain Controller and sets up Active Directory Domain Services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
---
- name: Install Active Directory Domain Services (AD DS)
  win_feature:
    name: AD-Domain-Services
    include_management_tools: yes
    include_sub_features: yes
    state: present
  register: adds_installed

- name: Create new Domain in a new Forest ()
  win_domain:
    dns_domain_name: ""
    safe_mode_password: ""
    install_dns: yes
    domain_mode: Win2012R2
    domain_netbios_name: ""
    forest_mode: Win2012R2
    sysvol_path: C:\Windows\SYSVOL
    database_path: C:\Windows\NTDS
  register: dc_promo

- name: Restart Server
  win_reboot:
    msg: "Active Directory installed via Ansible - rebooting..."
    pre_reboot_delay: 10
  when: dc_promo

- name: Waiting for reconnect after reboot...
  wait_for_connection:
    delay: 60
    timeout: 1800

- name: Add DNS A record for LABELK01
  win_shell: Add-DnsServerResourceRecordA -Name labelk01 -ZoneName lab.local -AllowUpdateAny -IPv4Address  -TimeToLive 01:00:00
  • vulnerable-ad - This role is dedicated to the Domain Controller and executes two different PowerShell scripts; Vulnerable-AD and a custom script intended to make NULL LDAP binds possible
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: Sleep for 5 minutes post-AD Install
  pause:
    minutes: 5

- name: Copy 'Vulnerable-AD.ps1' to C:\LabFiles
  copy:
    src: "files/Vulnerable-AD.ps1"
    dest: C:\LabFiles\Vulnerable-AD.ps1

- name: Copy 'LDAP_NULL_Bind.ps1' to C:\LabFiles
  copy:
    src: "files/LDAP_NULL_Bind.ps1"
    dest: C:\LabFiles\LDAP_NULL_Bind.ps1

- name: Make LDAP NULL Bind Possible
  win_shell: C:\\LabFiles\\LDAP_NULL_Bind.ps1

- name: Import VulnAD Module and Invoke-VulnAD
  win_shell: |
    Import-Module C:\LabFiles\Vulnerable-AD.ps1
    Invoke-VulnAD -UsersLimit 100 -DomainName ""
  ignore_errors: true
  • join-domain - This role is used on all Windows 10 clients as well as the generic Windows Server 2019 VM. It will join these machines to the lab domain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- name: Join client to Attack Lab Domain ()
  win_domain_membership:
    dns_domain_name: ""
    domain_admin_user: administrator@
    domain_admin_password: P@ssW0rd1!
    state: domain
  register: domain_state

- name: Restart Client
  win_reboot:
    msg: "Client joined to Active Directory - rebooting..."
    pre_reboot_delay: 10
  when: domain_state.reboot_required

- name: Waiting for reconnect after reboot...
  wait_for_connection:
    delay: 60
    timeout: 1800
  • elasticstack - This role is dedicated to the Elastic Stack VM which installs and configures Elasticsearch, Logstash, and Kibana. It also enrolls Kibana into Elasticsearch and auto-resets the elastic user password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
---
- name: Download & Extract Elastic Agent TAR ()
  shell:
    "cd /root && wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.tar.gz && tar -xzvf elastic-agent--linux-x86_64.tar.gz"

- name: Delete Elastic Agent TAR File
  file:
    path: /root/elastic-agent--linux-x86_64.tar.gz
    state: absent

- name: Copy Elastic Repo File
  copy:
    src: "files/elasticsearch.repo"
    dest: /etc/yum.repos.d/elasticsearch.repo

- name: Update /etc/hosts file
  lineinfile:
    dest: /etc/hosts
    regexp: '^127\.0\.0\.1[ \t]+localhost'
    line: '127.0.0.1 labelk01 localhost'

- name: Add Elasticsearch key
  shell:
    "rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch"

- name: Install Elasticsearch
  yum:
    name: elasticsearch
    enablerepo: "elasticsearch"

- name: Install Kibana
  yum:
    name: kibana
    enablerepo: "elasticsearch"

- name: Install Logstash
  yum:
    name: logstash
    enablerepo: "elasticsearch"

- name: Modifying elasticsearch.yml (network.host)
  lineinfile:
    path: /etc/elasticsearch/elasticsearch.yml
    regexp: '^#network.host'
    line: 'network.host: '

- name: Modifying elasticsearch.yml (http.port)
  lineinfile:
    path: /etc/elasticsearch/elasticsearch.yml
    regexp: '^#http.port'
    line: 'http.port: 9200'

- name: Ignore 'cluster.initial_master_nodes' line
  lineinfile:
    path: /etc/elasticsearch/elasticsearch.yml
    regexp: '^cluster.initial_master_nodes'
    line: '#cluster.initial_master_nodes: ["LABELK01"]'

- name: Modifying elasticsearch.yml (discovery.type)
  lineinfile:
    path: /etc/elasticsearch/elasticsearch.yml
    line: 'discovery.type: single-node'

- name: Modifying kibana.yml (server.port)
  lineinfile:
    path: /etc/kibana/kibana.yml
    regexp: '^#server.port'
    line: "server.port: 5601"

- name: Modifying kibana.yml (server.host)
  lineinfile:
    path: /etc/kibana/kibana.yml
    regexp: '^#server.host'
    line: 'server.host: ""'

- name: Modifying kibana.yml (server.publicBaseUrl)
  lineinfile:
    path: /etc/kibana/kibana.yml
    regexp: '^#server.publicBaseUrl'
    line: 'server.publicBaseUrl: "http://:5601"'

- name: Modifying kibana.yml (elasticsearch.hosts)
  lineinfile:
    path: /etc/kibana/kibana.yml
    regexp: '^#elasticsearch.hosts'
    line: 'elasticsearch.hosts: ["http://:9200"]'

- name: Copy Logstash configuration file - input-beat.conf
  copy:
    src: "files/input-beat.conf"
    dest: /etc/logstash/conf.d/input-beat.conf

- name: Copy Logstash configuration file - output-elasticsearch.conf
  copy:
    src: "files/output-elasticsearch.conf"
    dest: /etc/logstash/conf.d/output-elasticsearch.conf

- name: Reloading daemons
  systemd:
    daemon_reload: yes

- name: Enable Elasticsearch on Startup
  systemd:
    name: elasticsearch
    enabled: yes

- name: Enable Logstash on Startup
  systemd:
    name: logstash
    enabled: yes

- name: Enable Kibana on Startup
  systemd:
    name: kibana
    enabled: yes

- name: Start Elasticsearch Service
  systemd:
    name: elasticsearch
    state: started

- name: Start Logstash Service
  systemd:
    name: logstash
    state: started

- name: Start Kibana Service
  systemd:
    name: kibana
    state: started

- name: Generate Kibana Enrollment Token
  shell:
    "/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana --url \"https://:9200\""
  register: enrollment_token
#- debug:
#    var: enrollment_token.stdout_lines  
- set_fact:
    token=

- name: Enroll Kibana in Elasticsearch
  shell:
    "/usr/share/kibana/bin/kibana-setup --enrollment-token "
#  register: enrollment
#- debug:
#    var: enrollment.stdout_lines

- name: Reset 'elastic' Password
  shell:
    "/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -b"
  register: elastic_password
- debug:
    var: elastic_password.stdout_lines

- name: Restart Kibana Service
  systemd:
    name: kibana
    state: restarted

Variables that are called, such as the or variables, are found within each Role’s vars/ directory. It’s a bit redundant, and I’m sure there’s a better way to define this, along with other variables, but for now it’ll do.

Elastic Stack Playbook

The primary (and only) playbook I current have set up for the CentOS 8 server is to install and perform a preliminary setup of the Elastic Stack (or ELK) - Elasticsearch, Logstash, and Kibana, which is the last one shown above. I use Elastic Stack to see what logging looks like from the Blue Team point of view with each attack, tool, command, or payload executed.

Elastic’s Fleet is leveraged in this installation of the Elastic Stack to manage Elastic Agents that will be installed on the Windows systems in the lab environment. Through the Ansible roles, the latest version of the Elastic Agent is present on each Windows virtual machine, however, automating the installation isn’t something I’ve tackled and requires manually installing and registering the agents with the server. We’ll cover that later in the post-provisioning activities section.

Installing Packer, Terraform, and Ansible

Obviously to make all of this work, you’re going to need the requisite tools installed.

You’re free to install these tools where you see fit, but for the sake of keeping things together, I’m installing Packer, Terraform, and Ansible on an Ubuntu virtual machine. I’ve found it’s especially beneficial to have Packer installed on it’s own system for one main reason that I’ll also mention in the CI/CD blog post later, which is:

  • Both Windows Subsystem Linux (WSL) and Windows did not honor the primary network interface of the machine. This seems to be the case especially when you have multiple network interfaces from VMware Workstation or VirtualBox installations, which causes WinRM communications and hosting the CentOS Kickstart template to fail - see an example GitHub issue here

Installing Terraform, Packer, Ansible on Ubuntu

Refer to Hashicorp’s documentation on installing Terraform and Packer on Ubuntu here, and Ansible’s documentation for installing here. For a consolodation of the commands to install Packer, Terraform, and Ansible on an Ubuntu/Debian-bashed host, see below:

1
2
3
4
5
6
7
8
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common gpg
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install terraform packer
sudo apt-add-repository ppa:ansible/ansible
sudo apt update
auso apt install ansible

Installing Terraform & Packer on Windows

1) Download the Windows Packer and Terraform binaries from the official Hashicorp repositories

2) Create a folder in C:\ called Hashicorp (C:\Hashicorp)

3) Place the downloaded Packer and Terraform binaries into the C:\Hashicorp folder

4) Open “Control Panel” and search for “System” and select “Edit the system environment variables”

controlpanel

5) In the “System Properties” window, under the “Advanced” tab, click the “Environment Variables…” button

6) Under the “System variables” section, scroll down until you find Path and click “Edit…“

7) Click “New” and add the path you created earlier - C:\Hashicorp and click “OK”

8) Click “OK” on all remaining windows

9) Open the Command Prompt and type in packer.exe and terraform.exe and ensure both execute

Installing Ansible on Windows

You will need to enable Windows Subsystem Linux (WSL) and install Ansible within that environment. Here is a good tutorial (Method 3) on how to enable WSL on Windows 10/Windows 11 and install Ansible.

Putting it All Together - Deploying the Lab

Below are examples of going through each stage manually. The following assumes you are using these tools on a Linux-based system, but are used the same on Windows; just directory path differences, is all!

This is a high-level of going through the stages to deploy and provision the lab environment. The Wiki on the AttackLab-Lite GitHub goes into more detail, so feel free to refer to it if the following sections aren’t clear enough.

Don’t forget to upload your Windows VMware Tools, CentOS 8, Windows 10, and Windows Server 2019 ISO files to your ESXi datastore!

Packer - Building VM Images

Before jumping into building your base images with Packer, you’ll need to modify the variable files for each Packer template, which can be found in the following paths:

  • packer/Win10/win10.pkrvars.hcl
  • packer/Win2019/win2019.pkrvars.hcl
  • packer/CentOS8/centos8-stream.pkrvars.hcl

Adjust virtual machine settings such as vRAM, vCPU, network interfaces, and especially your datastore name/paths, and ISO filenames.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#Virtual Machine Settings
vm_name                     = "Win10Pro-Template"
vm_guestos                  = "windows9_64Guest"
vm_cpus                     = "1"
vm_cpucores                 = "4"
vm_ram                      = "4096"
vm_disk_controller          = ["lsilogic-sas"]
vm_disk_size                = "61440"
vm_network                  = "VM Network"
vm_nic                      = "e1000e"
vm_notes                    = "AttackLab-Lite - Windows 10 Pro Template - "
vm_cdrom_type               = "sata"

#WinRM Communicator Authentication; based on Autounattend.xml file
builder_username            = "Administrator"
builder_password            = "P@ssW0rd1!"

#ISO URL (https://gist.github.com/mndambuki/35172b6485e40a42eea44cb2bd89a214) - Windows 10 Enterprise 1909
#os_iso_url                  = "https://software-download.microsoft.com/download/pr/18362.30.190401-1528.19h1_release_svc_refresh_CLIENTENTERPRISEEVAL_OEMRET_x64FRE_en-us.iso"
#os_iso_checksum             = "ab4862ba7d1644c27f27516d24cb21e6b39234eb3301e5f1fb365a78b22f79b3"

#Path to Windows 10 ISO on VM Host
os_iso_path                 = "[Datastore] ISO/Win10_21H1_English_x64.iso"
os_iso_checksum             = "6911e839448fa999b07c321fc70e7408fe122214f5c4e80a9ccc64d22d0d85ea"

#Enable SSH on ESXi host and use WinSCP to browse to "/vmimages/tools-isoimages" and download "windows.iso" from there, then upload to datastore
vmtools_iso_path            = "[Datastore] vmtools/windows.iso"

Again, it’s important at this stage that the OS and VMware Tools ISO files are uploaded to your ESXi datastore and that you’ve reflected those changes in these variable files.

The file vsphere.pkvars.hcl, which is the vSphere variables file and is located in each operating system’s Packer directory also needs to be modified. Specifically, you’ll need to populate your vSphere instance IP/FQDN, username, password, Datacenter, Cluster, and Datastore information.

1
2
3
4
5
6
7
8
#vSphere Settings
vsphere_ip              = ""
vsphere_username        = ""
vsphere_password        = ""
vsphere_dc              = ""
vsphere_datastore       = ""
vsphere_cluster         = ""
vsphere_folder          = ""

Below are the commands to build each image:

1
2
cd packer/Win2019
packer build -force -var-file win2019.pkrvars.hcl -var-file vsphere.tfvars.hcl win2019.pkr.hcl
1
2
cd packer/Win10
packer build -force -var-file win10.pkrvars.hcl -var-file vsphere.pkrvars.hcl win10.pkr.hcl
1
2
cd packer/CentOS8
packer build -force -var-file centos8-stream.pkvars.hcl -var-file vsphere.pkvars.hcl centos8-stream.pkr.hcl

Depending on your available resources, it should take Packer about 10-15 minutes to build each of the VM templates. After Packer has completed creating VM templates, it’s time to move on to deploying the infrastructure from these templates using Terraform.

Terraform - Deploying Infrastructure

Before deploying the infrastructure with Terraform, you’ll need to edit a few files, beginning with the variables.tf file.

In this file, you will need to reflect changes for your ESXi/vSphere environment such as your:

  • ESXi host IP or FQDN
  • Datacenter
  • Target Datastore
  • Target Network (i.e.: vSwitch)
  • Cluster Name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
variable "vsphere_username" {
	description = "vSphere user account"
	type = string
}

variable "vsphere_password" {
	description = "Password for vSphere user account"
	type = string
}

variable "vsphere_server" {
	description = "IP address or FQDN of vSphere server"
	type = string
	default = "x.x.x.x"
}

variable "datacenter_name" {
	description = "Datacenter Name to deploy VM resources to within vSphere"
	type = string
	default = "Your-Datacenter"
}

variable "datastore_name" {
	description = "Name of Datastore where VM resources will be saved to"
	type = string
	default = "Your-Datastore"
}

variable "network_name" {
	description = "Network that VM resources will be connected to (i.e.: vSwitch)"
	type = string
	default = "Your-VM-Network"
}

variable "cluster_name" {
	description = "Name of Cluster in vSphere where VM resources will be deployed to"
	type = string
	default = "Your-Cluster-Name"
}

variable "windows-2019" {
	description = "Predefined values for deploying multiple Windows Server 2016 VM's with different requirements - see terraform.tfvars"
	type = list
	default = [ ]
}

variable "windows-10" {
	description = "Predefined values for deploying multiple Windows 10 VM's with diffierent requirements - see terraform.tfvars"
	type = list
	default = [ ]
}

variable "centos8" {
	description = "Predefined values for deploying multiple CentOS 8 VM's with different requirements - see terraform.tfvars"
	type = list
	default = [ ]
}

Next, you may also want to make some changes to some parameters in the terraform.tfvars file to reflect your network schema, along with other variables such as vRAM, vCPU, and so on.

  • vm_name - This will become the effective hostname of the VM which Terraform will provision during deployment folder - Folder within vSphere where the VM resources will be organized into; make sure you’ve created this folder manually within vSphere first
  • ipv4_address - Static IP address for the virtual machine which Terraform will provision during deployment
  • ipv4_gateway - Default gateway for your network which Terraform will provision during deployment
  • dns_server_list - Primary and secondary DNS IP addresses which Terraform will provision during deployment; for the Domain Controller, keep 127.0.0.1 as the primary DNS IP

Modifying domain and dns_suffix is optional - I’d recommend leaving the domain names in place for Ansible’s sake later on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Windows Server 2019 (DC, General Purpose)
windows-2019 = [
   {
		vm_name = "LABDC01"
		folder = "AD Attack Lab"
		vcpu = "2"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.200"
		ipv4_gateway = "192.168.50.1"
		dns_server_list = ["127.0.0.1","192.168.50.1"]
    },
    {
		vm_name = "LABSVR01"
		folder = "AD Attack Lab"
		vcpu = "2"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.100"
		ipv4_gateway = "192.168.50.1"
		dns_server_list = ["192.168.50.200","192.168.50.1"]
    }
]

# Windows 10 Endpoints
windows-10 = [
    {
		vm_name = "WIN10LAB-01"
		folder = "AD Attack Lab"
		vcpu = "4"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.50"
		ipv4_gateway = "192.168.50.1"
		dns_server_list = ["192.168.50.200","192.168.50.1"]
    },
    {
		vm_name = "WIN10LAB-02"
		folder = "AD Attack Lab"
		vcpu = "4"
		memory = "4096"
		admin_password = "P@ssW0rd1!"
		ipv4_address = "192.168.50.51"
		ipv4_gateway = "192.168.50.1"
		dns_server_list = ["192.168.50.200","192.168.50.1"]
    }
]

# CentOS 8 Server (ELK)
centos8 = [
    {
		vm_name = "LABELK01"
		folder = "AD Attack Lab"
		vcpu = "4"
		memory = "8000"
		ipv4_address = "192.168.50.10"
		ipv4_gateway = "192.168.50.1"
		dns_server_list = ["192.168.50.200","192.168.50.1"]
		dns_suffix_list = ["lab.local"]
		domain = "lab.local"
    }
]

You can pass your vSphere username and password into the command below, set them as environment variables, or put them in the variables.tf file directly. I’m not going for security and secrets-management-best-practices here since it’s just a lab environment.

1
2
3
cd terraform
terraform init .
terraform apply --var="vsphere_username=$VSPHERE_USERNAME" --var="vsphere_password=$VSPHERE_PASSWORD" --auto-approve

Again, depending on resources, deployment of the VM templates as outlined above (5 VM resources) takes about 10 minutes.

Ansible - Provisioning Infrastructure

Before getting started with Ansible, you’ll need to make modifications to the inventory file - specifically, the IP addresses.

Modify the inventory.yml file and adjust the IP addresses in accordance with your network schema and as they were also defined in the terraform.tfvars file from the previous section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
---
lab_dc:
  hosts:
    #labdc01.lab.local
    192.168.50.200:
  vars:
    ansible_user: Administrator
    ansible_password: P@ssW0rd1!
    ansible_connection: winrm
    ansible_winrm_server_cert_validation: ignore

lab_server:
  hosts:
    #labsvr01.lab.local
    192.168.50.100:
  vars:
    ansible_user: Administrator
    ansible_password: P@ssW0rd1!
    ansible_connection: winrm
    ansible_winrm_server_cert_validation: ignore

lab_clients:
  hosts:
    #win10-01.lab.local
    192.168.50.50:
    #win10-02.lab.local
    192.168.50.51:
  vars:
    ansible_user: Administrator
    ansible_password: P@ssW0rd1!
    ansible_connection: winrm
    ansible_winrm_server_cert_validation: ignore

lab_elastic:
  hosts:
    #labelk01.lab.local
    192.168.50.10:
  vars:
    ansible_user: root
    ansible_password: P@ssW0rd1!
    ansible_connection: ssh
    ansible_ssh_common_args: '-o StrictHostKeyChecking=no'

You will also likely need to modify the elastic_ip and the elastic_ip_address variables to reflect your network schema, too. Remember, this should align with what was provided to Terraform earlier in the terraform.tfvars file. The variable files can be found in the following paths:

  • ansible/roles/active-directory/vars/main.yml
  • ansible/roles/elasticstack/vars/main.yml
1
2
3
4
lab_domain_name: lab.local
lab_netbios_name: LAB
lab_domain_recovery_password: P@ssW0rd1!
elastic_ip_address: 192.168.50.10
1
elastic_ip: 192.168.50.10

To provision each aspect of the infrastructure, we’ll use the command referenced below to run the Ansible playbook to provision our Windows 10, Windows Server 2019, and CentOS 8 virtual machines.

1
2
cd ansible/
ansible-playbook -i inventory.yml attacklab.yml

If for some reason you modify or need to re-run Ansible against a specific host (or set of hosts), you can rely on the tags that have been added to the attacklab.yml playbook to narrow the scope. The following tags are available:

  • dc - Domain Controller Roles
  • winserver - Generic Windows Server Roles
  • clients - Windows 10 Client Roles
  • elk - Elastic Stack Server Roles

To run any given set of roles against a specific tag, use the following command as an example:

1
2
cd ansible/
ansible-playbook -i inventory.yml attacklab.yml --tags "elk"

When Ansible begins to run the Elastic Stack tasks, pay close attention to the final output as it provides the password for the elastic user that you’ll use to log into Kibana with later. The tasks associated with the Elastic Stack run last, so the following output example should be one of the final messages provided by Ansible.

1
2
3
4
5
6
7
8
9
10
TASK [elasticstack : Reset 'elastic' Password] ****************************************************************************************************************************
changed: [192.168.50.10]

TASK [elasticstack : debug] ***********************************************************************************************************************************************
ok: [192.168.50.10] => {
    "elastic_password.stdout_lines": [
        "Password for the [elastic] user successfully reset.",
        "New value: *NTsfc0IArTXRuvteWFJ"
    ]
}

Manual Post-Provisioning Activities

There are some manual activities that need to be done post-deployment and post-provisioning. Specifically, installing and configuring Fleet Server on the Elastic Stack and enrolling the Elastic Agents on the Windows 10 and Windows Server 2019 virtual machines into the Elastic Stack. This is so we can ensure all the logs we want are forwarded from our lab machines to the Elastic Stack for later viewing… and dashboards!

Consult the AttackLab-Lite Wiki for detailed steps on how to setup and configure the initial Fleet Server and enroll Windows Elastic Agents into the Fleet Server/Elastic Stack for monitoring.

Retrospective

There are a lot of things I want to improve upon and expand upon within the AttackLab-Lite infrastructure to make it more robust. This post focused more on the DevOps side of getting the lab environment up and running using various tools. There’s more I’d like to incorporate and do to make this more misconfigured and full of real-world telemetry and behaviors.

  • I could probably improve on the Ansible layout, but it’s functional for now!
  • Create a custom script to populate and misconfigure Active Directory to my needs; Vulnerable-AD and BadBlood are great, but the more I can customize, the better - like the PowerShell script I put together to allow NULL LDAP binds
  • Develop simulation scripts or agents to emulate specific end user activity; I know some of these exist out there, but some haven’t been updated in a while. Besides, it’s something to work towards!
  • Incorporate the free Splunk Enterprise trial - automating installation/configuration of Splunk Universal Forwarders (UFs) is much more promising
  • Incorporate pfSense as the central router/firewall for the environment - do some Network Security Monitoring (NSM); looking at you, Zeek!
  • How do I get Wireshark and WinPcap to install nicely with Chocolatey!?
This post is licensed under CC BY 4.0 by the author.