If you have followed along with Part One, you should have Linux and Windows virtual machine templates, and terraform scripts to create the network topology.
To configure the hosts and add vulnerabilities, we will be using Ansible. The vulnerabilities were going to be adding in are designed to be quick and easy to exploit.
Working with Dynamic Inventories
Since some of our hosts have DHCP assigned IP addresses, we will need to write an inventory script to query the Proxmox API and determine the allocated IP addresses. The following Python code serves this purpose.
#!/usr/bin/env python3
import json
import requests
import sys
import urllib3
PROXMOX_HOST = 'https://192.168.1.201:8006'
USERNAME = 'root@pam'
PASSWORD = 'Password1'
VERIFY_SSL = False
NODE = 'pve'
if not VERIFY_SSL:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def get_auth_ticket():
url = f"{PROXMOX_HOST}/api2/json/access/ticket"
data = {'username': USERNAME, 'password': PASSWORD}
response = requests.post(url, data=data, verify=VERIFY_SSL)
response.raise_for_status()
result = response.json()['data']
return result['ticket'], result['CSRFPreventionToken']
def get_all_vmids(ticket):
headers = {'Cookie': f"PVEAuthCookie={ticket}"}
url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu"
response = requests.get(url, headers=headers, verify=VERIFY_SSL)
response.raise_for_status()
vms = response.json()['data']
return [vm['vmid'] for vm in vms]
def get_vm_ip(ticket, vmid):
headers = {'Cookie': f"PVEAuthCookie={ticket}"}
url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu/{vmid}/agent/network-get-interfaces"
try:
response = requests.get(url, headers=headers, verify=VERIFY_SSL)
if response.status_code != 200:
return None
data = response.json().get('data', {})
interfaces = data.get('result', [])
for interface in interfaces:
for addr in interface.get('ip-addresses', []):
ip = addr.get('ip-address')
if ip and not ip.startswith('127.') and not ip.startswith('169.254.') and ':' not in ip:
return ip
except Exception as e:
print(f"Error reading VM {vmid}: {e}", file=sys.stderr)
return None
return None
def get_vm_os_type(ticket, vmid):
headers = {'Cookie': f"PVEAuthCookie={ticket}"}
url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu/{vmid}/agent/get-osinfo"
try:
response = requests.get(url, headers=headers, verify=VERIFY_SSL)
if response.status_code != 200:
return None
data = response.json().get('data', {})
os_info = data.get('result', {})
os_name = os_info.get('name', '').lower()
if 'windows' in os_name:
return 'windows'
return 'linux'
except Exception as e:
print(f"Error reading OS info for VM {vmid}: {e}", file=sys.stderr)
return None
def build_inventory():
ticket, _ = get_auth_ticket()
headers = {'Cookie': f"PVEAuthCookie={ticket}"}
url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu"
response = requests.get(url, headers=headers, verify=VERIFY_SSL)
response.raise_for_status()
vms = response.json()['data']
hosts = []
hostvars = {}
for vm in vms:
vmid = vm['vmid']
vm_name = vm.get('name', f'vm-{vmid}')
ip = get_vm_ip(ticket, vmid)
os_type = get_vm_os_type(ticket, vmid)
if ip:
hosts.append(vm_name)
# Default to Linux settings
hostvars[vm_name] = {
'ansible_host': ip,
'ansible_user': 'bordergate',
'ansible_password': 'Password1',
'ansible_connection': 'ssh'
}
if os_type == 'windows':
hostvars[vm_name].update({
'ansible_user': 'Administrator',
'ansible_connection': 'winrm',
'ansible_winrm_transport': 'ntlm',
'ansible_winrm_port': 5985,
'ansible_winrm_server_cert_validation': 'ignore',
'ansible_password': 'Password1'
})
inventory = {
'all': {
'hosts': hosts,
'vars': {}
},
'_meta': {
'hostvars': hostvars
}
}
return inventory
def main():
if len(sys.argv) == 2 and sys.argv[1] == '--list':
inventory = build_inventory()
print(json.dumps(inventory, indent=2))
elif len(sys.argv) == 2 and sys.argv[1] == '--host':
print(json.dumps({}))
else:
print("Usage: inventory.py --list|--host <hostname>")
sys.exit(1)
if __name__ == '__main__':
main()
Log into the director system (which has access to all configured networks), and test the dynamic inventory management is working.
bordergate@N0-DIRECTOR:~$ sudo apt install ansible sshpass
bordergate@N0-DIRECTOR:~$ export ANSIBLE_HOST_KEY_CHECKING=False
bordergate@N0-DIRECTOR:~$ ansible -i ./inventory.py N2-DEMETER -m win_ping
N2-DEMETER | SUCCESS => {
"changed": false,
"ping": "pong"
}
bordergate@N0-DIRECTOR:~$ ansible -i ./inventory.py N1-ZEUS -m ping
N1-ZEUS | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
Adding Vulnerabilities
N1-ZEUS
This system has the following vulnerabilities.
- SNMP v1 configuration that shows the network 2 range
- IP routing is enabled, so the system will forward traffic to network 2
- The system has a web server listening that’s only accessible from network 2. This contains credentials to access the system using SSH
Create an Ansible configuration (CONFIGS/N1-ZEUS.yaml)
- hosts: N1-ZEUS
user: root
become: true
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N1-ZEUS/'
- name: install nginx
apt: pkg=nginx state=present
- name: install snmpd
apt: pkg=snmpd state=present
- name: install iptables persistent
apt: pkg=iptables-persistent state=present
- name: Enable IP forwarding
ansible.builtin.shell: echo 1 > /proc/sys/net/ipv4/ip_forward
- name: Clear previous IPtables rules
ansible.builtin.shell: iptables -F
- name: Enable NAT
ansible.builtin.shell: iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
- name: Drop ICMP on input chain
ansible.builtin.shell: iptables -I INPUT -p icmp --icmp-type 8 -j DROP
- name: Drop ICMP on forward chain
ansible.builtin.shell: iptables -I FORWARD -p icmp --icmp-type 8 -j DROP
- name: Drop SMB
ansible.builtin.shell: iptables -I FORWARD -p tcp --dport 445 -j DROP
- name: Drop SSH
ansible.builtin.shell: iptables -I FORWARD -p tcp --dport 22 -j DROP
- name: Save IPtables rules
ansible.builtin.shell: iptables-save > /etc/iptables/rules.v4
- name: Enable IP forwarding permanently
lineinfile:
path: /etc/sysctl.conf
regexp: '^#?net.ipv4.ip_forward'
line: 'net.ipv4.ip_forward = 1'
- name: Remove all 'listen 80' or 'listen 80 default_server' directives
replace:
path: /etc/nginx/sites-available/default
regexp: '^\s*listen\s+80.*;'
replace: ''
- name: Update NGINX to listen only on 172.16.24.7
lineinfile:
path: /etc/nginx/sites-available/default
regexp: '^(\s*)listen\s+.*;'
line: ' listen 172.16.24.7:80;'
state: present
backrefs: yes
- name: Copy SNMP configuration file
copy:
dest: /etc/snmp/snmpd.conf
content: |
sysLocation Sitting on the Dock of the Bay
sysContact Me <me@example.org>
sysServices 72
agentaddress udp:0.0.0.0:161
view systemonly included .1.3.6.1.2.1.1
view systemonly included .1.3.6.1.2.1.25.1
rocommunity public
rocommunity6 public default -V systemonly
rouser authPrivUser authpriv -V systemonly
includeDir /etc/snmp/snmpd.conf.d
- name: Copy sysctl configuration to enable IP forwarding
copy:
dest: /etc/sysctl.conf
content: |
net.ipv4.ip_forward=1
- name: Restart SNMP daemon
service:
name: snmpd
state: started
- name: Restart Nginx
service:
name: nginx
state: reloaded
- name: Ensure zeus user exists
user:
name: zeus
shell: /bin/bash
state: present
create_home: yes
password: "{{ 'ZeusJupiter' | password_hash('sha512') }}"
groups: sudo
append: yes
- name: Deploy robots.txt file
ansible.builtin.copy:
dest: /var/www/html/robots.txt
content: |
zeus:ZeusJupiter
owner: root
group: root
mode: '0644'
force: yes
- name: Create FLAG
copy:
dest: /root/FLAG.txt
content: |
FLAG{5a1684d7d326b054f5e4e6c6e2cd4a9507049b96}
N1-HERA
This system has the following vulnerabilities.
- An open NFS containing an encrypted zip file with credentials
- The zip file credentials allow for SSH access
- A mis-configured cron job to elevate to root
- The system is duel homed, which provides access to network 2
Use the following command to create the encrypted zip:
bordergate@DIRECTOR:~$ cat creds.txt
hera:hercules
bordergate@DIRECTOR:~$ zip -e backup.zip creds.txt
Enter password: 123hera
Verify password: 123hera
adding: creds.txt (stored 0%)
- hosts: N1-HERA
user: root
become: true
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N1-HERA/'
- name: install nfs-kernel-server
apt: pkg=nfs-kernel-server state=present
- name: Copy NFS information leak
copy:
dest: /etc/exports
content: |
/srv *(ro,sync,subtree_check)
- name: Create user 'hera'
user:
name: hera
password: "{{ 'hercules' | password_hash('sha512') }}"
shell: /bin/bash
home: /home/hera
state: present
create_home: yes
- name: Download backup.zip to /srv/
get_url:
url: "http://10.0.24.250/backup.zip"
dest: "/srv/backup.zip"
- name: Restart NFS daemon
service:
name: nfs-kernel-server.service
state: restarted
- name: Create vulnerable script
copy:
dest: /usr/local/bin/backup.sh
content: |
#!/bin/bash
echo "Backup started..."
owner: root
group: root
mode: '0777'
- name: Make sure script is executable
file:
path: /usr/local/bin/backup.sh
mode: '0777'
- name: Add cron job running as root
cron:
name: "Root cron for backup.sh"
user: root
job: "/usr/local/bin/backup.sh"
minute: "*/1"
- name: Create FLAG
copy:
dest: /root/FLAG.txt
content: |
FLAG{00c5d6c5df2e4a4494e1b2631eae9fbb52607d76}
N1-AEOLUS
This system runs a TFTP server that contains credentials which can be used to SSH into it. In addition, it’s running a DNS server for the pantheon.local domain, which supports zone transfers.
- hosts: N1-AEOLUS
user: root
become: true
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N1-AEOLUS'
# DNS Server Setup
- name: Install BIND9 DNS server
apt:
name: bind9
state: present
update_cache: yes
- name: Configure named.conf.options
copy:
dest: /etc/bind/named.conf.options
content: |
options {
directory "/var/cache/bind";
allow-transfer { any; };
recursion yes;
allow-recursion { any; };
listen-on { any; };
listen-on-v6 { any; };
dnssec-validation no;
};
- name: Configure zone in named.conf.local
copy:
dest: /etc/bind/named.conf.local
content: |
zone "pantheon.local" {
type master;
file "/etc/bind/zones/db.pantheon.local";
allow-transfer { any; };
};
zone "24.16.172.in-addr.arpa" {
type master;
file "/etc/bind/zones/db.172.16.24";
allow-transfer { any; };
};
- name: Create zone directory
file:
path: /etc/bind/zones
state: directory
owner: bind
group: bind
mode: '0755'
- name: Create zone file for pantheon.local
copy:
dest: /etc/bind/zones/db.pantheon.local
content: |
;
; BIND data file for pantheon.local
;
$TTL 604800
@ IN SOA ns1.pantheon.local. admin.pantheon.local. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns1.pantheon.com.
ns1 IN A 172.16.24.99
demeter IN A 172.16.24.70
hermes IN A 172.16.24.55
apollo IN A 172.16.24.75
hades IN A 172.16.24.24
- name: Create reverse zone file for 172.16.24.x network
copy:
dest: /etc/bind/zones/db.172.16.24
content: |
;
; BIND reverse data file for 172.16.24.0/24 network
;
$TTL 604800
@ IN SOA ns1.pantheon.local. admin.pantheon.local. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns1.pantheon.local.
; PTR Records for Hosts
70 IN PTR demeter.pantheon.local.
55 IN PTR hermes.pantheon.local.
75 IN PTR apollo.pantheon.local.
24 IN PTR hades.pantheon.local.
- name: Restart BIND9
service:
name: bind9
state: restarted
enabled: yes
# TFTP Server Setup
- name: Install tftpd-hpa
apt:
name: tftpd-hpa
state: present
update_cache: yes
- name: Create TFTP root directory
file:
path: /srv/tftp
state: directory
owner: nobody
group: nogroup
mode: '0755'
- name: Configure tftpd-hpa options
copy:
dest: /etc/default/tftpd-hpa
content: |
TFTP_OPTIONS="--secure"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_USERNAME="nobody"
- name: Enable and restart tftpd-hpa
service:
name: tftpd-hpa
state: restarted
enabled: yes
- name: Create user 'aeolus'
user:
name: aeolus
password: "{{ 'dinlas' | password_hash('sha512') }}"
state: present
shell: /bin/bash
groups: sudo
- name: Copy credentials to TFTP directory
copy:
dest: /srv/tftp/startup-config
content: |
aeolus:dinlas
mode: '0644'
- name: Create custom SSH banner
copy:
dest: /etc/issue.net
content: |
###################################################
# Welcome to the aeolus.pantheon.local !
# Unauthorized access will be prosecuted.
###################################################
- name: Configure SSH to use the custom banner
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Banner'
line: 'Banner /etc/issue.net'
create: yes
state: present
- name: Restart SSH daemon
service:
name: ssh
state: restarted
enabled: yes
- name: Create FLAG
copy:
dest: /root/FLAG.txt
content: |
FLAG{07ca234f133dcf10cd5812c3faef980f954505d2}
N1-ARES
ARES contains the following vulnerabilities.
- The Guest account is enabled, and allows RDP access.
- Credentials for user ‘ares’ are available in C:\creds.txt
- A Windows service with weak privileges allows for privilege escalation
- The system is duel homed, allowing access to network 2
- hosts: N1-ARES
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N1-ARES/'
- name: Disable Windows update
win_service:
name: Windows update
start_mode: disabled
state: stopped
- name: Disable Windows Defender Real-Time Protection via registry
win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection
name: DisableRealtimeMonitoring
data: 1
type: dword
state: present
- name: Exclude C:\ from Windows Defender scanning
win_shell: "Add-MpPreference -ExclusionPath C:"
- name: Disable Defender realtime scanning
win_shell: "Set-MpPreference -DisableRealtimeMonitoring $true"
- name: Create user 'ares'
win_user:
name: ares
password: 'P@ssw0rd123!'
state: present
groups:
- Users
password_never_expires: yes
user_cannot_change_password: no
- name: Disable Windows firewall
win_firewall:
state: disabled
profiles:
- Domain
- Private
- Public
tags: disable_firewall
- name: Enable guest account
win_command: net user Guest /active:yes
- name: Add Guest to Remote Desktop Users group
win_group_membership:
name: "Remote Desktop Users"
members:
- Guest
- ares
state: present
- name: Enable insecure guest logons
win_regedit:
path: HKLM:\Software\Policies\Microsoft\Windows\Lanmanworkstation
name: "AllowInsecureGuestAuth"
data: "1"
type: dword
- name: Add anonymous to everyone group
win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Control\Lsa
name: "everyoneincludesanonymous"
data: "1"
type: dword
- name: Disable null session restrictions
win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
name: "restrictnullsessaccess"
data: "0"
type: dword
- name: Enable RDP in the registry
win_regedit:
path: HKLM:\System\CurrentControlSet\Control\Terminal Server
name: fDenyTSConnections
data: 0
type: dword
- name: Ensure RDP service is running and set to auto-start
win_service:
name: TermService
start_mode: auto
state: started
- name: Create service directory
win_file:
path: C:\vulnsvc
state: directory
- name: Download service executable
ansible.windows.win_get_url:
url: http://192.168.24.250/service.exe
dest: C:\vulnsvc\service.exe
- name: Set permissions so 'Users' can overwrite the .exe file
win_acl:
path: C:\vulnsvc\service.exe
user: Users
rights: FullControl
type: allow
state: present
inherit: ContainerInherit,ObjectInherit
- name: Create and start the vulnerable Windows service
win_service:
name: VulnService
display_name: Vulnerable Service
description: Insecure service for privesc testing
path: 'cmd.exe /c C:\vulnsvc\service.exe'
start_mode: auto
state: started
- name: Get current SDDL of VulnService
win_shell: |
(sc.exe sdshow "VulnService" | Out-String).Trim()
register: current_sddl_raw
- name: Extract raw SDDL string
set_fact:
current_sddl: "{{ current_sddl_raw.stdout | regex_replace('^.*SDDL: ', '') }}"
- name: Add ACE for Everyone to allow full control over the service
set_fact:
everyone_ace: "(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)"
- name: Add ACE to beginning of DACL
set_fact:
updated_sddl: "{{ current_sddl | regex_replace('^D:', 'D:' ~ everyone_ace) }}"
- name: Apply updated SDDL to the service
win_shell: |
sc.exe sdset "VulnService" "{{ updated_sddl }}"
register: sdset_result
- name: Show result of SDDL update
debug:
var: sdset_result.stdout
- name: Copy creds.txt file
copy:
dest: C:\creds.txt
content: |
ares:P@ssw0rd123!
- name: Copy flag file
copy:
dest: C:\Users\Administrator\Desktop\FLAG.txt
content: |
FLAG{2ff3e0b8bb5cdac3a2ea7baca6b48be0ed919de2}
For the vulnerable Windows service, I’m using the following code.
#include <windows.h>
#define SERVICE_NAME "MySampleService"
SERVICE_STATUS_HANDLE g_StatusHandle;
HANDLE g_StopEvent;
void SetStatus(DWORD state) {
SERVICE_STATUS status = {
.dwServiceType = SERVICE_WIN32_OWN_PROCESS,
.dwCurrentState = state,
.dwControlsAccepted = (state == SERVICE_RUNNING) ? SERVICE_ACCEPT_STOP : 0,
.dwWin32ExitCode = 0
};
SetServiceStatus(g_StatusHandle, &status);
}
VOID WINAPI ServiceCtrlHandler(DWORD ctrl) {
if (ctrl == SERVICE_CONTROL_STOP) {
SetStatus(SERVICE_STOP_PENDING);
SetEvent(g_StopEvent);
}
}
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv) {
g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
if (!g_StatusHandle) return;
SetStatus(SERVICE_START_PENDING);
g_StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!g_StopEvent) {
SetStatus(SERVICE_STOPPED);
return;
}
SetStatus(SERVICE_RUNNING);
WaitForSingleObject(g_StopEvent, INFINITE);
CloseHandle(g_StopEvent);
SetStatus(SERVICE_STOPPED);
}
int main() {
SERVICE_TABLE_ENTRY table[] = {
{ SERVICE_NAME, ServiceMain },
{ NULL, NULL }
};
return StartServiceCtrlDispatcher(table) ? 0 : GetLastError();
}
Compile the service using MinGW.
x86_64-w64-mingw32-gcc service.c -o myservice.exe -ladvapi32
N2-APOLLO
APOLLO is a Windows system with VNC configured to auto-login as the administrator.
- hosts: N2-APOLLO
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N2-APOLLO/'
- name: Add a static route back to Network One
ansible.windows.win_command: route -p add 192.168.24.0 mask 255.255.255.0 172.16.24.7
- name: Add default gateway
ansible.windows.win_command: route -p add 0.0.0.0 mask 0.0.0.0 172.16.24.1
- name: Ensure the "Winlogon" registry path exists
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
name: AutoAdminLogon
type: string
data: "1"
state: present
ignore_errors: yes
- name: Ensure DefaultUserName registry key exists for auto-login
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
name: DefaultUserName
type: string
data: Administrator
state: present
ignore_errors: yes
- name: Ensure DefaultPassword registry key exists for auto-login
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
name: DefaultPassword
type: string
data: "Password1"
state: present
ignore_errors: yes
- name: Ensure ForceAutoLogon registry key exists
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
name: ForceAutoLogon
type: string
data: "1"
state: present
ignore_errors: yes
- name: Ensure DefaultDomainName registry key exists for auto-login
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
name: DefaultDomainName
type: string
data: "WORKGROUP"
state: present
ignore_errors: yes
- name: Disable lock screen
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization
name: NoLockScreen
type: dword
data: 1
- name: Disable password on wake/resume
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI
name: DisableAcrylicBackgroundOnLogon
type: dword
data: 1
- name: Disable Windows update
win_service:
name: Windows update
start_mode: disabled
state: stopped
- name: Download TightVNC MSI
ansible.windows.win_get_url:
url: http://172.16.24.250/tightvnc-2.8.81-gpl-setup-64bit.msi
dest: C:\Windows\Temp\tightvnc-setup.msi
- name: Install TightVNC
ansible.windows.win_package:
path: C:\Windows\Temp\tightvnc-setup.msi
arguments: /quiet
state: present
- name: Allow TightVNC connections with no password
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\TightVNC\Server
name: UseVncAuthentication
data: 0
type: dword
- name: Ensure TightVNC allows multiple connections
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\TightVNC\Server
name: AlwaysShared
data: 1
type: dword
- name: TightVNC
win_service:
name: TightVNC Server
start_mode: auto
state: started
- name: Copy flag file
copy:
dest: C:\Users\Administrator\Desktop\FLAG.txt
content: |
FLAG{46e77971a498e89b5ac767dbfe0edadf06ddb4d3}
- name: Reboot the system
win_reboot:
reboot_timeout: 600
N2-DEMETER
A Windows host running Tomcat that can be exploited to gain administrative access to the host.
- name: N2-DEMETER - Tomcat Install
hosts: N2-DEMETER
gather_facts: yes
vars:
java_installer_url: http://172.16.24.250/OpenJDK8U-jdk_x64_windows_hotspot_8u402b06.msi
#https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u402-b06/OpenJDK8U-jdk_x64_windows_hotspot_8u402b06.msi
java_installer_path: C:\Temp\OpenJDK8.msi
java_install_dir: 'C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot'
tomcat_version: 8.0.24
tomcat_zip_url: http://172.16.24.250/apache-tomcat-8.0.24-windows-x64.zip
#https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.24/bin/apache-tomcat-8.0.24-windows-x64.zip
install_dir: 'C:\Tomcat'
unzip_dir: 'C:\Tomcat\apache-tomcat-8.0.24'
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N2-DEMETER/'
- name: Add a static route back to Network One
ansible.windows.win_command: route add -p 192.168.24.0 mask 255.255.255.0 172.16.24.7
- name: Add default gateway
ansible.windows.win_command: route -p add 0.0.0.0 mask 0.0.0.0 172.16.24.1
- name: Disable Windows Defender Real-Time Protection via registry
win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection
name: DisableRealtimeMonitoring
data: 1
type: dword
state: present
- name: Exclude C:\ from Windows Defender scanning
win_shell: "Add-MpPreference -ExclusionPath C:"
- name: Disable Defender realtime scanning
win_shell: "Set-MpPreference -DisableRealtimeMonitoring $true"
- name: Ensure C:\Temp exists
ansible.windows.win_file:
path: C:\Temp
state: directory
- name: Disable Windows update
win_service:
name: Windows update
start_mode: disabled
state: stopped
- name: Download Java JDK 8 installer
ansible.windows.win_get_url:
url: "{{ java_installer_url }}"
dest: "{{ java_installer_path }}"
- name: Install Java JDK 8 silently
ansible.windows.win_package:
path: "{{ java_installer_path }}"
arguments: INSTALL_SILENT=Enable
product_id: ''
state: present
- name: Set JAVA_HOME system environment variable
ansible.windows.win_environment:
name: JAVA_HOME
value: "{{ java_install_dir }}"
level: machine
state: present
- name: Ensure Tomcat install directory exists
ansible.windows.win_file:
path: "{{ install_dir }}"
state: directory
- name: Download Tomcat ZIP
ansible.windows.win_get_url:
url: "{{ tomcat_zip_url }}"
dest: "{{ install_dir }}\\tomcat.zip"
- name: Unzip Tomcat
win_unzip:
src: "{{ install_dir }}\\tomcat.zip"
dest: "{{ install_dir }}"
remote_src: yes
ignore_errors: yes
- name: Configure tomcat-users.xml
win_copy:
content: |
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<user username="tomcat" password="tomcat" roles="manager-gui,manager-script"/>
</tomcat-users>
dest: "{{ unzip_dir }}\\conf\\tomcat-users.xml"
- name: Install Tomcat as a Windows Service
ansible.windows.win_shell: |
set "JAVA_HOME={{ java_install_dir }}" && set "CATALINA_HOME={{ unzip_dir }}" && "{{ unzip_dir }}\\bin\\service.bat" install
args:
executable: cmd
register: tomcat_service_install
- name: Show Tomcat service install output
debug:
var: tomcat_service_install.stdout_lines
- name: Start Tomcat service
ansible.windows.win_service:
name: Tomcat8
state: started
start_mode: auto
- name: Copy flag file
copy:
dest: C:\Users\Administrator\Desktop\FLAG.txt
content: |
FLAG{a3d32a92a8b7ea9dce971974e954786ed7a684ba}
N2-HADES
This is a Linux system running MySQL. The root user can login to MySQL remotely, to extract the password for the ‘alice’ user account, which is an administrator.
- hosts: N2-HADES
user: root
become: true
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N2-HADES/'
- name: Add new default gateway
ansible.builtin.command: ip route add default via 172.16.24.1 dev eth0
ignore_errors: yes
- name: Update apt package cache
ansible.builtin.apt:
update_cache: yes
async: 60
poll: 10
- name: install python3 SQL
apt: pkg=python3-pymysql state=present
- name: install MariaDB
apt: pkg=mariadb-server state=present
- name: Copy MariaDB configuration
copy:
dest: /etc/mysql/mariadb.conf.d/50-server.cnf
content: |
[server]
[mysqld]
pid-file = /run/mysqld/mysqld.pid
basedir = /usr
bind-address = 0.0.0.0
expire_logs_days = 10
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
[embedded]
[mariadb]
[mariadb-10.6]
- name: Restart MariaDB
service:
name: mariadb
state: restarted
- name: Set root password and switch auth plugin to mysql_native_password
ansible.builtin.shell: |
mysql -u root <<EOF
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('MySuperSecureRootPW!');
FLUSH PRIVILEGES;
EOF
args:
executable: /bin/bash
- name: Create /root/.my.cnf with empty password for MySQL root
ansible.builtin.copy:
dest: /root/.my.cnf
content: |
[client]
user=root
password=MySuperSecureRootPW!
owner: root
group: root
mode: '0600'
- name: Create database 'users'
community.mysql.mysql_db:
name: users
state: present
- name: Create user 'alice' with password
mysql_user:
name: alice
password: "DownTheRabbitHole..."
host: '%'
state: present
- name: Create user 'alice'
user:
name: alice
password: "{{ 'DownTheRabbitHole...' | password_hash('sha512') }}"
state: present
shell: /bin/bash
groups: sudo
- name: Grant privileges to 'alice' on 'users' database
mysql_user:
name: alice
host: '%'
priv: "users.*:SELECT,INSERT,UPDATE,DELETE,CREATE,INDEX,DROP,ALTER,CREATE TEMPORARY TABLES,LOCK TABLES"
state: present
- name: Allow remote access to MySQL root without password
mysql_user:
name: root
password: ""
host: '%'
state: present
priv: "*.*:ALL"
- name: Create users table
mysql_query:
query: |
CREATE TABLE IF NOT EXISTS users (
userid INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(150) NOT NULL,
password VARCHAR(150) NOT NULL,
PRIMARY KEY (userid)
);
login_db: users
- name: Insert 'alice' into 'users' table
mysql_query:
query: |
INSERT INTO users (username, password)
VALUES ('alice', 'DownTheRabbitHole...')
login_db: users
- name: Insert 'charlie' into 'users' table
mysql_query:
query: |
INSERT INTO users (username, password)
VALUES ('charlie', 'TiCiaMyEaTeN')
login_db: users
- name: Insert 'bob' into 'users' table
mysql_query:
query: |
INSERT INTO users (username, password)
VALUES ('bob', 'cHianTAlAiNO')
login_db: users
- name: Copy FLAG
copy:
dest: /root/FLAG.txt
content: |
FLAG{59e653e13ca373f0bc05b0b449fbd6427c9e0f53}
N2-HERMES
Another Linux host, this time hosting credentials on an FTP server. The credentials can be used to connect to a postgres database, and extract credentials to SSH to the system.
- hosts: N2-HERMES
user: root
become: true
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N2-HERMES'
- name: Add new default gateway
ansible.builtin.command: ip route add default via 172.16.24.1 dev eth0
ignore_errors: yes
- name: install vsftpd
apt: pkg=vsftpd state=present
- name: install samba
apt: pkg=samba state=present
- name: install postfix
apt: pkg=postfix state=present
- name: install postgres
apt: pkg=postgresql state=present
- name: install postgres-contrib
apt: pkg=postgresql-contrib state=present
- name: install libq-dev
apt: pkg=libpq-dev state=present
- name: Copy VSFTP configuration
copy:
dest: /etc/vsftpd.conf
content: |
listen=NO
listen_ipv6=YES
anonymous_enable=YES
local_enable=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO
- name: Create user 'bob'
user:
name: bob
password: "{{ 'Secret123' | password_hash('sha512') }}"
state: present
shell: /bin/bash
groups: sudo
- name: Copy credentials
copy:
dest: /srv/ftp/sampledb.txt
content: |
postgres:postgres
- name: Enable PostgreSQL remote connections
lineinfile:
path: /etc/postgresql/16/main/postgresql.conf
regexp: '^#?listen_addresses\s*='
line: "listen_addresses = '*'"
- name: Allow remote login for bob in pg_hba.conf
lineinfile:
path: /etc/postgresql/16/main/pg_hba.conf
insertafter: EOF
line: "host all postgres 0.0.0.0/0 md5"
- name: Ensure PostgreSQL service is running
service:
name: postgresql
state: started
enabled: yes
- name: Set PostgreSQL password for user bob (via raw SQL)
shell: |
sudo -u postgres psql -c "DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'postgres') THEN
CREATE ROLE postgres LOGIN PASSWORD 'postgres';
ELSE
ALTER ROLE postgres WITH PASSWORD 'postgres';
END IF;
END \$\$;"
- name: Create 'sampledb' database
shell: |
sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = 'sampledb'" | grep -q 1 || \
sudo -u postgres createdb -O postgres sampledb
- name: Create 'passwords' table
shell: |
sudo -u postgres psql -d sampledb -c "CREATE TABLE IF NOT EXISTS passwords (
id SERIAL PRIMARY KEY,
username VARCHAR(100),
password_hash TEXT
);"
- name: Enable pgcrypto extension in sampledb
shell: |
sudo -u postgres psql -d sampledb -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
- name: Insert sample rows into passwords table
shell: |
sudo -u postgres psql -d sampledb -c "
INSERT INTO passwords (username, password_hash) VALUES
('alice', crypt('AlicePass', gen_salt('bf'))),
('bob', crypt('Secret123', gen_salt('bf'))),
('charlie', crypt('CharliePass', gen_salt('bf')))
ON CONFLICT DO NOTHING;"
- name: Grant SELECT on passwords table to postgres
shell: |
sudo -u postgres psql -d sampledb -c "GRANT SELECT ON passwords TO postgres;"
- name: Restart vsftpd daemon
service:
name: vsftpd
state: restarted
- name: Restart samba daemon
service:
name: smbd
state: restarted
- name: Restart postfix daemon
service:
name: postfix
state: restarted
- name: Restart postgres daemon
service:
name: postgresql
state: restarted
N3-PROMETHEUS
This is a Linux host that leaks a MD5 hashed password using the finger daemon. For privilege escalation, we’re using a custom SetUID binary that can read arbitrary files.
#include <stdio.h>
#include <iostream>
#include <fstream>
// g++ read_config.c -o read_config
int main(int argc, char* argv[])
{
if (argc == 1)
{
printf("Usage ./read_config <configuration_filename>\n");
}
if (argc == 2)
{
printf("Using configuration file: %s\n",argv[1]);
std::string myText;
std::ifstream MyReadFile(argv[1]);
while (getline (MyReadFile, myText)) {
std::cout << myText;
}
MyReadFile.close();
}
return 0;
}
- name: Configure N3-PROMETHEUS
hosts: N3-PROMETHEUS
become: true
tasks:
- name: Add new default gateway
ansible.builtin.command: ip route add default via 10.0.24.1 dev eth0
ignore_errors: yes
- name: Ensure required packages are installed
apt:
name:
- finger
- fingerd
- openbsd-inetd
state: present
update_cache: yes
- name: Enable finger service in /etc/inetd.conf
lineinfile:
path: /etc/inetd.conf
regexp: '^finger\s+stream'
line: 'finger stream tcp nowait nobody /usr/sbin/tcpd /usr/sbin/in.fingerd'
create: yes
state: present
- name: Ensure inetd is enabled and restarted
systemd:
name: openbsd-inetd
enabled: true
state: restarted
- name: Ensure user exists
user:
name: prometheus
shell: /bin/bash
state: present
create_home: yes
password: "{{ 'Password1' | password_hash('sha512') }}"
- name: Ensure finger can read the plan file
ansible.builtin.shell: |
chmod a+x /home/prometheus/
- name: Create a .plan file for the user
copy:
dest: /home/prometheus/.plan
content: |
MD5:2ac9cb7dc02b3c0083eb70898e549b63
owner: prometheus
group: prometheus
mode: '0644'
- name: Check if tmux session for prometheus exists
ansible.builtin.shell: "tmux has-session -t prometheus_session 2>/dev/null"
register: tmux_session_exists
failed_when: false
ignore_errors: true
become: true
- name: Ensure prometheus has a tmux session
ansible.builtin.shell: |
su - prometheus -c "tmux new-session -d -s prometheus_session 'whoami; sleep 36000'"
when: tmux_session_exists.rc != 0
become: true
ignore_errors: true
- name: Attach tmux session for user
ansible.builtin.shell: "su - prometheus -c 'tmux attach -t prometheus_session'"
when: tmux_session_exists.rc == 0
become: true
- name: Ensure tmux session starts on reboot
cron:
name: 'Start tmux session for prometheus'
user: prometheus
special_time: 'reboot'
job: 'tmux new-session -d -s prometheus_session "whoami; sleep 36000"'
- name: Download read_config to /sbin/
get_url:
url: "http://10.0.24.250/read_config"
dest: "/sbin/read_config"
mode: '4755' # Set SUID root
- name: Create /root/FLAG.txt
copy:
dest: /root/FLAG.txt
content: "FLAG{9de37a38ab55a917a70cbf4adf4ce2f45c147c08}"
N3-ARTEMIS
This is a Windows system running Jenkins. No authentication is required to interact with Jenkins, and the service is running as local system.
- hosts: N3-ARTEMIS
vars:
java_installer_url: http://10.0.24.250/OpenJDK21U-jdk_x64_windows_hotspot_21.0.7_6.msi
#https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_windows_hotspot_21.0.7_6.msi
java_installer_path: C:\Temp\OpenJDK21.msi
java_install_dir: 'C:\Program Files\Eclipse Adoptium\jdk-21.0.7.6-hotspot'
jenkins_groovy_dir: "C:\\Windows\\System32\\config\\systemprofile\\AppData\\Local\\Jenkins\\.jenkins\\init.groovy.d"
disable_auth_script: |
import jenkins.model.*
def instance = Jenkins.getInstance()
instance.setSecurityRealm(null)
instance.setAuthorizationStrategy(new hudson.security.AuthorizationStrategy.Unsecured())
instance.save()
tasks:
- name: Set configuration file path
set_fact:
config_file_path: '../FILES/N3-ARTEMIS/'
- name: Add default gateway
ansible.windows.win_command: route -p add 0.0.0.0 mask 0.0.0.0 10.0.24.1
- name: Ensure C:\Temp exists
ansible.windows.win_file:
path: C:\Temp
state: directory
- name: Disable Windows update
win_service:
name: Windows update
start_mode: disabled
state: stopped
- name: Disable Windows Defender Real-Time Protection via registry
win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection
name: DisableRealtimeMonitoring
data: 1
type: dword
state: present
- name: Exclude C:\ from Windows Defender scanning
win_shell: "Add-MpPreference -ExclusionPath C:"
- name: Disable Defender realtime scanning
win_shell: "Set-MpPreference -DisableRealtimeMonitoring $true"
- name: Download Java JDK 21 installer
ansible.windows.win_get_url:
url: "{{ java_installer_url }}"
dest: "{{ java_installer_path }}"
- name: Install Java JDK 21 silently
ansible.windows.win_package:
path: "{{ java_installer_path }}"
arguments: INSTALL_SILENT=Enable
product_id: ''
state: present
- name: Set JAVA_HOME system environment variable
ansible.windows.win_environment:
name: JAVA_HOME
value: "{{ java_install_dir }}"
level: machine
state: present
- name: Download Jenkins installer
win_get_url:
url: http://10.0.24.250/jenkins.msi
dest: C:\Temp\jenkins.msi
- name: Enable Windows Installer by setting DisableMSI to 0
ansible.windows.win_regedit:
path: HKLM:\Software\Policies\Microsoft\Windows\Installer
name: DisableMSI
data: 0
type: dword
state: present
- name: Install Jenkins silently
win_package:
path: C:\Temp\jenkins.msi
arguments: /quiet /norestart
state: present
- name: Ensure Jenkins service is running
win_service:
name: jenkins
start_mode: auto
state: started
- name: Open port 8080 in firewall
win_firewall_rule:
name: "Allow Jenkins"
localport: 8080
protocol: tcp
action: allow
direction: in
enabled: yes
- name: Ensure init.groovy.d directory exists
win_file:
path: "{{ jenkins_groovy_dir }}"
state: directory
- name: Create disable-security.groovy
win_copy:
content: "{{ disable_auth_script }}"
dest: "{{ jenkins_groovy_dir }}\\disable-security.groovy"
- name: Set JENKINS_INSTALL_STATE environment variable
win_environment:
name: JENKINS_INSTALL_STATE
value: RUNNING
level: machine
- name: Restart Jenkins service
win_service:
name: jenkins
state: restarted
- name: Copy flag file
copy:
dest: C:\Users\Administrator\Desktop\FLAG.txt
content: |
FLAG{97fd1ea4a0ee2393beb926c2b72e8fccb0e58f7c}
Host Configuration
Once all the Ansible configuration files have been created, we can use a bash script (configure_lab.sh) to configure all the hosts at once.
export ANSIBLE_HOST_KEY_CHECKING=False
#Network 1
ansible-playbook -i ./inventory.py CONFIGS/N1-ZEUS.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-HERA.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-ARES.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-AEOLUS.yaml
#Network 2
ansible-playbook -i ./inventory.py CONFIGS/N2-APOLLO.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-DEMETER.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-HADES.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-HERMES.yaml
#Network 3
ansible-playbook -i ./inventory.py CONFIGS/N3-PROMETHEUS.yaml
ansible-playbook -i ./inventory.py CONFIGS/N3-ARTEMIS.yaml
In Conclusion
At this stage, we have Ansible scripts to add vulnerabilities to the target hosts. Next, we need to work on improving the amount of randomisation in the CTF.