You're a smart and well-informed pentester/consultant/hacker so you've obviously read the superb blog post written by @myexploit2600 about building and attacking your own Active Directory lab. Now you'd like a repeatable way to set it up all over again with fresh virtual machines, without doing all the steps manually, right? Lucky you, here we go.

This short post will not be a technical deep dive, but rather a quick overview of the PowerShell scripts I used and Ansible playbook I wrote to automate the deployment of my hacklab using @myexploit2600's guide.

"But Aidan, I've never heard of Ansible and I'd like to learn more before using it on my machine/network/smart fridge."

Good news is that Ansible is owned by Red Hat who have an exceptional attitude towards documentation. If you'd like something audio-visual, there is a 1h30m webinar with all the basics you need to know. Alternatively, if you prefer to read about Ansible, Red Hat's got you covered there too.

Virtual Machines (Windows server and workstation)

This blog is going to presume you've followed the short instructions on creating your server and workstation virtual machines; whether in Virtualbox, VMware, or Hyper-V. Since you can create snapshots or checkpoints in most commercial virtualisation software, rolling back to the earliest stage is a simple process that avoids having to re-deploy the virtual machine (VM). Get your VM's prepared as far as having reached this point in @myexploit2600's blog.

Building and Attacking an Active Directory lab with PowerShell by @myexploit2600
Building and Attacking an Active Directory lab with PowerShell by @myexploit2600

Once you've established this baseline installation, you can quickly roll back to here should you need or want to. In my next blog, I might jump into using Terraform or other IaC to provision the virtual machines and install the operating systems too. But let's not get ahead of ourselves. Narrator's voice: "He did in fact write such a blog."

Using Vagrant to achieve FULL AUTOMATION of @myexploit2600′s Hacklab
Power up your automated hacklab deployment using Vagrant to attain Star Trek-like replicator powers.

Firstly, we'll repeat the sanity check of confirming our PowerShell version before trying to execute anything. Here's a little statement taking advantage of PowerShell's comparison functions:

if ($PSVersionTable.PSVersion.Major -ge "5") {
	Write-Host "PowerShell $($PSVersionTable.PSVersion) is installed. Let's go!"
} else {
	Write-Host "PowerShell 5 or newer is not installed!"
    Write-Host "Installed version is $($PSVersionTable.PSVersion). You will (for now) be stuck setting up the domain using the manual CMD/Dcpromo approach for now. Check back later for the possibility of a playbook for this method."
}
PowerShell function to determine whether the installed version of PowerShell is 5 or newer
PowerShell Version String - 5.1.17763.592
We've got PowerShell 5.1 so we're good to go. Yes, I'm running things as Administrator - do I look like I work in security?

Preparing Ansible and inventory

You may already be aware that Ansible cannot be run on a Windows host but it can manage them. If you have Windows Subsystem for Linux (WSL), you're good to go, otherwise you will need a Linux host of some description which can reach the hacklab hosts. As mentioned before, this is not a technical deep dive or a guide to Ansible, so I'll point to the relevant documentation.

Managing Windows targets with Ansible requires the WinRM service be enabled. It is possible to use Windows OpenSSH but this remains experimental. Since we're setting up a hacklab, I'm going to take a few liberties when it comes to "best practice". We're going to use the script provided by the Ansible team to set up WinRM for all the Windows hosts in our lab. The script can be found here and can be executed with wanton disregard for safety by using the following snippet:

$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)

powershell.exe -ExecutionPolicy ByPass -File $file
PowerShell statements to enable WinRM HTTP and HTTPS listeners

This will expose an HTTP listener on port 5985 and a matching HTTPS listener on port 5986 using a self-signed certificate. It also enables Basic auth although we are going to use NTLM authentication, rather than just plaintext credentials. You can set up listeners in whichever way you feel comfortable with. Ansible's WinRM module supports all of the common Windows authentication methods so if you're concerned with establishing real confidentiality, the options are there. I'm setting this up in an isolated network segment, since it's going to involve configuring an intentionally vulnerable domain, therefore a self-signed certificate is no big deal.

Ansible WinRM Module Authentication Options

Once the listeners are established on your hacklab hosts, we can start work on our inventory. This part is straight forward. The inventory can be declared globally using /etc/ansible/hosts, or a contextual inventory file can be defined when executing a command or playbook using -i /file/path. I prefer to use the YAML format over the older INI format for inventory but both are valid and supported by default.

all:
  hosts:
    server1.hacklab.local:
      ansible_user: Administrator
      ansible_password: Passw0rd!
    labpc1:
      ansible_user: IEUser
      ansible_password: Passw0rd!
      ansible_host: 192.168.1.201
  vars:
    ansible_connection: winrm
    ansible_winrm_transport: ntlm
    ansible_port: 5986
    ansible_winrm_server_cert_validation: ignore
Your inventory file should vaguely resemble this example

I've intentionally used an FQDN for the server and specified an IP for the workstation here to illustrate how host declarations can work. If your Ansible host cannot resolve the FQDN (no DNS, not in hosts file, etc.), you would need to specify the ansible_host variable as exemplified for labpc1. I've used the global variable declaration because the authentication method for both is the same, but have declared per-host variables for usernames and passwords. These are going to be the local credentials for the relevant hosts since there are no domain credentials yet. If you're using a different authentication method, you will need to swap out the variables as appropriate.

Next step is installing the necessary Ansible plugins to support interaction with Windows. community.windows will automatically install the ansible.windows plugins but we'll stick it in the command for good measure.

$ ansible-galaxy collection install community.windows ansible.windows

If you'd like to test that the inventory file is sound and that WinRM has been configured correctly, you can now fire off a quick ad-hoc command using win_ping. The inventory flag and target can be omitted if you're using the global inventory.

$ ansible -i my_inventory all -m win_ping

All being well, you should get console output that resembles mine.

Ansible win_ping response from inventory targets
Ansible win_ping response from inventory targets

There are a wide range of errors you could run into at this stage and it goes beyond the scope of this post to troubleshoot them all. Fear not! I am on Twitter and I am more than happy to help fix it. Fire me a mention and we'll work it out.

Assuming you have successful ping responses, we can now execute our playbook to configure the domain and then we're almost finished. Ansible, much like Python, is self-documenting code, which explains what it's doing via the syntax. So in the interests of brevity, I'm going to share the full playbook right here with some light commenting and then explain the few quirky parts.

---
- hosts: server
  gather_facts: true

  tasks:
  - name: Disable IPv6 because we're not barbarians
    win_shell: |
      if ((Get-NetAdapterBinding -Name "Ethernet" -ComponentID ms_tcpip6 | Select-Object -ExpandProperty Enabled) -match "True") {
        Disable-NetAdapterBinding -Name "*" -ComponentID ms_tcpip6
      }

  - name: Install domain services
    win_feature:
      name: AD-Domain-Services
      state: present
      include_management_tools: yes # This gives us Remote Server Administration Tools
      include_sub_features: yes
    register: install_ad_domain_services
  
  - name: Reboot if domain services installation requires it
    win_reboot:
    when: install_ad_domain_services.reboot_required
  
  - name: Add PowerShell module for AD commands
    win_psmodule:
      name: ADDSDeployment
      state: present

  - name: Create hacklab domain if it doesn't exist
    win_domain:
      create_dns_delegation: no
      database_path: C:\Windows\NTDS
      dns_domain_name: hacklab.local
      domain_netbios_name: hacklab
      forest_mode: Win2012R2
      install_dns: yes
      log_path: C:\Windows\NTDS
      safe_mode_password: Passw0rd!
      sysvol_path: C:\Windows\SYSVOL
    register: domain_creation
  
  - name: Reboot if domain creation requires it
    win_reboot:
    when: domain_creation.reboot_required
  
  - name: Install DNS feature if absent after domain setup
    win_feature:
      name: DNS
      state: present
      include_management_tools: yes
      include_sub_features: yes
    register: install_dns

  - name: Reboot if DNS installation requires it
    win_reboot:
    when: install_dns.reboot_required

  - name: Change hostname to server1
    win_hostname:
      name: server1
    register: dc_hostname_change

  - name: Reboot if hostname change requires it
    win_reboot:
    when: dc_hostname_change.reboot_required

  - name: Add victim user to the domain and Domain Admins group
    win_domain_user:
      name: user1
      state: present
      password: Passw0rd!
      update_password: on_create
      groups:
      - Domain Admins

  - name: Create SPN
    win_shell: Set-ADUser -Identity user1 -ServicePrincipalNames @{Add='http/server1.hacklab.local:80'}
  
  - name: Add attacker user to the domain
    win_domain_user:
      name: user2
      state: present
      password: Passw0rd!
      update_password: on_create
  
  - name: Point server DNS to itself
    win_dns_client:
      # This needs to be the name of the server's primary network adapter.
      adapter_names: Ethernet
      # We do both of the following directives because some Ansible versions require ipv4_addresses.
      # And some require dns_servers. I didn't dig through the changelogs to find out which.
      # They play fine together but if someone wants to do the conditional logic, I'll chuck it in here.
      ipv4_addresses: 192.168.1.200
      dns_servers: 192.168.1.200

- hosts: workstation
  gather_facts: true

  tasks:
  - name: IPv6 is a tool of the bourgeoisie and must be eradicated
    win_shell: |
      if ((Get-NetAdapterBinding -Name "Ethernet" -ComponentID ms_tcpip6 | Select-Object -ExpandProperty Enabled) -match "True") {
        Disable-NetAdapterBinding -Name "*" -ComponentID ms_tcpip6
      }

  - name: Change hostname to labpc1
    win_hostname:
      name: labpc1
    register: ws_hostname_change

  - name: Reboot if hostname change requires it
    win_reboot:
    when: ws_hostname_change.reboot_required

  - name: Point workstation DNS to DC
    win_dns_client:
      # This needs to be the name of the workstation's network adapter which shares a logical network with the DC.
      adapter_names: Ethernet
      # We do both of the following directives because some Ansible versions require ipv4_addresses.
      # And some require dns_servers. I didn't dig through the changelogs to find out which.
      # They play fine together but if someone wants to do the conditional logic, I'll chuck it in here.
      ipv4_addresses: 192.168.1.200
      dns_servers: 192.168.1.200

  - name: Add workstation to domain
    win_domain_membership:
      dns_domain_name: hacklab.local
      # hostname: labpc1 # This is ~~fucked~~ bugged. Doing it manually above instead before we add to domain.
      domain_admin_user: user2@hacklab.local
      domain_admin_password: Passw0rd!
      state: domain
    register: add_ws_to_domain

  - name: Reboot if adding to domain requires it
    win_reboot:
    when: add_ws_to_domain.reboot_required
playbook.yml

If you want to jump ahead to executing this with reckless abandon, then you can do so with ansible-playbook playbook.yml -i inventory.

As mentioned, there are a few quirky bits in the playbook to look at more closely. Firstly, we're calling the domain hacklab.local instead of server1.hacklab.local. The reason for this is to allow the domain name to more accurately reflect a real-world domain while we use server1 as the hostname for the Windows Server/Domain Controller. Simple enough really but if you want to revert this change, then switch the values on line 33 and 34 with server1.hacklab.local and server1 respectively, and then the value on line 60 with some other name of your choosing like dc01 or keith.

Secondly, we jump down to line 76 where we're breaking out into a raw PowerShell command to add our service principal name (SPN). I was unable to find an Ansible module that specifically supported adding an SPN so this is the dirty solution. Since it's not taking in any user-supplied arguments, it's safe enough, but it's just a bit untidy.

Lastly, there is some jiggery pokery on lines 123 and 124 where we point our workstation to the newly configured domain controller. Varying versions of Ansible support one or both of the directives used here. In the interests of pOrTaBiLiTy, we're using both because they do not conflict and Ansible will pass the task correctly even with both present.

All the other tasks, I believe, are self-explanatory and the name key-value on each one gives a plain idea of what they're doing. You can search for ansible $insert_unknown_phrase_here to find extensive documentation on most of it.

As mentioned, the execution of the playbook is achieved by performing the following command, omitting the -i inventory if you're using the global inventory in /etc/ansible/hosts.

$ ansible-playbook playbook.yml -i inventory

This will begin the execution of the playbook in sequence, configuring the server as the domain controller and provisioning the hacklab domain. Then it will create the victim and attacker users before attaching the workstation to the domain, ready to be used to carry out your kerberoasting attacks and more.

"You're using passwords in plaintext you monster"

I am indeed because I live fast and have designs to die young. Ansible has a really neat secrets management tool called Ansible Vault which you can read about and implement pretty quickly yourself. This is left as an exercise for the reader... *ahem*. Alternatively, you can employ an .env file, environment variables, or an Ansible vars file to separate the egregious use of plaintext passwords in the playbook. As previously alluded to, I'm running this in a Fort-Knox-slash-Fritzl-dungeon-style network segment where I'm not concerned about APT-01 robbing my noodz. If you have the desire for most robust practices, Ansible is very much designed to enable that.

The beauty of the playbook is that you can replay it against the same hosts repeatedly without the fear of duplicating configurations, users, or other objects. Ansible is idempotent and will not try to repeat tasks unless explicitly told to do so. This is useful in the event that you have reduced resources available and the installation of features takes longer than the timeout. Indeed, if the playbook fails due to a timeout, you can run it again with pretty high confidence that it will carry on right after the failed step.

Once the playbook is finished, you can jump on to your workstation, log in as hacklab.local\user2 and get roastin'.

Quick demonstration of the last step of @myexplot2600's blog.
Pro strats for y'all. Quick demonstration of the last step of @myexplot2600's blog.

Automating virtual machine deployment with Vagrant

If you'd like to take things further, take a look at my follow up post showing how we can leverage Vagrant to go full send on automation.

Using Vagrant to achieve FULL AUTOMATION of @myexploit2600′s Hacklab
Power up your automated hacklab deployment using Vagrant to attain Star Trek-like replicator powers.

Troubleshooting, problem solving, bug squishing, addendums, and errata

If you're having any kind of issues with the playbook or Ansible set-up, give me a shout on Twitter @0x616e6874. I'm happy to give you a hand and, if you run into a problem that helps improve this post, I'll give you a hat-tip here! If you get on absolutely problem free, I'd love to hear about that too.

And a huge shout out, of course, to @myexploit2600 for writing a brilliant set-up guide for the AD hacklab and for continuing to share his expertise so freely.

Masthead image credit: Sid Sower, Edison's rubber labratory