├── .gitignore ├── Ansible ├── ansible.cfg ├── cloudlabs.yml ├── files │ ├── DesktopWallpaper.png │ └── bginfo.bgi ├── group_vars │ ├── linux.tmpl │ └── windows.tmpl ├── inventory.yml └── roles │ ├── common │ └── tasks │ │ └── main.yml │ ├── dc │ └── tasks │ │ └── main.yml │ ├── domain │ └── tasks │ │ └── main.yml │ ├── win10 │ └── tasks │ │ └── main.yml │ └── winserv2019 │ ├── files │ └── webserver │ │ ├── default.aspx │ │ ├── default.aspx.cs │ │ └── web.config │ └── tasks │ └── main.yml ├── LICENSE ├── README.md ├── Terraform ├── 01-init.tf ├── 02-network.tf ├── 03-dc.tf ├── 04-winserv2019.tf ├── 05-windows10.tf ├── 07-hackbox.tf ├── 08-ansible.tf ├── files │ ├── ConfigureRemotingForAnsible.ps1 │ └── FirstLogonCommands.xml ├── outputs.tf ├── terraform.tfvars.example └── variables.tf └── assets └── labs.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | -------------------------------------------------------------------------------- /Ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = inventory.yml 3 | host_key_checking = False -------------------------------------------------------------------------------- /Ansible/cloudlabs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Configure the DC to prepare the domain 3 | - hosts: all 4 | strategy: free 5 | tasks: 6 | - include_role: 7 | name: domain 8 | when: "'dc' in group_names" 9 | 10 | # Run common tasks on all Windows machines in parallel 11 | - hosts: windows 12 | roles: 13 | - common 14 | 15 | # Domain-join clients and run post-domain configuration in parallel 16 | - hosts: windows 17 | strategy: free 18 | tasks: 19 | - include_role: 20 | name: winserv2019 21 | when: "'winserv2019' in group_names" 22 | - include_role: 23 | name: win10 24 | when: "'win10' in group_names" 25 | - include_role: 26 | name: dc 27 | when: "'dc' in group_names" -------------------------------------------------------------------------------- /Ansible/files/DesktopWallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehackerish/ADLab/1cb674565fbe1a08c4f4c2b8e8a8f2c0907b6a0b/Ansible/files/DesktopWallpaper.png -------------------------------------------------------------------------------- /Ansible/files/bginfo.bgi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehackerish/ADLab/1cb674565fbe1a08c4f4c2b8e8a8f2c0907b6a0b/Ansible/files/bginfo.bgi -------------------------------------------------------------------------------- /Ansible/group_vars/linux.tmpl: -------------------------------------------------------------------------------- 1 | 2 | ansible_user: ${username} 3 | ansible_password: ${password} 4 | ansible_become: true 5 | ansible_connection: ssh 6 | ansible_winrm_server_cert_validation: ignore -------------------------------------------------------------------------------- /Ansible/group_vars/windows.tmpl: -------------------------------------------------------------------------------- 1 | 2 | ansible_user: ${username} 3 | ansible_password: ${password} 4 | ansible_connection: winrm 5 | ansible_winrm_server_cert_validation: ignore 6 | domain_name: ${domain_name} 7 | reverse_dns_zone: "10.13.37.0/24" -------------------------------------------------------------------------------- /Ansible/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | windows: 3 | children: 4 | dc: 5 | hosts: 6 | 10.13.37.10: 7 | winserv2019: 8 | hosts: 9 | 10.13.37.100: 10 | win10: 11 | hosts: 12 | 10.13.37.150: -------------------------------------------------------------------------------- /Ansible/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Copy desktop wallpaper 4 | win_copy: 5 | src: DesktopWallpaper.png 6 | dest: C:\Users\Public\Pictures\DesktopWallpaper.png 7 | 8 | - name: Copy bginfo configuration 9 | win_copy: 10 | src: bginfo.bgi 11 | dest: C:\Users\Public\bginfo.bgi 12 | 13 | # Note: Chocolatey packages should be minimized to avoid hitting the rate limit (only 1 outbound IP is used) 14 | - name: Install utilities 15 | ignore_errors: yes 16 | win_chocolatey: 17 | name: 18 | - GoogleChrome 19 | - bginfo 20 | state: present 21 | ignore_checksums: true 22 | 23 | - name: Configure bginfo as autorun 24 | win_regedit: 25 | path: 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' 26 | name: bginfo 27 | data: 'C:\ProgramData\chocolatey\bin\bginfo.exe c:\users\public\bginfo.bgi /silent /timer:0 /nolicprompt' 28 | 29 | - name: Ensure 'Search' unpinned from the taskbar 30 | ansible.windows.win_regedit: 31 | path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Search 32 | name: SearchboxTaskbarMode 33 | data: 0 34 | type: dword 35 | 36 | - name: Ensure 'News and Interests' unpinned from the taskbar 37 | ansible.windows.win_regedit: 38 | path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Feeds 39 | name: EnableFeeds 40 | data: 0 41 | type: dword 42 | state: present 43 | 44 | - name: Ensure 'People' unpinned from the taskbar 45 | ansible.windows.win_regedit: 46 | path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\People 47 | name: PeopleBand 48 | data: 0 49 | type: dword 50 | 51 | - name: Ensure 'Edge', 'Store' other built-in shortcuts unpinned from the taskbar 52 | ansible.windows.win_regedit: 53 | path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband 54 | name: Favorites 55 | state: absent 56 | 57 | - name: Hide Task View, Chat and Cortana, always show file extensions 58 | ansible.windows.win_regedit: 59 | path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced 60 | name: "{{ item }}" 61 | data: 0 62 | type: dword 63 | loop: 64 | - ShowCortanaButton 65 | - ShowTaskViewButton 66 | - TaskbarDa 67 | - TaskbarMn 68 | - HideFileExt -------------------------------------------------------------------------------- /Ansible/roles/dc/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Install ADCS 4 | - name: Install ADCS 5 | win_feature: 6 | name: AD-Certificate 7 | state: present 8 | include_sub_features: yes 9 | include_management_tools: yes 10 | register: win_feature 11 | 12 | - name: Reboot after ADCS installation 13 | win_reboot: 14 | when: win_feature.reboot_required 15 | 16 | # Populate AD and add vulnerabilities using BadBlood 17 | - name: Download BadBlood 18 | win_get_url: 19 | url: https://github.com/chvancooten/BadBlood/archive/refs/heads/master.zip 20 | dest: C:\Users\Public\Downloads\BadBlood-master.zip 21 | 22 | - name: Extract BadBlood archive 23 | win_unzip: 24 | src: C:\Users\Public\Downloads\BadBlood-master.zip 25 | dest: C:\Users\Public\Downloads 26 | creates: C:\Users\Public\Downloads\BadBlood-master 27 | delete_archive: yes 28 | 29 | - name: Run BadBlood to populate AD insecurely 30 | ansible.windows.win_powershell: 31 | script: C:\Users\Public\Downloads\BadBlood-master\Invoke-BadBlood.ps1 32 | parameters: 33 | NonInteractive: true 34 | SkipLapsInstall: true 35 | creates: C:\Windows\AD.populated 36 | 37 | - name: Touch file to track AD state 38 | win_file: 39 | path: C:\Windows\AD.populated 40 | state: touch 41 | 42 | - name: Clean up BadBlood files 43 | win_file: 44 | path: C:\Users\Public\Downloads\BadBlood-master 45 | state: absent -------------------------------------------------------------------------------- /Ansible/roles/domain/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Install AD 4 | - name: Create new AD domain 5 | win_domain: 6 | dns_domain_name: "{{ domain_name }}" 7 | safe_mode_password: "{{ ansible_password }}" 8 | register: domain_install 9 | 10 | - name: Reboot after AD installation 11 | win_reboot: 12 | when: domain_install.reboot_required -------------------------------------------------------------------------------- /Ansible/roles/win10/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Join domain 4 | - name: Configure DNS settings 5 | win_dns_client: 6 | adapter_names: Ethernet 7 | ipv4_addresses: 8 | - 10.13.37.10 9 | - 8.8.8.8 10 | 11 | - name: Join machine to domain 12 | win_domain_membership: 13 | dns_domain_name: "{{ domain_name }}" 14 | domain_admin_user: "{{ ansible_user }}@{{ domain_name }}" 15 | domain_admin_password: "{{ ansible_password }}" 16 | state: domain 17 | register: domain_state 18 | 19 | - name: Reboot after joining domain 20 | win_reboot: 21 | when: domain_state.reboot_required -------------------------------------------------------------------------------- /Ansible/roles/winserv2019/files/webserver/default.aspx: -------------------------------------------------------------------------------- 1 | <%@ Page Language="C#" AutoEventWireup="true" CodeFile="default.aspx.cs" Inherits="CS" %> 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Ansible/roles/winserv2019/files/webserver/default.aspx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.UI; 6 | using System.Web.UI.WebControls; 7 | 8 | using System.IO; 9 | 10 | public partial class CS : System.Web.UI.Page 11 | { 12 | protected void UploadFile(object sender, EventArgs e) 13 | { 14 | string folderPath = Server.MapPath("~/Files/"); 15 | 16 | //Check whether Directory (Folder) exists. 17 | if (!Directory.Exists(folderPath)) 18 | { 19 | //If Directory (Folder) does not exists. Create it. 20 | Directory.CreateDirectory(folderPath); 21 | } 22 | 23 | //Save the File to the Directory (Folder). 24 | FileUpload1.SaveAs(folderPath + Path.GetFileName(FileUpload1.FileName)); 25 | 26 | //Display the success message. 27 | lblMessage.Text = Path.GetFileName(FileUpload1.FileName) + " has been uploaded."; 28 | } 29 | } -------------------------------------------------------------------------------- /Ansible/roles/winserv2019/files/webserver/web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Ansible/roles/winserv2019/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Join domain 4 | - name: Configure DNS settings 5 | win_dns_client: 6 | adapter_names: Ethernet 7 | ipv4_addresses: 8 | - 10.13.37.10 9 | - 8.8.8.8 10 | 11 | - name: Join machine to domain 12 | win_domain_membership: 13 | dns_domain_name: "{{ domain_name }}" 14 | domain_admin_user: "{{ ansible_user }}@{{ domain_name }}" 15 | domain_admin_password: "{{ ansible_password }}" 16 | state: domain 17 | register: domain_state 18 | 19 | - name: Reboot after joining domain 20 | win_reboot: 21 | when: domain_state.reboot_required 22 | 23 | # Install vulnerable IIS web server 24 | - name: Install IIS with management tools 25 | win_feature: 26 | name: Web-Server 27 | state: present 28 | include_sub_features: yes 29 | include_management_tools: yes 30 | register: win_feature 31 | 32 | - name: Copy webserver files 33 | win_copy: 34 | src: webserver/ 35 | dest: C:\inetpub\wwwroot 36 | 37 | - name: Remove default webserver files 38 | win_file: 39 | path: "{{ item }}" 40 | state: absent 41 | loop: 42 | - C:\inetpub\wwwroot\iisstart.htm 43 | - C:\inetpub\wwwroot\iisstart.png 44 | 45 | - name: Give IIS_IUSRS control of webserver files 46 | win_acl: 47 | path: C:\inetpub\wwwroot 48 | user: IIS_IUSRS 49 | rights: FullControl 50 | type: allow 51 | state: present 52 | inherit: ContainerInherit, ObjectInherit 53 | propagation: 'None' 54 | 55 | - name: Reboot if installing Web-Server feature requires it 56 | win_reboot: 57 | when: win_feature.reboot_required -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ADLab 2 | _By [@thehackerish](https://twitter.com/thehackerish)_ 3 | Video tutorial on https://youtu.be/H76WtmP2Je4 4 | 5 | _Based heavily on the work of [@chvancooten](https://twitter.com/chvancooten), Ansible role for Elastic Security deployment by [@nodauf](https://twitter.com/nodauf)_ 6 | 7 | Provisioning scripts for an Active Directory lab environment. Designed to be deployed to Azure. 8 | 9 | ## Setup 10 | 11 | The lab is provisioned automatically using Terraform and Ansible. First, Terraform deploys all the infrastructure and prepares the machines for provisioning. It then kicks off a role-based Ansible playbook from the Debian attacker machine to provision the Windows-based machines. The full process takes about 15 to 20 minutes to complete. 12 | 13 | > 💸 **Note:** The machine sizes are moderately small by default ('Standard_B1ms'). For better performence, use Standard_B2ms or Standard_B4ms. With the latter, the bill will be approx. €10 per day of active use, your mileage may vary. Change the appropriate 'size' settings in `terraform.tfvars` to change machine sizes. 14 | 15 | ### Deployment 16 | 17 | - Clone the repo to your Azure cloud shell. It conveniently has all you need (Terraform and an authenticated Azure provider). Alternatively, install and configure Terraform and the Azure provider yourself. Ansible is installed automatically as part of the provisioning process. 18 | - Copy `terraform.tfvars.example` to `terraform.tfvars` in the `Terraform` directory, and configure the variables appropriately. 19 | - In the same directory, run `terraform init`. 20 | - When you're ready to deploy, run `terraform apply` (or `terraform apply --auto-approve` to skip the approval check). 21 | 22 | Once deployment and provisioning have finished, the output variables (public IP / DNS name, administrative passwords, machine names, etc.) will be displayed. You are now ready to connect to the labs! 23 | 24 | ### Removal 25 | 26 | - When you're done with the labs, run `terraform destroy` to tear down the environment. 27 | 28 | ## Labs 29 | 30 | ![Lab overview](assets/labs.png) 31 | 32 | The labs consist of a selection of machines: 33 | 34 | - Windows Server 2016 DC 35 | - Active Directory Certificate Services (ADCS) installed 36 | - Windows Server 2019 37 | - Internet Information Services (IIS) web server with simple vulnerable app 38 | - Windows 10 client 39 | - Debian attacker box 40 | 41 | One public IP is exposed for the whole lab. The IP ranges defined in the `ip-whitelist` are allowed to access the following ports on this IP address, which are bound to the following services using a load balancer: 42 | 43 | - Port 22 -> Attacker machine SSH 44 | - Port 80 -> Windows Server 2019 IIS web server with vulnerable page 45 | - Port 3389 -> Windows 10 Client RDP 46 | 47 | Another public IP is used for outbound Internet connectivity for all lab machines. 48 | -------------------------------------------------------------------------------- /Terraform/01-init.tf: -------------------------------------------------------------------------------- 1 | # Terraform initialization 2 | terraform { 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 2.98.0" 7 | } 8 | } 9 | 10 | required_version = ">= 1.1.5" 11 | } 12 | 13 | provider "azurerm" { 14 | skip_provider_registration = true 15 | features {} 16 | } 17 | 18 | # Specify where the custom data file is for WinRM initialization 19 | locals { 20 | custom_data_content = base64encode(file("${path.module}/files/ConfigureRemotingForAnsible.ps1")) 21 | } 22 | 23 | # Generate random password for windows local admins 24 | resource "random_string" "windowspass" { 25 | length = 16 26 | special = false 27 | } 28 | 29 | # Generate random password for windows local admins 30 | resource "random_string" "linuxpass" { 31 | length = 16 32 | special = false 33 | } 34 | 35 | 36 | # Get a reference to the existing resource group 37 | data "azurerm_resource_group" "thehackerish-rg" { 38 | name = var.resource-group 39 | } -------------------------------------------------------------------------------- /Terraform/02-network.tf: -------------------------------------------------------------------------------- 1 | # Get the current public IP for deployment whitelisting 2 | data "http" "public-ip" { 3 | url = "http://ipv4.icanhazip.com" 4 | } 5 | 6 | # Create a virtual network within the resource group 7 | resource "azurerm_virtual_network" "thehackerish-vnet" { 8 | name = "thehackerish-vnet" 9 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 10 | location = data.azurerm_resource_group.thehackerish-rg.location 11 | address_space = ["10.0.0.0/8"] 12 | } 13 | 14 | # Create a subnet within the virtual network 15 | resource "azurerm_subnet" "thehackerish-subnet" { 16 | name = "thehackerish-subnet" 17 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 18 | virtual_network_name = azurerm_virtual_network.thehackerish-vnet.name 19 | address_prefixes = ["10.13.37.0/24"] 20 | } 21 | 22 | # Create a network security group for the subnet 23 | resource "azurerm_network_security_group" "thehackerish-nsg" { 24 | name = "thehackerish-nsg" 25 | location = data.azurerm_resource_group.thehackerish-rg.location 26 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 27 | 28 | security_rule { 29 | name = "SSH" 30 | priority = 1001 31 | direction = "Inbound" 32 | access = "Allow" 33 | protocol = "Tcp" 34 | source_port_range = "*" 35 | destination_port_range = "22" 36 | source_address_prefixes = "${distinct(concat(var.ip-whitelist, [chomp(data.http.public-ip.body)]))}" 37 | destination_address_prefix = "*" 38 | } 39 | 40 | security_rule { 41 | name = "HTTP" 42 | priority = 1002 43 | direction = "Inbound" 44 | access = "Allow" 45 | protocol = "Tcp" 46 | source_port_range = "*" 47 | destination_port_range = "80" 48 | source_address_prefixes = var.ip-whitelist 49 | destination_address_prefix = "*" 50 | } 51 | 52 | security_rule { 53 | name = "RDP" 54 | priority = 1003 55 | direction = "Inbound" 56 | access = "Allow" 57 | protocol = "Tcp" 58 | source_port_range = "*" 59 | destination_port_range = "3389" 60 | source_address_prefixes = var.ip-whitelist 61 | destination_address_prefix = "*" 62 | } 63 | 64 | security_rule { 65 | name = "Internal" 66 | priority = 1004 67 | direction = "Inbound" 68 | access = "Allow" 69 | protocol = "Tcp" 70 | source_port_range = "*" 71 | destination_port_range = "*" 72 | source_address_prefix = "10.13.37.0/24" 73 | destination_address_prefix = "*" 74 | } 75 | } 76 | 77 | resource "azurerm_subnet_network_security_group_association" "thehackerish-nsga" { 78 | subnet_id = azurerm_subnet.thehackerish-subnet.id 79 | network_security_group_id = azurerm_network_security_group.thehackerish-nsg.id 80 | } 81 | 82 | # Create a public IP address for the lab 83 | resource "azurerm_public_ip" "thehackerish-ip" { 84 | name = "thehackerish-ip" 85 | location = data.azurerm_resource_group.thehackerish-rg.location 86 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 87 | allocation_method = "Static" 88 | domain_name_label = var.domain-name-label 89 | sku = "Standard" 90 | } 91 | 92 | # Create another public IP address for outbound traffic 93 | resource "azurerm_public_ip" "thehackerish-ip-outbound" { 94 | name = "thehackerish-ip-outbound" 95 | location = data.azurerm_resource_group.thehackerish-rg.location 96 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 97 | allocation_method = "Static" 98 | sku = "Standard" 99 | } 100 | 101 | # Create a load balancer on the public IP 102 | resource "azurerm_lb" "thehackerish-lb" { 103 | name = "thehackerish-lb" 104 | location = data.azurerm_resource_group.thehackerish-rg.location 105 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 106 | sku = "Standard" 107 | 108 | frontend_ip_configuration { 109 | name = "thehackerish-lb-ip-public" 110 | public_ip_address_id = azurerm_public_ip.thehackerish-ip.id 111 | } 112 | } 113 | 114 | resource "azurerm_lb_nat_rule" "thehackerish-lb-nat-http" { 115 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 116 | loadbalancer_id = azurerm_lb.thehackerish-lb.id 117 | name = "HTTPAccess" 118 | protocol = "Tcp" 119 | frontend_port = 80 120 | backend_port = 80 121 | frontend_ip_configuration_name = "thehackerish-lb-ip-public" 122 | } 123 | 124 | resource "azurerm_lb_nat_rule" "thehackerish-lb-nat-ssh" { 125 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 126 | loadbalancer_id = azurerm_lb.thehackerish-lb.id 127 | name = "SSHAccess" 128 | protocol = "Tcp" 129 | frontend_port = 22 130 | backend_port = 22 131 | frontend_ip_configuration_name = "thehackerish-lb-ip-public" 132 | } 133 | 134 | resource "azurerm_lb_nat_rule" "thehackerish-lb-nat-rdp" { 135 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 136 | loadbalancer_id = azurerm_lb.thehackerish-lb.id 137 | name = "RDPAccess" 138 | protocol = "Tcp" 139 | frontend_port = 3389 140 | backend_port = 3389 141 | frontend_ip_configuration_name = "thehackerish-lb-ip-public" 142 | } 143 | 144 | # Create NAT gateway for outbound internet access 145 | resource "azurerm_nat_gateway" "thehackerish-nat-gateway" { 146 | name = "thehackerish-nat-gateway" 147 | location = data.azurerm_resource_group.thehackerish-rg.location 148 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 149 | } 150 | 151 | resource "azurerm_nat_gateway_public_ip_association" "thehackerish-nat-gateway-ip" { 152 | nat_gateway_id = azurerm_nat_gateway.thehackerish-nat-gateway.id 153 | public_ip_address_id = azurerm_public_ip.thehackerish-ip-outbound.id 154 | } 155 | 156 | resource "azurerm_subnet_nat_gateway_association" "thehackerish-nat-gateway-subnet" { 157 | subnet_id = azurerm_subnet.thehackerish-subnet.id 158 | nat_gateway_id = azurerm_nat_gateway.thehackerish-nat-gateway.id 159 | } 160 | -------------------------------------------------------------------------------- /Terraform/03-dc.tf: -------------------------------------------------------------------------------- 1 | # Network Interface 2 | resource "azurerm_network_interface" "thehackerish-vm-dc-nic" { 3 | name = "thehackerish-vm-dc-nic" 4 | location = data.azurerm_resource_group.thehackerish-rg.location 5 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 6 | 7 | ip_configuration { 8 | name = "thehackerish-vm-dc-config" 9 | subnet_id = azurerm_subnet.thehackerish-subnet.id 10 | private_ip_address_allocation = "Static" 11 | private_ip_address = "10.13.37.10" 12 | } 13 | } 14 | 15 | # Virtual Machine 16 | resource "azurerm_windows_virtual_machine" "thehackerish-vm-dc" { 17 | name = "thehackerish-vm-dc" 18 | computer_name = var.dc-hostname 19 | size = var.dc-size 20 | provision_vm_agent = true 21 | enable_automatic_updates = true 22 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 23 | location = data.azurerm_resource_group.thehackerish-rg.location 24 | timezone = var.timezone 25 | admin_username = var.windows-user 26 | admin_password = random_string.windowspass.result 27 | custom_data = local.custom_data_content 28 | network_interface_ids = [ 29 | azurerm_network_interface.thehackerish-vm-dc-nic.id, 30 | ] 31 | 32 | os_disk { 33 | name = "thehackerish-vm-dc-osdisk" 34 | caching = "ReadWrite" 35 | storage_account_type = "StandardSSD_LRS" 36 | } 37 | 38 | source_image_reference { 39 | publisher = "MicrosoftWindowsServer" 40 | offer = "WindowsServer" 41 | sku = "2016-Datacenter" 42 | version = "latest" 43 | } 44 | 45 | additional_unattend_content { 46 | setting = "AutoLogon" 47 | content = "${random_string.windowspass.result}true1${var.windows-user}" 48 | } 49 | 50 | additional_unattend_content { 51 | setting = "FirstLogonCommands" 52 | content = "${file("${path.module}/files/FirstLogonCommands.xml")}" 53 | } 54 | 55 | tags = { 56 | DoNotAutoShutDown = "yes" 57 | } 58 | } -------------------------------------------------------------------------------- /Terraform/04-winserv2019.tf: -------------------------------------------------------------------------------- 1 | # Network Interface 2 | resource "azurerm_network_interface" "thehackerish-vm-winserv2019-nic" { 3 | name = "thehackerish-vm-winserv2019-nic" 4 | location = data.azurerm_resource_group.thehackerish-rg.location 5 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 6 | 7 | ip_configuration { 8 | name = "thehackerish-vm-winserv2019-config" 9 | subnet_id = azurerm_subnet.thehackerish-subnet.id 10 | private_ip_address_allocation = "Static" 11 | private_ip_address = "10.13.37.100" 12 | } 13 | } 14 | 15 | resource "azurerm_network_interface_nat_rule_association" "thehackerish-vm-winserv2019-nic-nat" { 16 | network_interface_id = azurerm_network_interface.thehackerish-vm-winserv2019-nic.id 17 | ip_configuration_name = "thehackerish-vm-winserv2019-config" 18 | nat_rule_id = azurerm_lb_nat_rule.thehackerish-lb-nat-http.id 19 | } 20 | 21 | # Virtual Machine 22 | resource "azurerm_windows_virtual_machine" "thehackerish-vm-winserv2019" { 23 | name = "thehackerish-vm-winserv2019" 24 | computer_name = var.winserv2019-hostname 25 | size = var.winserv2019-size 26 | provision_vm_agent = true 27 | enable_automatic_updates = true 28 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 29 | location = data.azurerm_resource_group.thehackerish-rg.location 30 | timezone = var.timezone 31 | admin_username = var.windows-user 32 | admin_password = random_string.windowspass.result 33 | custom_data = local.custom_data_content 34 | network_interface_ids = [ 35 | azurerm_network_interface.thehackerish-vm-winserv2019-nic.id, 36 | ] 37 | 38 | os_disk { 39 | name = "thehackerish-vm-winserv2019-osdisk" 40 | caching = "ReadWrite" 41 | storage_account_type = "StandardSSD_LRS" 42 | } 43 | 44 | source_image_reference { 45 | publisher = "MicrosoftWindowsServer" 46 | offer = "WindowsServer" 47 | sku = "2019-Datacenter" 48 | version = "latest" 49 | } 50 | 51 | additional_unattend_content { 52 | setting = "AutoLogon" 53 | content = "${random_string.windowspass.result}true1${var.windows-user}" 54 | } 55 | 56 | additional_unattend_content { 57 | setting = "FirstLogonCommands" 58 | content = "${file("${path.module}/files/FirstLogonCommands.xml")}" 59 | } 60 | 61 | tags = { 62 | DoNotAutoShutDown = "yes" 63 | } 64 | } -------------------------------------------------------------------------------- /Terraform/05-windows10.tf: -------------------------------------------------------------------------------- 1 | # Network Interface 2 | resource "azurerm_network_interface" "thehackerish-vm-windows10-nic" { 3 | name = "thehackerish-vm-windows10-nic" 4 | location = data.azurerm_resource_group.thehackerish-rg.location 5 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 6 | 7 | ip_configuration { 8 | name = "thehackerish-vm-windows10-config" 9 | subnet_id = azurerm_subnet.thehackerish-subnet.id 10 | private_ip_address_allocation = "Static" 11 | private_ip_address = "10.13.37.150" 12 | } 13 | } 14 | 15 | resource "azurerm_network_interface_nat_rule_association" "thehackerish-vm-windows10-nic-nat" { 16 | network_interface_id = azurerm_network_interface.thehackerish-vm-windows10-nic.id 17 | ip_configuration_name = "thehackerish-vm-windows10-config" 18 | nat_rule_id = azurerm_lb_nat_rule.thehackerish-lb-nat-rdp.id 19 | } 20 | 21 | # Virtual Machine 22 | resource "azurerm_windows_virtual_machine" "thehackerish-vm-windows10" { 23 | name = "thehackerish-vm-windows10" 24 | computer_name = var.win10-hostname 25 | size = var.win10-size 26 | provision_vm_agent = true 27 | enable_automatic_updates = true 28 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 29 | location = data.azurerm_resource_group.thehackerish-rg.location 30 | timezone = var.timezone 31 | admin_username = var.windows-user 32 | admin_password = random_string.windowspass.result 33 | custom_data = local.custom_data_content 34 | network_interface_ids = [ 35 | azurerm_network_interface.thehackerish-vm-windows10-nic.id, 36 | ] 37 | 38 | os_disk { 39 | name = "thehackerish-vm-windows10-osdisk" 40 | caching = "ReadWrite" 41 | storage_account_type = "StandardSSD_LRS" 42 | } 43 | 44 | source_image_reference { 45 | publisher = "MicrosoftWindowsDesktop" 46 | offer = "Windows-10" 47 | sku = "win10-21h2-ent-g2" 48 | version = "latest" 49 | } 50 | 51 | additional_unattend_content { 52 | setting = "AutoLogon" 53 | content = "${random_string.windowspass.result}true1${var.windows-user}" 54 | } 55 | 56 | additional_unattend_content { 57 | setting = "FirstLogonCommands" 58 | content = "${file("${path.module}/files/FirstLogonCommands.xml")}" 59 | } 60 | 61 | tags = { 62 | DoNotAutoShutDown = "yes" 63 | } 64 | } -------------------------------------------------------------------------------- /Terraform/07-hackbox.tf: -------------------------------------------------------------------------------- 1 | # Network Interface 2 | resource "azurerm_network_interface" "thehackerish-vm-hackbox-nic" { 3 | name = "thehackerish-vm-hackbox-nic" 4 | location = data.azurerm_resource_group.thehackerish-rg.location 5 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 6 | 7 | ip_configuration { 8 | name = "thehackerish-vm-hackbox-nic-config" 9 | subnet_id = azurerm_subnet.thehackerish-subnet.id 10 | private_ip_address_allocation = "Static" 11 | private_ip_address = "10.13.37.200" 12 | } 13 | } 14 | 15 | resource "azurerm_network_interface_nat_rule_association" "thehackerish-vm-hackbox-nic-nat" { 16 | network_interface_id = azurerm_network_interface.thehackerish-vm-hackbox-nic.id 17 | ip_configuration_name = "thehackerish-vm-hackbox-nic-config" 18 | nat_rule_id = azurerm_lb_nat_rule.thehackerish-lb-nat-ssh.id 19 | } 20 | 21 | # Virtual Machine 22 | resource "azurerm_linux_virtual_machine" "thehackerish-vm-hackbox" { 23 | name = "thehackerish-vm-hackbox" 24 | computer_name = var.hackbox-hostname 25 | resource_group_name = data.azurerm_resource_group.thehackerish-rg.name 26 | location = data.azurerm_resource_group.thehackerish-rg.location 27 | size = var.hackbox-size 28 | disable_password_authentication = false 29 | admin_username = var.linux-user 30 | admin_password = random_string.linuxpass.result 31 | network_interface_ids = [ 32 | azurerm_network_interface.thehackerish-vm-hackbox-nic.id, 33 | ] 34 | 35 | os_disk { 36 | name = "thehackerish-vm-hackbox-osdisk" 37 | caching = "ReadWrite" 38 | storage_account_type = "StandardSSD_LRS" 39 | } 40 | 41 | source_image_reference { 42 | publisher = "Debian" 43 | offer = "debian-11" 44 | sku = "11-gen2" 45 | version = "latest" 46 | } 47 | 48 | tags = { 49 | DoNotAutoShutDown = "yes" 50 | } 51 | } -------------------------------------------------------------------------------- /Terraform/08-ansible.tf: -------------------------------------------------------------------------------- 1 | # Prepare the Windows group variable template with the right username and password 2 | resource "local_file" "ansible-groupvars-windows-creation" { 3 | 4 | depends_on = [ 5 | var.windows-user, 6 | var.domain-dns-name, 7 | random_string.windowspass 8 | ] 9 | 10 | content = templatefile("${path.module}/../Ansible/group_vars/windows.tmpl", 11 | { 12 | username = var.windows-user 13 | password = random_string.windowspass.result 14 | domain_name = var.domain-dns-name 15 | }) 16 | 17 | filename = "../Ansible/group_vars/windows.yml" 18 | } 19 | 20 | # Prepare the Linux group variable template with the right username and password 21 | resource "local_file" "ansible-groupvars-linux-creation" { 22 | 23 | depends_on = [ 24 | var.linux-user, 25 | random_string.linuxpass 26 | ] 27 | 28 | content = templatefile("${path.module}/../Ansible/group_vars/linux.tmpl", 29 | { 30 | username = var.linux-user 31 | password = random_string.linuxpass.result 32 | }) 33 | 34 | filename = "../Ansible/group_vars/linux.yml" 35 | } 36 | 37 | # Provision the lab using Ansible from the hackbox machine 38 | resource "null_resource" "ansible-provisioning" { 39 | 40 | # All VMs have to be up before provisioning can be initiated 41 | depends_on = [ 42 | azurerm_windows_virtual_machine.thehackerish-vm-dc, 43 | azurerm_windows_virtual_machine.thehackerish-vm-winserv2019, 44 | azurerm_windows_virtual_machine.thehackerish-vm-windows10, 45 | azurerm_linux_virtual_machine.thehackerish-vm-hackbox, 46 | ] 47 | 48 | triggers = { 49 | always_run = "${timestamp()}" 50 | } 51 | 52 | connection { 53 | type = "ssh" 54 | host = azurerm_public_ip.thehackerish-ip.ip_address 55 | user = var.linux-user 56 | password = random_string.linuxpass.result 57 | } 58 | 59 | # Copy Ansible folder to hackbox machine for provisioning 60 | provisioner "file" { 61 | source = "../Ansible" 62 | destination = "/dev/shm" 63 | } 64 | 65 | # Kick off ansible 66 | provisioner "remote-exec" { 67 | inline = [ 68 | "sudo apt -qq update >/dev/null && sudo apt -qq install -y git ansible sshpass > /dev/null", 69 | "ansible-galaxy collection install ansible.windows community.general > /dev/null", 70 | "cd /dev/shm/Ansible", 71 | "ansible-playbook -v cloudlabs.yml" 72 | ] 73 | } 74 | } -------------------------------------------------------------------------------- /Terraform/files/ConfigureRemotingForAnsible.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3.0 2 | 3 | # Configure a Windows host for remote management with Ansible 4 | # ----------------------------------------------------------- 5 | # 6 | # This script checks the current WinRM (PS Remoting) configuration and makes 7 | # the necessary changes to allow Ansible to connect, authenticate and 8 | # execute PowerShell commands. 9 | # 10 | # All events are logged to the Windows EventLog, useful for unattended runs. 11 | # 12 | # Use option -Verbose in order to see the verbose output messages. 13 | # 14 | # Use option -CertValidityDays to specify how long this certificate is valid 15 | # starting from today. So you would specify -CertValidityDays 3650 to get 16 | # a 10-year valid certificate. 17 | # 18 | # Use option -ForceNewSSLCert if the system has been SysPreped and a new 19 | # SSL Certificate must be forced on the WinRM Listener when re-running this 20 | # script. This is necessary when a new SID and CN name is created. 21 | # 22 | # Use option -EnableCredSSP to enable CredSSP as an authentication option. 23 | # 24 | # Use option -DisableBasicAuth to disable basic authentication. 25 | # 26 | # Use option -SkipNetworkProfileCheck to skip the network profile check. 27 | # Without specifying this the script will only run if the device's interfaces 28 | # are in DOMAIN or PRIVATE zones. Provide this switch if you want to enable 29 | # WinRM on a device with an interface in PUBLIC zone. 30 | # 31 | # Use option -SubjectName to specify the CN name of the certificate. This 32 | # defaults to the system's hostname and generally should not be specified. 33 | 34 | # Written by Trond Hindenes 35 | # Updated by Chris Church 36 | # Updated by Michael Crilly 37 | # Updated by Anton Ouzounov 38 | # Updated by Nicolas Simond 39 | # Updated by Dag Wieërs 40 | # Updated by Jordan Borean 41 | # Updated by Erwan Quélin 42 | # Updated by David Norman 43 | # 44 | # Version 1.0 - 2014-07-06 45 | # Version 1.1 - 2014-11-11 46 | # Version 1.2 - 2015-05-15 47 | # Version 1.3 - 2016-04-04 48 | # Version 1.4 - 2017-01-05 49 | # Version 1.5 - 2017-02-09 50 | # Version 1.6 - 2017-04-18 51 | # Version 1.7 - 2017-11-23 52 | # Version 1.8 - 2018-02-23 53 | # Version 1.9 - 2018-09-21 54 | 55 | # Support -Verbose option 56 | [CmdletBinding()] 57 | 58 | Param ( 59 | [string]$SubjectName = $env:COMPUTERNAME, 60 | [int]$CertValidityDays = 1095, 61 | [switch]$SkipNetworkProfileCheck, 62 | $CreateSelfSignedCert = $true, 63 | [switch]$ForceNewSSLCert, 64 | [switch]$GlobalHttpFirewallAccess, 65 | [switch]$DisableBasicAuth = $false, 66 | [switch]$EnableCredSSP 67 | ) 68 | 69 | Function Write-ProgressLog { 70 | $Message = $args[0] 71 | Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 1 -Message $Message 72 | } 73 | 74 | Function Write-VerboseLog { 75 | $Message = $args[0] 76 | Write-Verbose $Message 77 | Write-ProgressLog $Message 78 | } 79 | 80 | Function Write-HostLog { 81 | $Message = $args[0] 82 | Write-Output $Message 83 | Write-ProgressLog $Message 84 | } 85 | 86 | Function New-LegacySelfSignedCert { 87 | Param ( 88 | [string]$SubjectName, 89 | [int]$ValidDays = 1095 90 | ) 91 | 92 | $hostnonFQDN = $env:computerName 93 | $hostFQDN = [System.Net.Dns]::GetHostByName(($env:computerName)).Hostname 94 | $SignatureAlgorithm = "SHA256" 95 | 96 | $name = New-Object -COM "X509Enrollment.CX500DistinguishedName.1" 97 | $name.Encode("CN=$SubjectName", 0) 98 | 99 | $key = New-Object -COM "X509Enrollment.CX509PrivateKey.1" 100 | $key.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" 101 | $key.KeySpec = 1 102 | $key.Length = 4096 103 | $key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)" 104 | $key.MachineContext = 1 105 | $key.Create() 106 | 107 | $serverauthoid = New-Object -COM "X509Enrollment.CObjectId.1" 108 | $serverauthoid.InitializeFromValue("1.3.6.1.5.5.7.3.1") 109 | $ekuoids = New-Object -COM "X509Enrollment.CObjectIds.1" 110 | $ekuoids.Add($serverauthoid) 111 | $ekuext = New-Object -COM "X509Enrollment.CX509ExtensionEnhancedKeyUsage.1" 112 | $ekuext.InitializeEncode($ekuoids) 113 | 114 | $cert = New-Object -COM "X509Enrollment.CX509CertificateRequestCertificate.1" 115 | $cert.InitializeFromPrivateKey(2, $key, "") 116 | $cert.Subject = $name 117 | $cert.Issuer = $cert.Subject 118 | $cert.NotBefore = (Get-Date).AddDays(-1) 119 | $cert.NotAfter = $cert.NotBefore.AddDays($ValidDays) 120 | 121 | $SigOID = New-Object -ComObject X509Enrollment.CObjectId 122 | $SigOID.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value) 123 | 124 | [string[]] $AlternativeName += $hostnonFQDN 125 | $AlternativeName += $hostFQDN 126 | $IAlternativeNames = New-Object -ComObject X509Enrollment.CAlternativeNames 127 | 128 | foreach ($AN in $AlternativeName) { 129 | $AltName = New-Object -ComObject X509Enrollment.CAlternativeName 130 | $AltName.InitializeFromString(0x3, $AN) 131 | $IAlternativeNames.Add($AltName) 132 | } 133 | 134 | $SubjectAlternativeName = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames 135 | $SubjectAlternativeName.InitializeEncode($IAlternativeNames) 136 | 137 | [String[]]$KeyUsage = ("DigitalSignature", "KeyEncipherment") 138 | $KeyUsageObj = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage 139 | $KeyUsageObj.InitializeEncode([int][Security.Cryptography.X509Certificates.X509KeyUsageFlags]($KeyUsage)) 140 | $KeyUsageObj.Critical = $true 141 | 142 | $cert.X509Extensions.Add($KeyUsageObj) 143 | $cert.X509Extensions.Add($ekuext) 144 | $cert.SignatureInformation.HashAlgorithm = $SigOID 145 | $CERT.X509Extensions.Add($SubjectAlternativeName) 146 | $cert.Encode() 147 | 148 | $enrollment = New-Object -COM "X509Enrollment.CX509Enrollment.1" 149 | $enrollment.InitializeFromRequest($cert) 150 | $certdata = $enrollment.CreateRequest(0) 151 | $enrollment.InstallResponse(2, $certdata, 0, "") 152 | 153 | # extract/return the thumbprint from the generated cert 154 | $parsed_cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 155 | $parsed_cert.Import([System.Text.Encoding]::UTF8.GetBytes($certdata)) 156 | 157 | return $parsed_cert.Thumbprint 158 | } 159 | 160 | Function Enable-GlobalHttpFirewallAccess { 161 | Write-Verbose "Forcing global HTTP firewall access" 162 | # this is a fairly naive implementation; could be more sophisticated about rule matching/collapsing 163 | $fw = New-Object -ComObject HNetCfg.FWPolicy2 164 | 165 | # try to find/enable the default rule first 166 | $add_rule = $false 167 | $matching_rules = $fw.Rules | Where-Object { $_.Name -eq "Windows Remote Management (HTTP-In)" } 168 | $rule = $null 169 | If ($matching_rules) { 170 | If ($matching_rules -isnot [Array]) { 171 | Write-Verbose "Editing existing single HTTP firewall rule" 172 | $rule = $matching_rules 173 | } 174 | Else { 175 | # try to find one with the All or Public profile first 176 | Write-Verbose "Found multiple existing HTTP firewall rules..." 177 | $rule = $matching_rules | ForEach-Object { $_.Profiles -band 4 }[0] 178 | 179 | If (-not $rule -or $rule -is [Array]) { 180 | Write-Verbose "Editing an arbitrary single HTTP firewall rule (multiple existed)" 181 | # oh well, just pick the first one 182 | $rule = $matching_rules[0] 183 | } 184 | } 185 | } 186 | 187 | If (-not $rule) { 188 | Write-Verbose "Creating a new HTTP firewall rule" 189 | $rule = New-Object -ComObject HNetCfg.FWRule 190 | $rule.Name = "Windows Remote Management (HTTP-In)" 191 | $rule.Description = "Inbound rule for Windows Remote Management via WS-Management. [TCP 5985]" 192 | $add_rule = $true 193 | } 194 | 195 | $rule.Profiles = 0x7FFFFFFF 196 | $rule.Protocol = 6 197 | $rule.LocalPorts = 5985 198 | $rule.RemotePorts = "*" 199 | $rule.LocalAddresses = "*" 200 | $rule.RemoteAddresses = "*" 201 | $rule.Enabled = $true 202 | $rule.Direction = 1 203 | $rule.Action = 1 204 | $rule.Grouping = "Windows Remote Management" 205 | 206 | If ($add_rule) { 207 | $fw.Rules.Add($rule) 208 | } 209 | 210 | Write-Verbose "HTTP firewall rule $($rule.Name) updated" 211 | } 212 | 213 | # Setup error handling. 214 | Trap { 215 | $_ 216 | Exit 1 217 | } 218 | $ErrorActionPreference = "Stop" 219 | 220 | # Get the ID and security principal of the current user account 221 | $myWindowsID = [System.Security.Principal.WindowsIdentity]::GetCurrent() 222 | $myWindowsPrincipal = new-object System.Security.Principal.WindowsPrincipal($myWindowsID) 223 | 224 | # Get the security principal for the Administrator role 225 | $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator 226 | 227 | # Check to see if we are currently running "as Administrator" 228 | if (-Not $myWindowsPrincipal.IsInRole($adminRole)) { 229 | Write-Output "ERROR: You need elevated Administrator privileges in order to run this script." 230 | Write-Output " Start Windows PowerShell by using the Run as Administrator option." 231 | Exit 2 232 | } 233 | 234 | $EventSource = $MyInvocation.MyCommand.Name 235 | If (-Not $EventSource) { 236 | $EventSource = "Powershell CLI" 237 | } 238 | 239 | If ([System.Diagnostics.EventLog]::Exists('Application') -eq $False -or [System.Diagnostics.EventLog]::SourceExists($EventSource) -eq $False) { 240 | New-EventLog -LogName Application -Source $EventSource 241 | } 242 | 243 | # Detect PowerShell version. 244 | If ($PSVersionTable.PSVersion.Major -lt 3) { 245 | Write-ProgressLog "PowerShell version 3 or higher is required." 246 | Throw "PowerShell version 3 or higher is required." 247 | } 248 | 249 | # Find and start the WinRM service. 250 | Write-Verbose "Verifying WinRM service." 251 | If (!(Get-Service "WinRM")) { 252 | Write-ProgressLog "Unable to find the WinRM service." 253 | Throw "Unable to find the WinRM service." 254 | } 255 | ElseIf ((Get-Service "WinRM").Status -ne "Running") { 256 | Write-Verbose "Setting WinRM service to start automatically on boot." 257 | Set-Service -Name "WinRM" -StartupType Automatic 258 | Write-ProgressLog "Set WinRM service to start automatically on boot." 259 | Write-Verbose "Starting WinRM service." 260 | Start-Service -Name "WinRM" -ErrorAction Stop 261 | Write-ProgressLog "Started WinRM service." 262 | 263 | } 264 | 265 | # WinRM should be running; check that we have a PS session config. 266 | If (!(Get-PSSessionConfiguration -Verbose:$false) -or (!(Get-ChildItem WSMan:\localhost\Listener))) { 267 | If ($SkipNetworkProfileCheck) { 268 | Write-Verbose "Enabling PS Remoting without checking Network profile." 269 | Enable-PSRemoting -SkipNetworkProfileCheck -Force -ErrorAction Stop 270 | Write-ProgressLog "Enabled PS Remoting without checking Network profile." 271 | } 272 | Else { 273 | Write-Verbose "Enabling PS Remoting." 274 | Enable-PSRemoting -Force -ErrorAction Stop 275 | Write-ProgressLog "Enabled PS Remoting." 276 | } 277 | } 278 | Else { 279 | Write-Verbose "PS Remoting is already enabled." 280 | } 281 | 282 | # Ensure LocalAccountTokenFilterPolicy is set to 1 283 | # https://github.com/ansible/ansible/issues/42978 284 | $token_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" 285 | $token_prop_name = "LocalAccountTokenFilterPolicy" 286 | $token_key = Get-Item -Path $token_path 287 | $token_value = $token_key.GetValue($token_prop_name, $null) 288 | if ($token_value -ne 1) { 289 | Write-Verbose "Setting LocalAccountTOkenFilterPolicy to 1" 290 | if ($null -ne $token_value) { 291 | Remove-ItemProperty -Path $token_path -Name $token_prop_name 292 | } 293 | New-ItemProperty -Path $token_path -Name $token_prop_name -Value 1 -PropertyType DWORD > $null 294 | } 295 | 296 | # Make sure there is a SSL listener. 297 | $listeners = Get-ChildItem WSMan:\localhost\Listener 298 | If (!($listeners | Where-Object { $_.Keys -like "TRANSPORT=HTTPS" })) { 299 | # We cannot use New-SelfSignedCertificate on 2012R2 and earlier 300 | $thumbprint = New-LegacySelfSignedCert -SubjectName $SubjectName -ValidDays $CertValidityDays 301 | Write-HostLog "Self-signed SSL certificate generated; thumbprint: $thumbprint" 302 | 303 | # Create the hashtables of settings to be used. 304 | $valueset = @{ 305 | Hostname = $SubjectName 306 | CertificateThumbprint = $thumbprint 307 | } 308 | 309 | $selectorset = @{ 310 | Transport = "HTTPS" 311 | Address = "*" 312 | } 313 | 314 | Write-Verbose "Enabling SSL listener." 315 | New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset 316 | Write-ProgressLog "Enabled SSL listener." 317 | } 318 | Else { 319 | Write-Verbose "SSL listener is already active." 320 | 321 | # Force a new SSL cert on Listener if the $ForceNewSSLCert 322 | If ($ForceNewSSLCert) { 323 | 324 | # We cannot use New-SelfSignedCertificate on 2012R2 and earlier 325 | $thumbprint = New-LegacySelfSignedCert -SubjectName $SubjectName -ValidDays $CertValidityDays 326 | Write-HostLog "Self-signed SSL certificate generated; thumbprint: $thumbprint" 327 | 328 | $valueset = @{ 329 | CertificateThumbprint = $thumbprint 330 | Hostname = $SubjectName 331 | } 332 | 333 | # Delete the listener for SSL 334 | $selectorset = @{ 335 | Address = "*" 336 | Transport = "HTTPS" 337 | } 338 | Remove-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset 339 | 340 | # Add new Listener with new SSL cert 341 | New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset 342 | } 343 | } 344 | 345 | # Check for basic authentication. 346 | $basicAuthSetting = Get-ChildItem WSMan:\localhost\Service\Auth | Where-Object { $_.Name -eq "Basic" } 347 | 348 | If ($DisableBasicAuth) { 349 | If (($basicAuthSetting.Value) -eq $true) { 350 | Write-Verbose "Disabling basic auth support." 351 | Set-Item -Path "WSMan:\localhost\Service\Auth\Basic" -Value $false 352 | Write-ProgressLog "Disabled basic auth support." 353 | } 354 | Else { 355 | Write-Verbose "Basic auth is already disabled." 356 | } 357 | } 358 | Else { 359 | If (($basicAuthSetting.Value) -eq $false) { 360 | Write-Verbose "Enabling basic auth support." 361 | Set-Item -Path "WSMan:\localhost\Service\Auth\Basic" -Value $true 362 | Write-ProgressLog "Enabled basic auth support." 363 | } 364 | Else { 365 | Write-Verbose "Basic auth is already enabled." 366 | } 367 | } 368 | 369 | # If EnableCredSSP if set to true 370 | If ($EnableCredSSP) { 371 | # Check for CredSSP authentication 372 | $credsspAuthSetting = Get-ChildItem WSMan:\localhost\Service\Auth | Where-Object { $_.Name -eq "CredSSP" } 373 | If (($credsspAuthSetting.Value) -eq $false) { 374 | Write-Verbose "Enabling CredSSP auth support." 375 | Enable-WSManCredSSP -role server -Force 376 | Write-ProgressLog "Enabled CredSSP auth support." 377 | } 378 | } 379 | 380 | If ($GlobalHttpFirewallAccess) { 381 | Enable-GlobalHttpFirewallAccess 382 | } 383 | 384 | # Configure firewall to allow WinRM HTTPS connections. 385 | $fwtest1 = netsh advfirewall firewall show rule name="Allow WinRM HTTPS" 386 | $fwtest2 = netsh advfirewall firewall show rule name="Allow WinRM HTTPS" profile=any 387 | If ($fwtest1.count -lt 5) { 388 | Write-Verbose "Adding firewall rule to allow WinRM HTTPS." 389 | netsh advfirewall firewall add rule profile=any name="Allow WinRM HTTPS" dir=in localport=5986 protocol=TCP action=allow 390 | Write-ProgressLog "Added firewall rule to allow WinRM HTTPS." 391 | } 392 | ElseIf (($fwtest1.count -ge 5) -and ($fwtest2.count -lt 5)) { 393 | Write-Verbose "Updating firewall rule to allow WinRM HTTPS for any profile." 394 | netsh advfirewall firewall set rule name="Allow WinRM HTTPS" new profile=any 395 | Write-ProgressLog "Updated firewall rule to allow WinRM HTTPS for any profile." 396 | } 397 | Else { 398 | Write-Verbose "Firewall rule already exists to allow WinRM HTTPS." 399 | } 400 | 401 | # Test a remoting connection to localhost, which should work. 402 | $httpResult = Invoke-Command -ComputerName "localhost" -ScriptBlock { $using:env:COMPUTERNAME } -ErrorVariable httpError -ErrorAction SilentlyContinue 403 | $httpsOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck 404 | 405 | $httpsResult = New-PSSession -UseSSL -ComputerName "localhost" -SessionOption $httpsOptions -ErrorVariable httpsError -ErrorAction SilentlyContinue 406 | 407 | If ($httpResult -and $httpsResult) { 408 | Write-Verbose "HTTP: Enabled | HTTPS: Enabled" 409 | } 410 | ElseIf ($httpsResult -and !$httpResult) { 411 | Write-Verbose "HTTP: Disabled | HTTPS: Enabled" 412 | } 413 | ElseIf ($httpResult -and !$httpsResult) { 414 | Write-Verbose "HTTP: Enabled | HTTPS: Disabled" 415 | } 416 | Else { 417 | Write-ProgressLog "Unable to establish an HTTP or HTTPS remoting session." 418 | Throw "Unable to establish an HTTP or HTTPS remoting session." 419 | } 420 | Write-VerboseLog "PS Remoting has been successfully configured for Ansible." -------------------------------------------------------------------------------- /Terraform/files/FirstLogonCommands.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | cmd /c "mkdir C:\terraform" 9 | Create the Terraform working directory 10 | 11 11 | 12 | 13 | cmd /c "copy C:\AzureData\CustomData.bin C:\terraform\ConfigureRemotingForAnsible.ps1" 14 | Move the CustomData file to the working directory 15 | 12 16 | 17 | 18 | powershell.exe -sta -ExecutionPolicy Unrestricted -file C:\terraform\ConfigureRemotingForAnsible.ps1 19 | Move the CustomData file to the working directory 20 | 13 21 | 22 | -------------------------------------------------------------------------------- /Terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public-ip" { 2 | value = azurerm_public_ip.thehackerish-ip.ip_address 3 | description = "The public IP address used to connect to the lab." 4 | } 5 | 6 | output "public-ip-dns" { 7 | value = azurerm_public_ip.thehackerish-ip.fqdn 8 | description = "The public DNS name used to connect to the lab." 9 | } 10 | 11 | output "windows-domain" { 12 | value = var.domain-dns-name 13 | description = "The the Active Directory domain name." 14 | } 15 | 16 | output "windows-user" { 17 | value = var.windows-user 18 | description = "The username used to connect to the Windows machine." 19 | } 20 | 21 | output "windows-password" { 22 | value = random_string.windowspass.result 23 | description = "The password used for Windows local admin accounts." 24 | } 25 | 26 | output "hackbox-hostname" { 27 | value = var.hackbox-hostname 28 | description = "The hostname of the attacker VM." 29 | } 30 | 31 | output "linux-user" { 32 | value = var.linux-user 33 | description = "The SSH username used to connect to Linux machines." 34 | } 35 | 36 | output "linux-password" { 37 | value = random_string.linuxpass.result 38 | description = "The password used for Linux admin accounts." 39 | } 40 | 41 | output "dc-hostname" { 42 | value = var.dc-hostname 43 | description = "The hostname of the Domain Controller." 44 | } 45 | 46 | output "winserv2019-hostname" { 47 | value = var.winserv2019-hostname 48 | description = "The hostname of the Windows Server 2019 VM." 49 | } 50 | 51 | output "win10-hostname"{ 52 | value = var.win10-hostname 53 | description = "The hostname of the Windows 10 VM." 54 | } 55 | 56 | output "ip-whitelist" { 57 | value = join(", ", var.ip-whitelist) 58 | description = "The IP address(es) that are allowed to connect to the various lab interfaces." 59 | } 60 | 61 | output "Finished"{ 62 | value = "Stay curious, keep learning, and go find some bugs!" 63 | } -------------------------------------------------------------------------------- /Terraform/terraform.tfvars.example: -------------------------------------------------------------------------------- 1 | # Terraform variables 2 | # See 'variables.tf' for definitions 3 | 4 | # Required 5 | resource-group = "resourcegroupname" 6 | ip-whitelist = ["1.2.3.4/32", "8.8.8.0/24"] 7 | 8 | # Optional (defaults are shown) 9 | timezone = "Central Europe Standard Time" 10 | domain-name-label = "thehackerish" 11 | domain-dns-name = "th.local" 12 | windows-user = "cooten" 13 | linux-user = "cooten" 14 | hackbox-hostname = "hackbox" 15 | dc-hostname = "dc" 16 | winserv2019-hostname = "winserv2019" 17 | win10-hostname = "win10" 18 | win10-size = "Standard_B1ms" 19 | winserv2019-size = "Standard_B1ms" 20 | dc-size = "Standard_B1ms" 21 | hackbox-size = "Standard_B1ms" 22 | -------------------------------------------------------------------------------- /Terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource-group" { 2 | type = string 3 | description = "The name of the sandbox resource group." 4 | } 5 | 6 | variable "timezone" { 7 | type = string 8 | description = "The timezone of the lab VMs." 9 | default = "Central Europe Standard Time" 10 | } 11 | 12 | variable "ip-whitelist" { 13 | description = "A list of CIDRs that will be allowed to access the exposed services." 14 | type = list(string) 15 | } 16 | 17 | variable "domain-name-label" { 18 | description = "The DNS name of the Azure public IP." 19 | type = string 20 | default = "cloudlabs" 21 | } 22 | 23 | variable "domain-dns-name" { 24 | description = "The DNS name of the Active Directory domain." 25 | type = string 26 | default = "cloud.labs" 27 | } 28 | 29 | variable "hackbox-hostname" { 30 | type = string 31 | description = "The hostname of the attacker VM." 32 | default = "hackbox" 33 | } 34 | 35 | variable "hackbox-size" { 36 | type = string 37 | description = "The machine size of the attacker VM." 38 | default = "Standard_B4ms" 39 | } 40 | 41 | variable "dc-hostname" { 42 | type = string 43 | description = "The hostname of the Windows Server 2016 DC VM." 44 | default = "dc" 45 | } 46 | 47 | variable "dc-size" { 48 | type = string 49 | description = "The machine size of the Windows Server 2016 DC VM." 50 | default = "Standard_B4ms" 51 | } 52 | 53 | variable "winserv2019-hostname" { 54 | type = string 55 | description = "The hostname of the Windows Server 2019 VM." 56 | default = "winserv2019" 57 | } 58 | 59 | variable "winserv2019-size" { 60 | type = string 61 | description = "The machine size of the Windows Server 2019 VM." 62 | default = "Standard_B4ms" 63 | } 64 | 65 | variable "win10-hostname" { 66 | type = string 67 | description = "The hostname of the Windows 10 VM." 68 | default = "win10" 69 | } 70 | 71 | variable "win10-size" { 72 | type = string 73 | description = "The machine size of the Windows 10 VM." 74 | default = "Standard_B4ms" 75 | } 76 | 77 | variable "windows-user" { 78 | type = string 79 | description = "The local administrative username for Windows machines. Password will be generated." 80 | default = "labadmin" 81 | } 82 | 83 | variable "linux-user" { 84 | type = string 85 | description = "The username used to access Linux machines via SSH." 86 | default = "labadmin" 87 | } -------------------------------------------------------------------------------- /assets/labs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehackerish/ADLab/1cb674565fbe1a08c4f4c2b8e8a8f2c0907b6a0b/assets/labs.png --------------------------------------------------------------------------------