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.
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."
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:
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:
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.
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.
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.
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.
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 respectively, and then the value on line 60 with some other name of your choosing like
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
$ 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'.
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.
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